orangain flavor

じっくりコトコト煮込んだみかん2。知らないことを知りたい。

「Pythonクローリング&スクレイピング」という本を書きました

2016-12-23更新: 電子書籍書籍版の情報を更新しました。電子書籍版も好評発売中です!

Pythonを使ってクローリング・スクレイピングを行い、データを収集・活用する方法を解説した書籍です。 Pythonの基本から、サードパーティライブラリを使ったスクレイピング、様々なサイトからのデータ収集・活用、フレームワークScrapyの使い方、クローラーの運用までを扱っています。

クローリング・スクレイピングPython

Pythonは言語自体の書きやすさ、ライブラリが充実していること、データ解析との親和性が高いことなどから、クローリング・スクレイピングに向いている言語です。 Pythonの初歩から解説しているので、何らかの言語の経験があればPythonの経験がない方でも読み進められる内容になっています。

また、クローリング・スクレイピングフレームワークScrapyの存在も大きいです。 本書ではScrapyに1つの章を割いて解説しています。日本語でScrapyの使い方をここまで詳しく解説した書籍は初めてではないかと思います。

ちなみに全編Python 3による解説です。Python 3.4で追加されたasyncioによる非同期I/Oの説明もあります。

章の構成

第1章「クローリング・スクレイピングとは何か」では、まずPythonを使わずにUnixコマンドだけで簡単にクローリング・スクレイピングを行います。 Unixコマンドを使うと、Pythonスクリプトを書くほどでないデータ処理が簡単にできるので知っておいて損はないと思います。

第2章「Pythonではじめるクローリング・スクレイピングでは、Pythonの標準ライブラリのみでクローリング・スクレイピングを行います。Pythonを使うと柔軟にスクレイピングできることを体感します。

第3章「強力なライブラリの活用」では、便利なサードパーティライブラリを使ってクローリング・スクレイピングを行います。 Requests、Beautiful Soup、lxmlなどの定番ライブラリを使うことで、標準ライブラリだけでは難しい処理が簡単にできるようになります。

第4章「実用のためのメソッド」では、実際のWebサイトを対象としてクローリング・スクレイピングを行う際に相手のWebサイトに迷惑をかけないための注意点を解説します。

第5章「クローリング・スクレイピングの実践とデータの活用」では実際に様々なWebサイトからデータを収集・活用します。 第5章は1番長く、書籍全体の1/4を占めています。 データセットAPIを使ったデータ収集、CSV/Excelファイルからの時系列データの取得、オープンデータの収集、Webページへのログイン、JavaScriptを使ったページへの対応などのトピックがあります。 データ活用の面では、日本語の自然言語処理、グラフによる可視化、RSSフィードの生成、地図による可視化、BigQueryによる解析などを扱っています。

第6章「フレームワーク Scrapy」では、強力なクローリング・スクレイピングフレームワークのScrapyを使って、効率のよいクローラーを簡単に作成します。 第6章も第5章に次ぐ長さで、ScrapyによるSpiderの作成方法からScrapyの設定やMiddlewareによる拡張方法まで幅広く解説しています。 Elasticsearchを使った検索サイトの作成やOpenCVによる顔画像の抽出などのデータ活用も解説します。

第7章「クローラーの継続的な運用・管理」では、クローラーを継続的に運用していくためのノウハウを解説します。 クラウド上でのサーバーでの運用や、メッセージキューを使ったクローリングとスクレイピングの分離などのトピックがあります。 マルチスレッド・マルチプロセス・非同期I/Oを活用したクローリングの高速化についても紹介します。

出版にあたって

最初にお話を頂いた時はちょっと驚きましたが、書いていたらクローリング・スクレイピングの話は意外と自分に合っていると感じました。 面倒な作業を自動化したり、使いづらいUIを再構成したりするために、クローラーを作成することがよくあったからです。

最初は数百ページも書けるか不安もありましたが、実際に書いてみると書きたいことが多くあり、ページ数を減らすのに苦労したなどいくつかの事情で丸2年もかかってしまいました。ただ、遅くなったお陰でScrapyやその他Python 2のみ対応だったライブラリがPython 3に対応して、わかりやすくなった面もあります。

クローリング・スクレイピングは泥臭い作業ですが、アイデア次第でいろいろな応用が考えられ、プログラミングの楽しさを手軽に味わえる分野だと思います。 ご興味ありましたらお買い求めいただけると幸いです。

目次

第1章 クローリング・スクレイピングとは何か
    1.1.  本書が取り扱う領域
        1.1.1.  クローリングとスクレイピング
        1.1.2.  クローリング・スクレイピングとPython
        1.1.3.  本書が対象とするプラットフォーム
        1.1.4.  本書の構成
    1.2.  Wgetによるクローリング
        1.2.1.  Wgetとは
        1.2.2.  Wgetの使い方
        1.2.3.  実際のサイトのクローリング
    1.3.  Unixコマンドによるスクレイピング
        1.3.1.  Unixコマンドの基礎知識
        1.3.2.  テキスト処理に役立つUnixコマンド
        1.3.3.  正規表現
    1.4.  gihyo.jpのスクレイピング
        1.4.1.  電子書籍の総数を取得する
        [column]   正規表現における欲張り型のマッチ
        1.4.2.  書籍名の一覧を取得する
    1.5.  まとめ
第2章 Pythonではじめるクローリング・スクレイピング
    2.1.  Pythonを使うメリット
        2.1.1.  言語自体の特性
        2.1.2.  強力なサードパーティライブラリの存在
        2.1.3.  スクレイピング後の処理との親和性
    2.2.  Pythonのインストールと実行
        2.2.1.  Python 2とPython 3
        2.2.2.  パッケージマネージャーによるPython 3のインストール
        2.2.3.  仮想環境(venv)の使用
        2.2.4.  インタラクティブシェルの使用
    2.3.  Pythonの基礎知識
        2.3.1.  スクリプトファイルの実行と構成
        2.3.2.  基本的なデータ構造
        2.3.3.  制御構造と関数・クラス定義
        2.3.4.  組み込み関数
        2.3.5.  モジュール
    2.4.  Webページを取得する
        2.4.1.  urllibによるWebページの取得
        2.4.2.  文字コードの扱い
    2.5.  Webページからデータを抜き出す
        2.5.1.  正規表現によるスクレイピング
        [column]   search() と match()
        2.5.2.  XML(RSS)のスクレイピング
    2.6.  データを保存する
        2.6.1.  CSV形式での保存
        2.6.2.  JSON形式での保存
        2.6.3.  データベース(SQLite 3)への保存
    2.7.  Pythonによるスクレイピングの流れ
    2.8.  まとめ
