Nim 言語で画像収集クローラーを作ってみた

今回は Nim 言語を使って、Wikipedia の新着ファイルページから画像を収集するシンプルなクローラーを作ってみたいと思います

作業環境

HTML を取得する

まずは URL にアクセスして、HTML を取得するところまでやってみます

標準ライブラリの httpclient を使って HTTP リクエストを送りました

import httpclient

const url = "https://ja.wikipedia.org/wiki/特別:新着ファイル"

var client = newHttpClient()
var response = client.get(url)

echo response.body

HttpClient を作成し、指定した URL にリクエストを送ってレスポンスを取得しています

これを実行してみると次のようなエラーが出ます

Error: unhandled exception: SSL support is not available. Cannot connect over SSL. [HttpRequestError]
Error: execution of an external program failed: '/path/to/hogehoge'

このエラーはリクエスト先の URL が https の場合に出るエラーのようで、コンパイルするときに -d:ssl をつけるとうまくコンパイルできるようです

-d:ssl をつけてコンパイル、実行してみます

$ nim c -d:ssl -r hogehoge.nim
...(略)
    </body>
</html>

これで HTML が取得できるようになりました!

HTML から img タグを抽出し、画像 URL を取得する

取得した HTML から正規表現で img タグのみ抽出していきます

正規表現re という標準ライブラリがあったので、これを使って img タグを取得します

import httpclient, re  # re ライブラリを追加

const url = "https://ja.wikipedia.org/wiki/特別:新着ファイル"

var client = newHttpClient()
var response = client.get(url)
# HTML から img タグのみ取得
var elements = response.body.findAll(re"<img(.*?)>")

findAll() を使うとパターンにマッチした文字列を順に配列に格納していくようです

配列にうまく入っているか for 文を回して 1 つずつ出力してみます

for element in elements:
  echo element
$ nim c -d:ssl -r hogehoge.nim
...(略)
<img src="/static/images/poweredby_mediawiki_88x31.png" alt="Powered by MediaWiki" srcset="/static/images/poweredby_mediawiki_132x47.png 1.5x, /static/images/poweredby_mediawiki_176x62.png 2x" width="88" height="31"/>

これで img タグのみ取得することができました!

次にそれぞれの img タグからさらに src 属性の値(画像 URL)を取得します

ここでも正規表現で img タグの中から src="〜" の部分を抽出していきます

for element in elements:
  # img タグの中で src="〜" の部分が img タグ全体の何文字目から何文字目までかをタプルで取得
  var range = element.findBounds(re"""src=\"(.*?)\"""")
  # img タグから src="〜" の部分を取得
  var attr = element[range.first..range.last]
  # 最初の src=" と最後の " は不要なので、それ以外の部分を再度取得
  var image = attr[5..^2]

findBounds() を使うとパターンにマッチした部分が何文字目から何文字目までかを start, last が key のタプルで取得します

re"""src=\"(.*?)\"""" の部分は re"src=\"(.*?)\"" ではエラーになってしまったので、このようなかたちになってしまいました・・・)

^2 の部分は負の値を設定しているところで ..^2 と書くと末尾から2文字目までという意味になるみたいです

Wikipedia の投稿画像は //upload.wikimedia.org/〜 という形式で img タグに設定されているようなので、次は画像にアクセスするために URL を組み立てていきます

まずは //upload.wikimedia.org/〜 という形式か簡単にチェックします

for element in elements:
  # ...
  if image[0..7] == "//upload":
    # ...

そしてスキームを先頭につけます

for element in elements:
  # ...
  if image[0..7] == "//upload":
    image = "https:" & image

これで画像 URL は完成しましたが、画像をローカルに保存するときはファイル名はそのままにしたいので、画像 URL からファイル名を取得します

for element in elements:
  # ...
  if image[0..7] == "//upload":
    image = "https:" & image
    var splited = image.split(re"/")
    var fileName = splited[high(splited)]

かなり強引なんですが、画像 URL を / 区切りで配列にし、その配列の最後の要素をファイル名として取得しています

画像 URL と保存時のファイル名が定義できたので、画像をダウンロードします

for element in elements:
  # ...
  if image[0..7] == "//upload":
    # ...
    client.downloadFile(image, fileName)

これで実行すると実行ファイルが置いてあるディレクトリに画像が保存されていくのが確認できると思います

完成したコードはこんな感じです

import httpclient, re

const url = "https://ja.wikipedia.org/wiki/特別:新着ファイル"

var client = newHttpClient()
var response = client.get(url)
var elements = response.body.findAll(re"<img(.*?)>")

for element in elements:
  var range = element.findBounds(re"""src=\"(.*?)\"""")
  var attr = element[range.first..range.last]
  var image = attr[5..^2]
  if image[0..7] == "//upload":
    image = "https:" & image
    var splited = image.split(re"/")
    var fileName = splited[high(splited)]
    client.downloadFile(image, fileName)

まとめ

チュートリアル的な記事であったり、参考になるサイトや情報が少なく、ほとんど本家のドキュメントを見ながらの作業になりました

たぶんもっと良い書き方がたくさんあると思うので、色々ご指摘いただけると幸いです

Nim in Action

Nim in Action