VB.Net

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

More than 3 years have passed since last update.

クローラー/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 を楽しみましょう!