第3章 強力なライブラリの活用
    3.1.  ライブラリのインストール
        3.1.1.  pipによるインストール
    3.2.  Webページを簡単に取得する
    3.3.  HTMLのスクレイピング
        3.3.1.  XPathとCSSセレクター
        3.3.2.  lxmlによるスクレイピング
        3.3.3.  Beautiful Soupによるスクレイピング
        [column]   pyqueryによるスクレイピング
    3.4.  RSSのスクレイピング
    3.5.  データベースに保存する
        3.5.1.  MySQLへのデータの保存
        [column]   Python Database API 2.0
        3.5.2.  MongoDBへのデータの保存
    3.6.  クローラーとURL
        3.6.1.  URLの基礎知識
        3.6.2.  パーマリンクとリンク構造のパターン
        3.6.3.  再実行を考慮したデータの設計
    3.7.  Pythonによるクローラーの作成
        3.7.1.  一覧ページからパーマリンク一覧を抜き出す
        3.7.2.  詳細ページからスクレイピングする
        3.7.3.  詳細ページをクロールする
        3.7.4.  スクレイピングしたデータを保存する
    3.8.  まとめ
第4章 実用のためのメソッド
    4.1.  クローラーの分類
        4.1.1.  状態を持つかどうかによる分類
        4.1.2.  JavaScriptを解釈するかどうかによる分類
        4.1.3.  不特定多数のサイトを対象とするかどうかによる分類
    4.2.  クローラー作成にあたっての注意
        4.2.1.  著作権と利用規約
        4.2.2.  robots.txtによるクローラーへの指示
        4.2.3.  XMLサイトマップ
        4.2.4.  クロール先の負荷
        4.2.5.  連絡先の明示
        4.2.6.  ステータスコードとエラー処理
    4.3.  繰り返しの実行を前提とした設計
        4.3.1.  更新されたデータだけを取得する
        [column]   プロキシサーバーでのキャッシュ
    4.4.  クロール先の変化に対応する
        4.4.1.  変化を検知する
        4.4.2.  変化を通知する
    4.5.  まとめ
第5章 クローリング・スクレイピングの実践とデータの活用
    5.1.  データセットの取得と活用
        5.1.1.  Wikipediaのデータセットのダウンロード
        5.1.2.  自然言語処理技術を用いた頻出単語の抽出
    5.2.  APIによるデータの収集と活用
        5.2.1.  Twitterからのデータの収集
        5.2.2.  Amazonの商品情報の収集
        5.2.3.  YouTubeからの動画情報の収集
    5.3.  時系列データの収集と活用
        5.3.1.  為替などの時系列データの収集
        5.3.2.  CSV/Excelファイルの読み込み
        5.3.3.  グラフによる可視化
        [column]   pandasとmatplotlib
        [column]   科学技術計算やデータ分析のための便利なツール: IPython・Jupyter・Anaconda
    5.4.  オープンデータの収集と活用
        5.4.1.  オープンデータとは
        5.4.2.  PDFからのデータの抽出
        5.4.3.  Linked Open Dataからのデータの収集
        [column]   オープンデータとシビックテック
    5.5.  Webページの自動操作
        5.5.1.  自動操作の実現方法
        5.5.2.  Amazon.co.jpの注文履歴を取得する
    5.6.  JavaScriptを使ったページのスクレイピング
        5.6.1.  JavaScriptを使ったページヘの対応方法
        5.6.2.  noteのおすすめコンテンツを取得する
        5.6.3.  RSSフィードを生成する
    5.7.  取得したデータの活用
        5.7.1.  地図による可視化
        [column] JSONに対してクエリを実行するjqコマンド
        5.7.2.  BigQueryによる解析
    5.8.  まとめ
第6章 フレームワーク Scrapy
    6.1.  Scrapyの概要
        6.1.1.  Scrapyのインストール
        6.1.2.  Spiderの実行
    6.2.  Spiderの作成と実行
        6.2.1.  Scrapyプロジェクトの開始
        6.2.2.  Itemの作成
        6.2.3.  Spiderの作成
        6.2.4.  Scrapy Shellによるインタラクティブなスクレイピング
        [column]   ScrapyのスクレイピングAPIの特徴
        6.2.5.  作成したSpiderの実行
        [column]   FTPサーバーやAmazon S3などにデータを保存する
    6.3.  実践的なクローリング
        6.3.1.  クローリングでリンクをたどる
        6.3.2.  XMLサイトマップを使ったクローリング
    6.4.  抜き出したデータの処理
        6.4.1.  Item Pipelineの概要
        6.4.2.  データの検証
        6.4.3.  MongoDBへのデータの保存
        6.4.4.  MySQLへのデータの保存
    6.5.  Scrapyの設定
        6.5.1.  設定の方法
        6.5.2.  クロール先に迷惑をかけないための設定項目
        6.5.3.  並行処理に関する設定項目
        6.5.4.  HTTPリクエストに関する設定項目
        6.5.5.  HTTPキャッシュの設定項目
        6.5.6.  エラー処理に関する設定
        6.5.7.  プロキシを使用する
    6.6.  Scrapyの拡張
        6.6.1.  ダウンロード処理を拡張する
        6.6.2.  Spiderの挙動を拡張する
        [column]   ScrapyでJavaScriptを使ったページに対応する:Splash
    6.7.  クローリングによるデータの収集と活用
        6.7.1.  レストラン情報の収集
        6.7.2.  不特定多数のWebサイトのクローリング
        6.7.3.  Elasticsearchによる全文検索
    6.8.  画像の収集と活用
        6.8.1.  Flickrからの画像の収集
        6.8.2.  OpenCVによる顔画像の抽出
        [column]   UbuntuでのOpenCV 3のビルド
    6.9.  まとめ
