- 知見まとめ -
最初に言うまでもないが、Pythonのバージョンは3系を選択すべきである。
2系ではユニコードの問題に悩まされ、小学生にもバカにされる。
とにかくencodingに悩まされる事を防ぎたければ3.x系。
requests
pip install requests
標準的なHTTPライブラリ。
HTTPを使うならrequestsにするのが吉。
Python 2.x系では標準としてurllibとurllib2があり、urllibはPython 3.xでは廃止され、urllib2もurllib.requestとなった。そのurllibをメインページ(http://requests-docs-ja.readthedocs.io/en/latest/)で「APIがまともに使えません」「Python的ではない」とまで言うのがrequestsというライブラリである。
それら以外にもurllib3(requests内部でも使われている)やhttplib、http.clientなど多数HTTPライブラリがあるのがPythonの現状。
PythonのHTTPライブラリ urllib, urllib2, urllib3, httplib, httplib2 … | スラド
日本語リファレンスには書いてない話:urllibとurllib2の違いってなんだ « DailyHckr
メインページで自ら「人が使いやすいように作られた、エレガントでシンプルなPythonのHTTPライブラリ」と言い切るクールなやつ。
スクレイピングに関してのみ挙げておくと以下が簡易に行えるのがメリット。
HTTPリクエストかける時は、timeoutとheaderをかけて実行するのが吉。
timeoutに関しては30secを設定する等のネット記事が多々見られる。
以下によればtimeoutはページのロード時間を含まないので、ガッツリWebスクレイピングをかけるならボトルネックにならないよう短くしておいて大丈夫。ちなみにデフォルトでは60sec。
http://requests-docs-ja.readthedocs.io/en/latest/user/quickstart/#timeouts
headerには自身の正しいブラウザとOSをUser-Agentとして設定しておきましょう。
UserAgentString.com - unknown version
import requests
headers = {"User-Agent": "hoge"}
resp = requests.get(URL, timeout=1, headers=headers)
print(resp.text)
requestsではオレオレ証明書等を利用したWebサービスやお固めのWebページを取得しようとすると以下のようなエラーがでる場合がある。
Caused by NewConnectionError('<urllib3.connection.VerifiedHTTPSConnection object at 0x7fe861db6908>: Failed to establish a new connection: [Errno 111] Connection refused',))
[CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:749)
UNKNOWN_PROTOCOL
SSL関連についてはverifyなるフラグが設定できるので以下のように。
requests.get(URL, timeout=1, headers=headers, verify=False)
その他post, deleteなどのメソッドの他、oauth認証やストリーム接続も用意されている。
このやり方でHTMLを取得した場合文字化けする可能性があるが、そちらについては後述する。
Python for iOSなどではデフォルトでrequestsがインテグレーションされている。良い。
あとはHTTP処理をガツガツ回す前にDNSの設定をしていた方が良い。
理由としては以下
Robobrowser
mechanize、PyQueryと似たパッケージとしてRobobrowserが存在する。
導入はpipでできる
pip install Robobrowser
RoboBrowserの特徴として「Browserを操作するようにコードが書ける」点がある。
from robobrowser import RoboBrowser
browser = RoboBrowser(parser=html5lib)
broswer.open('hogehoge.com')
link = browser.get_link('form_link')
browser.follow_link(link)
form = browser.get_form(action='hogehoge.com/forms')
form['email'] = 'hogehoge@mail.com'
browser.submit_form(form, headers={
"Referer": browser.url,
"Accept-Language": "utf-8"})
順序よく書けるだけでなく、フォーム入力やスライドバーの操作などが直感的に書ける。
すでに決まった操作を自動化する場合などに良い。
GitHub - jmcarp/robobrowser
Welcome to robobrowser’s documentation! — robobrowser 0.1 documentation
Selenium.webdriverとPhantomJS
requests、MechanizeだけではJavaScript等動的な表示が進むタイプのWebページは上手く取得できないので、ブラウザ経由でhtmlを取得する。
そのためにSeleniumとブラウザを直接Pythonから触る方法利用する。
PhantomJS
FireFox等の既存ブラウザも操作できなくないが、いちいちGUI開くと重いとかいうアレがあるのでPhantomJSなるWebkitベースのヘッドレスブラウザを使う。
PhantomJS | PhantomJS
ヘッドレスブラウザは別に他にもあるのだけれど、正直これしかないからみんな使っているというアレ (かなりバギーである)。
使った感想だと次点でCasperJSが良い(Seleniumからの操作もできるが中身がPhantomJSなのであまり変わりはないが)。
※記事最後記述:今はChromeとFirefoxがheadless対応しておりベストはChromeかと
headless browserメモ - なっく日報 http://yukidarake.hateblo.jp/entry/2014/09/12/204813
各headlessブラウザをまとめたRepositoryも
github.com
Seleniumから利用できる、各ブラウザの関係性等は今のところ以下の記事がわかりやすくまとまっている。
qiita.com
最近jsによる動的なWebページがめちゃくちゃ多い事もあって、スクレイピングでのJS実行環境は作るべきではある。
(専用の色々一緒に作りたい人居ないかな…)
PhantomJSはWindows環境下だと少し面倒(PATHとかexeプロセスの処理とか)。
Linux環境なら以下で導入できる。
wget -O /tmp/phantomjs-2.1.1-linux-x86_64.tar.bz2 https://bitbucket.org/ariya/phantomjs/downloads/phantomjs-2.1.1-linux-x86_64.tar.bz2
cd /tmp
bzip2 -dc /tmp/phantomjs-2.1.1-linux-x86_64.tar.bz2 | tar xvf -
sudo mv /tmp/phantomjs-2.1.1-linux-x86_64/bin/phantomjs /usr/local/bin/
phantomjs --version
Win環境ならexeを落としてきてPATHを通せばある程度は動く。
Thread環境で動かすとゾンビプロセスになりやすいので注意が必要。
SeleniumとPhontomJSを利用した実装
Selenium経由でPhantomJSを利用する最もベターな実装は以下
from selenium import webdriver
driver = webdriver.PhantomJS()
driver.get(url)
print(driver.page_source)
前述のrequestsと同様timeoutを設定する必要がある。
細かくセットするなら以下のようにセットできる。
デフォルトは全て30秒となっている。
driver = webdriver.PhantomJS(
desired_capabilities={
'phantomjs.page.settings.resourceTimeout': str(30)})
driver.set_script_timeout = 30
driver.timeout = 30
driver.implicitly_wait = 30
driver.set_page_load_timeout = 30
色々なWeb記事を見ると「ページを読み込むまでtime.sleep()を設定しよう!」というものがあるが、selenium.webdriver.supportを使う事で、ページのロード待機や操作の最大TIMEOUTを設定できるので、上記詳細設定が不要な場合はこちらを利用する。
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait
from selenium import webdriver
driver = webdriver.PhantomJS()
driver.set_page_load_timeout(10)
driver_wait = WebDriverWait(driver, 10)
driver = driver.get(url)
driver_wait.until(ec.presence_of_all_elements_located)
Exceptionが発生するのでcatchすればよい。
ページのロードが終わると、Webページがリダイレクトされ別のサイトに飛んでいる場合がある。
urlを適宜確認して、リダイレクトが発生してないか確認する実装が必要である。
driver.current_url
ここまでで「じゃあrequests要ら無くない?」となるが、seleniumのwebdriverはstatus_code取得が実装されていない。
スクレイピングだと問題なのだが「本来Seleniumはテストツールであり、幾回かHTTP通信を行っている。そのため完全なstatus_codeの取得およびそれに伴った改修は難しい」といった議論が既にある。
WebDriver lacks HTTP response header and status code methods · Issue #141 · SeleniumHQ/selenium-google-code-issue-archive · GitHub
How to get status code by using selenium.py (python code) - Stack Overflow
そこで後述するようにstatus_codeの取得やencodingの取得をrequestsで別途行ったりするのが良い。
また、PhantomJSを実行する際には、requests同様User-Agentの設定等を行っておくと吉。
desired_capabilitiesにブラウザ設定を、service_argsで引数を設定できる。
スクレイピングで重要そうな設定は以下のような感じ。
dcap = {
"phantomjs.page.settings.userAgent": "hoge",
"marionette": true}
args = ["--ignore-ssl-errors=true",
"--ssl-protocol=any",
"--proxy=hogehoge:0000",
"--disk-cache=false",
"--load-images=false",
"--script-encoding=utf-8",
"--output-encoding=utf-8"
]
driver = webdriver.PhantomJS(desired_capabilities=dcap, service_args=args)
中でも引数に利用する--script-encoding辺りが厄介で、scriptさえencodingできずWebスクレイピングできないといった場合を見越して事前にセットする必要がある。
ここのencodingの取得の為に、別途requestsで投げるのが良いのではないかなと思う。
ちなみに利用できる引数のリストはここ。
Command Line Interface | PhantomJS
他にもログ出力の設定やweb-securityの指定ができる。
あとPhantomJSにはdriver.close()なるメソッドがあり、これによってdriverのProcessを殺す事ができる。
クローズしないとメモリをガバガバ食っていくのでちゃんとクローズしましょう。
driver.close()
PhantomJSを利用していると多々あるのが、driver.close()の失敗、driver.get()が全てErrorに、といったProcessのゾンビ化の問題である。
普通にコマンドでProcess killを定期的に実行するだけだが、まあまあ面倒。
pgrep -f 'phantom' | xargs kill
呼応してheadless Chromeなるものもあって、開発が停止されたもんだと思ったら最近日本語記事が更新されてビビる。
ヘッドレス Chrome ことはじめ | Web | Google Developers https://developers.google.com/web/updates/2017/04/headless-chrome?hl=ja
そして油断している間にPhantomJSのメンテナが辞める。
www.infoq.com
そしてheadless Chromeを使う記事も自分で書きました。
(PhantomJSよりこっちが良いです...)
vaaaaaanquish.hatenablog.com
今ならこのあたりが分かりやすいです。
Headless Chrome をさわってみた | CYOKODOG
またFirefoxも。
vaaaaaanquish.hatenablog.com
記事最後にFirefoxもheadlessに対応した旨等の経緯を書いていますが、Chromeが現状最高です。
urllib.parse
本記事の最初の方でrequests辺りにけちょんけちょんに書いたurlibだが、Python3以降、urlib.parseにurlparse, urljoinなるWebスクレイピングに必要な関数が統合されたので一応書いておく。
21.8. urllib.parse — URL を解析して構成要素にする — Python 3.6.5 ドキュメント
urlparse, urljoinはよしなにURLの操作をしてくれる。
例えば、URLのドメイン部が欲しい場合は以下のように書ける。
from urllib.parse import urlparse
if not url.startswith("http"):
url = "http://" + url
parsed_url = urlparse(url)
domain = parsed_url.netloc
paramsを切り離さないurlsplitもあるので適宜活用すると吉。
他にもURLをよしなに接続したい場合
(http://hogehoge.com/main/と/main/index.htmlをよしなに繋ぎたいなど)
url = urljoin("http://hogehoge.com/main/", "/main/index.html")
print(url)
この辺りurlib以外でもっと便利なメソッドが多いのでチェックしておくと良い。
chardet, cchardetによるエンコーディング検出
Webスクレイピングをしている際に非常に厄介なのが多岐に渡る「エンコーディングの種類」である。
utf-8以外のWebサイトみんな地獄で呪ってやるという気持ちになる時もある。
cp932なMicrosoftもIBMも絶対許さんという気持ちになる時もある。
ちなみにrequestsはデフォルトで一応、encoding推定が動作する。
デフォルトではHTTPヘッダに基いて推定されるようだが、ヘッダに文字が入っている場合に、Content-Typeのエンコーディング、タグのエンコーディングを無視してエンコーディングをISO-8859-1としてしまう(RFC 2616のデフォルト値である)。
Quickstart — Requests 2.18.4 documentation
http://d.tpdn.kim/2014/10/11/python-requests-mojibake
requestsでは上記代替としてapparent_encodingなるメソッドも用意されている。
import requests
response = requests.get(URL)
print(response.encoding)
response.encoding = response.apparent_encoding
print(response.text)
apparent_encodingの中身はchardetで実装された文字エンコーディング推定。
requests/models.py at bd3cf95e34aa49c8d764c899672048df107e0d70 · requests/requests · GitHub
GitHub - chardet/chardet: Python 2/3 compatible character encoding detector.
chardetはよくできたエンコーディング推定器であるのだが、中身がPythonで処理が重い。
Webスクレイピングの速度に耐えきれない程重い場合があるので、C実装のcChardetなるライブラリを利用すると吉。
github.com
import cchardet
resp = requests.get(URL)
ccencoding = cchardet.detect(resp.content)["encoding"]
if len(ccencoding):
resp.encoding = ccencoding.lower()
encoding = ccencoding
print(resp.text, encoding)
以下にもあるように、Python2系の頃はエンコーディングに関して色々なパッケージがあったようだが、ユニコード問題も解決し今のところはcchardet一強かなという感じ。
Python でエンコーディングを判定する | 傀儡師の館.Python - 楽天ブログ
ppkf で日本語の文字コード判別 | 傀儡師の館.Python - 楽天ブログ
上記記事内の中であれば「easy_install pykf」「pip install cssutils」は成功する。
pykf プロジェクト日本語トップページ - OSDN
encutils - cTHEdot
どちらも以下のように文字コードが判定できる
import encutils
import pykf
encutils.tryEncodings(html)
pykf.guess(html)
pykfは文字コード変換ツールであるので、利用の仕方によっては強い面があるが、若干重いのが欠点である。
機種依存文字などに対応してるのは良い。
機種依存文字対策でpykfをインストールしてみた
encutilsはgetEncodingInfoがCSSの中身を見に行っているため、Webサイトならかなり高い確率で正答する。
parseString等のメソッドがあり、CSS特化のパッケージではあるものの場合によっては採用できる。
これまでの事も含めて、requestsでstatus_codeの確認とcchardetによるencodingの検出(ISO-8859-1以外のContent-Typeを利用しつつ)をして、WebDriverに渡しましょうというコード。
import requests
import cchardet
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait
from selenium import webdriver
URL = "http://hogehoge.com"
headers = {"User-Agent": "hoge"}
resp = requests.get(URL, timeout=1, headers=headers, verify=False)
if resp.status_code != 200:
print("200じゃない時の処理")
encoding = resp.encoding
ccencoding = cchardet.detect(resp.content)["encoding"]
if len(ccencoding) > 0 and encoding != "ISO-8859-1":
resp.encoding = ccencoding.lower()
encoding = ccencoding
dcap = {
"phantomjs.page.settings.userAgent": "hoge",
"marionette": true}
args = ["--ignore-ssl-errors=true", "--ssl-protocol=any", "--proxy=hogehoge:0000",
"--disk-cache=false", "--load-images=false", "--output-encoding=utf-8",
"--script-encoding={}".format(encoding)]
driver = webdriver.PhantomJS(desired_capabilities=dcap, service_args=args)
driver.set_page_load_timeout(10)
driver_wait = WebDriverWait(driver, 10)
driver = driver.get(URL)
driver_wait.until(ec.presence_of_all_elements_located)
html = driver.page_source
from bs4 import BeautifulSoup
try:
soup = BeautifulSoup(html, "lxml")
except:
soup = BeautifulSoup(html, "html5lib")
print(soup.get)
多分これが一番つよいと思います。
timeout_decoratorによるタイムアウト
上記のような設定をしても、複数のWebスクレイピングを回している時にはSocketの不足やHTTPメソッドが返ってこない等によるPythonのフリーズが発生する。
PhantomJSにせよHTTP通信を使っているメソッドは、通信状態によって設定したtimeoutを無視して周り続けたりゾンビプロセスになってしまう場合がある。
デコレータで関数にタイムアウトを実装できるtimeout-decorator使う。
導入はpip
pip install timeout-decorator
デコレータでtimeout_decoratorを付けてあげると良さげ。
@timeout_decorator.timeout(5, timeout_exception=StopIteration)
def print_while():
while(1):
print(".")
try:
print_while()
except StopIteration as es:
print(es, "Stopped")
Windowsなる不自由なOSだと(多分bash.exe経由したりすると) module 'signal' has no attribute 'SIGALRM' で使えそうにないが、概ね良さそう。
joblib, multiprocess, asyncio, threadingで並列処理
PythonでWebスクレイピングするなら並列、もしくは非同期処理が必要になる事がある。
一般的に想像される"並列処理"ならjoblib, multiprosessing、"非同期処理"ならasyncioを使うと大体良い(正確にはどちらもできる)。
それぞれの違い、及びasyncioの手引はicoxfog417氏の下記記事が最も分かりやすいと思います。
Pythonにおける非同期処理: asyncio逆引きリファレンス - Qiita http://qiita.com/icoxfog417/items/07cbf5110ca82629aca0
asyncioはPython3.4から標準化されたパッケージで、最初こそ不安定さでissueだらけだったのだが大分安定的に並列処理できるようになった模様。
ただ、未だ若干の記述のしにくさ、拡張のしにくさがある。
同様にPythonの標準モジュールにthreadingがあるが、1つ1つ継承したクラスを作ったりと若干記述が面倒である。
マルチプロセッシング(multiprocessing) - Python | Welcome to underground
asyncio, threadingのような非同期処理によるWebスクレイピングするのであれば、その辺りを担ったパッケージaiohttp, grequests, requests-futuresを使うのが吉(後述)。
joblib
joblibはpipで導入
pip install joblib
こちらの方が記述が簡単であり、子プロセスの自動削除など使いやすさがある。
n_jobsに-1を指定すれば使えるだけプロセスを使ってくれるし、1にすれば1Threadで普通に処理できるのでテストもしやすい。
from joblib import Parallel, delayed
Parallel(n_jobs=-1)(delayed(main)(x) for x in url_list)
ただjoblibは反応しなくなる事がまれにあるかなと使っていて感じた。
(多分各プロセスのマージ処理がかなり重たい)
以下記事ではmultiprocessingの方が高速という話もある。(真偽不明)
Python並列処理(multiprocessingとJoblib) - Qiita
aiohttp, grequests, requests-futures
非同期なHTTP通信を扱うライブラリ群は多々ある。
grequests
grequestsは非同期ネットワーク処理が可能なgeventにrequestsを乗せたライブラリ。
GitHub - kennethreitz/grequests: Requests + Gevent = <3
pip install grequests
かなり簡易に非同期なrequestsを扱える
import grequests
url_list = ["hoge", "piyo", ...]
header = {}
response = (grequests.get(url, headers=header) for url in url_list)
for x in grequests.map(response, size=3):
print(x.text)
はあ簡単。
sizeで処理数の指定もできるし、基本的にrequestsと同様に扱える。
JSの実行が不要ならこれで十分そうですね。
返るリストの失敗した箇所はNONEな注意すれば、requestsと使い方が同じで慣れやすい。
まだまだ全然開発途上という感じなのと、日本語の記事はキツいのしかないので今後また別で記事書くかも。
requests-futures
requests-futuresはgrequestsと違って標準moduleの中の並列処理のconcurrent.futuresに乗っかっているライブラリの1つ。
GitHub - ross/requests-futures: Asynchronous Python HTTP Requests for Humans using Futures
requests-futures 0.9.7 : Python Package Index
pipで導入できる。
pip install requests_futures
例えば以下のように書くだけで非同期処理が実行できる。
from requests_futures.sessions import FuturesSession
session = FuturesSession()
header = {}
one = session.get("hogehoge.com", headers=header)
two = session.get("hogehoge.com", headers=header)
response_one = one.result()
print(response_one.content)
response_two = two.result()
print(response_two.content)
人によってはgrequestsより直感的かもしれない。
grequests同様、中身がrequestsなので引数の管理もしやすい。
例えばだがresponse_twoの取得に失敗した場合、response_twoが「NameError: name 'response_two' is not defined」となってしまうのでそこだけ注意したい。
scrapy
色々書いたが、同じサービスで形式がほぼ決まっている(例えば特定のニュースサイトの記事をグルグルスクレイピングしたい, 機械学習向けの画像データを収集したい)ような場合は、既にScrapyなるフレームワークがあるのでそちらを利用した方が早い。
多分このフレームワーク1つで本が書ける程なので、ここでは紹介程度にしておきたい。
Scrapyの思想があまり好きになれないというのもありますが(HTMLを保存せず解析するため負荷が大きい, JSの実行は結局独Spiderを自前, あーだこーだ...)。
Scrapy CloudなるScrapy設定をdeployすれば勝手に回してくれるサービスがあったり、ScrapyだけでQiita連載書いてる人が居たりする。
シンプルにScrapy入門は良いと思うので読むと良いと思います。
Scrapy入門(1) - Qiita
2016年5月にPython3に対応したのですが、それまで2系でのネット記事や書籍出版が多く行われているので、注意が必要。
Scrapy | A Fast and Powerful Scraping and Web Crawling Framework
アーキテクチャが図としてあるので概念と紹介だけ。
Architecture overview — Scrapy 1.5.0 documentation
大きくは「Engine」「Scheduler」「Downloader」「Spiders」「Item Pipeline」の5つが絡んで動作している。
Engineが各コンポーネントのフロー制御を行い、Schedulerがスケジューリングとキューイングを行う。
DownloaderとSpidersが主にスクレイピングを行う部分で、Custom Classを適宜設定する事が可能。
Item Pipelineに流し込んでDBやtext、画像で保存するアーキテクチャ。
別々のコンポーネントをそれぞれ動かす事で高速な処理を可能にしており、Spidersをちょろっと書けばすぐスクレイピングを始められる事も含め非常に良くできている。
しかし、パラメータの数はゆうに100を超えるので調整がなかなか難しい。
また。サーバへの負荷が主にSleepでの調整だったり(別途記述しようと思えばできるが)と、JSを動かすにはwebdriver周りを結局自前で書くことになるので、大きめのフレームワークらしい欠点は備えている。
(便利かつデフォルト設定でよければすぐ回せるので自分も機械学習用の画像集めとかには使っています)