シェルスクリプトでスクレイピングするために`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)
Bash

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
Bash

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

$ go version
go version go1.11.5 linux/amd64
Bash

pupのインストール

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

$ go get github.com/ericchiang/pup
Bash

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

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

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
Bash

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>
Bash

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>
Bash

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>
Bash

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

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

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

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

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

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

ここでは 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": "そう"
 }
]
Bash

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

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

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

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

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

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

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

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

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

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

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

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

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

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

pup の引数を確認

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

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

先の例で行くと 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>
Bash

タグでフィルタリング

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

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

id でフィルタリング

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

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

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

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

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

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

$ 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>
Bash

ちなみに、今回のサンプル 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>
Bash

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

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

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

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

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

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

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

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

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

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

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>
Bash

+>, 、 ,

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

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

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

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

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

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

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

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

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

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

出力制御

タグ内のテキストのみ表示 : 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
Bash

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

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

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

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

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"
 }
]
Bash

ひとこと

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

2021-05-18Bash