第7章 クローラーの継続的な運用・管理
    7.1.  クローラーをサーバーで動かす
        7.1.1.  仮想サーバーの立ち上げ
        [column]   Windowsから公開鍵認証を使ってSSH接続する
        [column]   AWS利用におけるセキュリティの注意点
        [column]   Windowsでサーバーにファイルを転送する
        7.1.2.  サーバーへのデプロイ
    7.2.  クローラーの定期的な実行
        7.2.1.  Cronの設定
        7.2.2.  エラーの通知
    7.3.  クローリングとスクレイピングの分離
        7.3.1.  メッセージキューRQの使い方
        [column]   Redisのデータの永続化に関する設定
        7.3.2.  メッセージキューによる連携
        [column]   ScrapyからRQにジョブを投入する
        7.3.3.  メッセージキューの運用
    7.4.  クローリングの高速化・非同期化
        7.4.1.  マルチスレッド化・マルチプロセス化
        7.4.2.  非同期I/Oを使った効率的なクローリング
        [column]   Python 3.4でasyncioを使う
        [column]   複数のマシンによる分散クローリング
    7.5.  クラウドを活用する
        7.5.1.  クラウドを使うメリット
        7.5.2.  AWSのSDKを使う
        7.5.3.  クラウドストレージを使う
    7.6.  まとめ
        [column]   外部サービスを活用したスクレイピング
Appendix Vagrantによる開発環境の構築
    A.1.  VirtualBoxとVagrant
        A.1.1.  VirtualBoxとは
        A.1.2.  Vagrantとは
    A.2.  CPUの仮想化支援機能を有効にする
        A.2.1.  Windows 10の場合
        A.2.2.  Windows 7の場合
        A.2.3.  ファームウェアの設定で仮想化支援機能を有効にする
    A.3.  VirtualBoxのインストール
    A.4.  Vagrantのインストール
    A.5.  仮想マシンを起動する
    A.6.  ゲストOSにSSH接続する
        A.6.1.  Tera Termのインストール
        A.6.2.  Tera TermでゲストOSにSSH接続する
    A.7.  Linuxの基本操作
        A.7.1.  ソフトウェアをインストールする
    A.8.  Vagrantで仮想マシンを操作するコマンド
        A.8.1.  仮想マシンを起動する(vagrant up)
        A.8.2.  仮想マシンを終了・再起動する(vagrant halt/reload)
        A.8.3.  仮想マシンを削除する(vagrant destroy)
        A.8.4.  仮想マシンの状態を表示する(vagrant status)
        A.8.5.  仮想マシンにSSH接続する(vagrant ssh)
        A.8.6.  仮想マシンをエクスポートする(vagrant package)

電子書籍版の発売日は未確定ですが、電子書籍版も発売予定です。 電子書籍版もPDF/EPUB版Kindle版共に紙版と同時発売です。

関連

orangain.hatenablog.com

画像内の秘密情報をOCRでマスクするコマンドmasecretを作った

2017-02-05 更新 v0.2.0へのバージョンアップに伴って、使い方を更新しました。

スクリーンショットを撮影していると、しばしばマスクしておきたい値が存在します。数枚ならば手作業で塗りつぶしてもいいですが、手順書を作成するようなときは枚数が多くなって面倒です。

いつも同じ場所に表示されるのであれば、機械的に塗りつぶすのも簡単ですが、画像によって異なることもあります。そこでOCRを使ってスクリーンショット内の秘密情報をマスクするコマンドmasecretを作りました。

OCRなので思い通りに塗りつぶされないことも多々ありますが、限定された局面ではそれなりに便利なので公開します。

masecret

秘密情報の正規表現を書いたファイルSECRETS.txtと画像ファイルを与えると、秘密情報をマスクした画像を保存するコマンドです。

github.com

必要なもの

  • Python 3.3+
  • Tesseract
    • OCRに使用する言語が使える必要があります。Homebrewならばbrew install tesseract --all-languagesでインストールすれば全言語が使えます。

インストール

以下のコマンドでインストールできます。環境によってはsudoが必要かもしれません。

$ pip3 install masecret

使い方

SECRETS.txtをカレントディレクトリに作成しておきます。このファイルは各行に秘密情報の正規表現を書きます。秘密情報をそのまま書くとこのファイルが流出した時に悲しいことになるので、それっぽい正規表現を書きます。

例えばAWSのアカウントナンバーを塗りつぶそうとすると、以下のようになります。日本語だとハイフンがダッシュと認識されることがあったので、両方のパターンをカバーできるようにしています。

[-—\d]{12,}

以下のように実行すると、original-ja.png内で上記の正規表現にマッチした箇所が灰色に塗りつぶされたmasked-ja.pngが生成されます。--langオプションで言語を指定します。+で繋げることで複数書けます。

$ masecret --lang eng+jpn original-ja.png -o masked-ja.png
Processing original-ja.png...
Found 1 secrets at [((1500, 235), (1705, 259))]
Saved to masked-ja.png

-oオプションにディレクトリを指定すれば、複数のファイルを処理対象に指定することも可能です。

v0.2.0からはSECRETS.txtを作らずに、-rオプションで直接正規表現を指定できます。

実行結果

以下のように正規表現にマッチする箇所がマスクされます。

Before

f:id:mi_kattun:20160619195025p:plain

After

f:id:mi_kattun:20160619195047p:plain

内部実装

OSSOCRソフトの中でも有名なTesseractPythonから使用しました。TesseractをPythonから使うためのライブラリとしてPyOCRを使いました。PythonからTesseractを使う簡単なサンプルになっていると思いますので、興味があればソースコードをご覧ください。

本当はこういうCLIのコマンドはGo言語で書いたほうが簡単に実行できて良いのですが、思いついたものをさくっと作るだけのGo言語力がなかったので、Pythonで書きました。そのうちGo言語で書き直すかもしれません。

まとめ

OCRソフトTesseractを使って、画像内の秘密情報を塗りつぶすコマンドmasecretを作りました。良ければ使ってみてください。画像によっては正しく塗りつぶされないこともあるので、必ず正しく塗りつぶされたことをご確認ください。

Python 3をサポートしたScrapy 1.1が公開されました

2016-12-09追記

Pythonクローリング&スクレイピング」という本を書きました!Scrapyについても1つの章を割いて解説しています。

PythonスクレイピングフレームワークであるScrapyの新バージョン1.1がついに公開され*1Python 2.7に加えてPython 3.3以降がサポートされました🎉 。

Scrapy 1.1におけるPython 3のサポートは以下の制限があり、ベータサポートとされていますが、使えないのはどれもマイナーな機能なのでそれほど問題ないでしょう*2

  • Windows上のPython 3ではテストされていない。
  • メールの送信はサポートされていない。
  • FTPによるダウンロードはサポートされていない。
  • Telnetコンソールはサポートされていない。

