Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

PythonでWebスクレイピングする時の知見をまとめておく

- はじめに -

最近はWebスクレイピングにお熱である。

趣味の機械学習のデータセット集めに利用したり、自身のカードの情報や各アカウントの支払い状況をスクレイピングしてスプレッドシートで管理したりしている。

最近この手の記事は多くあるものの「~してみた」から抜けた記事が見当たらないので、大規模に処理する場合も含めた大きめの記事として知見をまとめておく。


- 知見まとめ -

最初に言うまでもないが、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)
# textでunicode, contentでstr
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の設定をしていた方が良い。

理由としては以下


BeautifulSoup

pip install beautifulsoup4

「pip install beautifulsoup」だとbeautifulsoup3が入ってしまう。
所々module名が変わっていたりいる。違いはこの辺りを参照。
Beautiful Soup Documentation — Beautiful Soup 4.4.0 documentation https://www.crummy.com/software/BeautifulSoup/bs4/doc/index.html#porting-code-to-bs4

HTMLの構造解析をした上で、HTMLを綺麗に整形、問題のある点の修正等を行ってくれる。
簡易な所では、headタグがbodyタグの中にあったら丁寧に外出ししてくれるとか。
XML, RSS等にも対応している。

名前の元ネタは「Alice in Wonderland」に出てくる美味しいスープの歌らしい。
Alice in Wonderland: 1999 Professor Tortoise - YouTube

一番シンプルなところだと以下。

import requests
from bs4 import BeautifulSoup
resp = requests.get(URL)
soup = BeautifulSoup(resp.text)
# aタグの取得
print(soup.get("a"))
# 整形後のhtml表示
print(soup.prettify())

ガッツリスクリプトにして回す時は、解析器を選択しておくと吉。

  • html.parser
    • デフォルトで付いてくるやつ
    • 本当に些細なミスでも落ちるし中身がPythonなので遅い
    • Pythonのバージョンに依存して中身も違う
  • lxml
    • C言語の高速実装な解析器
    • 最近のWebの複雑な構造や動的な物に少し弱い
    • apt-get install libxml2-dev libxslt1-dev python-dev
    • apt-get install python-lxml
  • html5lib
    • html5の規則に対応
    • ブラウザで表示するのとほぼ同じメソッド
    • かなり重い
    • pip install html5lib

Beautiful Soup 4.x では parser を明示指定しよう - AWS / PHP / Python ちょいメモ http://hideharaaws.hatenablog.com/entry/2016/05/06/175056
Beautiful Soup Documentation — Beautiful Soup 4.4.0 documentation

パーサの良し悪しを考えるとlxmlでチャレンジしてダメならhtml5libを試すのが良さそう。

try:
    soup = BeautifulSoup(html, "lxml")
except:
    soup = BeautifulSoup(html, "html5lib")


parseして情報を得るだけならlxml単体でも可能である(lxmlはそもそもそういうパッケージ)。
lxml単体でやるメリットとしてlxml.html.fromstringでDOMのツリー構造を得られるとか、make_links_absolute()、rewrite_links()によって全てのリンクを相対パスに書き換えられたりとかが標準で実装されている事がある。
単体であればかなり、高速なので簡易なスクレイピング(特定のページ内からの取得など)ならBeautifulSoupを使わずlxmlで十分である。


Mechanize

上記のrequestsとBeautifulSoupくらいの機能ならMechanizeで十分間に合ったりする。
Mechanizeは元々PerlによるWebスクレイピングツールで、ブラウザをエミュレートして操作する事ができる。

最低限「何かにログインしてフォームを入力して情報を出してスクレイピング」ならMechanizeでよい。
(JavaScriptは動かないっぽい)

mechanize http://wwwsearch.sourceforge.net/mechanize/

pip install mechanize
import mechanize

# ブラウザをエミュレート
browser = mechanize.Browser()
response = browser.open(URL)
#HTMLを表示
print(response.read())

urllib2ベースな使い方もできる。

