シェルスクリプトでスクレイピングするために`pup`を使う

2021-05-18Bash

はじめに

以前、シェルスクリプトからhtmlのスクレイピングをしたときの方法を共有します。

Go言語で作られたpupというツールを使いました。

検証環境

$ uname -moi
x86_64 x86_64 GNU/Linux

$ bash -version
GNU bash, version 4.2.46(2)-release (x86_64-redhat-linux-gnu)

pup とは

コマンドラインの HTML 解析ツールです。

標準入力から情報を読み込み、標準出力に結果を出力します。

それだけだと cat と同じなのですが、 HTML の要素をフィルタリングすることができます。フィルタリングのための HTML 要素指定方法として、一般的に利用されている CSS Selectors が利用できます。

pup ツール開発の背景

jq コマンドにインスパイアされたようです。

jq といえばコマンドライン上で JSON データを操作するためのツールですが、HTML データをターゲットとしたものとして開発されたようです。

pup インストール

別のエントリで、環境を汚さずにDockerを使ってpubを使うための方法を投稿しています。 Dockerが使い慣れている方はこちらもどうぞ。

以下のツールを利用します。

go get コマンドを使ってインストールするため、事前に go のインストールが必要です。

Goのインストール

# 何はともあれパッケージをアップデート
$ sudo yum update -y

# epelリポジトリを追加して、Goをインストールできるようにする
$ sudo yum install epel-release -y

$ sudo yum install golang -y

以下のコマンドが正しく実行できていればGoのインストールは完了です。

$ go version
go version go1.11.5 linux/amd64

pupのインストール

go get コマンドでインストールします。

$ go get github.com/ericchiang/pup

以下のコマンドが正しく実行できていればpupのインストールは完了です。

# コマンドは以下のディレクトリにインストールされている
$ ~/go/bin/pup --version
0.4.0

PATH="$PATH:$HOME/go/bin"のように、パスを通しておくと使いやすいでしょう。

その他、インストールされていると便利なツール

  • curl

curl については必須ではないのですが、URL で指定されたページの HTML データを取得する方法として curl を利用することが一般的となっている状況ですので、インストールしておくのが良いでしょう。

pup を使ってみる

スクレイピングの検証にサンプルhtmlを作成してスクレイピングしてみます。

cat <<'EOF' > index.html
<html>
  <body>
    <div>
      <h1>テストページ</h1>
      <h2>子どもたち</h2>
      <ul>
        <li class="name">ゆう
        <span class="age">10</span></li>
        <li class="name">れん
        <span class="age">7</span></li>
        <li class="name">ひろ
        <span>4</span></li>
        <li>そう
        <span class="age">1</span></li>
      </ul>
    </div>
  </body>
</html>
EOF

li タグ部分を抽出

$ cat index.html | $HOME/go/bin/pup 'ul > li'
<li class="name">
 ゆう
 <span class="age">
  10
 </span>
</li>
<li class="name">
 れん
 <span class="age">
  7
 </span>
</li>
<li class="name">
 ひろ
 <span>
  4
 </span>
</li>
<li>
 そう
 <span class="age">
  1
 </span>
</li>

li タグのうち class="name" の属性を持つもののみを抽出

$ cat index.html | $HOME/go/bin/pup 'ul > li[class="name"]'
<li class="name">
 ゆう
 <span class="age">
  10
 </span>
</li>
<li class="name">
 れん
 <span class="age">
  7
 </span>
</li>
<li class="name">
 ひろ
 <span>
  4
 </span>
</li>

class="age" の属性を持つもののみを抽出

$ cat index.html | $HOME/go/bin/pup '[class="age"]'
<span class="age">
 10
</span>
<span class="age">
 7
</span>
<span class="age">
 1
</span>

text情報のみを出力する機能

タグの構成は不要、text情報のみがほしいという場合は以下のように text{} という命令を付与します。

$ cat index.html | $HOME/go/bin/pup '[class="age"] text{}'
10
7
1

属性情報のみを出力する機能

属性情報のみがほしいという場合は以下のように attr{} という命令を付与します。

$ cat index.html | $HOME/go/bin/pup 'ul > li attr{class}'
name
name
name

ここでは class 属性の情報がすべて同じなので有効性がわかりにくいですが、
aタグのhref属性や、imgタグのsrc属性を取得するときに便利ですね。

スクレイピングした要素のJSON変換機能

ちょっと変わった機能として、スクレイピングした要素をJSON変換する機能があります。
json{}という命令を付与します。

$ cat index.html | $HOME/go/bin/pup 'ul > li json{}'
[
 {
  "children": [
   {
    "class": "age",
    "tag": "span",
    "text": "10"
   }
  ],
  "class": "name",
  "tag": "li",
  "text": "ゆう"
 },
 {
  "children": [
   {
    "class": "age",
    "tag": "span",
    "text": "7"
   }
  ],
  "class": "name",
  "tag": "li",
  "text": "れん"
 },
 {
  "children": [
   {
    "tag": "span",
    "text": "4"
   }
  ],
  "class": "name",
  "tag": "li",
  "text": "ひろ"
 },
 {
  "children": [
   {
    "class": "age",
    "tag": "span",
    "text": "1"
   }
  ],
  "tag": "li",
  "text": "そう"
 }
]

