塊2022

We :love: Katamari

; easy-scraper が便利なので Python に移植した(してる)

Tl;dr

pypi.org

これはなにか

とにかく簡単に使えることに注力した web スクレイピングライブラリであるところの

github.com

Pythonで再実装した。 まだ差異があるが、その内寄せる。

既存の何が駄目で easy-scraper の何が良いか

web スクレイピングとは次のような操作だ

  1. ターゲットにする web ページを調べる
  2. 欲しい情報が在る場所をルートからトラバースして見つける
  3. 欲しい情報が構造としてあるなら, 情報たちの関係も特定する
  4. 2, 3, で人手でやったことをシミュレーションするプログラムをコーディングする

2,3 は容易な作業ではないが、それをまたシミュレーションする必要があるのも一苦労だ。ライブラリはたいてい jQuery のような API を提供するからドキュメントと睨みっこしながらプログラミングすることになる。easy-scraper は、このシミュレーション部分を省略できる。

どう使うか

欲しい情報はたいてい、すぐ周りの構造を見れば位置を特定できる。 最も簡単なパターンはある名前のタグ、あるいはある名前の class のついたタグに囲まれているといった具合だ。例えば、WEBページのタイトルは <title> に囲まれている。これはほとんどいつでもそう決まってる。

よくあるライブラリであれば, 1. title という名前のタグを取得する 2. 取得したDOMの中の innerText を得る(innerText という用語はラフに使ってます) ということを自分でする必要がある.

easy-scraper では代わりにHTMLパターンを書く。title タグの中の innerText は次のパターンで書く。ここで {{ x }} がマッチしたテキストを補足するためのプレースホルダーで、x というのは変数名である(自由な名前を使える)。

<title>{{ x }}</title>

easy-scraper はこれをパターンマッチさせることで、欲しいテキストを取得する。

import easy_scraper

body = '<html><head><... 検索対象HTML...'
pattern = '<title>{{ x }}</title>'

result = easy_scraper.match(body, pattern)
# [ {"x": "..."}, ... ]

一つのマッチ結果は辞書で表現される。ここではパターン中の変数が x だから、x をキーとして、値にマッチしたテキストを格納した辞書 dict[str, str] になる。そしてマッチ結果は一つとは限らない。<title> タグが何度も使われてるならその個数だけマッチする。というわけで match の返り値は辞書のリスト list[dict[str, str]] になる。

今のは最も単純なパターン表現だ。 パターンでは入れ子関係を表現できる。 例えばさっきの例では body 以下の title タグを(もしあれば)拾ってしまう。 head 以下の title であることを表現するパターンは次の通り。

<head><title>{{ x }}</title></head>

注意点としてはこの入れ子はあくまで子孫関係を表していて、直接の親子関係に限ってはいない。

パターンは innerText だけでなく属性にマッチさせることもできる。例えば a タグの href 属性を innerText と一緒に拾うには次のパターンを使う。

<a href="{{ linkUrl }}">{{ innerText }}</a>

再実装に過ぎないライブラリの説明にしては丁寧すぎたと自分で思うので、オリジナルライブラリの作者による解説

tanakh.hatenablog.com

も併せて読んでほしい。

オリジナルとの差異

次の2つの差異がある。あることは認識しているが、こういうのは不便さを自分で実感しないと埋める意欲が沸かなくて…。

  1. 兄弟関係に連続性を仮定していない
  2. 部分文字列パターンマッチが無い

パターン <div><A /><B /></div> とあるときにオリジナルのライブラリでは <A /> があって、そのすぐ後ろに <B /> が来ることを表現している。その間に 0 個以上の余分な要素があることを許すには ... を使って <div><A />...<B /></div> と書くことになっている。この再実装版では ... という文法をそもそも用意しておらず、後者の意味でしか解釈していない。つまり、

<div>
  <A />
  <B />
</div>

の意味は、div タグの内部であって、A タグがあって、0個以上の要素を飛ばして、B タグが出現すること、を意味する。A と B は直接隣り合う必要はなく、順序だけが保証されている。

部分文字列のパターンマッチがない。 本家では <span>{{ num }} usres</span> というパターンが書けて、これは <span>123 users</span> にマッチして {"num": "123"} を返す。便利だ。今回の再実装版ではタグの innerText はプレーンなテキストか、{{ ... }} 一個が置かれてるかしか許していない。 これはあったら絶対便利だろうな… パターンに関する字句解析をもうちょっと真面目にする必要があるから、モチベーションがもう2つくらい上がったら手をつける。