response = mechanize.urlopen("http://www.example.com/")
print(response.read())

デフォルトでrobots.txtスクレイピング禁止事項を読み込んでエラーを返してくれるのもありがたい。
(外す時はbrowser.set_handle_robots(False)とする)
BrowserのaddheadersでHeaderの追加、その他cacheの設定もできる。

標準的な要素指定だけでなく、form()やsubmit()ができるのがエミュレートしてるmechanizeの利点でもある。

pythonモジュールmechanizeでWeb上の作業を自動化する | TRIVIAL TECHNOLOGIES 4 @ats のイクメン日記
Python/mechanize - kuro-tech http://wiki.kurokobo.com/index.php?Python%2Fmechanize


PyQuery

mechanizeに似た、BeautifulSoup等より簡単にWebから情報を取得、かつjQueryライクなAPIを提供するパッケージとしてPyQueryがある。

導入はpipでできる。

pip install pyquery
from pyquery import PyQuery

pq = PyQuery(url='hogehoge.com')
print(pq)

引数にhtml形式のテキストを投げても良いし、urlパラメータにurlを投げればWebサイトを取得する事ができる。

# リンク(aタグ)の所得
for elem in pq.find('a'):
    q = PyQuery(elem)
    print(q.text())

かなり簡単。
普段からjQuery書いてる人はこれが親しみやすいかも。

中身で使われているパーサはlxmlなのでlxmlのインストールも忘れずに。

【python】ウェブスクレイピングで社内ツールを作る | 技術広場

ライブラリ:PyQuery - Life with Python
pyqueryでHTMLからデータを抽出 - 唯物是真 @Scaled_Wurm

 

Robobrowser

mechanize、PyQueryと似たパッケージとしてRobobrowserが存在する。

導入はpipでできる

pip install Robobrowser

RoboBrowserの特徴として「Browserを操作するようにコードが書ける」点がある。

from robobrowser import RoboBrowser

# インスタンス(ブラウザを開いてURLへ)
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から触る方法利用する。

Selenium

Selenium with PythonSelenium Python Bindings 2 documentation http://selenium-python.readthedocs.io/

Seleniumはブラウザオートメーションツールで、そもそもWebサービスのテスト用の統合ツール。
Webスクレイピング用ではなく、その為のメソッドが豊富な訳ではないが、今のところこれしかないのでみんな使っているという感じはある。
Pythonバインディングもあってwebdriver経由でブラウザを操作することができる。

Selenium何とかっていうツールがやたら色々あるのはどういうわけなのか | 品質向上ブログ http://blog.trident-qa.com/2013/05/so-many-seleniums/


導入はpipでできる

pip install selenium

 

PhantomJS

FireFox等の既存ブラウザも操作できなくないが、いちいちGUI開くと重いとかいうアレがあるのでPhantomJSなるWebkitベースのヘッドレスブラウザを使う。
PhantomJS | PhantomJS

ヘッドレスブラウザは別に他にもあるのだけれど、正直これしかないからみんな使っているというアレ (かなりバギーである)。
使った感想だと次点でCasperJSが良い(Seleniumからの操作もできるが中身がPhantomJSなのであまり変わりはないが)。

※記事最後記述:今はChromeFirefoxが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)
# htmlの表示
print(driver.page_source)

前述のrequestsと同様timeoutを設定する必要がある。
細かくセットするなら以下のようにセットできる。
デフォルトは全て30秒となっている。

# PhantomJS側のtimeoutを直接設定する
driver = webdriver.PhantomJS(
             desired_capabilities={
                 'phantomjs.page.settings.resourceTimeout': str(30)})
# 非同期なJSが動き続ける最大秒
driver.set_script_timeout = 30
# リクエストに使う最大秒
driver.timeout = 30
# 要素が見つかるまでの待機時間
driver.implicitly_wait = 30
# HTMLファイル群を読み込むためのtimeout
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)
# 操作した時のtimeoutする時間をdriverに設定
driver_wait = WebDriverWait(driver, 10)
# Webページの取得
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で引数を設定できる。
スクレイピングで重要そうな設定は以下のような感じ。

