Sat Jan 23 2021

web-grep を crates に登録した

少し前に。やった。

これで cargo が使える環境であれば

$ cargo install web-grep

すれば、たぶん ~/.cargo/bin/ 下に web-grep という実行ファイルが出来るから、 ここにパスだけ通してくれれば web-grep が使えるようになる。

web-grep の使い方

概要

ドキュメントはかろうじて github.com/cympfh/web-grep の README がドキュメントとしてあるかなくらいなんだけど、 全然詳細じゃないし、一度日本語で書いとこうと思う。

予め言っておくと、このコマンドは実態としては tanakh さんによるライブラリである github.com/tanakh/easy-scraper のインターフェイスでしかないので、 使い方の説明をちゃんとすると、このライブラリの使い方を説明するようなことになってしまう。

web-grep は名前の通り grep を意識しているが、多くのUNIX系コマンドがそうであるように大まかには次のように使える。

$ curl -sL http://example.com/index.html | web-grep <QUERY>

$ web-grep <QUERY> ./index.html

つまり入力は、標準入力として渡すか、第2引数にファイルパスを指定することで与える。 この入力は HTML または XML であることが期待される。 ちなみに現実の web ページの HTML はしばしば XML として正しくなく閉じてるべきタグが閉じていないなどがあるが、 ある程度はいい感じに解釈してパースし、そんなことで実行時エラーは怒らない。

QUERY には HTMLパターン を記述する。 この説明が web-grep の説明の主となるだろう。

HTML パターン

HTMLパターンは妥当な HTML (またはXML)文字列であって、いくつかの属性または innerText がプレースホルダーに置き換えられたようなもののこと。 プレースホルダーには3種類あり、無名プレースホルダー、番号プレースホルダー、名前付きプレースホルダーを用意している。 最も単純なのは無名プレースホルダー。

無名プレースホルダー

パターンにちょうど一つしかプレースホルダーを含めなくて良い場合はこれを使う。 {} と記述する。 例えば <a> タグの innerText にこれを使うならHTMLパターンは

<a>{}</a>

と書く。

<a> タグの href 属性にこれを使うなら

<a href={}></a>

と書く。web-grep は入力からこれらにマッチする innerText もしくは href 属性を全て列挙する。 前者の場合は <a> タグ全てを見つけてきて、その innerText 全てを行単位で列挙する。 後者の場合は <a> タグであって href 属性を持つようなものにマッチして、その href 属性の中身を列挙する。

パターンにはプレースホルダーとは関係なしに属性を付与することで、マッチする条件を追加できる。

<a class="title" href={}></a>

これは href 属性を持ち、title クラスを持つ <a> のその href 属性を列挙する。 例えば

<a class="title hoge" href="fuga">piyo</a>

にマッチして fuga を出力する。

番号プレースホルダー

パターンに複数のプレースホルダーを使いたい場合に使う。 しかも出力をタブ区切りにしたいときは番号プレースホルダーにすれば、その番号の順番で出力する。

番号付きのプレースホルダーは {1}, {2}, {3} などと書く。1 から始まることに注意。 0 スタートでもよかったけど、awk とかでもフィールド番号は $1 から始まって $0 には特別な意味をもたせてるしな。

<a href="{1}">{2}</a>

これは href 属性を持つ <a> タグにマッチする。 そして href 属性と innerText をタブ区切りで出力する。 区切り文字は -F オプションで変更もできる。

$ echo '<a href=HOGE>hoge</a><a href=FUGA>fuga</a>' | web-grep '<a href={1}>{2}</a>'
HOGE    hoge
FUGA    fuga

$ echo '<a href=HOGE>hoge</a><a href=FUGA>fuga</a>' | web-grep '<a href={2}>{1}</a>'
hoge    HOGE
fuga    FUGA

名前付きプレースホルダー

自由に名前を付けて {text} とか {href} といったプレースホルダーを使う事ができる。

<a href={href}>{text}</a>

ただし単にこれをクエリに web-grep しても出力は今までのようにタブ区切りで出力する。 その順番は名前の辞書順なので、固定ではあるのだが、使う側としては何番目がどのプレースホルダーへのマッチなのか分かりにくいと思う。 そこで --json フラグを付けてもらうと、出力を JSON の辞書にする。

$ echo '<a href="aaa">bbb</a>' | web-grep '<a href={href}>{text}</a>'
aaa     bbb

$ echo '<a href="aaa">bbb</a>' | web-grep '<a href={href}>{text}</a>' --json
{"href":"aaa","text":"bbb"}

jq のおかげでコマンドライン上でJSONを扱うのはそう難しくなくなった。 実は今までの方法では問題があって、マッチしたテキスト全てを列挙するため、元々改行が含まれているテキストなのか、複数がマッチしたのかの区別がつかなかった。

$ echo '<a>hoge
quote> fuga</a>' | web-grep '<a>{}</a>'
hoge
fuga

$ echo '<a>hoge</a><a>fuga</a>' | web-grep '<a>{}</a>'
hoge
fuga

--json の場合は改行も含めて正確にシリアライズしたものを出力するのでこれらを区別できる。

$ echo '<a>hoge
fuga</a>' | web-grep '<a>{text}</a>' --json
{"text":"hoge\nfuga"}

$ echo '<a>hoge</a><a>fuga</a>' | web-grep '<a>{text}</a>' --json
{"text":"hoge"}
{"text":"fuga"}

マッチング

ここらへん形式的に書くの大変そうだけど、パターンはもっと入り組んだものを書いてもいい。 まず、よく出てきた

<a>{}</a>

というパターンだが、これは <a> タグ直下の innerText という意味 ではなく<a> タグより下にある innerText という意味。

$ echo '<a>AAA<div>BBB</div></a>' | web-grep '<a>{}</a>'
AAA
BBB

$ echo '<a>AAA<div>BBB</div></a>' | web-grep '<a><div>{}</div></a>'
BBB

$ echo '<a>AAA<div>BBB</div></a>' | web-grep '<a>{}<div></div></a>'
AAA

$ echo '<a>AAA<div>BBB</div></a>' | web-grep '<a><div></div>{}</a>'

最後のケースはマッチするものが無いので何も出力しない。

パターンに具体的に innerText を書いてもいい。完全に一致する場合にマッチする。

$ echo '<a>AAA<div>BBB</div></a>' | web-grep '<a>{}<div>BBB</div></a>'
AAA

$ echo '<a>AAA<div>BBB</div></a>' | web-grep '<a>{}<div>B</div></a>'