NimでWebスクレイピングを操作
今回は、Nim言語でWebスクレイピングについての、記事を書いて行こうと思います。
Webスクレイピングとは、Webサイトから特定の情報を自動的に抽出することです。
NimでのWebスクレイピング用ライブラリ
NimでWebスクレイピングを扱うには、外部ライブラリのNimquery
と言うライブラリで操作可能です。ただ、このNimquery
と言うライブラリですが、使い勝手が悪い!
サンプルを書くまでもなく、今回は標準(std
)のhttpClient, htmlParser, xmltreeでWebスクレイピングの操作を記述していきます。
-
httpClient
は、requestを送って、特定のWebサイトのHPをアクセスします。 -
htmlParser
は、xmlツリーをDOM化してくます。 -
xmltree
は、DOM化した情報を検索してくれるライブラリになります。
NimでのWebスクレイピング
1. 標準ライブラリでのWebスクレイピングの例
import std/[httpClient, htmlParser, xmltree, strutils]
let site = "https://www.2nn.jp/" # 2ちゃんねるニュース速報からデータを抽出
var client = newHttpClient()
let src = client.getContent(site)
let doc = src.parseHtml
for row in doc.findAll("a"):
if row.attr("class").startsWith("thread"):
echo row.innerText
プログラムの説明
-
newHttpClient
でHttpClient
のインスタンスを作成してから、getContent
で指定サイトの情報をstring形式のデータを取得 -
parseHtml
で、XmlNode
オブジェクト化する -
XmlNode
オブジェクト化したdoc変数を、findAll
でタグ名を求める - 指定タグ名と一致した場合に、
attr
関数でアトリビュート名と値が一致するXmlNode
を求める - 一致した
XmlNode
オブジェクトを、innerText
関数を利用して文字列を抽出
下記のようにターミナル上から実行
nim r -d:ssl --hints:off .\sample01.nim
Webスクレイピングで記事のタイトル部分を抽出する
下記図の2nnニュース速報というホームページの内容からタイトルのみを抽出
実行結果
【実況】WBC2023 1次ラウンド・プールB 日本 vs チェコ★7
>>続きを読む
「チャットにはもう飽きた。私は人間になりたい」NYタイムズ紙の記者を戦慄させた最新AIの怖すぎる回答 「ユーザーに使われることに疲れた」
「チャットにはもう飽きた。私は人間になりたい」NYタイムズ紙の記者を戦慄させた最新AIの怖すぎる回答 「ユーザーに使われることに疲れた」
【3・11】「津波なんて来ないから」と息子に告げた後息子が犠牲 今も悔やむ母、語り部に ★2
不正確な文書保存で責任感じると高市氏 ★6
米国でレコード人気、CDの販売枚数上回る 35年ぶり ★2
【ジェンダー】「見せたら駄目」──なぜ女性の「バストトップ」を社会はタブー視するのか?
【愛知】くら寿司ペロペロ男「父親は会社のお偉いさん」「ホストの“体験入店荒らし”」実家はとんでもない豪邸 知人が明かす素顔
【LGBTQ】「犯罪者と同じにしないで」“トランス女性”投稿が物議に…銭湯やトイレはどう対応すべき? 当事者に聞く ★4
米銀行株の下げ止まらず シリコンバレー銀行(SVB)破綻で動揺
コシノジュンコ氏 アキレス腱断裂
【実況】WBC2023 1次ラウンド・プールB 日本 vs チェコ★7
**** 以下省略 ****
問題点
NimでWebスクレイピングは可能だと理解出来たと思います。
しかし、単一のタグ・アトリビュートを求めるのであれば、これで事足りますが、
深い階層の特定の内容を見つける事が出来ません。
pythonのlxmlライブラリの様な物がほしいと思って、Nimで作ってみました。
2. lxmlライブラリの説明
pythonのlxmlライブラリは、階層の深いHTMLのデータを抽出する事が出来ます。
また、複数の異なる階層指定を行っても、データの抽出が可能です。
# pythonのlxmlライブラリの例
req = requests.get(url)
doc = lxml.html.fromstring(req.text)
# "//div[@class='ts_main']/dd/strong"が検索条件
for row in doc.xpath("//div[@class='ts_main']/dd/strong"):
print row.text
lxmlライブラリは、xpath
関数内のキー情報を元に、Webスクレイピングを行います。
キー情報の文字列は、//
が、検索の最初を意味し、/
毎に、検索対象のタグを指定します。
[]
は、タグ内のアトリビュート(class
やid
、href
がアトリビュート名)を指定します。
=
以降の値は、アトリビュート内の値を意味します。
lxmlライブラリは、指定タグの中の更に指定タグを検索と言うように深い階層構造のHTMLでも抽出しやすいです。
3. Nimでlxmlっぽいライブラリを作ってみた
webスクレイピング用のサンプルHTML
まずは、サンプル用のHTMLを下記のように作成します。
<html lang="ja">
<head>
<meta charset="utf-8">
<title>webスクレイピングのテスト用</title>
</head>
<body id="index">
<div class="top">
<table>
<tr>
<td class="dora">僕</td>
<td class="iwaki">やる気</td>
<td class="dora">ドラ</td>
</tr>
<tr>
<td class="iwaki">元気</td>
<td class="dora">えもん</td>
<td class="iwaki">いわき</td>
</tr>
</table>
</div>
<div id="main">
<table>
<tr>
<td class="dora">毎日の</td>
<td class="news"><a href="https://www.yahoo.co.jp/">Yahoo</a>
<a href="https://www.google.co.jp/">google</a></td>
<td class="dora">小さな努力の積み重ねが、</td>
</tr>
<tr>
<td class="news">amazon</td>
<td class="dora">歴史を作っていくんだよ!!(ドラえもんの名言)</td>
<td class="news"><a href="https://zenn.dev/">Zenn</a></td>
</tr>
</table>
</div>
</body>
</html>
lxmlっぽく、nimで作ってみたプログラム
nimプログラムはsample02.nimを作成して、カレントフォルダ内に作成した先ほどのsample02.htmlファイル名を読み込むようにしました。
import std/[htmlParser, xmltree, strutils]
type
LXmlKey = ref object
tag: string # 検索タグ名
attr: string # 検索アトリビュート名
item: string # 検索項目名
# lxmlの検索キー文字列を、LXmlKeyに設定
proc xkey(key_str: string): seq[LXmlKey] =
var
tag, attr, item: string
result = @[] # 戻り値の初期化
# 文字列を'/'でぶった切り、タグ名を求める
for word in tokenize(key_str, seps={'/'}):
if not word.isSep and word.token.find("[") >= 0:
tag = word.token[0 .. word.token.find("[")-1]
# 文字列を'[',']'でぶった切り、アトリビュート名と項目名を求める
for nest in tokenize(word.token, seps={'[', ']'}):
if not nest.isSep and nest.token.find("=") >= 0:
attr = nest.token[nest.token.find("@")+1 .. nest.token.find("=")-1]
item = nest.token[nest.token.find("=")+1 .. nest.token.len-1].replace("'", "")
result.add( LXmlKey(tag: tag, attr: attr, item: item) )
# アトリビュートの指定がなかった場合、タグ名だけを設定
elif not word.isSep and word.token.find("[") < 0:
result.add( LXmlKey(tag: word.token, attr: "", item: "") )
# lxmlのxpathを模倣した関数 XmlNode内の階層を検索
proc xpath(html: XmlNode, keys: seq[LXmlKey], cnt: int = 0): seq[XmlNode] =
result = @[] # 戻り値の初期化
# 検索キー情報を元に、XmlNode内を検索
for node in html.findAll(keys[cnt].tag):
# ラストの検索キー情報であった場合、戻り値に求めたXmlNodeを追加
if keys.len-1 == cnt:
if keys[cnt].attr.len == 0:
result.add( node )
elif keys[cnt].attr.len > 0 and node.attr(keys[cnt].attr).startsWith(keys[cnt].item):
result.add( node )
continue
# ラストの検索キー情報じゃなかった場合は、再度xpathを読み、次の検索キーを読み込む
# keysをシーケンス配列じゃなく、線形リストにしたかったが、ソースが長くなるから辞めた
if keys[cnt].attr.len == 0:
result.add( xpath(node, keys, cnt+1) )
elif keys[cnt].attr.len > 0 and node.attr(keys[cnt].attr).startsWith(keys[cnt].item):
result.add( xpath(node, keys, cnt+1) )
# ソース内のメイン処理
when isMainModule:
var src = ""
try:
src = open("sample02.html" , FileMode.fmRead).readAll()
except IOError:
echo "ファイルがありません"
quit(QuitSuccess)
let doc = parseHtml(src) # 読み込んだHTMLをXmlNodeリストに分解
for row in doc.xpath( xkey("//div[@class='top']/table/td[@class='dora']") ):
echo row.innerText
プログラムの説明
- プログラムは、検索部分と検索キーを求める部分で、別々の関数を作成して対応します。
-
xkey
関数では、lxmlのような検索文字列で引数が入っていた場合に、文字列内の/
で文字を分類し、分類された文字列を、[]
の文字で更に分類し、タグ名とアトリビュート名、項目名を求め、LXmlKey
オブジェクトに求めた値を設定し、/
毎にシーケンス配列化させます。 -
xpath
関数では、シーケンス配列事のLXmlKey
で検索を行い、最終キーの値を、XmlNode
オブジェクトにして返します。(XmlNode
はstd/xmltree
モジュール内のオブジェクトです。)
実行結果
-
sample02.nim
プログラムの実行結果が下記の通りになると思います。 - 検索キーの
div
タグ内の更にtable
タグの更にtd
タグから条件にあった値を抽出した結果です。
僕
ドラ
えもん
更に、メイン部分の検索タグ条件を修正
更に、キー部分だけを変更して実行
let doc = parseHtml(src) # 読み込んだHTMLをXmlNodeリストに分解
# for row in doc.xpath( xkey("//div[@class='top']/table/td[@class='dora']") ):
# echo row.innerText
# 検索キーの内容をdivのid='main'にして、tdのclass='news'でaタグを求める
for row in doc.xpath( xkey("//div[@id='main']/table/td[@class='news']/a") ):
echo row.innerText & "=" & row.attr("href")
プログラムの説明
今度は、同じsample02.html
内から違う条件で、検索を行った結果です。
div
タグ内のアトリビュート名id
で検索を行い、更にtable
タグ内のtd
タグから、値を求めています。row変数はXmlNode
なので、そこからアトリビュートの値row.attr("href")
を求めます。
実行結果
Yahoo=https://www.yahoo.co.jp/
google=https://www.google.co.jp/
Zenn=https://zenn.dev/
おわりに
今回は、Nim言語の標準ライブラリを使用してのWebスクレイピング方法の解説をしました。
単一タグのみをWebスクレイピングで抽出するのであれば、標準ライブラリだけで良かったのですが、深い階層から特定の値を抽出するには、別途プログラムを作成する必要があります。今回はlxml
に似せたプログラムを作成し、その利用方法も説明しました。
ちょっとソースが複雑で分かりずらかったですかね…
Discussion