# Mozillaを指定, WebDriverにデフォルトのGhostDriverではなくFirefox後継のmarionetteを利用する
dcap = {
    "phantomjs.page.settings.userAgent": "hoge",
    "marionette": true}
# 引数の設定
args = ["--ignore-ssl-errors=true",   # sslエラーの許容
        "--ssl-protocol=any",         # sslプロトコルはよしなに
        "--proxy=hogehoge:0000",      # Proxyサーバがあるなら指定
        "--disk-cache=false",         # キャッシュを残さない
        "--load-images=false",        # 画像をロードしない(文書のスクレイピング等画像不要の場合に)
        "--script-encoding=utf-8",    # 既に特定済みの文字エンコードがあるなら(デフォutf-8)
        "--output-encoding=utf-8"     # 出力する際の文字エンコード(デフォ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.3 ドキュメント

urlparse, urljoinはよしなにURLの操作をしてくれる。
例えば、URLのドメイン部が欲しい場合は以下のように書ける。

from urllib.parse import urlparse
# httpを先頭に付与しないと上手くいかない場合がある
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)
# $ http://hogehoge.com/main/index.html


この辺りurlib以外でもっと便利なメソッドが多いのでチェックしておくと良い。

 

chardet, cchardetによるエンコーディング検出

Webスクレイピングをしている際に非常に厄介なのが多岐に渡る「エンコーディングの種類」である。
utf-8以外のWebサイトみんな地獄で呪ってやるという気持ちになる時もある。
cp932なMicrosoftIBMも絶対許さんという気持ちになる時もある。


ちなみに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)                       # Content-Typeに文字が入っていた場合ISO-8859-1
response.encoding = response.apparent_encoding #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"

# requests
headers = {"User-Agent": "hoge"}
resp = requests.get(URL, timeout=1, headers=headers, verify=False)

# status_codeの確認
if resp.status_code != 200:
    print("200じゃない時の処理")

# encodingの検出
encoding = resp.encoding
ccencoding = cchardet.detect(resp.content)["encoding"]
if len(ccencoding) > 0 and encoding != "ISO-8859-1":
    resp.encoding = ccencoding.lower()
    encoding = ccencoding

# WebDriver
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)
# GET
driver.set_page_load_timeout(10)
driver_wait = WebDriverWait(driver, 10)
driver = driver.get(URL)
driver_wait.until(ec.presence_of_all_elements_located)

# 引数でoutput-encodingを指定していなければ > html = driver.page_source.decode(encoding)
# 大体文字化けせずJSをうまいこと回して取得できる
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' で使えそうにないが、概ね良さそう。


 

retryingデコレータによるリトライ

GitHub - rholder/retrying: Retrying is an Apache 2.0 licensed general-purpose retrying library, written in Python, to simplify the task of adding retry behavior to just about anything.

スクレイピングではネットワーク環境などによって、サーバへのアクセスが失敗する可能性がある。

そこでretryingデコレータを利用する。
導入はpipで。

pip install retrying

関数内のExceptionをcatchして、最大10回リトライするような実装は以下。

from retrying import retry
import requests

@retry(stop_max_attempt_number=10)
def scraping(url):
    return requests.get(url)

print scraping("http://hogehoge.com")

リトライ時のdelayもwait_fixedで指定できるので有用。

 

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を使うのが吉(後述)。

multiprocessing

既に作成済みの関数群を並列化するならjoblibかmultiprocessingが良さそう
Python並列処理(multiprocessingとJoblib) - Qiita

multiprocessingであれば標準module
ごく簡単な Python multiprocessing の使い方 - Librabuch

以下のようによしなに

from multiprocessing import Pool, cpu_count
import requests

def main(x):
    """スクレイピング処理."""
    return requests.get(x)

