Upgrade to Pro — share decks privately, control downloads, hide ads and more …

スクレイピング用 Gem を作った話

スクレイピング用 Gem を作った話

nyamadori

August 01, 2017
Tweet

More Decks by nyamadori

Other Decks in Programming

Transcript

  1. スクレイピング
    Gem
    を作った話
    株式会社 Speee
    開発部
    中嶋 学

    View Slide

  2. 自己紹介
    不動産売却一括査定サイト

    イエウー
    ル」
    のエンジニア
    17
    新卒
    Twitter: @nyamadorim

    View Slide

  3. Juknife
    スクレイピング対象の HTML
    ノー
    ドを
    DSL
    で構造的に書ける
    Ruby Gem

    View Slide

  4. モチベー
    ション
    スクレイピングのコー
    ドをサクッと書きたい
    スクレイプする項目が増えたり
    サイトの仕様が変わっても
    すぐ対応できるようにしたい
    コー
    ドは、
    綺麗になおかつ簡潔に書きたい

    View Slide

  5. 単純なら問題ない



    タイトル

    http://example.com/



    # Nokogiri
    なら
    {
    title: doc.css('#ires .g h3').text,
    url: doc.css('#ires .g .kv > cite').text
    }

    View Slide

  6. 取得する項目増やしましょう



    タイトル

    http://example.com/


    説明文



    { title: doc.css('#ires .g h3').text,
    url: doc.css('#ires .g .kv > cite').text,
    description: doc.css('#ires .g .st').text.strip }

    View Slide

  7. 相手サイトの仕様変わったよ



    タイトル

    http://example.com/


    説明文



    #
    項目数だけセレクタを変える
    { title: doc.css('#ires .item h3').text,
    url: doc.css('#ires .item .kv > cite').text,
    description: doc.css('#ires .item .st').text.strip }

    View Slide

  8. リファクタ
    CONTAINER = '#ires .item'
    #
    項目数だけセレクタを変える
    { title: doc.css("#{CONTAINER} h3").text,
    url: doc.css("#{CONTAINER} .kv > cite").text,
    description: doc.css("#{CONTAINER} .st").text.strip }
    これでセレクタが変わっても問題ないが
    #{CONTAINER}
    がダサい

    View Slide

  9. 構造を考えてリファクタする
    container = doc.css('#ires .item')
    # `container#css`
    は container
    内の子孫要素を検索する
    { title: container.css("h3").text,
    url: container.css(".kv > cite").text,
    description: container.css(".st").text.strip }
    # container
    の子要素を取得したい
    #
    となっても、
    この方法ならセレクタを二度書きせずに済む
    child = container.css('.child')
    { child: {
    child_item_1: child.css('.child-item-1').text,
    child_item_2: child.css('.child-item-2').text,
    }
    }

    View Slide

  10. 完成形
    def scrape_item(context)
    container = context.css('#ires .item')
    { title: context.css("h3").text,
    url: context.css(".kv > cite").text,
    description: context.css(".st").text.strip,
    child: scrape_child(container) }
    end
    def scrape_child(context)
    child = container.css('.child')
    { child_item_1: child.css('.child-item-1').text,
    child_item_2: child.css('.child-item-2').text, }
    end
    scrape_item(doc)
    ####
    ただし、
    行数は最初の3
    倍!

    View Slide

  11. たかがスクレイピング
    そんなに書きたくない

    View Slide

  12. GEM
    作った

    View Slide

  13. Juknife /
    じゅないふ
    スクレイピング対象の HTML
    ノー
    ドを
    DSL
    で構造的に書ける
    Ruby Gem

    View Slide

  14. 例: Google
    検索
    リクエストに必要な情報と
    スクレイピングする HTML
    ノー
    ドを DSL
    で記述する
    class GoogleSearchAgent < Juknife::Agent
    request do #
    リクエストに必要な情報
    get 'https://www.google.co.jp/search'
    user_agent 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.
    query { { q: params[:query] } }
    end
    scraping do #
    スクレイピングする HTML
    ノー

    items :results, '#ires .g' do
    item :title, 'h3' #
    ペー
    ジタイトル
    item :url, '.kv > cite' #
    ペー
    ジ URL
    end
    end
    end

    View Slide

  15. 例: Google
    検索
    agent = GoogleSearchAgent.new
    agent.scrape(query: 'test')
    # =>
    # {:results=>
    # [
    # {:title=>"test
    の意味・
    用例|
    英辞郎 on the WEB:
    アルク",
    # :url=>"https://eow.alc.co.jp/search?q=test",
    # :description=>
    # "test 【1
    自動】
    試験[
    検査]
    を受ける 《
    医・
    化学》
    検査[
    分析]
    を行
    # "
    届けする進化するオンライン英和・
    和英辞書デー
    タベー
    ス。
    一般的な単語
    # "
    イディオム、
    専門用語、
    スラングまで幅広く収録。"}
    # }
    # ]
    # }

    View Slide

  16. Juknife DSL
    Request DSL
    get / post / put / delete
    user_agent
    query
    body
    Scraping DSL
    item / items
    scope

    View Slide

  17. Juknife DSL
    Request DSL
    get / post / put / delete
    user_agent
    query
    body
    Scraping DSL <=
    今日はこれだけ
    item / items
    scope

    View Slide

  18. Scraping DSL
    基本的な考え方
    ブロックの構造 ≒
    取得結果 (Hash)
    の構造
    セレクタで指定された要素を親要素として、
    Ruby
    ブロック内の HTML
    要素が検索される
    scraping do
    items :results, '#ires .g' do
    #
    この中は、'#ires .g'
    を親要素として、
    以下の要素を検索する
    item :title, 'h3' # ≒ #ires .g h3
    item :url, '.kv > cite' # ≒ #ires .g .kv > cite
    end
    end

    View Slide

  19. Scraping DSL
    scraping do
    items :results, '#ires .g' do
    # items: `#ires .g`
    で得られる各要素を
    #
    ブロック内で処理して、
    結果を配列として返す
    item :title, 'h3' # ≒ #ires .g h3
    item :url, '.kv > cite' # ≒ #ires .g .kv > cite
    end
    end
    # =>
    # {:results=>
    # [
    # {:title=>"test
    の意味・
    用例|
    英辞郎 on the WEB:
    アルク",
    # :url=>"https://eow.alc.co.jp/search?q=test",
    # :description=> "test 【1
    自動】
    試験[
    検査]..."}, ...
    # }
    # ]
    # }

    View Slide

  20. 現状
    スクレイピングしかできない
    取得結果を DB
    に格納したりできない
    JS
    でフォー
    ムパラメー
    タ入れて
    遷移先を決めるような残念なサイトを
    楽してスクレイピングしたい

    View Slide

  21. 展望
    取得結果の DB
    格納も含め、
    できるようにしたい
    取得に失敗したら再取得できるようにしたい
    ヘッドレスブラウザで取得できるようにしたい
    スクレイピングのために、
    リクエストに必要な
    デー
    タを作るのめんどくさいので、
    Rails
    プラグインとして実装したい
    Active Record
    とか Active Job
    とかあって、
    上記
    2
    つが楽に実装できそう
    https://github.com/nyamadori/juknife

    View Slide

  22. おわり

    View Slide