Парсер хабрахабра на F#

25.08.2014 at 16:53

Tachikoma
Продолжаю изучать F#. Сегодня расскажу о первой программе – парсере хабра.
Для парсинга я выбрал один хаб – “Информационная безопасность”.
Страницы этого хаба имеют вид http://habrahabr.ru/hub/infosecurity/pageN/ где N – номер страницы.
Парсить HTML я буду с помощью библиотеки HtmlAgilityPack.

Сначала подготовим несколько служебных функций:

let validPath (path : string) =
  Array.fold (fun (acc:string) item -> acc.Replace(item, '_')) path (Path.GetInvalidFileNameChars())

Эта функция преобразует строку в корректное имя файла, заменяя невалидные символы.

let isGZipEncoding (contentEncoding: string) = 
  contentEncoding.ToLower().StartsWith("gzip")

Здесь я проверяю, архивирован ли ответ сервера.

let getStreamContent(response : HttpWebResponse) : string = 
  if (isGZipEncoding response.ContentEncoding)
  then
    let zip = new GZipStream(response.GetResponseStream(), CompressionMode.Decompress) 
    (new StreamReader(zip, Text.Encoding.UTF8)).ReadToEnd()
  else
    (new StreamReader(response.GetResponseStream(), Text.Encoding.UTF8)).ReadToEnd()

Эта функция возвращает ответ сервера и, при необходимости, разархивирует его.

let loadHtml (pageUrl:string) = 
  let client = new WebClient()
  let request : HttpWebRequest = WebRequest.Create(pageUrl) :?> HttpWebRequest
  request.Timeout <- 5000
  let html = getStreamContent(request.GetResponse() :?> HttpWebResponse)
  let htmldoc = new HtmlDocument() 
  htmldoc.LoadHtml(html)
  htmldoc

Здесь я получаю содержимое страницы по ее адресу. Возвращаемое значение – объект HtmlDocument из библиотеки HtmlAgilityPack.

let loadPost(pageUrl:string) = 
  try
    let htmldoc = loadHtml pageUrl
    let titlenode = htmldoc.DocumentNode.SelectSingleNode("//span[@class='post_title']")
    let rootnode = htmldoc.DocumentNode.SelectSingleNode("//div[@class='content html_format']")
    let filePath = @"D:\temp\habr\" + validPath(titlenode.InnerText) + ".txt"
    use wr = new StreamWriter(filePath, false)
    wr.WriteLine(rootnode.InnerText)
  with
    | ex -> Console.WriteLine("Exception: " + ex.Message)

Здесь загружается страница со статьей. Ее текст сохраняется в файл, именем которого является заголовок статьи. Заголовок и текст получаются с помощью XPath.

let processHubPage url = 
  try
    let htmldoc = loadHtml url
    let nodes = htmldoc.DocumentNode.SelectNodes("//a[@class='post_title']")    
    nodes |> Seq.toList|> List.map(fun x -> x.Attributes.["href"].Value) 
  with
    | ex -> 
      Console.WriteLine("Exception: " + ex.Message) 
      []

Эта функция обрабатывает страницу хаба и возвращает список ссылок на статьи на этой странице.

Теперь осталось только обработать все страницы хаба. Их количество я посмотрел вручную.

let pages = 
  [ 1 .. 362 ]
  |> List.map (fun x -> processHubPage (@"http://habrahabr.ru/hub/infosecurity/page" + x.ToString() + "/"))
  |> List.concat
  |> List.iter (fun x -> loadPost x)