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氏 による研究成果。 | Rubyの extractcontent.rb をPythonに移植したもの。 | Scalaの Goose をPythonに移植・改良したもの。 | Bookmarkletの Readability をPythonに移植・改良したもの。 |
アルゴリズムはあまり詳しく把握していませんが、extractcontentのみ正規表現ベースで、その他はHTMLの木構造を見ているようです。 extractcontentのみが日本語圏で開発され、その他は非日本語圏で開発されたものと思われます。
検証するサンプル
適当に思いついた日本語と英語のブログサービスから、サンプルとなるページをピックアップしました。サンプルの選定は適当ですが、画像のみのページでは意味がないので、本文が存在するものを選んでいます。本文の長さはいろいろです。
検証スクリプト
実行環境は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では以下の問題があったので、修正したものを使用しました。
以下のコマンドでインストールできます。
$ 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というオープンソースソフトウェアを紹介します。
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.
(訳)
- 複数のWebページを並列処理できる。
- HTMLやスクリーンショットを取得できる。
- 画像の読み込みを無効化したり、Adblock Plusのルールを使用してレンダリングを高速化できる。
- カスタムJavaScriptをページのコンテキストで実行できる。
- Luaでブラウジングスクリプトを書ける。
- Splash LuaスクリプトをSplash-Jupyter Notebooksで開発できる。
- 詳細なレンダリング情報をHARフォーマットで取得できる。
要するに、以下のように動くということでしょう。なかなか面白そうです。
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を開くと、以下のようなデモ用の画面が表示されます。
「Render me!」という緑のボタンをクリックすると、以下のようにGoogleのページがレンダリングされた画像が表示されます。 さらに、スクロールするとネットワークに関する情報とHTMLが表示されます。
「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 という記事でサンプルとして使われていた、テレ朝ニュースからスクレイピングしてみます。
このページでは、ページ下部の「関連ニュース」のリストが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
などがあります。
また、他にも以下のようなエンドポイントがあります。
- render.png: PNGでスクリーンショットを取得する。
- render.jpeg: JPEGでスクリーンショットを取得する。
- render.har: HARファイルを取得する。
- render.json: 複数の形式を一度に取得する。
- execute: サーバーでLuaスクリプトを実行してもっと複雑な処理を行う。
詳しくはAPIドキュメントを参照してください。
Splash HTTP API — Splash 1.8 documentation
Splashの使いドコロ
推測ですが、Splash開発のモチベーションは以下のようなものでしょう。
- JavaScriptを使ったページが増えており、JavaScriptへの対応は不可欠。
- しかしあらゆるページでPhantomJSを使うと遅くなってしまう。
- かといって、JavaScriptを使わないページはlxmlで、使うページはSelenium+PhantomJSなどと使い分けると、APIが違って面倒。
- Splashを使って、JavaScriptを実行したあとのHTMLをlxmlなどでスクレイピングすれば、JavaScriptに対応しつつAPIの一貫性を維持できる。
このため、lxmlなどを使って昔ながらのスクレイピングをしている人が、あまりコードを変更せずにJavaScriptに対応したいという目的で使えるのではないでしょうか。
一方でステートレス性を重視しているので、JavaScriptで対話的な操作をしてスクレイピングするという用途には向いていないように思います。サーバーでLuaスクリプトを実行できますが、それであれば正直SeleniumとかCasperJSとかでやっても同じように思います。
また、サーバーを立てる必要があるので、使い捨てのスクリプトにはあまり向かないでしょう。Scrapinghub社はクローラーのSaaSを提供する会社であり、Splashもサービスの一つとして提供されています。このため、スケーラビリティが重視されており、ある程度大規模に使用しないと他の手段に比べてメリットを感じにくいかもしれません。
まとめ
SplashはJavaScriptレンダリングサーバーで、JavaScriptを使ったページからスクレイピングするための手段の一つとして使える可能性を持ったソフトウェアです。
ここで紹介したのは一番基本的な機能だけで、ドキュメントを読んでいると冒頭で紹介した機能以外にもさまざまな機能があります。 例えば、HTTPプロキシとして振る舞うことができたり、Scrapyと連携したりもできるなど、色々と面白い使い方ができそうです。
良ければ使ってみてください。
参考
Scrapy 1.0が公開されました
Pythonの有名なWebスクレイピングフレームワークのScrapyがバージョン1.0になりました。*1
0.24からの主要な変更点は下記のとおりです。
- SpiderでItemの代わりにdictを返せるようになった
- Spiderごとにsettingsを設定できるようになった
- Twistedのloggingの代わりにPythonのloggingを使うようになった
- CrawlerのコアAPIがリファクタリングされた
- いくつかのモジュール配置場所が変更された
他にも数多くの変更点がリリースノートに記載されています。
Scrapy 1.0の感想
大きな機能の追加よりも、APIの整理と安定性の向上がメインのようです。これまではバージョンを重ねるごとに便利になっていくものの、あまりAPIが安定していない印象でしたが、APIを安定させた区切りのリリースと言えるでしょう。1.0というメジャーバージョンに到達したことで、安心して使えるようになったと思います。
Item
の代わりにdict
が使えるようになったのはありがたいです。Item
の存在意義は正直謎でしたので。
個人的に気になっていた、単一の要素を取得したい場合でもlist
を返すextract()
しかなくて使いにくい問題も、extract_first()
メソッドが追加されたことで解決したので嬉しいです。
1.0では依然としてPython 2.7のみの対応ですが、1.1のマイルストーンにはPython 3のサポートが含まれています。実際、Google Summer of Code 2015のテーマとして採択されており、この夏の成果が楽しみです。
それでは早速Scrapy 1.0を使ってクローラーを書いてみましょう。
例1:1ファイルのシンプルなクローラー
Scrapyは大規模なクローリングを得意とするフレームワークですが、1ファイルから構成されるシンプルなクローラーも書けます。
ScrapyのWebサイトのトップに表示されている、Scrapinghubのブログをクロールするクローラーを動かしてみます。ちなみにScrapinghubはScrapyの開発者が立ちあげた会社です。
表示されているコードをそのままターミナルに貼り付けて実行してもよいのですが、ちょっと手を加えて解説も加えておきます。
1. Scrapyをインストールする
Python 2.7が必要です。
--upgrade
をつけることでインストール済みの場合はアップグレードします。
$ pip install --upgrade scrapy
2. service_identityもインストールしておく
service_identityはMITMを防ぐモジュールであり、これがインストールされていないと警告が出ます。
$ pip install service_identity
3. Spiderを作成する
myspider.py
という名前で以下の内容のファイルを作成します。
日本語のコメントを付けたので、1行目にエンコーディングを指定しています。ファイルはUTF-8で保存してください。
# coding: utf-8 import scrapy # scrapy.Spiderを継承してBlogSpiderを定義する class BlogSpider(scrapy.Spider): name = 'blogspider' start_urls = ['http://blog.scrapinghub.com'] def parse(self, response): # トップページをパースするメソッド。 # URLに /yyyy/mm/ を含むアーカイブページへのリンクを抽出してクロールする。 # それらのページはparse_titles()メソッドでパースする。 for url in response.css('ul li a::attr("href")').re(r'.*/\d\d\d\d/\d\d/$'): yield scrapy.Request(response.urljoin(url), self.parse_titles) def parse_titles(self, response): # アーカイブページからエントリーのタイトルを取得する for post_title in response.css('div.entries > ul > li a::text').extract(): yield {'title': post_title}
4. クローラーを実行する
以下のコマンドで実行できます。
$ scrapy runspider myspider.py 2015-06-20 22:14:05 [scrapy] INFO: Scrapy 1.0.0 started (bot: scrapybot) 2015-06-20 22:14:05 [scrapy] INFO: Optional features available: ssl, http11 2015-06-20 22:14:05 [scrapy] INFO: Overridden settings: {} 2015-06-20 22:14:05 [scrapy] INFO: Enabled extensions: CloseSpider, TelnetConsole, LogStats, CoreStats, SpiderState 2015-06-20 22:14:05 [scrapy] INFO: Enabled downloader middlewares: HttpAuthMiddleware, DownloadTimeoutMiddleware, UserAgentMiddleware, RetryMiddleware, DefaultHeadersMiddleware, MetaRefreshMiddleware, HttpCompressionMiddleware, RedirectMiddleware, CookiesMiddleware, ChunkedTransferMiddleware, DownloaderStats 2015-06-20 22:14:05 [scrapy] INFO: Enabled spider middlewares: HttpErrorMiddleware, OffsiteMiddleware, RefererMiddleware, UrlLengthMiddleware, DepthMiddleware 2015-06-20 22:14:05 [scrapy] INFO: Enabled item pipelines: 2015-06-20 22:14:05 [scrapy] INFO: Spider opened 2015-06-20 22:14:05 [scrapy] INFO: Crawled 0 pages (at 0 pages/min), scraped 0 items (at 0 items/min) 2015-06-20 22:14:05 [scrapy] DEBUG: Telnet console listening on 127.0.0.1:6023 2015-06-20 22:14:06 [scrapy] DEBUG: Crawled (200) <GET http://blog.scrapinghub.com> (referer: None) 2015-06-20 22:14:07 [scrapy] DEBUG: Crawled (200) <GET http://blog.scrapinghub.com/2012/07/> (referer: http://blog.scrapinghub.com) 2015-06-20 22:14:07 [scrapy] DEBUG: Crawled (200) <GET http://blog.scrapinghub.com/2011/11/> (referer: http://blog.scrapinghub.com) ...
実行するとアーカイブページを30ページほどクロールして、タイトルを取得できます。
-o titles.jl
という引数をつけると、取得したタイトルがtitles.jl
という名前のファイルにJSONlines形式で書き込まれます。
複雑なクローラーの作成
続いて、1年半前の記事と同様にBBCとCNET Newsを対象として、もう少し複雑なクローラーを書いてみます。
以前の記事に書いたクローラーは、Webサイト側の変更によって既に2つとも動かなくなっています。Webスクレイピングの無常さを感じますが、めげずに書き直します。 当時のバージョンは0.20.2でしたが、1.0の機能を使うとよりシンプルに書けます。
スクレイピングした後にデータベースに保存するなどの処理を行ったり、設定を共有した複数のSpiderを動かしたりするなど、1ファイルでは収まらないクローラーを作成するときには、プロジェクトを作るのがScrapyの流儀です。
以下のコマンドで、helloscrapy
という名前のプロジェクトを作成します。
$ scrapy startproject helloscrapy
以下のファイルが生成されます。
$ tree helloscrapy helloscrapy ├── helloscrapy │ ├── __init__.py │ ├── items.py │ ├── pipelines.py │ ├── settings.py │ └── spiders │ └── __init__.py └── scrapy.cfg
プロジェクトのディレクトリにcd
しておきます。
$ cd helloscrapy/helloscrapy
以降では、このディレクトリ(settings.py
が存在するディレクトリ)を基準とします。
とりあえず、settings.py
に以下の設定を追加しておきます。Webページのダウンロード間隔として3秒空け、Webサイトのrobots.txtに従うようになります。
DOWNLOAD_DELAY = 3 ROBOTSTXT_OBEY = True
例2:XML Sitemapを持つサイトのクローリング
XML Sitemapを持つWebサイトをクロールするにはSitemapSpiderが便利です。
例として、CNET Newsを取り上げます。以前の記事ではXML Sitemapを持たないサイトとして紹介しましたが、リニューアルしてXML Sitemapが提供されていました。
spiders/cnet.py
を以下の内容で作成します。scrapy genspider cnet www.cnet.com
を実行するとSpiderの雛形が生成されるので、これを変更しても構いません。
# coding: utf-8 from datetime import datetime import scrapy # SitemapSpiderを継承する class CNETSpider(scrapy.spiders.SitemapSpider): name = "cnet" allowed_domains = ["www.cnet.com"] sitemap_urls = ( # ここにはrobots.txtのURLを指定してもよいが、 # 無関係なサイトマップが多くあるので、今回はサイトマップのURLを直接指定する。 'http://www.cnet.com/sitemaps/news.xml', ) sitemap_rules = ( # 正規表現 '/news/' にマッチするページをparse_news()メソッドでパースする (r'/news/', 'parse_news'), ) def parse_news(self, response): yield { # h1要素の文字列を取得する 'title': response.css('h1::text').extract_first(), # div[itemprop="articleBody"]の直下のp要素以下にある全要素から文字列を取得して結合する 'body': ''.join(response.css('div[itemprop="articleBody"] > p ::text').extract()), # time[itemprop="datePublished"]のclass属性にUTCの時刻が格納されているので、パースする 'time': datetime.strptime( response.css('time[itemprop="datePublished"]::attr(class)').extract_first(), '%Y-%m-%d %H:%M:%S' ), }
以下のコマンドでクローラーを実行します。しばらくするとitems-cnet.jl
にスクレイピング結果が出力されていきます。
$ scrapy crawl cnet -o items-cnet.jl
例3:XML Sitemapを持たないサイトのクローリング
XML Sitemapを持たないサイトのクローリングにはCrawlSpiderが便利です。
例として、BBCを取り上げます。以前の記事ではXML Sitemapを持つサイトとして紹介しましたが、ニュース用のXML Sitemapが404になっていて使用できなかったので、XML Sitemapを使わずにクロールします。
spiders/bbc.py
を以下の内容で作成します。scrapy genspider -t crawl bbc www.bbc.com
を実行するとCrawlSpiderを使ったSpiderの雛形が生成されるので、これを変更しても構いません。
# coding: utf-8 from datetime import datetime from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule # CrawlSpiderを継承する class BBCSpider(CrawlSpider): name = "bbc" allowed_domains = ["www.bbc.com"] start_urls = ( 'http://www.bbc.com/news', ) rules = ( # /news/world/*** というカテゴリページを辿る Rule(LinkExtractor(allow=r'/news/world/'), follow=True), # /news/world-*** というニュースページはparse_news()メソッドでパースする Rule(LinkExtractor(allow=r'/news/world-'), callback='parse_news'), ) def parse_news(self, response): yield { # h1要素の文字列を取得する 'title': response.css('h1::text').extract_first(), # .story-body__innerの直下のp要素文字列を取得して改行で結合する 'body': '\n'.join(response.css('.story-body__inner > p::text').extract()), # .story-body .dateのdata-seconds属性にタイムスタンプが格納されているので時刻に変換する 'time': datetime.fromtimestamp(int( response.css('.story-body .date::attr("data-seconds")').extract_first())), }
以下のコマンドでクローラーを実行します。しばらくするとitems-bbc.jl
にスクレイピング結果が出力されていきます。たまにパースに失敗するページもありますが、無視してください。
$ scrapy crawl cnet -o items-bbc.jl
まとめ
Scrapy 1.0になり、すっきりと書けるようになったことがわかるかと思います。 使いやすくなり安定したScrapyでクローリングしていきましょう。 今後のPython 3対応も楽しみです。
LinuxディストリビューションにおけるPython 3デフォルト化の流れ
2015年6月2日修正:henrichさんのコメントを受け、Debianの記述を修正しました。
最近のLinuxディストリビューションにおいてPython 3がデフォルトになってきているという話をチラホラ聞くので、状況を調べてみました。
結論
- PEP 394にディストリビューション向けのガイドラインが公開されている。
- Arch Linuxは既にPython 3がデフォルトになっている。
- Fedora 23(2015-10-27リリース予定)でPython 3がデフォルトになる予定。
-
/usr/bin/python
は削除されるかpython3
にシンボリックリンクされるか議論されている。
-
- Ubuntu 16.04(2016-04リリース予定)でPython 3がデフォルトになる予定。
- Debian 9 (Stretch) または10 (Buster) でPython 3をデフォルトにするべく作業が始まっている。
PEP 394
PEP 394では、Arch Linuxが(アグレッシブにも)Python 3をデフォルトにしたことを受け、Unix-likeなディストリビューション向けに推奨されるpython
コマンドの振る舞いが記述されています。
PEP 394 -- The "python" Command on Unix-Like Systems
Recommendationを簡単に訳してみました。
推奨事項
- Unix-likeなディストリビューションはPython 2を
python2
コマンドとして、Python 3をpython3
コマンドとしてPATHにインストールするべきである。 python2
コマンドを実行するとPython 2が起動し、python3
コマンドを実行するとPython 3が起動するべきである。- Python 2がインストールされている場合は、
python
コマンドをインストールし、python
コマンドが実行された場合はpython2
コマンドが実行されたときと同じバージョンのPythonが起動すべきである。(ただしPython 3が起動する場合もある) - Python 2.xの
idle
,pydoc
,python-config
などのコマンドも同様に、idle2
,pydoc2
,python2-config
として実行でき、バージョンがついていないコマンドが実行された時は、これらのバージョンを実行すべきである。(ただしシステム管理者の設定によってPython 3.xのものが起動してもよい) - プラットフォームごとの差異に対応するため、Pythonインタプリタを起動する必要のあるコードでは
python
を指定すべきではなく、代わりにpython2
かpython3
(またはさらに特定のpython2.x
やpython3.x
)を指定すべきである。Shebangに書くときも同様である。 - 一点例外として、Python 2.xとPython 3.xの両方で動くスクリプトでは
python
を指定したりShebangに書いても良い。 - Pythonスクリプトから実行する場合は、
sys.executable
を使うのが望ましい。
Arch Linux
Arch Linuxはローリング・リリースを採用しており、明確なバージョンはありません。
2010-10-18に/usr/bin/python
がPython 3を指すようになったとアナウンスされています。
python
パッケージをインストールすると、Python 2ではなくPython 3が使えます。
Arch Linux - News: Python is now Python 3
参考:
実際に使ってみる
というわけで使ってみました。baseとbase-develグループをインストールしただけではPythonはインストールされなかったので、pythonパッケージをインストールしました。
python
コマンドで確かにPython 3が起動しました。
[root@archiso /]# python -V Python 3.4.3 [root@archiso /]# which python /usr/bin/python [root@archiso /]# ls /usr/bin/python* -l lrwxrwxrwx 1 root root 7 Mar 25 17:30 /usr/bin/python -> python3 lrwxrwxrwx 1 root root 14 Mar 25 17:30 /usr/bin/python-config -> python3-config lrwxrwxrwx 1 root root 9 Mar 25 17:30 /usr/bin/python3 -> python3.4 lrwxrwxrwx 1 root root 16 Mar 25 17:30 /usr/bin/python3-config -> python3.4-config -rwxr-xr-x 2 root root 10440 Mar 25 17:30 /usr/bin/python3.4 lrwxrwxrwx 1 root root 17 Mar 25 17:30 /usr/bin/python3.4-config -> python3.4m-config -rwxr-xr-x 2 root root 10440 Mar 25 17:30 /usr/bin/python3.4m -rwxr-xr-x 1 root root 3107 Mar 25 17:30 /usr/bin/python3.4m-config
なお、python2パッケージをインストールすれば、Python 2を使うことも可能です。
Fedora
次期メジャーリリースのFedora 23でPython 3をデフォルトにすることが提案されています。
Changes/Python 3 as Default - FedoraProject
ここでのデフォルトとは、以下のことを意味します。
- Python 2のみに対応していたYumに替わってDNFがデフォルトパッケージマネージャになる。(明記されていないがFedora 22で実施済みのはず)
- Python 3がminimal buildrootにおける唯一のPython実装になる。(Fedora 22で実施済み)
- Python 3がWorkstation LiveDVDにおける唯一のPython実装になる。
- Python 3がminimal cloud imageにおける唯一のPython実装になる。
- Python 3がAtomic hostにおける唯一のPython実装になる。
- Server LiveDVDでもPython 3が唯一のPython実装になると良いが、可能性は低い。
以下の点は議論されているようです。
参考:
- [Python-Dev] Python 3 as a Default in Linux Distros
- Python 3 Is Close To Becoming The Default In Fedora 22 - Phoronix
ちなみに最近のFedoraのリリース日*1は以下の感じです。
- Fedora 20 (Heisenbug): 2013-12-17
- Fedora 21: 2014-12-09
- Fedora 22: 2015-05-26
- Fedora 23: 2015-10-27(予定)
Ubuntu
Ubuntu 16.04でPython 3をデフォルトにすることが提案されています。
ここでのデフォルトとは、以下の意味です。
- Python 3がデフォルトでインストールされる唯一のPython実装になる。
- Python 3がインストールメディア(ISOなど)に含まれる唯一のPython実装になる。
- Ubuntu touchのイメージではPython 3のみが使用できる。
- Python 3をサポートするアップストリームのすべてのライブラリは、Python 3のバージョンがアーカイブに含まれる。
- Python 3上で実行できるすべてのアプリケーションはデフォルトでPython 3を使う。
- アーカイブに含まれるすべてのシステムスクリプトはPython 3を使う。
一方で、デフォルトが意味しないこととして、以下の点が挙げられています。
参考:
- Python - Ubuntu Wiki
- 2015年5月15日号 Ubuntu 15.10 “Wily Werewolf”の開発開始・ホワイトボックススイッチ版Ubuntu・Ubuntu版インターネット対応冷蔵庫 “ChillHub”:Ubuntu Weekly Topics|gihyo.jp … 技術評論社
- Python plans for LTS - | The Summit Scheduler
- Ubuntu Plans For Python 3 By Default For Ubuntu 16.04 LTS - Phoronix
ちなみに最近のUbuntuのリリース日*2は以下の感じです。
- 14.04 LTS (Trusty Tahr): 2014-04-17
- 14.10 (Utopic Unicorn): 2014-10-23
- 15.04 (Vivid Vervet): 2015-04-23
- 15.10 (Wily Werewolf): 2015-10(予定)
- 16.04 LTS: 2016-04(予定)
Debian
2015年4月8〜14日に開催されたPyCon 2015において、DebianのPythonメンテナ陣によるミーティングがあり、その後Python 3への移行がアナウンスされました。
移行の具体的な日付やタイミングは見つかりませんでしたが、2020年にPython 2のメンテナンスが終了することもあり、次期メジャーリリースのDebian 9 (Stretch) またはDebian 10 (Buster) でPython 3をデフォルトにするべく取り組みが始まっているようです。
参考:
- PyCon BoF: Stretch goals for cPython, PyPy & CFFI
- Python/StretchRoadmap - Debian Wiki
- py3porters-devel Info Page
- Python - Debian Wiki
- Debian Python Policy - Python Packaging
- Geoffrey Thomas (geofft)
- /usr/bin/python in Python 2 and 3
ちなみに最近のDebianのリリース日*3は以下の感じです。
- Debian 6.0 (Squeeze): 2011-02-06
- Debian 7 (Wheezy): 2013-05-04
- Debian 8 (Jessie): 2015-04-25
- Debian 9 (Stretch):
20182017? - Debian 10 (Buster):
20212019?
結論
最初に書いたものと同じです。
- PEP 394にディストリビューション向けのガイドラインが公開されている。
- Arch Linuxは既にPython 3がデフォルトになっている。
- Fedora 23(2015-10-27リリース予定)でPython 3がデフォルトになる予定。
-
/usr/bin/python
は削除されるかpython3
にシンボリックリンクされるか議論されている。
-
- Ubuntu 16.04(2016-04リリース予定)でPython 3がデフォルトになる予定。
- Debian 9 (Stretch) または10 (Buster) でPython 3をデフォルトにするべく作業が始まっている。
Python 3を明示的にインストールしなくても普通に使える環境が広がると嬉しいですね。 ディストリビューションのコミッターには本当に頭が下がります。
内容の正確さにあまり自信がないので、間違っていたりもっと良い情報がありましたらお知らせください。 他のディストリビューションの情報も歓迎です。
Joel on Softwareを読んだ
元MicrosoftのJoel氏が書いたエッセイ集です。 自分の中で感じていてもうまく言語化できなかったことが明確に言語化されていて素晴らしかったです。
- 作者: Joel Spolsky,青木靖
- 出版社/メーカー: オーム社
- 発売日: 2005/12
- メディア: 単行本
- 購入: 18人 クリック: 371回
- この商品を含むブログ (446件) を見る
特に印象的だったのは以下の2つの章です。
第12章 5つの世界
5つの世界とは以下の5つを指し、それぞれの世界でそれぞれの苦労があるという話です。
- パッケージ
- インターナル
- 組み込み
- ゲーム
- 使い捨て
インターナルソフトウェア(社内システム)とパッケージソフトウェア(オープンソース、Webベース、コンサルティングウェア)の違いが非常に頷けました。
実際、インターナルソフトウェアとパッケージソフトウェアの主要な違いの1つは、インターナルソフトウェアでは、ある時点以降、それをより頑健にしたり、使いやすくしたりするのにコストをかけることへのリターンは急速に少なくなる一方、パッケージソフトの方は、最後の1%の安定性や使いやすさが競争優位の鍵となり得る点だ。
第18章 二文化主義
UnixとWindowsの違いの話です。自分はUnixの方が便利だと思って常用していますが、自分が使いやすいものと一般的に受け入れられているものとのズレが的確に表現されていました。
Unixは他のプログラマにとって有用なコードに価値を置く文化であり、一方Windowsはプログラマでない人たちにとって有用なコードに価値を置く文化である。
その他
これら2つの章以外にも、商用パッケージソフトウェアの開発に携わってきた作者のOSSやアジャイル開発に対する懐疑的な見方も新鮮でした。
UNIXの考え方やハッカーと画家を読んだときは頷いてばかりでしたが、本書を読んだときは、そういう考え方もあるのかと視野が広がる思いでした。
ハッカーと画家を読んだ
Y Combinator創業者のポール・グレアム氏が書いたエッセイ集です。
- 作者: ポールグレアム,Paul Graham,川合史朗
- 出版社/メーカー: オーム社
- 発売日: 2005/01
- メディア: 単行本
- 購入: 109人 クリック: 4,884回
- この商品を含むブログ (581件) を見る
本書も読み進めていく中で頷いてばかりでしたが、特に表題作の「ハッカーと画家」が印象的でした。
「計算機科学」という言葉のしっくりこなさが明文化されていて、自分だけじゃなかったのだと救われた気がします。
私は「計算機科学」という用語がどうにも好きになれない。一番大きな理由は、そもそもそんなものは存在しないからだ。計算機科学とは、ほとんど関連のない分野が歴史的な偶然からいっしょくたに袋に放り込まれたもので、言ってみればユーゴスラビアみたいなものだ。
また、「昼間の仕事(day job)」という概念が明文化されていて、そういう割り切り方もありなんだと気付かされました。
ソフトウェアに関してのこの問題への解答は、 実は他のもの創りの人々には既に知られている。昼間の仕事(day job)というやつだ。 この言葉はミュージシャンの間で発生した。彼らは夜に演奏するからだ。 より一般的に言えば、生活費のためにひとつの仕事を、愛のためにもうひとつの仕事をするということだ。
これまで自分が目指していく先にハッカーがあるとは思ってなかったのですが、本書を読んで自分が目指す先はハッカーなのかもしれないと思うようになりました。
そしてやはりLispを使いたくなるのでした。