もう少し突っ込んだ使い方

curl を使えば、URL に該当するページの HTML データを取得できます。
( 標準出力に出力されます。 )

$ curl -s https://news.ycombinator.com/

取得された HTML をパイプで繋いで、 pup コマンドに渡してみます。

本来の使い方ではないのですが、 pup に通すと HTML が整形されますので、 ちょっとしたフォーマッタとしても利用できます。

$ curl -s https://news.ycombinator.com/ | pup

更に、 --color オプションを付けるとフォーマットされた HTML のタグが色付けされて更に見やすくなります。

$ curl -s https://news.ycombinator.com/ | pup --color

table タグの中の table タグの中の tr タグの ... といったようにフィルタリングをして、 a タグ要素のみを出力できます。

$ curl -s https://news.ycombinator.com/ | pup 'table table tr:nth-last-of-type(n+2) td.title a'

リンクの href 属性のみを表示します。

$ curl -s https://news.ycombinator.com/ | pup 'table table tr:nth-last-of-type(n+2) td.title a attr{href}'

JSON 形式に変換もできます。( jq での操作に慣れている場合はこちらが便利ですね。 )

$ curl -s https://news.ycombinator.com/ | pup 'table table tr:nth-last-of-type(n+2) td.title a json{}'

pup の引数を確認

pup コマンドの引数は以下のようになります。

$ cat index.html | pup [flags] '[selectors] [display function]'

先の例で行くと a タグの属性情報を表示するための attr{href} や JSON フォーマットで出力するための json{}[display function] に該当します。

更に突っ込んだ使い方

先程、少し使い方に触れましたが、更に例を追加して使い方を説明していきます。

以下のコマンドを実行し、フィルタを行う HTML ファイルをダウンロードしておきます。

$ curl -s -L http://en.wikipedia.org/wiki/Robots_exclusion_standard > robots.html

# 確認
$ head -n 3 robots.html
<!DOCTYPE html>
<html class="client-nojs" lang="en" dir="ltr">
<head>

タグでフィルタリング

HTML タグで絞り込みを行います。

$ cat robots.html | pup 'title'
<title>
 Robots exclusion standard - Wikipedia
</title>

id でフィルタリング

タグに付与されている id 属性 で絞り込みを行います。

$ cat robots.html | pup 'span#See_also'
<span class="mw-headline" id="See_also">
 See also
</span>

id 属性は HTML 内で一意 (となっていることが期待されている) ので、タグ名を指定しなくても構いません。

$ cat robots.html | pup '#See_also'
<span class="mw-headline" id="See_also">
 See also
</span>

属性情報でフィルタリング

属性情報で絞り込みを行います。

$ cat robots.html | pup 'th[scope="row"]'
<th id="Authority_control_frameless_|text-top_|10px_|alt=Edit_this_at_Wikidata_|link=https://www.wikidata.org/wiki/Q80776#identifiers|Edit_this_at_Wikidata" scope="row" class="navbox-group" style="width:1%">
 <a href="/wiki/Help:Authority_control" title="Help:Authority control">
  Authority control
 </a>
 <a href="https://www.wikidata.org/wiki/Q80776#identifiers" title="Edit this at Wikidata">
  <img alt="Edit this at Wikidata" src="//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/10px-OOjs_UI_icon_edit-ltr-progressive.svg.png" decoding="async" width="10" height="10" style="vertical-align: text-top" srcset="//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/15px-OOjs_UI_icon_edit-ltr-progressive.svg.png 1.5x, //upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/20px-OOjs_UI_icon_edit-ltr-progressive.svg.png 2x" data-file-width="20" data-file-height="20">
 </a>
</th>

ちなみに、今回のサンプル HTML に限っていうと <th scope="row"> 以外に scope="row" の属性を持っている HTML 要素はないので、以下のようにタグ指定を省略しても同様の結果が得られます。

$ cat robots.html | pup '[scope="row"]'
<th id="Authority_control_frameless_|text-top_|10px_|alt=Edit_this_at_Wikidata_|link=https://www.wikidata.org/wiki/Q80776#identifiers|Edit_this_at_Wikidata" scope="row" class="navbox-group" style="width:1%">
 <a href="/wiki/Help:Authority_control" title="Help:Authority control">
  Authority control
 </a>
 <a href="https://www.wikidata.org/wiki/Q80776#identifiers" title="Edit this at Wikidata">
  <img alt="Edit this at Wikidata" src="//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/10px-OOjs_UI_icon_edit-ltr-progressive.svg.png" decoding="async" width="10" height="10" style="vertical-align: text-top" srcset="//upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/15px-OOjs_UI_icon_edit-ltr-progressive.svg.png 1.5x, //upload.wikimedia.org/wikipedia/en/thumb/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg/20px-OOjs_UI_icon_edit-ltr-progressive.svg.png 2x" data-file-width="20" data-file-height="20">
 </a>