c = cpu_count()
pool = Pool(c)
l = [p.get() for p in [pool.apply_async(main, args=(x,)) for x in URL]]
c.close

もしくは

from multiprocessing import Pool
import multiprocessing as multi

p = Pool(multi.cpu_count())
p.map(main, url_list)
p.close()

この場合引数がリストになってないといけないので、リストのリストのような状態になる場合がある。

より実践的にPoolを作ってQueue,Pipeで処理するなら以下が参考になる。
マルチプロセッシング(multiprocessing) - Python | Welcome to underground
multiprocessingのあれこれ - Monthly Hacker's Blog

multiprocessingはclose()しないとガバガバメモリを食い続けるので気をつける必要がある。

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通信を扱うライブラリ群は多々ある。

aiohttp

aiohttpはasyncioを用いながらrequestsに似たAPIを使って情報を取得するライブラリ。
記述が簡単で、基本的なrequestsらしい操作に対してデコレータの設置とyieldを書いてやればよい。

リクエストの時間をコントロールするにはsemaphoreを使う。
Synchronizationで同期中にあるコルーチンが動く数を制限する事ができる。

GitHub - aio-libs/aiohttp: Async http client/server framework (asyncio)
http://aiohttp.readthedocs.io/en/latest/migration.html

最もシンプルな例を書くと

import aiohttp
import syncio

@asyncio.coroutine
def get_url(url):
    session = aiohttp.ClientSession()
    response = yield from session.request('GET', url)
    html = yield from response.content.read()
    # htmlを表示
    print(html)
    # セッションはちゃんとcloseする
    response.close()
    yield from session.close()

url_list= ['hoge', 'piyo', 'koko']
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait([curl(url) for url in url_list]))

記述面で少し制限される部分があるが、適切にsyncioを振り分けられるので良い。
パラメータが少し少ないのがネックではあるが、asyncioの中でPhantomJSの起動と停止を書いてやるだけで、よしなにやってくれる非同期処理が可能である。

以下gitのサンプルはめちゃくちゃ充実している。
aiohttp/examples at master · aio-libs/aiohttp · GitHub

日本語の記事は以下の記事くらい。
(もしかしたら今後ちゃんと書くかもしれない)
asyncioを用いたpythonの高速なスクレイピング | プログラミング | POSTD

 

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

アーキテクチャが図としてあるので概念と紹介だけ。
https://doc.scrapy.org/en/latest/_images/scrapy_architecture_02.png
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周りを結局自前で書くことになるので、大きめのフレームワークらしい欠点は備えている。
(便利かつデフォルト設定でよければすぐ回せるので自分も機械学習用の画像集めとかには使っています)


 

SimpleHTTPServerとThreadを使ったunittest

Webスクレイピングはコードが肥大化しやすく、かつ複雑化しやすいです。
なのでテストコードを書いておくのが良いでしょう。

内容については、以下に別途記事を書きました。

vaaaaaanquish.hatenablog.com


 

法律の話

重要なのは著作権法と動産不法侵入。
以下にまとめておく。

基本的に押さえておくべき法律

  • 著作権法
    • 47条の7 情報解析のための複製等
      • 情報解析のためならデータを複製していいがマナーを守れ
    • 47条の6 送信可能化された情報の送信元識別符号の検索のための複製等
    • 施行令第7条の5 送信可能化された情報の収集、整理及び提供の基準
      • プログラムにより自動的に収集、整理して他の規定にかかってたらちゃんと削除
    • 基本的にWeb上で公開されているものは著作者のものであり知的財産
    • データ分析に使用する場合は違法ダウンロード系でなければOK
    • データベースにするにしても最小限でスキーマが分からないレベルで
    • スクレイピングした情報をそのまま公開するのはほぼアウトだと思ったほうが良さそう
    • セーフハーバー保護やDMCAの下利用できるものもある
    • 画像は商標などもあるので注意
  • 動産不法侵入
    • サーバへのアクセスで抵触しかねない基準と対策
      • 同意の欠如 -> Webページが提示する条項を守ろう
      • 実害 -> サーバへの負荷等を考えよう
      • 意図性 -> プログラムを書いたなら意図がある
    • 1秒に1リクエストあれば良いんじゃないかという資料がある

 

