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

Webスクレイピング - 小手先の技術 -

Webスクレイピング - 小手先の技術 -

もうなんでもよい...

TakesxiSximada

July 06, 2016
Tweet

More Decks by TakesxiSximada

Other Decks in Technology

Transcript

  1. Webスクレイピング
    ⼩⼿先の技術
    嶋⽥健志 ( @TakesxiSximada )

    View Slide

  2. 嶋⽥健志 @TakesxiSximada
    Webエンジニア@フリーランス
    Django / Pyramid / Tornado あたりでWebを書くこと
    が多い
    https://github.com/TakesxiSximada
    ⾃⼰紹介

    View Slide

  3. 今⽇はscrapingの
    ⼩ネタ紹介

    View Slide

  4. requests 
    PycURL
    BeautifulSoup4
    lxml
    feedparser
    selenium
    scrapy
    ...こんなに時間あるのか?
    アジェンダ

    View Slide

  5. requests
    https://pypi.python.org/pypi/requests

    View Slide

  6. httpライブラリ
    urllib (Pythnoの標準HTTPライブラリ) に⽐べて綺麗に書ける
    インストールに困らない
    おそらくデファクトスタンダード
    import requests
    requests

    View Slide

  7. res = requests.get('http://127.0.0.1:8888')
    GET

    View Slide

  8. ステータスコードの取得
    res.status_code
    レスポンスヘッダ (dict like object)
    res.headers
    request objectの扱い 1

    View Slide

  9. レスポンスボディ(byte string)
    res.content
    レスポンスボディ (str object)
    res.text
    jsonをデコード(できない場合は例外をraise)
    res.json()
    request objectの扱い 2

    View Slide

  10. res = requests.post('http://127.0.0.1:8888')
    POST

    View Slide

  11. PUT
    res = requests.put('http://127.0.0.1:8888')
    HEAD
    res = requests.head('http://127.0.0.1:8888')
    DELETE
    res = requests.delete('http://127.0.0.1:8888')
    OPTIONS
    res = requests.options('http://127.0.0.1:8888')
    その他

    View Slide

  12. res = requests.post(
    'http://127.0.0.1:8888', params={'test': '1'})
    クエリパラメータ

    View Slide

  13. POST /?test=1 HTTP/1.1
    Host: 127.0.0.1:8888
    Accept: */*
    Accept-Encoding: gzip, deflate
    User-Agent: python-requests/2.10.0
    Connection: keep-alive
    Content-Length: 0
    送信されたリクエスト

    View Slide

  14. res = requests.post(
    'http://127.0.0.1:8888', data={'test': '1'})
    リクエストボディを渡す

    View Slide

  15. POST / HTTP/1.1
    Host: 127.0.0.1:8888
    Connection: keep-alive
    Accept-Encoding: gzip, deflate
    User-Agent: python-requests/2.10.0
    Accept: */*
    Content-Length: 6
    Content-Type: application/x-www-form-urlencoded
    test=1
    送信されたリクエスト

    View Slide

  16. requests.get(
    url, headers={'User-Agent': 'sximada'})
    headers引数に書き換えたいUser-Agentをもったdict
    を渡す
    User-Agent書き換え

    View Slide

  17. ipdb> print(self.request.recv(1024).decode())
    GET / HTTP/1.1
    Host: 127.0.0.1:8888
    Accept: */*
    Accept-Encoding: gzip, deflate
    User-Agent: sximada <- ここ
    Connection: keep-alive
    同じ要領でヘッダ情報を書き換えることがで
    きる
    送信されたリクエスト

    View Slide

  18. 便利

    View Slide


  19. View Slide

  20. PycURL
    https://pypi.python.org/pypi/pycurl

    View Slide

  21. httpライブラリ
    libcurlを使っているのでインストールにはlibcurlが必要
    直接ファイルに書き出したりできる
    正直ちょっと癖があるから特別な理由がない限りあんまり使わない..でも
    import pycurl
    PycURL

    View Slide

  22. curl = pycurl.Curl()
    curl.setopt(pycurl.URL, 'http://127.0.0.1:8888')
    curl.perform()
    GET

    View Slide

  23. GET / HTTP/1.1
    Host: 127.0.0.1:8888
    User-Agent: PycURL/7.43.0 libcurl/7.43.0 OpenSSL/1.0.2d zlib/1.2.8
    Accept: */*
    送信されたリクエスト

    View Slide

  24. ⾒切れたUA
    PycURL/7.43.0 libcurl/7.43.0 OpenSSL/1.0.2d
    zlib/1.2.8
    ...orz
    送信されたリクエスト

    View Slide

  25. ステータスコード
    curl.getinfo(pycurl.RESPONSE_CODE)
    curl は pycurl.Curl() で⽣成したインスタンス
    結果の取得 1

    View Slide

  26. get-heaer.py
    import io
    import pycurl
    fp = io.BytesIO()
    curl = pycurl.Curl()
    curl.setopt(pycurl.URL, 'http://127.0.0.1:8888')
    curl.setopt(pycurl.WRITEHEADER, fp)
    curl.perform()
    結果の取得 2 header

    View Slide

  27. 実⾏してみる
    $ python -i get-header.py
    >>> fp.seek(0)
    0
    >>> print(fp.read().decode())
    HTTP/1.1 200 OK
    Server: TornadoServer/4.2
    Date: Sat, 02 Jul 2016 06:29:15 GMT
    Receive: GET
    Etag: "ed4018fbd5f2a3d40c683820df3862f67b322328"
    Content-Type: text/html; charset=UTF-8
    Content-Length: 11
    >>>
    WRITEHEADERを指定しないとsys.stdoutに書き出す
    結果の取得 2 header

    View Slide

  28. ⾯倒
    結果の取得 2 header

    View Slide

  29. get-body.py
    import io
    import pycurl
    fp = io.BytesIO()
    curl = pycurl.Curl()
    curl.setopt(pycurl.URL, 'http://127.0.0.1:8888')
    curl.setopt(pycurl.WRITEDATA, fp)
    curl.perform()
    結果の取得 3 body

    View Slide

  30. 実⾏してみる
    $ python -i get-body.py
    >>> fp.seek(0)
    0
    >>> print(fp.read().decode())
    receive GET
    >>>
    結果の取得 3 body

    View Slide

  31. 結構⾯倒....
    結果の取得 3 body

    View Slide

  32. ただし⼤きなファイルとかを
    ダウンロードするときは
    直接ファイルに書き出せる
    結果の取得 3 body

    View Slide

  33. curl = pycurl.Curl()
    curl.setopt(pycurl.URL, 'http://127.0.0.1:8888')
    curl.setopt(pycurl.CUSTOMREQUEST, 'POST')
    curl.perform()
    curl.setopt()を使って
    pycurl.CUSTOMREQUESTに'POST'を設定する
    POST

    View Slide

  34. PUT
    curl.setopt(pycurl.CUSTOMREQUEST, 'PUT')
    HEAD
    curl.setopt(pycurl.CUSTOMREQUEST, 'HEAD')
    DELETE
    curl.setopt(pycurl.CUSTOMREQUEST, 'DELETE')
    OPTIONS
    curl.setopt(pycurl.CUSTOMREQUEST, 'OPTIONS')
    その他

    View Slide

  35. ⼤きなファイルを途中からダウンロードしたり
    複数のリクエストを送って分割してダウンロードしたり
    (ただしサーバがresumeに対応している必要がある)
    分割ダウンロード

    View Slide

  36. 100byte⽬からダウンロードする例
    import pycurl
    url = 'https://gist.githubusercontent.com/TakesxiSximada/d2792ef0b6ef2947402cca
    curl = pycurl.Curl()
    curl.setopt(pycurl.URL, url)
    curl.setopt(pycurl.RESUME_FROM, 100)
    curl.perform()
    分割ダウンロード

    View Slide

  37. 送信されるリクエスト
    GET / HTTP/1.1
    Host: 127.0.0.1:8888
    Range: bytes=100-
    User-Agent: PycURL/7.43.0 libcurl/7.43.0 OpenSSL/1.0.2d zlib/1.2.8
    Accept: */*
    分割ダウンロード

    View Slide

  38. Rangeヘッダー
    Range: bytes=100-
    このヘッダがそのファイルのseek位置を指している。
    requestsで同じことをやろうと思った場合は
    http://stackoverflow.com/questions/22894211/how-to-resume-file-
    download-in-python?answertab=votes#tab-top が参考になる。
    分割ダウンロード

    View Slide

  39. ダウンロードやアップロードなどに時間がかかる場合
    CLIツールであればプログレスバーで進捗を表⽰すると
    ちょっとかっこいい。
    90% |########### | <- こんなの
    プログレスバー

    View Slide

  40. ライブラリあります!!
    progressbar
    https://pypi.python.org/pypi/progressbar
    プログレスバー

    View Slide

  41. import io
    import pycurl
    import progressbar
    fp = io.BytesIO()
    curl = pycurl.Curl()
    curl.setopt(pycurl.URL, 'おおきなファイルのURL') # noqa
    curl.setopt(pycurl.NOPROGRESS, 0)
    curl.setopt(pycurl.WRITEDATA, fp)
    progress = progressbar.ProgressBar()
    def update(total_to_download, total_downloaded, total_to_upload, total_uploaded
    if total_to_download:
    percent = int(total_downloaded / total_to_download * 100)
    progress.update(percent)
    curl.setopt(pycurl.PROGRESSFUNCTION, update)
    try:
    progress.start()
    curl.perform()
    finally:
    progress.finish()
    プログレスバー

    View Slide

  42. コード収まらなかった...
    gistでどうぞ
    https://gist.github.com/TakesxiSximada/5f8d41cd81fe40e970249fc6da3aea60
    プログレスバー

    View Slide

  43. 忘れちゃいけないこと
    try:
    progress.start()
    curl.perform()
    finally:
    progress.finish() # <----- これ
    忘れるとターミナルが壊れる
    プログレスバー

    View Slide

  44. 実⾏はこんな感じ
    $ python progress.py
    100% |#########################################################################
    プログレスバー

    View Slide

  45. (-_-;

    View Slide

  46. pycurlのwrapperでhuman_curlというものもある
    https://pypi.python.org/pypi/human_curl
    使ったことないけど...
    その他

    View Slide


  47. View Slide

  48. BeautifulSoup4
    https://pypi.python.org/pypi/beautifulsoup4

    View Slide

  49. HTML/XMLパーサ
    とてもよく使われている
    pip install beautifulsoup でインストールされるのは beautifulsoupの
    version3系(旧バージョン)という罠
    BeautifulSoup4使ってください
    sourceが欲しい⼈は $ bzr branch lp:beautifulsoup
    gitじゃないよ!!
    BeautifulSoup4

    View Slide

  50. ざっくり分けると2通り
    解析⽅法

    View Slide

  51. BeautifulSoup Style
    BeautifulSoupの独⾃スタイル
    正式な呼び名は知らん
    soup.find_all(id='link2', class_="sister")
    解析⽅法 1

    View Slide

  52. CSS Selector
    CSSやJavascriptのquerySelector()の時のアレ
    他の⾔語やライブラリも対応していることが多い
    ブラウザの開発者ツールで確認できる
    soup.select('#link2.sister')
    解析⽅法 2

    View Slide

  53. https://pypi.python.org/pypi からパッケージ名を刈り取る
    package名のリンクのaタグを取得するcss selector
    (注) aタグを取得する (aタグのtextNodeではない)
    #content table.list tr td a
    PyPIを解析してみる

    View Slide

  54. サンプルコード
    import bs4
    import requests
    res = requests.get('PyPIのURL')
    soup = bs4.BeautifulSoup(res.content, 'html.parser')
    package_names = [
    elm.getText()
    for elm in soup.select(
    '#content table.list tr td a')]
    aタグのリストを取得したのちgetText()でtextNondeの値を取り出している
    PyPIを解析してみる

    View Slide

  55. BeautifulSoup3は古いversion
    BeautifulSoup4を使ってください
    BeautifulSoup4移⾏ガイドはこちら
    https://www.crummy.com/software/BeautifulSoup/bs4/doc/#porting-code-
    to-bs4
    BeautifulSoup3 と BeautifulSoup4

    View Slide


  56. View Slide

  57. lxml
    https://pypi.python.org/pypi/lxml/3.6.0

    View Slide

  58. HTML/XMLパーサ
    XPATHを使って解析する
    install時にlibxml2 と libxslt が必要 <- だいたいここで若⼲ハマる
    ドキュメントを読もう http://lxml.de/
    lxml

    View Slide

  59. XPATHとは
    XML Path Language (XPath(エックスパス))
    は、マークアップ⾔語 XML に準拠した⽂書の特定の
    部分を指定する⾔語構⽂である。
    Wikipediaより https://ja.wikipedia.org/wiki/XML_Path_Language
    //a[@href='help'] こんなの
    lxml

    View Slide

  60. https://pypi.python.org/pypi からパッケージ名を刈り取る
    https://pypi.python.org/pypi のpackage名をstrで取得するXPATH
    (注) textNodeの値をstrで取得している
    //div[@id="content"]//table[@class="list"
    ]//tr/td/a/text()
    pypiを解析してみる

    View Slide

  61. サンプルコード
    from lxml import etree
    import requests
    xpath = '//div[@id="content"]//table[@class="list"]//tr/td/a/text()'
    res = requests.get('PyPIのURL')
    html = etree.HTML(res.content)
    package_name = html.xpath(xpath)
    print(package_name)
    XPATHではtextNondeを直接指定できるため、 beautifulsoup4のように要素を取
    得してから textNondeを取る必要がない
    pypiを解析してみる

    View Slide

  62. beautifulsoup4を使ってて⾟くなったらlxmlに乗り換える感じが多いかも
    スクレイピングする場合、まずブラウザの開発者ツールでcss selectorを特定し
    てからコードに落とすことがおおいのでbeautifulsoup4の⽅がcss selectorを
    そのまま使えて便利
    表現⼒的にはXPATHの⽅が強⼒なのでlxmlの⽅が余計な処理をしなくてすむ
    ゴミHTMLとかを⾷わせる場合はlxmlにした⽅がトラブルに⾒舞われることは
    少ないかも
    あとは好み....
    正直どっちでもいいし、どっちかやってて、トラブったら変えてるのもいいかも <-
    なんでそういう設計にしておく必要が有る
    beautifulsoup4 vs lxml

    View Slide


  63. View Slide

  64. feedparser
    https://pypi.python.org/pypi/feedparser

    View Slide

  65. feedの取得と解析⽤のライブラリ
    かなりシンプル
    対応feed
    RSS 0.9x,
    RSS 1.0,
    RSS 2.0,
    CDF,
    Atom 0.3,
    Atom 1.0
    feedparser

    View Slide

  66. import feedparser
    res = feedparser.parse('https://pypi.python.org/pypi?%3Aaction=rss')
    resはfeedparser.FeedParserDict
    feedparser.FeedParserDict はdict like object
    pypiのrss feedを解析してみる

    View Slide


  67. View Slide

  68. selenium
    https://pypi.python.org/pypi/selenium

    View Slide

  69. SeleniumのPythonバインディング
    FirefoxやChromeやIEなどのブラウザをscriptで操作できる
    PhantomJSも使えるのでヘッドレスでもいける
    起動が遅い
    selenium

    View Slide

  70. 参考: http://qiita.com/TakesxiSximada/items/dedd81f1da4379f3006e
    Firefoxの起動

    View Slide

  71. Profileの作成
    from selenium.webdriver import FirefoxProfile
    default_profile = {
    'security.warn_entering_secure': False,
    'security.warn_entering_secure.show_once': True,
    'security.warn_entering_weak': False,
    'security.warn_entering_weak._show_once': True,
    'security.warn_leaving_secure': False,
    'security.warn_leaving_secure.show_once': True,
    'security.warn_leaving_weak': False,
    'security.warn_leaving_weak._show_once': True,
    'security.warn_submit_insecure': False,
    'security.warn_viewing_mixed': False,
    'security.warn_viewing_mixed.show_once': True,
    }
    profile = FirefoxProfile()
    for name, value in default_profile.items():
    profile.set_preference(name, value)
    Firefoxの起動

    View Slide

  72. 起動
    from selenium.webdriver import Firefox
    browser = Firefox(firefox_profile=profile, proxy=proxy)
    # pageをloadするまでの待ち時間を設定
    browser.implicitly_wait = 10
    # Cookieを全消し
    browser.delete_allcookies()
    Firefoxの起動

    View Slide

  73. 本当にFirefoxが起動する
    ちゃんと終了しないとブラウザが残る
    起動しているブラウザを⼿で操作してしまうと scriptで操作できなくなる
    Firefoxの起動

    View Slide

  74. nodejs製のヘッドレス(実際には描画されない)ブラウザ
    ただしcssやjsの展開はしてくれる
    そのため通常のブラウザと同じようにscriptから操作できる
    http://phantomjs.org/
    PhantomJSの起動

    View Slide

  75. from selenium.webdriver.phantomjs.webdriver import WebDriver
    driver = WebDriver()
    PhantomJSの起動

    View Slide

  76. ページ遷移
    browser.get(url)
    タグの取得(id)
    browser.find_element_by_id('elemet-id')
    タグの取得(class)
    browser.find_elements_by_class_name('class-name')
    ブラウザの主な操作 1

    View Slide

  77. タグの取得(css selector)
    browser.find_elements_by_css_selector('input')
    タグの取得(xpath)
    browser.find_element_by_xpath('input')
    ブラウザの主な操作 2

    View Slide

  78. クリック
    tag.click()
    ⽂字の⼊⼒
    textbox.send_keys('CHARACTOR')
    タグの主な操作

    View Slide

  79. LinuxやOSXでPhantomJSをnpmを使ってインストールした場合PhantomJSの
    プロセスが正常に終了せずzombieになることがある
    selenium(親) -> phantomjs(⼦) -> pahtomjs.js(孫)
    終了時はSIGTERM + SIGKILLで終了している
    ゴーストドライバモードのphantomjsのshutdonw APIを使っていない
    PhantomJSを使った時の注意点

    View Slide

  80. 通常の場合
    親 ⼦ 孫 メモ
    SIGTERM -> handle
    SIGTERM -> handle
    終了
    SIGKILL -> handle
    終了
    PhantomJSを使った時の注意点

    View Slide

  81. 問題が発⽣する場合
    親 ⼦ 孫 メモ
    SIGTERM -> handle
    SIGKILL -> handle ⼦がSIGTERMを孫に送る前にSIGKILL到達
    終了
    ZOMBIE
    PhantomJSを使った時の注意点

    View Slide

  82. 現状pkillで全部KILLしちゃうか
    ライブラリに⼿を⼊れる必要がある
    PhantomJSを使った時の注意点

    View Slide

  83. (-_-;

    View Slide


  84. View Slide

  85. scrapy
    https://pypi.python.org/pypi/Scrapy/1.1.0

    View Slide

  86. スクレイピングとかクローラを作るためのフレームワーク
    Python3対応された!!
    scrapy

    View Slide

  87. とりあえずscaffoldingしてみる
    scrapy

    View Slide

  88. scrapy startproject PROJECT_NAME
    scaffolding project

    View Slide

  89. scrapy genspider DOMAIN
    scaffolding spider

    View Slide

  90. ./scrapy.cfg
    ./scrapyexample
    ./scrapyexample/__init__.py
    ./scrapyexample/items.py
    ./scrapyexample/pipelines.py
    ./scrapyexample/settings.py
    ./scrapyexample/spiders
    ./scrapyexample/spiders/__init__.py
    ./scrapyexample/spiders/example.py
    構成

    View Slide

  91. このドキュメントを読むとイメージがつかめ

    http://doc.scrapy.org/en/master/topics/architecture.html
    構成

    View Slide

  92. Spider
    Webサイトをどのようにクロールするか
    responseをどのように扱うかを指定
    class ExampleSpider(Spider):
    name = "example"
    allowed_domains = ["example.com"]
    start_urls = (
    r'https://example.com/pypi?%3Aaction=rss', # ... (^^;
    )
    def parse(self, response):
    feed = feedparser.parse(response.body)
    for record in feed.entries:
    release = Release(record)
    item = ReleaseItem()
    item['name'] = release.name
    item['version'] = release.version
    item['link'] = release.link
    item['summary'] = release.summary
    yield item

    View Slide

  93. Pipeline
    Spiderで⽣成したobjectを渡されて処理をする
    例えばDBに保存したりする処理はここで⾏う
    class ExamplePipeline(object):
    @classmethod
    def from_crawler(cls, crawler):
    return cls(
    url=crawler.settings.get('DB_URL'),
    )
    def __init__(self, url):
    self.url = url
    def open_spider(self, spider):
    self.datastore = create_datastore(self.url)
    def process_item(self, item, spider):
    self.datastore.register(item)

    View Slide

  94. Item
    Spiderで⽣成されてPipelineに渡されるobject
    どんな属性を持つ必要があるかを定義する
    import scrapy
    class ReleaseItem(scrapy.Item):
    name = scrapy.Field()
    version = scrapy.Field()
    link = scrapy.Field()
    summary = scrapy.Field()

    View Slide

  95. 実⾏

    View Slide

  96. spider実⾏
    $ scrapy runspider
    path_to_your/spiders/example.py
    crawl実⾏
    $ scrapy crawl example
    実⾏

    View Slide

  97. おおがかり
    scrapyの流儀に従う必要がある
    ちょっとデータを抽出したい的案件には向かない
    システム化するときには導⼊を考えたい
    xpathもcss selectorも使える (good)
    scrapyべったりに実装するとscrapyと⼼中しそう
    scrapingはコマンドとか他の処理でも使いたいことが多いからcomponent化
    したい
    scrapy

    View Slide

  98. フレームワークだからそう
    いうものか...

    View Slide

  99. そろそろまとめ

    View Slide

  100. スクレイピングで使われるライブラリの基本的な使い⽅を紹介した
    Tips的な⼩ネタを紹介した
    ただ必ずしもこうしなければいけないというわけではない
    まとめ

    View Slide

  101. みんなやろうよ
    Webスクレピング

    View Slide