</th>

http:// で始まるリンクを表示。

$ cat robots.html | pup 'a[href*="http://"]'

https:// で始まるリンクを表示。

$ cat robots.html | pup 'a[href*="https://"]'

擬似クラスでフィルタリング

CSS Selectors で利用できる 擬似クラス 機能を使って絞り込みを行うこともできます。

アンカーテキストが空のリンクを表示。

$ cat robots.html | pup 'a[rel]:empty'
<a rel="license" href="//creativecommons.org/licenses/by-sa/3.0/" style="display:none;">
</a>

タグの中に History と書かれているタグを表示。

$ cat robots.html | pup ':contains("History")'
<span class="toctext">
 History
</span>
<span class="mw-headline" id="History">
 History
</span>

action="edit" という属性を持つタグの親タグを表示。

$ cat robots.html | pup ':parent-of([action="edit"])'
<span class="wb-langlinks-edit wb-langlinks-link">
 <a action="edit" href="//www.wikidata.org/wiki/Q80776#sitelinks-wikipedia" text="Edit links" title="Edit interlanguage links" class="wbc-editpage">
  Edit links
 </a>
</span>

+>, 、 ,

+>, 、 , は特殊な意味を持つ文字です。
複数のフィルタ条件を同時に指定することができます。

$ cat robots.html | pup 'title, h1'
<title>
 Robots exclusion standard - Wikipedia
</title>
<h1 id="firstHeading" class="firstHeading">
 Robots exclusion standard
</h1>

CSS Selector を連結(チェーン)する

スペース区切りで複数のセレクタが指定された場合、フィルタのチェーンとなります。
段階的に後続の条件でフィルタリングされていきます。

はじめに 1 つだけの条件で絞り込みます。

$ cat robots.html | pup 'h1#firstHeading'
<h1 id="firstHeading" class="firstHeading" lang="en">
 <span dir="auto">
  Robots exclusion standard
 </span>
</h1>

先程の条件での絞り込み後に span で更に絞り込みしたい場合は、先程の条件の後ろにスペースを開けた後 span と指定します。

$ cat robots.html | pup 'h1#firstHeading span'
<span dir="auto">
 Robots exclusion standard
</span>

pup コマンドをパイプで繋いで処理することもできます。

$ cat robots.html | pup 'h1#firstHeading' | pup 'span'
<span dir="auto">
 Robots exclusion standard
</span>

出力制御

タグ内のテキストのみ表示 : text{}

text{} 関数(関数と呼ぶのでしょうか?)を使って、タグ内のテキストのみを表示させることができます.

$ cat robots.html | pup '.mw-headline text{}'
History
Standard
Security
Alternatives
Examples
Nonstandard extensions
Crawl-delay directive
Allow directive
Sitemap
Host
Universal &#34;*&#34; match
Meta tags and headers
See also
References
External links

タグ内の属性のみ表示 : attr{attrname}

属性を表示させる場合には attr{} を使います。

attr{} のカッコの中には属性名を指定します。

$ cat robots.html | pup '.catlinks div attr{id}'
mw-normal-catlinks
mw-hidden-catlinks

JSON形式で表示 : json{}

json{} を指定して JSON形式で表示させることもできます。

$ cat robots.html | pup 'h3:nth-of-type(3) span'
<span class="mw-headline" id="Sitemap">
 Sitemap
</span>
<span class="mw-editsection">
 <span class="mw-editsection-bracket">
  [
 </span>
 <a href="/w/index.php?title=Robots_exclusion_standard&action=edit&section=9" title="Edit section: Sitemap">
  edit
 </a>
 <span class="mw-editsection-bracket">
  ]
 </span>
</span>
``

```bash
$ cat robots.html | pup 'h3:nth-of-type(3) span json{}'
[
 {
  "class": "mw-headline",
  "id": "Sitemap",
  "tag": "span",
  "text": "Sitemap"
 },
 {
  "children": [
   {
    "class": "mw-editsection-bracket",
    "tag": "span",
    "text": "["
   },
   {
    "href": "/w/index.php?title=Robots_exclusion_standard\u0026amp;action=edit\u0026amp;section=9",
    "tag": "a",
    "text": "edit",
    "title": "Edit section: Sitemap"
   },
   {
    "class": "mw-editsection-bracket",
    "tag": "span",
    "text": "]"
   }
  ],
  "class": "mw-editsection",
  "tag": "span"
 }
]

ひとこと

JSON形式にしてしまえばあとはどうとでもなりそうですね。 jq も使えますし。

2021-05-18Bash