実際、手元のいくつかのSpiderで試した限りでは、Python 3で問題なく使用できています。

Scrapy 1.1の変更点

Scrapy 1.1の変更点は多くありますが、基本的な機能は変わっていません。Python 2.7+Scrapy 1.0で動いていたSpiderやプロジェクトは、それほど変更することなくPython 3+Scrapy 1.1で動くでしょう。

個人的に興味深い変更点としては以下の点が挙げられます。

  • プロジェクト新規作成時にROBOTSTXT_OBEYの設定が有効になるようになった。
  • Python 3におけるstr型*3のレスポンスボディが、response.textで取得できるようになった*4

変更点について詳しくはリリースノートを参照してください。

Release notes — Scrapy 1.1.0 documentation

Scrapy 1.1のインストール

Python 3.5の環境でインストールしてみましょう。

$ python3.5 -m venv venv
$ . venv/bin/activate
(venv) $ python -V
Python 3.5.1

以下のようにpipでインストールできます。

(venv) $ pip install scrapy

Scrapy 1.1.0がインストールできていることがわかります。なおTwistedのバージョンは15.5以上が必要です。

(venv) $ pip freeze
attrs==15.2.0
cffi==1.6.0
cryptography==1.3.2
cssselect==0.9.1
idna==2.1
lxml==3.6.0
parsel==1.0.2
pyasn1==0.1.9
pyasn1-modules==0.0.8
pycparser==2.14
PyDispatcher==2.0.5
pyOpenSSL==16.0.0
queuelib==1.4.2
Scrapy==1.1.0
service-identity==16.0.0
six==1.10.0
Twisted==16.1.1
w3lib==1.14.2
zope.interface==4.1.3

Python 3でScrapy shellを使う

さてScrapyがPython 3で動くようになって嬉しいのは、単にPython 2.7を使わずに済むようになることだけではありません。日本語などASCII以外のWebページをスクレイピングする時には、これまでUnicodeエスケープされていた文字列がそのまま表示されるようになるので、非常に便利です。

ScrapyにはScrapy shellという機能があり、インタラクティブXPathCSSセレクタで要素の取得を試せます。 Scrapy shellでPython 3のメリットを感じてみましょう。

近頃は良い気候でどこかに出かけたい気分なので、じゃらんの観光ガイドのページから、私の住んでいる兵庫県の観光スポットをスクレイピングしてみます。 兵庫県の「5月にオススメランキング」の第1位は姫路城です。

www.jalan.net

Scrapy shellは以下のようにscrapy shellコマンドの引数にURLを指定して起動します。

$ scrapy shell http://www.jalan.net/kankou/spt_28201af2120008952/

起動すると指定したURLが取得され、以下のように入力待ちの状態になります。 Scrapy shell内で使用できる変数が表示されています。

[s] Available Scrapy objects:
[s]   crawler    <scrapy.crawler.Crawler object at 0x107903ef0>
[s]   item       {}
[s]   request    <GET http://www.jalan.net/kankou/spt_28201af2120008952/>
[s]   response   <200 http://www.jalan.net/kankou/spt_28201af2120008952/>
[s]   settings   <scrapy.settings.Settings object at 0x10a851d68>
[s]   spider     <DefaultSpider 'default' at 0x10ac28128>
[s] Useful shortcuts:
[s]   shelp()           Shell help (print this help)
[s]   fetch(req_or_url) Fetch request (or URL) and update local objects
[s]   view(response)    View response in a browser
>>>

以下のようにして、CSSセレクタXPathで要素を取得できます。なお、これらのCSSセレクタXPathはブラウザの開発者ツールを見ながら考えます。

# スポット名
>>> response.css('h1.detailTitle::text').extract_first()
'姫路城'
# スコア
>>> response.css('.reviewPoint::text').extract_first()
'4.3'
# 住所
>>> response.xpath('//th[text()="所在地"]/following-sibling::td/text()').extract_first().strip()
'〒670-0012\u3000\n\t\t\t\t\t\t\t\t\t兵庫県姫路市本町68'

Python 2.7で実行すると以下のようになるので、違いは明白です。Python 2.7でもprint文を使えば日本語をそのまま表示できますが、やや面倒です。ぜひPython 3を使いましょう。

# スポット名
>>> response.css('h1.detailTitle::text').extract_first()
u'\u59eb\u8def\u57ce'
# スコア
>>> response.css('.reviewPoint::text').extract_first()
u'4.3'
# 住所
>>> response.xpath(u'//th[text()="所在地"]/following-sibling::td/text()').extract_first().strip()
u'\u3012670-0012\u3000\n\t\t\t\t\t\t\t\t\t\u5175\u5eab\u770c\u59eb\u8def\u5e02\u672c\u753a68'

他にもPython 2.7ではSpider実行時のログに日本語が含まれていてもUnicodeエスケープされていましたが、Python 3ではそのまま表示されるので問題に気付きやすくなりました。Scrapyでログに出力されるItem内のUnicode文字列を読めるようにする - Qiita のような涙ぐましい努力はPython 3ではもう不要です。

Python 3サポートの軌跡

以下のIssueが立てられたのが2013年3月なので、実に3年以上の時を経てPython 3サポートが実現したことになります。

github.com

私はここ1年半ほどウォッチしてきただけですが、以下の記事でその苦労の一端を窺うことができます。興味があれば読んでみてください。

blog.scrapinghub.com

まとめ

Scrapy 1.1とPython 3で楽しくクローリング・スクレイピングを行っていきましょう。

*1:例によってこの記事の執筆時点で公式サイトの表記は1.0のままですが、PyPI上には1.1.0が公開されています。

*2:Windowsユーザーの方はごめんなさい。

*3:Python 2におけるunicode型。

*4:従来のresponse.body_as_unicode()も使えます。

Re:VIEWのDash用Docsetを作った

Re:VIEWはドキュメント作成のためのツールで、DashOS Xでドキュメントを閲覧するためのビューアーです。 タイトルの通り、Re:VIEWのFormat Guideを閲覧できるDash用Docsetを作りました。

f:id:mi_kattun:20160117121429p:plain

使い方

DashのPreferences > Downloads > User Contributedから検索してDocsetをダウンロードできます。

f:id:mi_kattun:20160117121439p:plain