判例と倫理

一応個人情報保護士なる資格を取るにあたって勉強したが、なかなかこの辺は難しいと感じる。
難しいのは判断基準。
分析に使う情報取得は権利的にOKだが「機械学習データこんな感じですと集めたデータをアップロード」辺りはアウトっぽい。
言うなれば「節度と倫理を守ろう」だ…


Serverの負荷は「1秒に1回リクエスト論」に対してよく「岡崎市立中央図書館事件」が議題に上がる。
2010年に図書館のDBに1秒Sleep入れてアクセスしたが(図書館のDBのバグが問題で)負荷になってしまい警察に呼ばれた人の話。
控えめに言って、この時のインターネットの盛り上がりはすごくてTwitter2chも以下のブログの虜だったので実際一回は全体眺めるのを勧めたい。
(不謹慎ながら端から見てる分にはクソ面白かった…)
librahack.jp


後企業だとeBayとBidder's Edgeの話とか。
1997年にメタオークションサイトを作るためebayサイトを1日100,000回叩いたら動産不法侵入になった話(Wikiがある)。
eBay v. Bidder's Edge - Wikipedia https://en.wikipedia.org/wiki/EBay_v._Bidder%27s_Edge
最終的に2001年に金で和解したらしいけど、その時の金額は個人で払えるレベルのアレじゃないっぽい。


正直コーディングする前に設計上で他の作業と並行させるとか、出来る工夫は沢山あると思うのですべき。
スクレイピングだけ切り離すと突然家に警察が来てもおかしくないなというグレーなやつ。

 

技術的なルールと各規約の読み込み

技術的なルールもあるのでまとめておく。これは遵守したいところ。

  • 技術的なスクレイピングルール
    • robots.txtやrobots metaタグを守る
      • クローラへの指示が示されている(アクセス禁止のパスとか)
      • 見つけたら守る必要がある
      • 読まなければ営業妨害になる可能性
    • HTMLにPragma:No-cacheとなっていたらデータ収集しない
    • aタグにrel="nofollow"が設定されていたら辿らない
    • User-agentなどをきちんと設定しIP偽装等をやめよう


追記で規約周りの読み込み記事を書きました。
vaaaaaanquish.hatenablog.com
スクレイピングの間隔はどうすればいい?」「規約は?」「違法でないの?」という人のために法律等もまとめています。


書籍

関連して読んだ書籍とか

日本の書籍

Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-


正直基本的な事だけならこれ一冊で良くねと思えるバイブルっぽい良書。
Pythonの基本的な事もそうだが、Unixコマンドも多々解説しており、基本的な事が出来るようになる流れで書いてある。
(まずはwgetしてgrepしてjqで~とか、Pythonならやりたい事別にこうしてとか~)

robotsに対する処理とか、負荷のかけかたも実践的なコードがある。

一応解析のやり方としてAPIの利用や、自然言語処理技術の紹介、SPARQLのBigQueryの利用方法等が1章分ある。

この本の良い所はScrapyを1章まるまる使って解説している所にあると思う。
あのクソ多いパラメータを重要な所だけ抜き取って書いてくれてるので、Reference読む前に読んでおくと楽。
Scrapyの中がどう動いてるか理解できたのはこの本のおかげ。

クローラの継続実行に関する解説もあり、Linuxへの理解もちょっと深まる。


 

Pythonによるクローラー&スクレイピング入門

これも日本語の書籍の中ではオススメできる本です。

一番新しい書籍なので当たり前ですが、Python3.6への対応や、実在するWebページに対するスクレイピングの例、Sampleソースコードのダウンロードができます。
curl等のコマンドやgit、正規表現MySQLの話、プログラミングの基礎みたいな話が3章まで続き、やや蛇足感がありますが、4章以降はしっかり例が示されており初心者に分かりやすい構成になっていると思います。

