Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

ScrapyとPhantomJSを用いたスクレイピングDSL

15,596 views

Published on

第一回Webスクレイピング勉強会 発表資料

Published in: Technology

ScrapyとPhantomJSを用いたスクレイピングDSL

  1. 1. 東京スクレイピング勉強会#1 2014.6.22 ScrapyとPhantomJSを用いたスクレイピングDSL Masayuki Isobe / 礒部 正幸 / @chiral Adfive, Inc.
  2. 2. 自己紹介 • 礒部正幸(いそべ まさゆき) • 職業: ソフトウェアエンジニア • 現在: アドファイブ(株) 代表 http://www.adfive.net – 現体制: 代表1名 + 外注数名 : お問い合わせ – 自社製品: モバイルO2Oアプリ/オウンドメディア/アドサーバ – 受託業務: システムコンサルティング/システム開発/データ分析 • 東工大卒 (B:情報工学科、M:計算工学専攻) • インターネット活動 – TwitterID: @chiral – (ブログ:アドファイブ日記) http://d.hatena.ne.jp/isobe1978/ • 「機械学習ハッカソン」主催: http://mlhackathon.connpass.com/ • 近年作ったスクレイピング利用システム – パケットデータから閲覧Webページの画面キャプチャ再構成 (PhantomJS+自作プロキシーサーバ) – WebサイトのEPub電子書籍化ツール(Node.js/CasperJS) – WebスクレイピングDSL( Scrapy/PhantomJS) – Webスクレイピングジョブ管理システム(Django) LTで発表 本資料
  3. 3. Scrapyとは • Pythonで書かれたWebスクレイピングフレームワーク • 2008年に初期リリース,比較的枯れていて安定動作 • Twisted(非同期イベント駆動処理ライブラリ)ベース • XPATHやCSSでセレクタとスクレイピング処理を記述可能 • 欧米の大手企業の自社システムとして多数利用実績あり • スケーラブル(例: EC2+Scrapyで2.5億ページを40時間で処理した天才科学者M.N) • スクレイピングシステムにおける各機能単位がブラガブルに 設計されている → アジャイル開発にすごく合う(発表者の私見)
  4. 4. Scrapyのアーキテクチャ ・必要十分な機能分割がなされた綺麗な設計になっている。 ① Spider - 取得したいページのURLを Schedulerにリクエスト - 取得したページの内容をスクレイ ピングしてItemとしItemPipelineへ ② Scheduler - リクエストのトラフィック調整 (同一ドメインへのアクセス間隔等) ③ Downloader - Webへのアクセス手段をフックする ④ Item Pipeline - スクレイピングしたItemを出力 Storage(File, DB, ...), e-mail, … etc.① ② ④ ③ → とりあえずSpiderを作れば動く。それ以外は必要に応じて追加する。
  5. 5. 一般的なスクレイピング手順 対象ページのURLを生成 Webアクセス/HTMLのパース セレクタ指定 & データ抽出 データの出力 / 保存 http://www.xxx.yyy/info?page=1 http://www.xxx.yyy/info?page=2 http://www.xxx.yyy/about … Scrapy作業ステップ (イメージ) wget / curl … Chrome / Firefox … jQuery / css / xpath .. JSONファイル, MySQL, etc.. ScrapyでのPythonプログラムはなるべく共通に作って サイトごとの違いを外部の設定ファイルで吸収したい。
  6. 6. (参考1)ドメイン特化言語 ・ DSL : ドメイン特化言語 - あるドメインの課題を解くのに特化した プログラミング言語。 - 汎用性に乏しい代わりに、「ドメイン知識の記述」 「効率的な動作」 「少ない記述量」 等が可能に → いわゆるアプリケーションの「設定ファイル」を よりリッチにしてソフトウェアのインタフェースを 柔軟にするといったコンセプト 内部DSL 外部DSL 米国の著名なアーキテクト マーティンファウラー御大は 近年、DSLを積極的に推進中 Webスクレイピング専用のDSLがあったら便利そう (参考)DSLの大分類 ・設定ファイルを、アプリケーションと同じ 言語で記述するというやり方 ・例えば、Ruby on RailsにおけるRakefile (メリット) 開発コストが低い (デメリット) 記述方法がシステム開発者寄り ・専用の文法を持った設定ファイルを定 義して実装するというやり方 ・例えば、Apacheの設定ファイル (メリット)記述方法がドメイン従事者寄り (デメリット)開発コストが高い
  7. 7. • 「WebスクレイピングDSL」 の検討 – 内部DSL案: Pythonのモジュールローダに則る必要があって大変 → 不採用 – 外部DSL案:専用の文法を作るとパーサー等が大変 → XML/JSON/YAMLなど既存の形式を利用 • XML / JSON / YAMLの特徴 – (当然ながら)いずれもパーサーがPythonライブラリとして存在する – 再帰的な木構造を自由に表現可能 (一般にプログラムは内部的に木構造であり、表現力が高い) – XML / JSONは普及しており、学習コストが低い – XMLは人間が読み書きするのにあまり向いてない – JSONは整形すれば読めるが、書くのが結構大変 – YAMLは人間が読み書きするのにより適している → 採用 (参考2) YAML ベースDSLのススメ 対象ページのURLを生成 Webアクセス/HTMLのパース セレクタ指定 & データ抽出 データの出力 / 保存 作業ステップ YAMLファイル 各ステップに対応する処理をYAMLの項目として記述
  8. 8. スクレイピングDSLの仕様と実装 name: "yahoo_news" url: format: - http://news.yahoo.co.jp/list/?p=%d - { from: 1, to: 30 } lis: "ul.list li ." scr: url: "a @href" title: "span.ttl text()" out: file: data/yahoo_news.jsons スクレイピングタスク名 対象ページのURL ページ内のベースとなるセレクタ 抽出項目(ベースからの相対セレクタ) データの出力/保存先 Scrapyの各パーツにて 上記の記述を解釈実行 する機能を実装 独自Spider 独自Downloader 独自Pipeline Scrapyのsetting.py
  9. 9. URL指定部の仕様と実装 仕様 - http://aaa.bbb/ - { or: [taro,jiro] } - / - { from: 1, to: 3 } 実装 ・配列=イテレータの積 * “http://aaa.bbb/taro/1”, “http://aaa.bbb/taro/2”, “http://aaa.bbb/taro/3”, “http://aaa.bbb/jiro/1”, “http://aaa.bbb/jiro/2”, “http://aaa.bbb/jiro/3” + ・from/to=数イテレータ { from: 1, to: 5 } [ 1,2,3,4,5 ] ・or=要素イテレータ { or: [apple,orange] } * “apple”,”orange” + ・format=文字列生成 format: - “%s?page=%d” - hoge - 777 *“hoge?page=777”+ これらを組み合わせて(多階層にネストも可能)、対象ページURL集合を指定する ・file=ファイル行イテレータ , “title”:”hogehoge”, “url”:”http://a.b.c/def” } , “title”:”fugafuga”, “url”:”http://g.h.i/jk” } … fileの例)/var/scrapy/data.jsons file: - /var/scrapy/data.jsons - url * “http://a.b.c/def”, “http://g.h.i/jk”, … + ・イテレータの積にはPythonのitertoolsパッケージにあるproduct()を用いる ・要素イテレータor、数イテレータfrom/toは、要素をforループで回してyield ・文字列生成formatは、1番目の要素 % (2番目以降) で生成してyield ・ファイル行イテレータは、ファイルを読み込んで行ごとにforループで回してyield
  10. 10. 実装 データ抽出部の仕様と実装 url: http://abc.de/ lis: "ul li ." scr: url: "a @href“ label: “a text()” title: { global: “title text()“ - site: { quote: abc.de } 仕様 ・ cssセレクタを並べて、最後に @属性, text(), ”.” のいずれかを記述する。 ・ データをリテラルとして埋め込みたい場合は , quote: “埋め込む文字列” - 等とする。 ・ベースセレクタより上のタグを参照したい場合は , global: “title text()” - 等とする。 <html> <head> <title>hello</title> <head> <body> <ul> <li><a href=“/page/1”>page1 </a></li> <li><a href=“/page/2”>page2 </a></li> <li><a href=“/page/3”>page3 </a></li> </ul> </body> </html> 取得ページの例 (http://abc.de/) yamlファイルの例 yield Item( url=“http://abc.de/page/1”, label=“page1”,title=“hello”, site=“abc.de” ) yield Item( url=“http://abc.de/page/2”, label=“page2”,title=“hello”, site=“abc.de” ) yield Item( url=“http://abc.de/page/3”, label=“page3”,title=“hello”, site=“abc.de” ) Itemの出力 (*) class Spider(BaseSpider): コンストラクタ: コマンド引数からYAMLファイル名取得し読込 URLイテレータの生成 次のURL取得メソッド: URLイテレータから次のURLを取得 HTTPレスポンスハンドラ: セレクタの生成 YAMLに書いてあるベースセレクタを指定 forループ(抽出対象セレクタ): YAMLの記述に従ってデータ取得しItem生成 quoteやglobalについての処理 Itemをyield (*) URLイテレータから次を取得してRequest生成 (URLがなければ終了) Requestをyield Spider Scrapy Engine Respons Request Item web 独自Spider project/app/spiders/yamlspider.py
  11. 11. データ出力/保存部の仕様と実装 実装仕様 (略) out: - { exists: [url,title,page,label] } - { concat: url } - { substr: [title,0,3] } - { match: [label,“page([0-9.]+)",page_no] } - { file: [data/abc_de.csv, site,title,page_no,url] } exists : 抽出対象の存在チェック concat: 改行の除去 substr: 文字列の一部を抜き出す match: 正規表現マッチ&キーの生成 copy: Itemの複製(YAMLネスト用) assign: キーの複製 replace: 文字列置換 file: 出力先ファイルと出力キーの指定 (Json,CSV,TSV,PlainTXTに対応) fluentd(実装中): fluentd出力 例) 以下のyamlファイルに前スライドの抽出Itemを与える Item Pipeline Scrapy Engine Spider #label,title,page_no,url abc.de,hel,1,http://abc.de/page/1 abc.de,hel,2,http://abc.de/page/2 abc.de,hel,3,http://abc.de/page/3 出力ファイルdata/abc_de.csv class MyOutputPipeline: 各種のファイル出力処理 Itemの処理(item,spider): spiderからyamlデータを取得しout部を参照 out部の処理を呼ぶ out部の処理(item,out): # (*) forループ(outの要素): fileの場合、ファイル出力処理 copyの場合、item.copy()してメソッドを再帰呼び出し existsの場合、指定キーが存在しなければ出力スキップ 以下、substrやmatchなどを処理 Item ファイル fluentd (略) ITEM_PIPELINES = { ‘app.pipelines.MyOutputPipeline': 100 } (略) 設定ファイル(project/app/settings.py) 独自Pipeline project/app/pipelines.py
  12. 12. PhantomJSを用いたAjaxページ遷移対応 ・サイトによってはAjaxを使ってページをめくっていくものもある ・ScrapyのDownloaderは、Ajaxを使ったDOMの更新に対応していない (なお、Basic認証やセッションクッキーの埋め込みには対応している) ・サーバサイドでajaxブラウジングする仕組みが必要 → Selenium Python bindings を使って、Scrapy内でサーバサイドブラウジングを行う Selenium Python bidingsの対応ブラウザ - Chrome - Firefox - PhantomJS ← headless動作(仮想フレームバッファにレンダリング&DOMツリーを構築) → headless動作可能なPhantomJSを基本的に利用 url: (略) js: driver: phantomjs wait: 5 page: format: -"__doPostBack('ctl00$ContentPlaceHolder1$pgTop$ctl01$ctl%02d','')“ - { from: 1, to: 10 } lis: (略) scr: (略) out: (略) とあるWebサイトのajaxページ遷移に対応するためのyaml記述 ブラウザの指定 (Firefoxも指定可) ページ遷移用のwait時間(秒) ページ遷移をキックするJavascriptコード (URL生成部と同様の記述方法) 仕様
  13. 13. Download middleware部へのSeleniumDriverの埋め込み ココに機能追加 実装 class YamlAjaxDownloader: リクエストの処理(request,spider): spiderからyamlデータを取得 yamlにajax対応の記述がある場合、 spiderからselenium webdriverオブジェクトを取得 yamlデータからページ遷移用のjsスニペットを取得 webdriverでjs実行 Responseオブジェクトを作って返す そうでない場合、Noneを返し既存のDownloaderへ 独自Downloader (略) DOWNLOADER_MIDDLEWARES = { 'ittemi.downloaders.YamlAjaxDownloader': 100 } (略) 設定ファイル(project/app/settings.py) こうした処理を独自Spiderの内部で行うこともでき るが、Scrapyの作法に則ることで Scheculerのトラフィック調整やSpiderの並列実行 などの恩恵が受けられるようになる。 (あと、コードの見通しもよくなる) このようにDownloaderでResponseを返せば 既存のDownloderをバイパスできる project/app/downloaders.py
  14. 14. 管理画面も作った 独自管理コマンド 管理コマンド スクレイピングDSL 管理画面 MySQL ジョブ管理部 タスク定義部 前スライドまで で述べた処理 ポーリングループ Scrapyタスク Popen3 StdoutとStderr をリダイレクト ワークフロータスク サイトごとのYAML記述をDBで管理. 管理画面上からスクレイピングの ジョブを実行&結果の確認を可能に. (管理コマンドの引数でyamlをべた書きするのは大変なので) Spiderの仕組みを使ってまずdjangoから yamlを取得し、それを見て動くように. 独自Spiderは最初に管理画面からYAMLを取得
  15. 15. 管理画面スクリーンショット タスク一覧、ジョブ実行 ジョブ一覧 Scrapy出力の確認 YAMLの登録
  16. 16. Demo1: Yahooニュースからの記事取得 name: "yahoo_news1" url: format: - http://news.yahoo.co.jp/list/?p=%d - { from: 1, to: 30 } lis: "ul.list li ." scr: url: "a @href" title: "span.ttl text()" out: file: data/yahoo_news1.jsons (A)一覧ページ、(B)冒頭だけ表示するページ、(C) 全文表示(元記事)ページ と3階層あるので、それぞれに対応するyamlファイルを3つ記述する name: "yahoo_news2" url: file: [../data/yahoo_news1.jsons, url] lis: ".newsLink ." scr: url: "a @href" out: file: data/yahoo_news2.jsons name: "yahoo_news3" url: file: [../data/yahoo_news2.jsons, url] lis: "#ynDetail ." scr: body: ".ynDetailText text()" title: "h1.yjXL text()" out: file: data/yahoo_news3.jsons 取得した記事データ 現状、スクレイピング結果をさらにクローリングしていく動きには対応していないが、階層の分だけyamlを記述 しておけば対応する複数の記事を一気に処理できるのでオペレーションコスト的にはそれほど問題にならない (A) (B) (C) 中間ファイル http://news.yahoo.co.jp/list/
  17. 17. Demo2: 郵政公社からの住所-経度緯度データの取得 name: yubin_jusho1 url: format: - "http://map.japanpost.jp/pc/addrlist.php?code=%02d" - { from: 1, to: 47 } lis: "td span.mfont3 ." scr: name: { global: "title text()" } url: "a @href" addr: "a text()" out: - { exists: [url,addr] } - { concat: url } - { substr: [name,0,-10] } - { file: data/yubin_jusho1.jsons } name: yubin_jusho2 url: file: [../data/yubin_jusho1.jsons, url] lis: "td span.mfont3 ." scr: name: { global: "title text()" } url: "a @href" addr: "a text()" out: - { exists: [name,url,addr] } - { concat: url } - { substr: [name,0,-10] } - { match: [url,"nl=([0-9.]+).*el=([0-9.]+)",lat,lon] } - { exists: [lat,lon] } - { file: [data/yubin_jusho2.tsv,name,addr,lat,lon] } (A)都道府県直下の市町村 (B) 市町村内の地名、の2階層 → YAML2つ http://map.japanpost.jp/pc/ (A) (B) (20.5MB) (230KB)
  18. 18. Demo3: ajax対応サイトからのデータの取得 (非公開:会場のみ実演)
  19. 19. まとめ • 典型的なWebスクレイピングタスクをYAMLベースのDSLで定義した • YAMLを解釈実行するWebスクレイピングプログラムをScrapyで作成 • Ajaxページ遷移に対応するためSeleniumWebDriverを組み込んだ – PhantomJSのおかげでブラウザを立ち上げずに処理可能 • サイトごとのYAMLと実行の管理負担を減らす画面をDjangoで作成した 最後に宣伝を少し… アドファイブ(株)では、本スクレイピングシステムの導入及びサポート、 またScrapyを用いたシステム開発の支援及びコンサルティングについての 事業を行っております。ご依頼ご質問等ありましたらぜひお問い合わせください。 あと、学生アルバイトさんも絶賛募集中です。よろしくお願いします。 → こうしたYAMLでリッチな設定ファイルを作る、 ドメイン特化言語的アプローチはビッグデータ処理の ベストプラクティスの一つではないかと考えています。 (というわけで弊社では現在、データのビジュアライズダッシュボードを作るDSLについても研究開発中です)

×