ダウンロードできたらコマンドの名前などで検索するだけです。

f:id:mi_kattun:20160117121652p:plain

作り方

Re:VIEWのFormat GuideをHTMLに変換したものから<h2>タグと<code>タグを抜き出して生成しています。詳しくはDocsetのリポジトリRakefileを参照してください。

機械的に抽出しただけですが、Format Guideが一定のルールに従って書かれているので割りといい感じにコマンドを網羅できているように思います。

ちなみにUser ContributedからダウンロードできるDocsetは英語のFormat Guideから作成したものですが、RakefileMARKDOWN_URLの末尾をformat.mdからformat.ja.mdに変更すると日本語のDocsetも作れると思います。

所感

  • Docsetの作成自体は簡単だが、TOC (Table of Contents) を表示させためにHTMLにアンカーを埋め込まないといけない (=HTMLを変更しないといけない) のはちょっと面倒*1
  • User ContributedなDocsetのマージは速い。
  • Re:VIEWのアイコンが欲しい。

参考

*1:公式のMarkdownのDocsetの中身を見ると、index.htmlにアンカーは埋め込まれておらず、index.html.dashtocというファイルにJSON形式でTOCが格納されていました。書式を真似して*.dashtocファイルを置いてみましたが、TOCは表示されませんでした。

2015年を振り返って

2015年を振り返って

今年はブログなどでのアウトプットが少なかったですが、一応活動していました。

昨年のMBSハッカソンで優勝したことで、ITを活用した30分のテレビ番組の企画に参加して、その中で使うアプリの開発を行いました。テレビ番組の撮影の裏側も見ることができ、レアな経験ができて面白かったです。

Code for Kobeというコミュニティに参加させてもらい、お世話になりました。それまでコミュニティに参加するという経験がほとんどなかったので、月に一回行けば人がいる帰る場所があるというのはいいものだと感じています。

また、子供が産まれました。自分の子供はやはりとても可愛いもので日々楽しく過ごしています。

2016年に向けて

2015年にずっとやってきて未だに公開できてないものを早く公開できればと思います。 今年はあまりコードを書く時間がなくて、いろいろ作りたいものだけは増えているので、それも作っていきたいです。

Code for Kobeでは自分としてのアウトプットは全然出せていないので、力を入れていかないといけないと思っています。

子供が産まれたことで自由に使える時間が減っているので、その中でアウトプットを出せるよう時間の使い方もうまくなりたいです。

来年もよろしくお願い致します。

PythonでブログのHTMLから本文抽出 2015

2015-12-20 19:14追記: readabilityの説明を追加・修正しました。

Webページをクロールした時に、ざっくりと本文 (ページ内の重要なコンテンツ) のみを抽出できると便利です。 Google検索すると、特に日本語だとExtractContent以外の情報があまり見つかりません。 ExtractContentは昔使ったことがあり、たしかに便利なのですが、公開が2007年と若干古いので今でも使えるのかという疑問がありました。また、Pythonで他の選択肢として使えるライブラリは、非日本語圏の方が作ったものと思われるので、日本語のページで問題なく使えるのか知りたかったので調べてみました。

比較するライブラリ

比較したのは以下の5つのライブラリです。

dragnet eatiht extractcontent goose readability
パッケージ名 dragnet eatiht extractcontent (GitHubのみ) goose-extractor readability-lxml
最新版 1.0.1 0.1.14 0.0.1 1.0.25 0.6.1
最新版公開日 2015-01-29 2015-03-28 2011-02-25 2015-01-03 2015-08-26
Python 3対応 × ◯ (GitHubのもの) × ×
ライセンス MIT License MIT License 不明 Apache License 2.0 Apache License 2.0
備考 Moz による研究成果。 rodrigo palacios氏 による研究成果。 Rubyextractcontent.rbPythonに移植したもの。 ScalaGoosePythonに移植・改良したもの。 BookmarkletReadabilityPythonに移植・改良したもの。

アルゴリズムはあまり詳しく把握していませんが、extractcontentのみ正規表現ベースで、その他はHTMLの木構造を見ているようです。 extractcontentのみが日本語圏で開発され、その他は非日本語圏で開発されたものと思われます。

検証するサンプル

適当に思いついた日本語と英語のブログサービスから、サンプルとなるページをピックアップしました。サンプルの選定は適当ですが、画像のみのページでは意味がないので、本文が存在するものを選んでいます。本文の長さはいろいろです。

ブログサービス サンプルページ
(ja) はてなブログ 澤なんて大したことない。 - Yukibou's Hideout on Hatena
http://potatostudio.hatenablog.com/entry/2015/12/17/073000
(ja) アメブロ 子育てらくがき20151126「磁石」|絵かき屋picoの「一期一絵」
http://ameblo.jp/pico-art/entry-12099702897.html
(ja) ライブドアブログ 湯布院にあるハリーポッターの世界!フローラルビレッジ : 九州ドライブ 面白スポット ナビ
http://drive-kyuusyuu.blog.jp/floralvillage.html
(ja) WordPress.com ipython_config.pyの設定メモ « ひよっこ。
https://prepro.wordpress.com/2012/01/05/ipython_config-py%E3%81%AE%E8%A8%AD%E5%AE%9A%E3%83%A1%E3%83%A2/
(ja) ココログ 勤勉にサービスしすぎるから生産性が低いのだよ!日本人は: hamachanブログ(EU労働法政策雑記帳)
http://eulabourlaw.cocolog-nifty.com/blog/2015/12/post-ce16.html
(ja) Seesaaブログ やっぱBABYMETALのスタッフは俺たちのサイトをチェックしてるんじゃね? 【海外の反応】: BABYMETALIZE
http://niyaniyakaigai.seesaa.net/article/431409315.html
(en) Medium My journey into the Berlin startup scene — Wandering CTO — Medium
https://medium.com/wandering-cto/my-journey-into-the-berlin-startup-scene-4dc8faecd305
(en) オリジナル (github.com/blog) How the Services team uses GitHub · GitHub
https://github.com/blog/2093-how-the-services-team-uses-github
(en) Blogger Official Google Blog: ICYMI: A few stocking stuffers from around Google
https://googleblog.blogspot.jp/2015/12/icymi-few-stocking-stuffers-from-around.html
(en) Tumblr Marissa's Tumblr
http://marissamayr.tumblr.com/day/2015/12/10
(en) Typepad Third Party Services: the Wrap Up - Everything Typepad
http://everything.typepad.com/blog/2015/12/third-party-services-the-wrap-up.html