取ってきたデータのXMLCSVへの変換やテキスト情報の抜き出し(w3lib)、webdriver、並列処理、loggingの記載、長めのScrapyの話があります。
DjangoでWeb API化して運用する構成の話など、やや蛇足かなと思う部分も多いですが、著者はGunosyの方だそうで、実務で得られた経験や知見を活かしながら書かれた本だと思います。

実務で突然スクレイピング始めろって言われたら買っておくといいです。


 

実践 Webスクレイピング&クローリング-オープンデータ時代の収集・整形テクニック

実践 Webスクレイピング&クローリング-オープンデータ時代の収集・整形テクニック

実践 Webスクレイピング&クローリング-オープンデータ時代の収集・整形テクニック

2章まで、Pyhonの使い方(インストールからprint関数の利用、四則演算とかそういうレベルから)が書いてあって、かつ2章までで書籍の4割くらいを使っている。

スクレイピングに関する話は中の数章で、WebAPIの利用、SPARQL、スクレイピングサービスのkimonoの紹介という感じ。
加えて法律とrobot.txtに関する話がちょこっと程度。

最後4割はExcelAWKコマンド、Pandasの使い方が載ってる。

あまりに知見が少なく、説明も痒いので正直かなり厳しい。
著者の情報を見るにそれなりのエンジニアっぽいコミュニティや受賞を持っているようだが果たしてこれは…という感想。


Pythonによるスクレイピング&機械学習 開発テクニック

「絶対これ機械学習エンジニアが書いたでしょ…」ってなる。

2章までが主にスクレイピングの話で、確かにrequests+BS4+Selenium+PhantomJSを使った実践的なコードと、cronの操作、WebAPIの話が書かれている。
(本当に基本的な所までが連ねてある感じで書いてある)

残りの6~7割程度が本当に機械学習の話しか載っていない。
sklearnからTensorFlow、MecabとCabochaによる自然言語処理ベイズマルコフ連鎖の実装、OCRやCNNでの画像認識…とかなりの実践機械学習本。

スクレイピングの知見含めて正直Qiita等で間に合うレベル感の内容なので、「PythonでのWebスクレイピングから機械学習を使った分析まで全体感を見ながら通しで勉強したい!」という人には向いているかも。

スクレイピングとは機械学習とはなんぞやという人が読めば、確かに世界が広がりそうな流れの書籍にはなっている。
あと、サンプルコードがダウンロードできるようになってるのも良い点。


 

PythonによるWebスクレイピング

PythonによるWebスクレイピング

PythonによるWebスクレイピング

Web Scraping with Python: Collecting Data from the Modern Web

Web Scraping with Python: Collecting Data from the Modern Web

Pythonを勉強した後、スクレイピングをとりあえず試したいという人向けなレベル感の書籍。

PythonでのCSVJSONの扱い方とか、GoogleTwitterAPIの使い方、PythonMySQLの解説、NLTKによる自然言語処理による分析、Tesseract OCRによる画像から文字抽出する部分辺りはスクレイピング本としては余計に感じた。
requestsとBeautifulSoupを扱っているが、めちゃくちゃ有益な知見とまではいかなかった(Qiitaやブログ記事でカバーできそう)。

順を追って色々勉強したいという人向けである。

良いところを挙げると、本記事には乗せていないPySocksを用いた、Torによるスクレイピングの匿名化の話等が書いてある。
GitHub - Anorov/PySocks: A SOCKS proxy client and wrapper for Python.
また、ハニーポットをどういう風に避けるかみたいな話もある。

付録の事案紹介や倫理の部分は丁寧で、実際に弁護士に伺った部分があるとの事。
事例が国内外解説してあって「日本法とアメリカ法は違うのでおおよそでも良いから読んだほうが良い」とのこと。

