Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

今⽇はscrapingの ⼩ネタ紹介

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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') その他

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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 送信されたリクエスト

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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 送信されたリクエスト

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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 同じ要領でヘッダ情報を書き換えることがで きる 送信されたリクエスト

Slide 18

Slide 18 text

便利

Slide 19

Slide 19 text

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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: */* 送信されたリクエスト

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

実⾏してみる $ 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

Slide 28

Slide 28 text

⾯倒 結果の取得 2 header

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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() 分割ダウンロード

Slide 37

Slide 37 text

送信されるリクエスト 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: */* 分割ダウンロード

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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() プログレスバー

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

(-_-;

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

サンプルコード 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を解析してみる

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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を解析してみる

Slide 61

Slide 61 text

サンプルコード 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を解析してみる

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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の起動

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

(-_-;

Slide 84

Slide 84 text

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

とりあえずscaffoldingしてみる scrapy

Slide 88

Slide 88 text

scrapy startproject PROJECT_NAME scaffolding project

Slide 89

Slide 89 text

scrapy genspider DOMAIN scaffolding spider

Slide 90

Slide 90 text

./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 構成

Slide 91

Slide 91 text

このドキュメントを読むとイメージがつかめ る http://doc.scrapy.org/en/master/topics/architecture.html 構成

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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)

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

実⾏

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

そろそろまとめ

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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