検証スクリプト

実行環境はPython 2.7.11です。抽出結果は目視で確認します。

検証結果

以下の表で、◯は正常に本文を取得できたもの、△は一部のみ取得できたもの、×は空白や関係ないコンテンツが返ってくるなど正常に取得できたとは言えないものを表します。

dragnet eatiht extractcontent goose readability
(ja) はてなブログ × (エラー) × ×
(ja) アメブロ × × (エラー) ×
(ja) ライブドアブログ × (エラー) ×
(ja) WordPress.com △ (ソースコード部分のみ) ×
(ja) ココログ △ (一部のみ) × (エラー) ×
(ja) Seesaaブログ × (エラー) × (エラー)
(en) Medium
(en) オリジナル (github.com/blog)
(en) Blogger × × × ×
(en) Blogger + Splash ×
(en) Tumblr × (エラー)
(en) Typepad
備考 改行は消える。 明らかに処理速度が遅い。筆者は違いを理解していないがv2とetv2という別の手法も使える。 言語の情報を使用している。画像や映像を抜き出す機能もある。 Bookmarkletという出自のせいか、プレーンテキストではなくHTMLの結果が得られる。

Bloggerは本文が<script type='text/template'>タグ内に格納されており、JavaScriptレンダリングされているという特殊性からか、eatiht以外の全ライブラリで本文を抽出できませんでした。このため、先日の記事で紹介したSplashを使ってレンダリングしたものからの抽出も試しました。

Seesaaブログは唯一エンコーディングShift_JISで、HTMLの構造がおかしいのかdragnetとgooseで他では起きないエラーが起きました。

処理速度について

およそ (速) extractcontent, dragnet > readability, goose >>>>> eatiht (遅) という傾向でした。

考察

dragnet

英語のページでは問題ありませんが、日本語のページでは一部エラーが出ました。 改行が失われるので若干使いづらそうです。

元々以下の記事を見て比較を始めたのですが、英語のページでは精度が高いようです。

eatiht

唯一Bloggerから正確に本文を抽出できましたが、日本語のページの多くではエラーが出て使用できませんでした。 処理速度は他のライブラリに比べて10〜50倍程度遅かったです。

今回はeatihtという一番古くからあると思われるモジュールを使いましたが、v2とetv2という別のモジュールも使用できます。特にetv2を使うと日本語のページからもエラーなく本文を取得できましたが、Bloggerからは本文を取得できなくなりました。また、処理速度が遅いのも変わりません。

extractcontent

正規表現ベースのシンプルな手法にも関わらず、はてなブログ以外は正しく抽出でき、精度は高かったです。 2011年からメンテナンスされていないので大丈夫かと思っていましたが、2.7で実行時にエラーが発生することはありませんでした。

はてなブログから正常に取得できないのは不可解なので調べてみると、Google Adsense用のコメントによって本文を抽出するための正規表現が間違っているのが原因のようです。 forkされた以下のものを使うと正常に取得できますが、Google検索しても見つからないので難しい気がします。

https://github.com/petitviolet/python-extractcontent

goose

基本的にエラーにはならないものの、日本語のページからはまったく本文を抽出できませんでした。 (試していませんが) 言語の設定があるので、予めWebページの言語がわかっている場合には指定してやることでうまく取得できるのかもしれません。 SeesaaブログはHTMLが壊れているためか、再帰呼出しの回数が上限を超えてRuntimeErrorになりました。

readability

Blogger以外からは正しく本文を抽出できました。 Python 3にも対応しており、使いやすそうです。 細かい点ですが、本文として取得した文字列の前後に空白が残っており、自分でstripする必要がありそうです。 Bookmarkletという出自のせいか、取得できるのはプレーンテキストではなくHTMLです。 プレーンテキストが欲しい場合には自分でタグを削除する必要があります。

おまけ: Webstemmerを使ってBloggerから本文を抽出する

これまで紹介したような、ルールに従って単一のページから本文を取得する手法のほかに、似た構造の複数のページを比較することで本文を取得する手法もあります。この手法では、最終更新が2009年とやや古いですが、Webstemmerが有名なようです。

Webstemmerを使って、ルールベースのライブラリが苦手としていたBloggerのHTMLから本文を取得してみます。 ただし、公式ページに公開されているバージョン0.7.1では以下の問題があったので、修正したものを使用しました。

  • bsddbモジュールを使用していてPython 2.7では動かない
  • HTTPSのWebページをクロールできない。

以下のコマンドでインストールできます。

$ pip install https://github.com/orangain/webstemmer/archive/master.tar.gz#egg=webstemmer

以下のように実行できます。 textcrawler.py コマンドに -a オプションをつけずに実行すると、 (今回は不要な) アーカイブページが大量にクロールされるので時間がかかります。

$ textcrawler.py -o googleblog -a 'https://googleblog\.blogspot\.jp/\d+/\d+/[^/]+\.html$' https://googleblog.blogspot.jp/
$ analyze.py googleblog.201512201516.zip > googleblog.pat
$ extract.py googleblog.pat googleblog.201512201516.zip > googleblog.txt

以下のようにいい感じに取得できました。素晴らしいですね。

