LoginSignup
3
4

More than 5 years have passed since last update.

Html Agility Pack を使って Qiita Advent Calendar に参加登録して未投稿の人をまとめる

Last updated at Posted at 2015-12-24

クローラー/Webスクレイピング Advent Calendar 2015 25日目です。

Html Agility Pack

Html Agility Pack は、.NET Framework による HTML パーサーで XPATH により DOM の読み書きができる、Web スクレイピングに役立つ人気ライブラリです。Nuget で手に入ります。

開発者的には、正規表現を使って値を抜き出すよりいい感じにコードが書ける気がしますが、HTML の開始・終了タグのペアに誤りがあると動作しないし、ぶっちゃけ正規表現でスクレイピングする方が堅牢なプログラムになると思います。また、jQuery のセレクターのように柔軟に要素を指定できるわけでもないので、結構もどかしいです。

詳しくは、こちらの記事が参考になると思います。.NET TIPS:Html Agility Packを使ってWebページをスクレイピングするには?[C#、VB] - @IT

Advent Calendar 2015 一覧の取得

さっそく使ってみます。Qiita の Advent Calendar 2015 の一覧を取得します。各カテゴリーのページから <td class="adventCalendarList_calendarTitle"><a> 要素を取得できれば、タイトルと URL が得られそうです。

title.png

Html Agility Pack を利用するコードは次の通り。"//td[@class=""adventCalendarList_calendarTitle""]/a" と指定しています。

どうですかね? 正規表現で抽出するのと比べて。

Module.vb
Function GetCalendars(categoryUrl As String) As IEnumerable(Of Calendar)
    Dim client = New WebClient With {.Encoding = Text.Encoding.UTF8}
    Dim html = client.DownloadString(categoryUrl)

    Dim doc = New HtmlAgilityPack.HtmlDocument
    doc.LoadHtml(html)

    Return doc.DocumentNode.SelectNodes("//td[@class=""adventCalendarList_calendarTitle""]/a").Select(
        Function(node)
            Return New Calendar With {
                .Title = node.InnerText,
                .Url = node.Attributes("href").Value}
        End Function)
End Function

Calendar クラスの定義と、呼び出し部分です。

Modeul.vb
Class Calendar
    Property Title As String
    Property Url As String
End Class

Sub Main()
    Dim categories = New String() {
        "http://qiita.com/advent-calendar/2015/categories/to_be_decided",
        "http://qiita.com/advent-calendar/2015/categories/programming_languages",
        "http://qiita.com/advent-calendar/2015/categories/libraries",
        "http://qiita.com/advent-calendar/2015/categories/databases",
        "http://qiita.com/advent-calendar/2015/categories/web_technologies",
        "http://qiita.com/advent-calendar/2015/categories/mobile",
        "http://qiita.com/advent-calendar/2015/categories/devops",
        "http://qiita.com/advent-calendar/2015/categories/iot",
        "http://qiita.com/advent-calendar/2015/categories/os",
        "http://qiita.com/advent-calendar/2015/categories/editors",
        "http://qiita.com/advent-calendar/2015/categories/academic",
        "http://qiita.com/advent-calendar/2015/categories/services",
        "http://qiita.com/advent-calendar/2015/categories/company",
        "http://qiita.com/advent-calendar/2015/categories/miscellaneous"}

    Dim calendars = New List(Of Calendar)

    For Each c In categories
        calendars.AddRange(GetCalendars(c))
        Threading.Thread.Sleep(TimeSpan.FromSeconds(0.5))
    Next
End Sub

未投稿 情報の取得

各 Advent Calendar から、未投稿の情報を取得します。ここでは、“参加登録済みで未投稿の情報” のみを取得し、参加登録を受け付けている日の情報は取得しません。<div class="adventCalendarItem">要素内に、<div class="adventCalendarItem_comment"> 要素があると、参加登録済みで未投稿の情報を得られそうです。

comment.png

コードは次の通り。少し冗長な感じになってしまいました。

Module.vb
Function GetItems(calendar As Calendar) As IEnumerable(Of BlankItem)
    Dim client = New WebClient With {.Encoding = Text.Encoding.UTF8}
    Dim html = client.DownloadString("http://qiita.com" & calendar.Url)

    Dim doc = New HtmlAgilityPack.HtmlDocument
    doc.LoadHtml(html)

    Return doc.DocumentNode.SelectNodes("//div[@class=""adventCalendarItem""]").Where(
            Function(node)
                Return node.SelectSingleNode("./div[@class=""adventCalendarItem_comment""]") IsNot Nothing
            End Function
        ).Select(
            Function(node)
                Dim item = New BlankItem With {
                    .Calendar = calendar,
                    .Date = DateTime.Parse("2015/" & node.SelectSingleNode("./div[@class=""adventCalendarItem_date""]").InnerText.Replace(" ", "")),
                    .Comment = node.SelectSingleNode("./div[@class=""adventCalendarItem_comment""]").InnerText
                }

                Dim authorNode = node.SelectSingleNode("./div[@class=""adventCalendarItem_author""]/a")
                item.AuhtorIconSrc = authorNode.SelectSingleNode("./img").Attributes("src").Value
                item.AuthorName = authorNode.InnerText.Trim

                Return item
            End Function)
End Function

BlankItem クラスの定義と、呼び出し部分は次の通り。

Module.vb
Class BlankItem
    Property Calendar As Calendar
    Property AuthorName As String
    Property AuhtorIconSrc As String
    Property Comment As String
    Property [Date] As DateTime
End Class

Sub Main
    ' (Calendar 一覧取得のコード)

    Dim blankItems = New List(Of BlankItem)

    For Each c In calendars
        blankItems.AddRange(GetItems(c))
        Threading.Thread.Sleep(TimeSpan.FromSeconds(0.5))
    Next
End Sub

以上で、今回必要とする Advent Calendar の情報をスクレイピングできました。

情報の整理

LINQ を使って、さらに整理してみます。

未投稿のあるカレンダー一覧

Dim blankCalendars = From i In blankItems
                     Select i.Calendar
                     Distinct

未投稿のユーザーごとの参加登録しているカレンダー一覧

Dim sortedList = From i In blankItems
                 Group By AuthorName = i.AuthorName Into Items = Group
                 Order By Items.Count Descending

Markdown 形式で出力

Dim sb = New StringBuilder

For Each c In blankCalendars
    sb.Append(String.Format("* [{0}](http://qiita.com/{1})", c.Title, c.Url) & vbCrLf)
Next

For Each s In sortedList
    sb.Append(String.Format("* [{0}](http://qiita.com/{0})", s.AuthorName) & vbCrLf)

    For Each i In s.Items
        sb.Append(String.Format("    * [{0}](http://qiita.com/{1}) 「{2}」({3})", i.Calendar.Title, i.Calendar.Url, i.Comment, i.Date.ToString("MM/dd")) & vbCrLf)
    Next
Next

実行結果

2015/12/26 0:00 に実行した結果。

結果1

結果2

おわりに

Advent Calendar の未投稿は、ユーザー自ら参加登録したにも関わらず期日に投稿せず、毎日特定のテーマ記事が読めるという楽しい多人数参加型のイベントに水をさし、Qiita Advent Calendar ランキングからも除外されてしまいますが、これだけ参加者が多いと中には死病が原因の場合や、また皆さん仕事は忙しいと思いますが、特別に忙しいという場合もあると思います。Qiita は、そういった方の参加を取り消し代わりに投稿できる、やさしい仕組みになっていますので、あまり気にせず Advent Calendar を楽しみましょう!

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4