英語版も似たような内容。
英語版筆者が元々Webスクレイピングより分析の仕事をしていたようで、まあそこに寄ったのかなって。


洋書

Web Scraping with Python

Web Scraping with Python (Community Experience Distilled)

Web Scraping with Python (Community Experience Distilled)

実践というより、Webに関する知見な書籍。
ただPythonがある程度書ける前提を持った人向けの書籍で、その分レベルは実践に近い(しかしPython2系)。

cacheを使って帯域幅を削減する方法や、スクレイピングの並列化、リンク走査の話辺りはかなり良い。
また、クッキーやブラウザレンダラの話は知見として有益な部分が多い。
それらのコードも載っているのだがPython2系なのが辛いところ。

あと「Pythonクローリング&スクレイピング」と同様、6章まるまるScrapyの話をふんだんに書いてくれているので、Scrapyの設計を理解するには使えそう(しかしPython2系)。

Pythonコードは少なめだが丁寧ではあると思う(しかしPython2系)。
以外と知見が散りばめられているので、値段にしては有益な書。

 

Python Web Scraping - Second Edition

Python Web Scraping - Second Edition: Hands-on data scraping and crawling using PyQT, Selnium, HTML and Python

Python Web Scraping - Second Edition: Hands-on data scraping and crawling using PyQT, Selnium, HTML and Python

多分邦書洋書含めた中でも最も実践的で丁寧な良書。

基本的なrequestからBS4, PhanotomJSとScrapyの解説が載っているだけでなく、キャッシュの利用や発生しがちなExceptionの解説、SeleniumPyQtによるGUI操作にまで及んでいる。
また、robot.txtのparseや、python-wohoisで取得先の情報見ましょうとか、「普段みんなスクレイピング周辺でどんな事してるの?」という疑問に対して痒い所まで手が届いた良い書籍。

またコードがGithubにあるのも良い。
GitHub - PacktPublishing/Python-Web-Scraping-Second-Edition: Python Web Scraping Second Edition, published by Packt

 

Create a web crawler in Python

http://amzn.to/2gHR3sy

URLだけ一応貼っておく。
文書の前半がPythonのインストール、エラー処理、四則演算といった内容。後半は本当にQiita以下の内容。

筆者を見ると、Qiita以下のクソ電子書籍を適当に出しまくってるオッサンらしいので本当クソ。
こういう本全部Amazonに報告するスクリプト書きたい。


 

おわりに

なんか色々書いてるうちに本が書けるくらいの量になってしまった…

Referenceも結構読んだので、それぞれのライブラリ毎にまた記事が書けると良いなと思っている気がします。

あと最後ですがこれ大事です。気軽にはじめたい人はScrapy。

Seleniumに変わるスクレイピング向けのPythonモジュール作りたい人居ないかな~。

他で簡素なまとめは以下が分かりやすいかも

Python3でクロールしようと思って調べたこと | mwSoft


あと他にこの書籍とかツールマジいいよ!ってやつあれば教えてくださいな。
 
 
追記 : User-Agentについて「UA偽装をやめよう」「よしなにUA書こう」という背反した記述があると指摘がフレンズからありました。

「確かに」以外の感想が得られなかったので修正しました。
User-Agentに自身の正しい情報を乗せるだけでなく、自身の連絡先等乗せてスクレイピングするくらいが紳士的ですね。

以下のような話も読んでおくとよさそう。

また以下のようにChrome headlessのアクセス情報の偽装に関する記事も

It is *not* possible to detect and block Chrome headless


 
追記 : !!

Firefoxのheadlessも使いました
vaaaaaanquish.hatenablog.com



追記 : 結構Python2系が小学生にバカにされる話に関するコメントが多いので元ネタを…

togetter.com
 

追記:環境構築や他headlessブラウザについて書きました


使ってみた感じChromeがゾンビ化しないし高速だしで現時点優勝候補です

 
追記:HTMLのエンコードですがUTF-8しか利用できなくなるので期待ですね