...
!MATCHED: 201512201516/googleblog.blogspot.jp/2015/12/icymi-few-stocking-stuffers-from-around.html
PATTERN: 201512201516/googleblog.blogspot.jp/2015/12/meet-pixel-c-our-take-on-tablet.html
SUB-0: Official Google Blog: ICYMI: A few stocking stuffers from around Google
TITLE: ICYMI: A few stocking stuffers from around Google
MAIN-5: Between last-minute gift shopping, airport pickups, cookie baking, and ugly-sweater parties, there’s a lot to do this season. So you may have missed a few updates from around Google that can actually make your holiday season a little brighter (or at least make your to-do list go a little faster. Won’t make your sweater any less ugly, though). Here’s a look at what we’ve unwrapped recently:
MAIN-5: Add this one to your to-do list: Reminders in Google Calendar
MAIN-5: Whether it’s “send holiday cards” or “use up FSA,” you can now add Reminders to Google Calendar to help you complete your to-do list. These aren’t like those calendar entries you create yourself that you plain-old ignore completely and that then disappear. With Reminders, if you don’t complete the task and dismiss the Reminder, it’ll pop up on your calendar again the next day. And the next. And the … until you can’t take it anymore and just send those holiday cards already. You’ll thank us when your list is checked off. Twice.
MAIN-5: Now on Tap gets handier for the holidays
MAIN-5: Now on Tap helps you get quick information without leaving the app you're using by tapping and holding the home button on Android phones—and new updates make it even handier for the holidays. So if you get a text with your cousin’s flight number, you can tap and hold to see the flight’s status, then respond without having to juggle between searching and texting. If you ordered a gift online and want to know if it will make it down the chimney and under the tree on time, tap and hold your confirmation email to get tracking info. Consider it your own personal Santa’s Little Helper.
MAIN-5: Tell the family when to expect you with trip bundles
MAIN-5: There’s probably a lot going on in your email right now if you’ve got an upcoming trip home or holiday getaway planned. From your flight confirmation to rental car details, Inbox by Gmail already groups these emails into trip bundles so you can find everything you need for your trip quickly. Those bundles just got even more useful—you can now access them offline (good for on the plane), share the trip summary with friends or family, and add other pertinent emails (like that message with your aunt’s new address) to the bundle.
MAIN-5: Templates in Google Docs go mobile
MAIN-5: If you’re collecting family recipes or planning a trip, templates in Docs, Sheets and Slides help you get started faster, so you can spend more time concentrating on the words you’re writing and less time worrying about how it looks. These pre-made templates are now available on Android and iOS so you can do more while on the go. Ho, ho, ho!
SUB-6: Posted by Abbi Tatton, Google Editorial Elf
SUB-7: IMAGE URL
SUB-8: Abbi Tatton
SUB-9: Editorial Elf
SUB-10: Google

まとめ

検証した範囲ではおおむね問題なく本文を取得でき、Python 3にも対応しているreadabilityが良さそうです。もちろん検証に使用するサンプルによって結果は変わってくるので、あくまで参考と捉えてください。

extractcontentは正規表現ベースの単純なスクリプトで高速かつ精度よく本文を抽出できます。ただ、Python版はGoogle検索のトップに引っかかるものは不具合があり、メンテナンスされているとも言えないので、人には勧めづらいです。

Webstemmerはアプローチが異なりますが、複数のページをクロールできる場合には精度よく本文を取得できそうです。

参考

JavaScriptレンダリングサーバーSplashでスクレイピング

これはクローラー/Webスクレイピング Advent Calendar 2015の9日目の記事です。

本記事では、Scrapinghub社*1が開発しているSplashというオープンソースソフトウェアを紹介します。

github.com

JavaScriptを使ったページからスクレイピングする方法としては、PhantomJSとSelenium/CasperJSなどの組み合わせが一般的ですが、これらとは少し違う手段として使えるかもしれないソフトウェアです。 私自身Splashを最近知ったばかりで、軽く探した限りでは日本語の情報もないので、調査しつつSplashの使いドコロを探ってみたいと思います。

Splashとは

READMEには以下のように書かれています。

 Splash is a javascript rendering service with an HTTP API. It's a lightweight browser with an HTTP API, implemented in Python using Twisted and QT.

It's fast, lightweight and state-less which makes it easy to distribute.

(訳)SplashはHTTP APIを持つJavaScriptレンダリングサービスです。HTTP APIを持つ軽量ブラウザで、TwistedとQTを使ってPythonで実装されています。 高速、軽量かつステートレスで、容易に分散できます。

ドキュメントにはもう少し詳しく機能が書かれています。

  • process multiple webpages in parallel;
  • get HTML results and/or take screenshots;
  • turn OFF images or use Adblock Plus rules to make rendering faster;
  • execute custom JavaScript in page context;
  • write Lua browsing scripts;
  • develop Splash Lua scripts in Splash-Jupyter Notebooks.
  • get detailed rendering info in HAR format.

(訳)

要するに、以下のように動くということでしょう。なかなか面白そうです。

f:id:mi_kattun:20151209121519p:plain

Splashを使ってみる

とりあえず使ってみましょう。 今どきのソフトウェアらしく、Dockerを使って簡単にサーバーを起動できます。

$ docker pull scrapinghub/splash
$ docker run -p 5023:5023 -p 8050:8050 -p 8051:8051 scrapinghub/splash

ポートの意味はそれぞれ以下のとおりです。

以下のようにログが表示されたら起動完了です。この記事ではSplash 1.8を使います。

2015-12-08 13:20:59+0000 [-] Log opened.
2015-12-08 13:20:59.239310 [-] Splash version: 1.8
2015-12-08 13:20:59.240288 [-] Qt 4.8.1, PyQt 4.9.1, WebKit 534.34, sip 4.13.2, Twisted 15.4.0, Lua 5.2
2015-12-08 13:20:59.242303 [-] Python 2.7.3 (default, Jun 22 2015, 19:33:41) [GCC 4.6.3]
2015-12-08 13:20:59.242396 [-] Open files limit: 1048576
2015-12-08 13:20:59.242472 [-] Can't bump open files limit
2015-12-08 13:20:59.447814 [-] Xvfb is started: ['Xvfb', ':1069', '-screen', '0', '1024x768x24']
2015-12-08 13:20:59.507558 [-] proxy profiles support is enabled, proxy profiles path: /etc/splash/proxy-profiles
2015-12-08 13:20:59.547976 [-] verbosity=1
2015-12-08 13:20:59.548159 [-] slots=50
2015-12-08 13:20:59.548618 [-] Web UI: enabled, Lua: enabled (sandbox: enabled), Proxy Server: enabled
2015-12-08 13:20:59.549491 [-] Site starting on 8050
2015-12-08 13:20:59.549761 [-] Starting factory <twisted.web.server.Site instance at 0x1b87a70>
2015-12-08 13:20:59.551205 [-] SplashProxyServerFactory starting on 8051
2015-12-08 13:20:59.551413 [-] Starting factory <splash.proxy_server.SplashProxyServerFactory instance at 0x1b8a3b0>

起動したら、ブラウザでhttp://<Dockerのホスト>:8050/ にアクセスします。私の環境ではhttp://192.168.59.103:8050/でした。 このURLを開くと、以下のようなデモ用の画面が表示されます。 f:id:mi_kattun:20151209121440p:plain

「Render me!」という緑のボタンをクリックすると、以下のようにGoogleのページがレンダリングされた画像が表示されます。 f:id:mi_kattun:20151209121454p:plain さらに、スクロールするとネットワークに関する情報とHTMLが表示されます。 f:id:mi_kattun:20151209121508p:plain

「Render me!」ボタンを押すと、入力したURLにアクセスし、黒いテキストボックス内のLuaスクリプトが実行される、ということのようです。

SplashのAPIを使う

デモはこれぐらいにしてHTTP APIを使ってみましょう。 /render.html というエンドポイントにurlパラメータを渡すことで、指定したURLのHTMLを取得できます。

$ curl 'http://192.168.59.103:8050/render.html?url=http://google.com'
<!DOCTYPE html><html itemscope="" itemtype="http://schema.org/WebPage" lang="ja"><head><meta content="世界中のあらゆる情報を検索するためのツールを提供しています。さまざまな検索機能を活用して、お探しの情報を見つけてください。" name="description"><meta content="noodp" name="robots"><meta content="text/html; charset=UTF-8" http-equiv="Content-Type">
...

ここで重要なのは、このAPIで取得できるHTMLは、サーバーから取得したHTMLではなく、JavaScriptのonloadイベントが発生した時点でのDOMツリーをHTMLにしたものであるという点です。

このことがわかりやすい例として、PythonでさくっとWebスクレイピングする (JavaScript読み込みにも対応しつつ) - Qiita という記事でサンプルとして使われていた、テレ朝ニュースからスクレイピングしてみます。

news.tv-asahi.co.jp

このページでは、ページ下部の「関連ニュース」のリストがAjaxで読み込まれます。このため、以下のように普通にcurlで取得すると空の<div>要素が存在するだけです。

$ curl http://news.tv-asahi.co.jp/news_international/articles/000064029.html  | iconv -f shift_jis
...
<!-- 関連ニュース -->
            <div id="relatedNews"></div>
...

以下のようにSplashを経由させると、<div>要素の中にコンテンツが含まれた状態のHTMLを取得できます。onloadのタイミングまで待つので少しだけ時間がかかります。

$ curl -v 'http://192.168.59.103:8050/render.html?url=http://news.tv-asahi.co.jp/news_international/articles/000064029.html'
...
<!-- 関連ニュース -->
            <div id="relatedNews">                        <div class="kanrennews">
              <h3>関連ニュース</h3>

              <ul class="newslist clearfix">
                                  <li>
                  <div class="text"><a href="http://news.tv-asahi.co.jp/news_international/articles/000063920.html">「若い皆さん研究頑張って」 物理学賞・梶田さん</a><br><span>(2015/12/07 17:53)</span></div>
                </li>
                                <li>
                  <div class="text"><a href="http://news.tv-asahi.co.jp/news_international/articles/000063849.html">ノーベル賞大村さん現地に 梶田さん燕尾服を採寸</a><br><span>(2015/12/06 11:52)</span></div>
                </li>
                                <li>
                  <div class="text"><a href="http://news.tv-asahi.co.jp/news_international/articles/000063837.html">“ノーベルウィーク”大村さん、梶田さん現地到着</a><br><span>(2015/12/06 05:50)</span></div>
                </li>
                                <li>
                  <div class="text"><a href="http://news.tv-asahi.co.jp/news_international/articles/000063826.html">大村さんも現地到着 いよいよ「ノーベルウィーク」</a><br><span>(2015/12/05 17:31)</span></div>
                </li>
                                <li>
                  <div class="text"><a href="http://news.tv-asahi.co.jp/news_international/articles/000063810.html">ノーベル賞梶田さん到着 これからイベント目白押し</a><br><span>(2015/12/05 11:51)</span></div>
                </li>
                              </ul>

            </div>
            </div>
...

あとはお好きなスクレイピングライブラリでHTMLからスクレイピングすればOKです。

細かい話ですが、このページはShift_JISエンコードされているので、最初の例ではiconvを使って文字コードを変換しています。Splashを通すと、UTF-8エンコードされた状態で取得できるので、iconvは不要です。metaタグは<meta charset="Shift_JIS">のまま取得できるので、ちょっと紛らわしいですね。

render.htmlエンドポイントにはurl以外にも様々なパラメータを渡せます。例えばonloadからさらに指定した時間待つwaitや、ページ内でJavaScriptを実行するjs_sourceなどがあります。

また、他にも以下のようなエンドポイントがあります。

詳しくはAPIドキュメントを参照してください。

Splash HTTP API — Splash 1.8 documentation

Splashの使いドコロ

推測ですが、Splash開発のモチベーションは以下のようなものでしょう。

  1. JavaScriptを使ったページが増えており、JavaScriptへの対応は不可欠。
  2. しかしあらゆるページでPhantomJSを使うと遅くなってしまう。
  3. かといって、JavaScriptを使わないページはlxmlで、使うページはSelenium+PhantomJSなどと使い分けると、APIが違って面倒。
  4. Splashを使って、JavaScriptを実行したあとのHTMLをlxmlなどでスクレイピングすれば、JavaScriptに対応しつつAPIの一貫性を維持できる。

このため、lxmlなどを使って昔ながらのスクレイピングをしている人が、あまりコードを変更せずにJavaScriptに対応したいという目的で使えるのではないでしょうか。

一方でステートレス性を重視しているので、JavaScriptで対話的な操作をしてスクレイピングするという用途には向いていないように思います。サーバーでLuaスクリプトを実行できますが、それであれば正直SeleniumとかCasperJSとかでやっても同じように思います。

また、サーバーを立てる必要があるので、使い捨てのスクリプトにはあまり向かないでしょう。Scrapinghub社はクローラーSaaSを提供する会社であり、Splashもサービスの一つとして提供されています。このため、スケーラビリティが重視されており、ある程度大規模に使用しないと他の手段に比べてメリットを感じにくいかもしれません。

まとめ

SplashはJavaScriptレンダリングサーバーで、JavaScriptを使ったページからスクレイピングするための手段の一つとして使える可能性を持ったソフトウェアです。

ここで紹介したのは一番基本的な機能だけで、ドキュメントを読んでいると冒頭で紹介した機能以外にもさまざまな機能があります。 例えば、HTTPプロキシとして振る舞うことができたり、Scrapyと連携したりもできるなど、色々と面白い使い方ができそうです。

良ければ使ってみてください。

参考

*1:Scrapinghub社は、Pythonの有名なスクレイピングフレームワークScrapyの開発者がやっている会社です。