Stimulator

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

MacへのJupyter導入からextensionと設定メモ

- はじめに -

業務PCがWinからMacになりまして、Jupyter notebookしたいので自分の設定とextensionの導入までやったメモ。

あとChrome拡張使ってCSSを書き換えている話とか。


最初に参考資料を示しておくと、どのネット記事よりも以下extensionのGithubリポジトリのREADMEが分かりやすい。


Pythonやpip、バージョン管理環境に合わせて適宜読み替えて。

この記事書いた時点

Mac OSX Sierra 10.12
pyenv上でPython3環境構築済


 

- JupyterをVivaldiで起動するまで -

jupytera 本体の導入はpip

sudo pip install jupyter

インストールが終わったらconfigファイルを作る

jupyter notebook --generate-config
sudo vim ~/.jupyter/jupyter_notebook_config.py

Windowsだと、マシンのデフォルトブラウザで開いてくれてた気がしたけど、MacだとSafariでnotebookが開いてしまう。
Vivaldiなるブラウザを常用しているので以下設定。

c.NotebookApp.browser =u'/Applications/Vivaldi.app/Contents/MacOS/Vivaldi %s'

Chromeだと多分こう。

c.NotebookApp.browser = u'/Applications/Google/ Chrome.app/Contents/MacOS/Google\ Chrome %s'

ブラウザへのパスは適宜書き換える。

python - Launch IPython notebook with selected browser - Stack Overflow
Jupyterをブラウザ指定して新しいウィンドウで開く【なれない日記20160709】 - けつあご日記

jupyter常用者なのでbashrcにはaliasの指定をしている。

alias ju='jupyter notebook'


 

- jupyter extension -

extension導入は前述の通りREADMEを見ながら。
GitHub - ipython-contrib/jupyter_contrib_nbextensions: A collection of various notebook extensions for Jupyter

sudo pip install -e jupyter_contrib_nbextensions
jupyter contrib nbextension install --user

上のコマンドだけとか、gitからcloneしてsetup.py動かせば導入終わりみたいなネット記事が多い理由は謎。(多分上だけだと、nbextensions見に行っても404 Not Foundだと思うんだけど…)

2つめのnbextension installでブラウザ向けのJSやCSSが入るのだけど、highlight_selected_wordが上手く導入できてないっぽくて死んだ。

NotADirectoryError: [Errno 20] Not a directory: '/usr/local/lib/python3.6/site-packages/jupyter_highlight_selected_word-0.0.11-py3.6.egg/jupyter_highlight_selected_word/static/highlight_selected_word'

highlight_selected_wordだけpipで再インストー

sudo pip uninstall jupyter_highlight_selected_word
sudo pip install jupyter_highlight_selected_word
jupyter contrib nbextension install --user

http://localhost:8888/nbextensions を見に行けばextensionのオン・オフができる。

エディタ内で色々開閉できる「Codefolding in Editor」「Codefolding」、メニューに拡張ショートカットを追加する「Nbextensions edit menu item」「Nbextensions dashboard tab」、Vimキーバインド使う用の「Select CodeMirror Keymap」、PEPの下僕として78文字超えたくないので「Ruler」を使っているのでONにしておわり。


 

- StylishでMonokai風にする -

JupyterはそもそもThemeを使う機能を備えているが、Jupyterを動かすサーバ等環境が変わったら一々設定しないといけないのと、コーディング中にも設定見直したいのでChrome拡張を使ってCSSを書き換えている。

以前までStylishを使っていたのだけど、情報送信の話があったでちょっと考えもの。
Webブラウザアドオン「Styish」、ユーザーデータの収集を始めて騒動に | スラド IT

今はCSSとJSのシンプルな拡張は全て自前。
Stylish以外で簡易なのだとStylus、Stylistあたりか。

一応StylishのユーザグループにJupyter用のCSSが公開されてたりして、これを土台に一部書き換えて使っている。

 

書き換えてMonokaiっぽくする文字周りはこんな感じ。白文字がfffなので適宜。

div.output_stderr {background-color: #050505;}
div.output_stderr pre {
color: #509050; 
font-size: 12px;}
.cm-s-ipython .CodeMirror-matchingbracket { text-decoration: underline; color: #c3c3c3 !important; }
.CodeMirror { color: #c3c3c3 !important; }
.cm-s-default .cm-link {color: #3974dd;}
.cm-s-default .cm-string {color: #de846c;}
.cm-s-default .cm-header {color: #1090f0;}
.cm-s-ipython div.CodeMirror-selected {background: #3C4555 !important;}
.cm-s-ipython .CodeMirror-gutters {background: #39414F; border: 0px; border-radius:0px;}
.cm-s-ipython .CodeMirror-linenumber {color: #5A647B !important; font-size: 11pt;}
.cm-s-ipython .CodeMirror-cursor {border-left: 2px solid #0095ff !important;}
.cm-s-ipython span.cm-comment {color: #6E7C95; font-style: normal !important;}
.cm-s-ipython span.cm-atom {color: #CAA6EC;}
.cm-s-ipython span.cm-number {color: #ae81ff;}
.cm-s-ipython span.cm-property {color: #fff;}
.cm-s-ipython span.cm-attribute {color: #E39194;}
.cm-s-ipython span.cm-keyword {color: #f92672; font-weight: normal;}
.cm-s-ipython span.cm-string {color: #e6db74; font-weight: normal;}
.cm-s-ipython span.cm-operator {color: #f92672; font-weight: normal;}
.cm-s-ipython span.cm-builtin {color: #66d9ef; font-weight: normal;}
.cm-s-ipython span.cm-boolean {color: #E39194;}
.cm-s-ipython span.cm-variable {color: #fff;}
.cm-s-ipython span.cm-variable-2 {color: #fd971f;}
.cm-s-ipython span.cm-error {background: rgba(191, 97, 106, .3) !important;}
.cm-s-ipython span.cm-tag {color: #CAA6EC;}
.cm-s-ipython span.cm-link {color: #E39194;}
.cm-s-ipython span.cm-storage {color: #CAA6EC;}
.cm-s-ipython span.cm-entity {color: #E39194;}
.cm-s-ipython span.cm-class {color: #E5DEA5;}
.cm-s-ipython span.cm-support {color: #77ABE7;}
.cm-s-ipython span.cm-qualifier {color: #77ABE7;}
.cm-s-ipython span.cm-property {color: #fff;}

あとextensionで導入したCodefoldingの三角のアレの位置とかを修正

.CodeMirror-foldgutter-open, .CodeMirror-foldgutter-folded { margin-left: -4px; }

跡は幅を100% にしたりしてこんな感じ

f:id:vaaaaaanquish:20170715184749p:plain

拡張なのでCSSで指定しにくいところの書き換えはできないけどまあ概ね満足。
コードもグラフ出力も黒背景の方が僕は好きです。


 

- おわりに -

extensionは導入はpyenvやAnaconda等のベース環境によってたまに失敗してるイメージがあるんだけど、なんかもっとこう絶対優勝できるようになりたい。

あと便利なextensionあったら知りたいところです。
はてブコメントでよしなに。

Jupyter生活は快適でサイコー。

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

- はじめに -

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

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

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


追記 2018/03/05:
大きな内容なのでここに追記します。
github.com
phantomJSについての記載が記事内でありますが、phantomJSのメンテナが止めたニュースが記憶に新しいですが、上記issueにて正式にこれ以上バージョンアップされないとの通達。
記事内でも推奨していますがheadless Chrome等を使う方が良さそうです。


- 知見まとめ -

最初に言うまでもないが、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.5 ドキュメント

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: HTTP client/server framework for 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サイトをクロールするための実践テクニック

クローリングハック あらゆるWebサイトをクロールするための実践テクニック

クローリングハック あらゆるWebサイトをクロールするための実践テクニック

Javaの本なので基本的にこの記事の趣向とは離れるが、HTTPや各認証の仕組み等について触れられて居るので良い。

クローリングとしては、JavaSeleniumを用いた基本を抑えつつ、その仕組みを重点的に抑えている書籍。
ビズリーチ社の社員が複数人で書いているため、章によって情報のバラつきが激しいのがちょっとネックだが、一歩踏み入った勉強ができる。


 

ウエブデータの機械学習

ウェブデータの機械学習 (機械学習プロフェッショナルシリーズ)

ウェブデータの機械学習 (機械学習プロフェッショナルシリーズ)

スクレイピングとしての書籍ではないが、「スクレイピングした後に機械学習をやりたい!」という人には絶対オススメ。

データのクレンジングや、様々な自然言語処理機械学習モデルの使い方が丁寧に書かれており、「Pythonによるスクレイピング&機械学習」を単体で買うよりは、この書籍とスクレイピング専門の書籍を1冊ずつ買うほうがお得。

話題のバースト検出やウェブのリンク解析等、幅広く業務に応用できる内容が書かれていると思う。

洋書

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しか利用できなくなるので期待ですね

 

Pythonのhttp.serverを利用してWebスクレイピングのunittestを書く

- はじめに -

「Webスクレイピングで情報を収集する」という内容は多い。

しかし、Webスクレイピングのコードは肥大化しやすいだけでなく、細かな変更が多くなる。
テストを書いて変更の影響をちゃんと見ておく必要性が高い。

unittestとhttp.serverを使ったテストの実装についてメモしておく。


参考:python - How to stop BaseHTTPServer.serve_forever() in a BaseHTTPRequestHandler subclass? - Stack Overflow


- http.server -

http.serverはPython 2.xではSimpleHTTPServerと呼ばれていたもの。
(http.serverよりSimpleHTTPServerの方がググラビリティ高いかも)

Webサービス等の開発用にローカルサーバとして利用している人も多い。

21.22. http.server — HTTP サーバ — Python 3.6.1 ドキュメント


コマンドでの実行も可能だが、Pythonからだとハンドラを指定したHTTPServerを以下のように記述し、簡易にサーバを立てる事ができる。

from http.server import HTTPServer, SimpleHTTPRequestHandler

httpd = HTTPServer(("localhost", 8888), SimpleHTTPRequestHandler)
httpd.serve_forever()

これによって http://localhost:8888 に、ローカルでのServer環境が整う。


 

- unittestにかませる -

serve_foreverによって起動されるサーバ(SocketServer.py)は以下のようにWhileで実行されている。

def serve_forever(self):
    """Handle one request at a time until doomsday."""
    while 1:
        self.handle_request()

なのでwrapperとなるclassを書いてやって、server.shutdown及びserver.server_closeを実行できるようによしなに書いておけば良い。

また、HTTPServerが動いているThreadでPythonのコードを動かすのは少し困難なので、別のThreadで動かしてやる。


別ThreadでHTTPServerを実行し、requestを使ってアクセスするunittestは以下のような感じ。

from http.server import HTTPServer, SimpleHTTPRequestHandler
import threading
import unittest

HOST = "localhost"
PORT = 8888

class StoppableHTTPServer(HTTPServer):
    """
    ThreadでSimpleHTTPServerを動かすためのwrapper class.
    Ctrl + Cで終了されるとThreadだけが死んで残る.
    KeyboardInterruptはpassする.
    """
    def run(self):
        try:
            self.serve_forever()
        except KeyboardInterrupt:
            pass
        finally:
            self.server_close()

class TestWebScraping(unittest.TestCase):
    def setUp(self):
        """Use unittest setUp method."""
        self.server = StoppableHTTPServer((HOST, PORT), SimpleHTTPRequestHandler)
        self.url = "http://{}:{}/test_case/index.html".format(HOST, PORT)
        self.thread = threading.Thread(None, self.server.run)
        self.thread.start()

    def test_requests_get(self):
        """
        requestsモジュールでURLを叩くunittest.
        contentが返ってくるか.
        """
        r = requests.get(self.url)
        self.assertEqual(type(resp.content), str)

    def tearDown(self):
        """Use unittest tearDown method."""
        self.server.shutdown()
        self.thread.join()

unittestのコードと同ディレクトリにから./test_case/index.htmlが設置されていれば良い。

最後はServerを終了してthreadをjoinするようになってる。


後はテスト内容に合わせてhtmlを修正したり複数ファイルを付与してく。

JSやPHPの実行もできるのでよしなにテストができる。


 

- おわりに -

BaseHTTPServerとかTCPServerとかTCP扱える枠組みなら多分似たような事ができると思います。

他にも良さげなやり方があったら知りたい。


 

実践 Python 3

実践 Python 3

headless chromeをPythonのseleniumから動かして引数を考えた (Ubuntu 16.04)

- はじめに -

Chrome 59が正式版となりheadless版も正式に動き始めました。めでたい。

New in Chrome 59  |  Web  |  Google Developers

headless chromeUbuntuに導入してPythonから触ったという記事です。

Ubuntuへの導入から、実行時の引数となるargsの考察などを含みます。



スクレイピング関連記事です。

vaaaaaanquish.hatenablog.com



- インストール -

まずPythonからの起動に必要なchromedriverを取得しておきます。
apt-getでも入りますが最新版が欲しいので以下のように。
(記事書いた当時で最新版は2.29)

wget https://chromedriver.storage.googleapis.com/2.29/chromedriver_linux64.zip
unzip chromedriver_linux64.zip
sudo mv chromedriver /usr/local/bin/


依存ライブラリとなるlibappindicator1をインストールしておきます。
事前にやらないとハマるっぽいので、先走ったら後述で入れたChromeをアンインストールしてやり直すのが良いです。

sudo apt-get install libappindicator1


Chromedebパッケージを取得してきます。
公式ページに行くと、小さな文字で「Google Chrome をインストールすると Google レポジトリが追加され、Google Chrome がシステムで自動更新されます。Google のレポジトリを追加したくない場合は、パッケージをインストールする前に「sudo touch /etc/default/google-chrome」を実行してください。」と書かれています。
スクレイピング等ガンガン回したい場合などはやっておくべきでしょう。(やらなくてもOK)

sudo touch /etc/default/google-chrome
wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb


以下でインストールします。

sudo dpkg -i google-chrome-stable_current_amd64.deb


この時以下のようなエラーが出るかもしれません。

Errors were encountered while processing:
google-chrome-stable


その時はapt-getでこうしてこう。

sudo apt-get install -f
sudo dpkg -i google-chrome-stable_current_amd64.deb


インストールおわりです。


- Pythonで実行 -

Python3.xを使います。2系は小学生にバカにされるからです。
seleniumのwebdriver配下にchrome.optionsというのがあるのでそれを利用して、--headlessを指定します。

「--disable-gpu」は以下Google曰く現状暫定必須だそうです。
ヘッドレス Chrome ことはじめ  |  Web  |  Google Developers


from selenium import webdriver
from selenium.webdriver.chrome.options import Options

options = Options()
options.add_argument('--headless')
options.add_argument('--disable-gpu')
driver = webdriver.Chrome(chrome_options=options)
driver.get('hogehoge.com')
print(driver.page_source)

後はPhontomJS等と同じように使えます。


driver.save_screenshotしてみたら以下のように

f:id:vaaaaaanquish:20170606194222p:plain


多分デフォルトだとUbuntuのフォントが足りず文字化けする。
私は導入済み。以下参考。


 

- パラメータに関する考察 -

Google Chromeはとにかく実行時の引数が多い。

List of Chrome Driver command line arguments – Assert Selenium

ここではスクレイピングに関係しそうなパラメータだけ抜粋して考える。

「--headless」「--disable-gpu」は必須。
SSL周りのエラーを許容するために「--ignore-certificate-errors」を付ける。
実行不可なコンテンツやセキュリティの許容範囲を広げるなら「--allow-running-insecure-content」「'--disable-web-security'」すると良さそうだけど、常用はできそうにない。
デスクトップ通知やExtensionの利用も基本不要だと思われるので「--disable-desktop-notifications」「--disable-extensions」する。
ユーザエージェントの指定は「--user-agent=hogehoge」でダブルクオーテーションで囲まなくても大丈夫。

かつては「--disable-images」なるフラグで画像を読み込まない設定が可能だったけど、バージョン5.0.335.0より使用不能に。
画像をオフにするにはprefsに対してprofileを設定する方法があるよ〜との記述もちらほらある。
こんなやつ

prefs = {"profile.managed_default_content_settings.images":2}
options.add_experimental_option("prefs",prefs)

でもこれではダメで、正解は「--blink-settings=imagesEnabled=false」っぽい。
設定画面でimagesを切りに行くと以下を参考にしたらできた。
https://groups.google.com/a/chromium.org/forum/#!topic/headless-dev/0zD4nAyVoCY


あとは「--lang」で言語の指定。
必要に応じて「--proxy-server」などを利用する。

例えばこんな感じ

from selenium import webdriver

options = webdriver.ChromeOptions()
# 必須
options.add_argument('--headless')
options.add_argument('--disable-gpu')
# エラーの許容
options.add_argument('--ignore-certificate-errors')
options.add_argument('--allow-running-insecure-content')
options.add_argument('--disable-web-security')
# headlessでは不要そうな機能
options.add_argument('--disable-desktop-notifications')
options.add_argument("--disable-extensions")
# UA
options.add_argument('--user-agent=hogehoge')
# 言語
options.add_argument('--lang=ja')
# 画像を読み込まないで軽くする
options.add_argument('--blink-settings=imagesEnabled=false')
driver = webdriver.Chrome(chrome_options=options)
driver.get('hogehoge.com')
print(driver.page_source)


以下も参考に
起動オプション - Google Chrome まとめWiki
 

 

- おわりに -

今までPythonでWebスクレイピングというとPhontomJSなるブラウザが圧倒的シェアでした。

しかしPhontomJSもなかなかバギーで、プロセスがゾンビ化するなどの問題を抱えていました。

headless chromeに関する日本語ページが更新されるなど、期待が高まっていたここ半年です。

こんな記事も。

www.infoq.com

このままheadless chromeが希望の星となる事を期待しています。


DCGANで名刺のデザインを試みた

- はじめに -

社内ハッカソンと社内勉強会のネタとして、今更ながらGenerative Adversarial Networks*1 (GAN)とその応用とも言えるモデルであるDeep Convolutional Generative Adversarial Networks*2 (DCGAN)について調査し、実際に検証を行った。

この記事は、DCGANについていくらか調査、検証した部分について記述しておくものである。
なお、画像生成系のモデルは以前より話題になっていたため論文には目を通していたが、実際に触ったのは初めてである。

題材として「名刺」の画像をDCGANで生成する事を試みた。
その過程と結果を示す。


- GANとDCGAN -

DCGANはGANに対してConvolutional Neural Networks(CNN)を適応する構成手法のようなものである。

生成モデルにおけるGAN

GANは、2つの生成モデルを相互に学習させることで学習データが形成する分布をより汎化な状態として保持できるというもので、PRML的に言うと伝承サンプリング(ancestral sampling)を使う生成モデルの一種である。
伝承サンプリングはMCMCのようなサンプリングを使うモデルに対して、計算量が少なく済む事がメリット。中でもHelmholtz Machineや変分AutoEncoderに代表される生成モデルと推論モデルを同時に学習させる手法に比べ、よりシャープな自然画像を生成できるのがGANである。

GAN自体は利用する生成モデルについては限定していないが、2つのモデル間のJSダイバージェンスに対してmin-max最適化を行うように捉える事で、2つのモデルは相互に更新しあいながら学習させられ、またBackpropagationが適応できるので勾配の近似と計算量削減ができて良いよねという旨である。

GANの2つのモデルはDiscriminatorとGeneratorに分かれる。
学習におけるGeneratorの目的は、ランダムノイズからDiscriminatorが誤認識するようなGenerator用の入力を作れるようになること。
学習におけるDiscriminatorの目的は、学習データとGeneratorが作る入力を正しく判定できること。
この2つが相互に上手く学習できれば、Generatorはより学習データに近いものが生成できるはずという旨である。

後述するが実際には、この学習は難しく、大きなネットワークモデル等を利用する場合にはパラメータ設定ゲーとなる。

DCGAN

本題のDCGANは、GANにCNNを上手く適応させるため以下のような事を行っている。

  • Pooling layerを以下に置き換えてアップサンプリング
    • D : Strided convolution
    • G : fractional-strided convolution
  • Batch Normalizationを使う
    • 学習を早くできるし過学習をそれなりに防げる
  • 全結合の隠れレイヤーは取り除く
    • 全結合層を全部除くとの文献を見かけるが正確にはglobal average poolingに置き換える(?)である。「出力層手前では、全結合するのではなくて一つの特徴マップに一つのクラスが対応するように設計する」というのが正しそう。
  • Leaky ReLU関数を使う
    • D : 全てLeaky ReLU
    • G : 基本はReLUで出力層はtanh
    • x<=0でも学習が進み、かつ過学習しないように

こういう感じでGANにCNNを適応していくと、CNN使っても安定して上手いこと画像データの生成ができるよ!というのがDCGANの立ち位置だと思われる(?)。
今回後述の実験では利用してないが、他にも「DCGAN(もしくはGAN)で上手いこと画像を生成する方法」は多く論文としてまとめられていて、社内勉強会で上記内容を発表した際にいくつか教えて頂いたので記述しておく。

[1606.03498] Improved Techniques for Training GANs
Feature matching含むいくつかの最適化手法と半教師あり学習によって、画像の生成の成功確率が上がる。

[1606.00709] f-GAN: Training Generative Neural Samplers using Variational Divergence Minimization
GANで使うJSダイバージェンスをfダイバージェンスというものに置き換えましょうという論文。
KLダイバージェンス等とも比較している。

他にも、GeneratorがDiscriminatorを確実に騙せるような画像を学習によって得てしまい、出力画像が特定の画風に固定化してしまう問題に対して「バッチ重みを上手く全体に適応すると良いよ」等の指摘を頂いた。今後時間があれば試してみたい。


- DCGAN関連のプロダクト -

社内勉強会なので、プロダクトと一緒に紹介した。
若干雑だがせっかくなのでそのままコピペで以下にまとめておく。

その他

- DCGAN参考文献 (Web) -

Web上で学ぶ時に有益だと感じた文献。

関連研究を知る

周辺のモデルの流れを掴むには@beam2d氏が公開する以下の資料が分かりやすい。
https://www.slideshare.net/beam2d/learning-generator

生成モデルに関する論文をまとめた以下のような記事もあり、RBMやVAE関連で調べる際は参考になる。
(こちらは随時更新されているようだが誰が書いているか知らない)
memonone: 生成モデル(Generative Model)関連の論文まとめ

GAN系の最適化とか応用研究についてのスライド
[DL輪読会] GAN系の研究まとめ (NIPS2016とICLR2016が中心)

概要を掴む

@miztiさんのブログ。DCGANの概要がつかめる。
できるだけ丁寧にGANとDCGANを理解する - 午睡二時四十分

わかりやすくDCGANについて書いている。概要がつかめる
なんちゃって!DCGANでコンピュータがリアルな絵を描く - PlayGround

スライド。概要がつかめる。画が多い。
Deep Convolutional Generative Adversarial Networks - Nextremer勉強会資料

GANの更新規則についての記事。GIFが分かりやすい。
An Alternative Update Rule for Generative Adversarial Networks

数式に対する解釈が分かりやすいと思う。
[Survey]Generative Image Modeling using Style and Structure Adversarial Networks - Qiita

めちゃくちゃ丁寧にGANについて書かれている。多分Webだと一番丁寧。
はじめてのGAN


- DCGANで名刺画像生成に挑戦した -

事前準備

今回は社内ハッカソンと社内勉強会のための時間を使って、DCGANを試した。
主には以下の@mattya氏がchainerで書いたDCGANをcloneし、モデルとパラメータを調整した。

GitHub - mattya/chainer-DCGAN: Chainer implementation of Deep Convolutional Generative Adversarial Network

課題として、「DCGANは文字や文字構成を学習できるか」という事を見ておきたく、画像サイズが小さく定形な物があるという事で「名刺」を選択した。

学習データに利用した名刺画像はStanfordのデータセットや「名刺 サンプル」「Business Card」で検索してスクレイピングし、2万枚程集めた。
たまに「企業名 名刺」で検索すると山のように名刺を出してる所があって、日本語の企業一覧使って検索してみて包含したので、データセットとしては若干偏っているかもしれない。

名刺のサンプル画像には1枚の画像に数枚入っていたり、お洒落を意識した傾きがあったりするので、ラプラシアンフィルタを使ってエッジを取って、四角があったら名刺だろうといったコードを書いた。
以下を参考に(というかほぼコピペ)したが、2値化するよりHough変換した方が良かった気がする。
ホワイトボードの画像からポストイットを検出する - Qiita

増量して大体5万枚くらいになって、1週間程仕事終わりに目視で「あ~まあこれならええやろ」くらいの絞り込みを行った。
学習データとしては画像集めるだけだったので、まだマシだった。

元々、これをやろうと思い立ったきっかけは画像認識系のコンテストで、そちらのデータセットには名刺の画像と各情報の矩形位置まであったのを見て「これ使ってアップサンプリングすれば行けそうだな」と思っていて使いたかったんだけど、コンテストページ行ったらもうダウンロードできなかった…

名刺管理系のサービスは大体スクレイピング禁止なる項目が書かれてあるので、大丈夫そうなデータしか集められなかったのは事実。

また、名刺は縦横があるんだけど、全部横向きの物を目で選んだ。
理由は同時に学習できるか分からないのと、widthとheightが違うので合わせるのが大変そうなのと、サンプル名刺では等に横向きの物が大半だったから。
縦向きの名刺ダサいし仕方ないね。

名刺は日本語や英語だけでなく、色々な言語の名刺を入れた。
(じゃないと学習データが確保できなかった)
一応アラビア文字とかは目視で見つけた場合のみ外したはず。
あと画像サイズを縮小して、名刺の縦横比率の2倍の110*182にした。

学習とその結果

学習には絶対最強のp2.16xlargeを利用した。申請して2日くらいかかって使えるようになったと思う。構成はChainer周りのDockerでドッカーン。

会社からAWSサービスを使う時に補助が出たりするけど、4~5日程p2インスタンスを利用したら、会社補助上限を突き抜けて+3万円くらいが消えた。
事前準備とサーバへの画像アップロードも時間がかかったが、大体学習の時間。
方法としてはモデルをまず考えて、数時間回してみて期待できそうなら~という感じ。アナログ。
最初の画像が見れるまでの速度的にp2.16xlargみたいな高火力なやつ使った方が、実験はできると思う。
これから見せる画像は、インターネットにある名刺画像と俺の財布が作り出した結果だと思って見ていって欲しい。

学習初期

> い つ も の <
f:id:vaaaaaanquish:20170319191349p:plain:w200:h200

f:id:vaaaaaanquish:20170319191754p:plain:w200:h200

それっぽさがあるがこの辺りはまだ不安。

画像が生成されはじめる

50万回程が学習を回した段階
f:id:vaaaaaanquish:20170319191920p:plain:w200:h200

100万
f:id:vaaaaaanquish:20170319192040p:plain:w200:h200

この辺で少し安心できる

かなり名刺

330万程学習を回した時

f:id:vaaaaaanquish:20170319193627p:plain:w550:h400

いやこれ名刺っぽいなと思った。
こういうデザインの名刺見たことある。
企業アイコンがあって、よくわからん線が入ってて名前の横に役職とかがあって~というやつ。
ちょっと感動した。

学習過程をGIFにしてみた。

f:id:vaaaaaanquish:20170319215634g:plain

学習終盤では一度生成画像がリセットされ固定化されてしまっている。
これは、GeneratorがDiscriminatorを確実に騙せるような画像を学習によって得てしまった故の結果であり、先述したようにバッチ重みを上手く全体に適応する等の対策があるらしい。一応途中途中でモデルと出力は保存しておくのが良い。


- おわりに -

GANとDCGANについて調べ、ソースを見てモデル調整を行った。
また、そのモデルを用いて名刺画像の生成を試みた。

文字を上手く出して欲しかったが、あまり文字として読めるものにはならなかった。
後で学習データを眺めると画像によっては文字が潰れていたので、学習結果で思ったより文字が上手く出てなかった原因はここにもあるかもしれない。

また、学習データが少なかった事と背景が真っ赤だったりする名刺が混ざっていたために、一部名刺とは言えない画像が生成されるのがネックという感じだった。学習データは大事。

ただ、名刺で見たことあるような全体のデザインや、有り得そうなロゴマーク、背景等が生成できた。
DCGANは、画像内に描かれているものが文字であってもそれらの配置を学習し、生成する事ができるという事が分かった。


ちなみに明日は僕の誕生日です。やったね。

Djangoで送信された画像データをPyhon上で処理するWebサービスを作る

- はじめに -

自分のWebサービスは基本PythonDjangoフレームを利用している。

Djangoでフォームから画像を投稿してもらって、それを受け取り、画像処理や機械学習で色々やって画像として返す、といったサービスを作りたい時のメモ。

実際に「ドイツのトリおるか」なる特定の鳥を赤枠で囲むクソサービスを運営しているので、そちらも参考に。


- Django周りのコード -

view.py辺りにいつもこんな感じで書いている。

# //-- Django周りのimport --

from PIL import Image
import sys
sys.path.append("/usr/local/lib/python2.7/site-packages")
import numpy
import dlib
import cv2

@csrf_protect
def main(request):
    # POSTを受けた時
    if request.method == 'POST':
        try:
      # 画像化して処理
            img_moto =Image.open(request.FILES['image'])
            img=numpy.asarray(numpy.array(img_moto))
            dets = original_detector_run(img)

        except Exception:
            # エラー(画像じゃない等)
            f = forms.TestForm(request.GET or None)
            return render_to_response('index.html',
                                      context_instance=RequestContext(request,
                                                                      {'form1': f,
                                                                       "text":"エラーだよ"}))
        # 画像を編集
        img = img_dets_ediy(img, dets)
        
        # Responseとして画像を返す
        response = HttpResponse(mimetype="image/png")
        img_moto.save(response, "PNG")
        return response
    
    # 普段の動作
    else:
        f = forms.TestForm(request.GET or None)
        return render_to_response('index.html', context_instance=RequestContext(request, {'form1': f}))


requestを受け取ったら、FILES['image']の中に画像情報が入っていてPILのImageベースで利用できる。
あとはいつもの変換作業でよしなに。
OpenCVとPIL(python Image library)のデータ変換 - tataboxの備忘録



- ありがちなやつ -

PYTHON_PATHだけでなく、`django-admin.py`にパスを書く必要がある。
やっておくとsys.path.appendは不要になる(場合がある)。

Django の django-admin.py でパスを通しても command not found の時 - Qiita

画像処理だと、OpenCVを使いたい場合があるが、Pythonバインディングはcv2.soファイルで行っているので、そこへのPATHも通す必要がある。(大体site-package内にあるはずだが自分は諸事情でなかったので)

また、wsgiのPATHの設定が居る場合もある。
wsgi.py(もしくはvirtualenv内のwsgiファイル)にWSGIPythonPathとしてsite-package周りを追加しておく。

Apacheだったらそれらでディレクトリの権限をちゃんと設定しておく必要あり。


- おわりに -

こんなのが出来る

vaaaaaanquish.hatenadiary.jp

dlibのSimple_Object_detectorを用いたPythonでの物体検出器の学習

- はじめに -

これはこの記事の続きで、dlibを使って物体検出をしようというものである。

まあ正確には、dlibには「顔検出器の学習」ってのは無くて「物体検出器の学習」の機能を使って、顔検出器の再学習がしたいという記事です。

dlibを使う際の参考になればよいです。


- dlibのObjectDetectorについて -

dlibに物体検出の学習が入ったのは2014年の時。
内部にはHoG+SVMを使っていて、OpenCVで学習する場合に比べて、遥かに少ない学習データで、かなりの精度を出す事ができる。

リリース時の本家記事 : dlib C++ Library: Dlib 18.6 released: Make your own object detector!

本記事では、Pythonのdlib apiを使って、物体検出器の学習を行っていく。

Python用のドキュメント : Classes — dlib documentation

dlib.simple_object_detectorを使う。
一応、こちらに公式の学習サンプルがある。

http://dlib.net/train_object_detector.py.html

大体の事は書いてあるけど、パラメータ等が全部書いてある訳ではないので、日本語訳してごにょごにょしたものをリポジトリに置いておいたので見て頂ければ。

github.com



- 学習形式とサンプル -

ディレクトリ内の画像と矩形情報が入ったテキストファイルを元に学習するスクリプトは以下。

#! /usr/bin/python
# -*- coding: utf-8 -*-
u"""rect.txtと画像データを用いてdlibを追加学習するスクリプト."""

import dlib
import os
from skimage import io

input_folder = "./test/"
rect_file = "./true_rect.txt"
output_svm = "detector.svm"

def get_rect(rect_file):
    u"""矩形ファイルを読み込みリスト化."""
    rect_list = []
    for line in open(rect_file, 'r'):
        rect_list.append(line)
    return rect_list


def make_train_data(rect_list):
    u"""矩形リストから学習用データを生成する."""
    boxes = []
    images = []
    for i, x in enumerate(rect_list):

        # 改行と空白を除去してリスト化
        x = x.replace('\n', '')
        x = x.replace('\r', '')
        one_data = x.split(' ')
        # 矩形の数k
        k = len(one_data) / 4

        # 矩形をdlib.rectangle形式でリスト化
        img_rect = []
        for j in range(k):
            left = int(one_data[j*4])
            top = int(one_data[j*4+1])
            right = int(one_data[j*4+2])
            bottom = int(one_data[j*4+3])
            img_rect.append(dlib.rectangle(left, top, right, bottom))

        # boxesに矩形リストをtupleにして追加
        # imagesにファイル情報を追加
        f_path = input_folder + one_data[k*4] + '.jpg'
        if os.path.exists(f_path):
            boxes.append(tuple(img_rect))
            images.append(io.imread(f_path))

    return boxes, images


def training(boxes, images):
    u"""学習するマン."""
    # simple_object_detectorの訓練用オプションを取ってくる
    options = dlib.simple_object_detector_training_options()
    # 左右対照に学習データを増やすならtrueで訓練(メモリを使う)
    options.add_left_right_image_flips = True
    # SVMを使ってるのでC値を設定する必要がある
    options.C = 5
    # スレッド数指定
    options.num_threads = 16
    # 学習途中の出力をするかどうか
    options.be_verbose = True
    # 学習許容範囲
    options.epsilon = 0.001
    # サンプルを増やす最大数(大きすぎるとメモリを使う)
    options.upsample_limit = 8
    # 矩形検出の最小窓サイズ(80*80=6400となる)
    options.detection_window_size = 6400

    # 学習してsvmファイルを保存
    print('train...')
    detector = dlib.train_simple_object_detector(images, boxes, options)
    detector.save(output_svm)


if __name__ == '__main__':
    rect_list = get_rect(rect_file)
    boxes, images = make_train_data(rect_list)
    training(boxes, images)


simple_object_detector_training内部でデータの増量を行っており、optionのupsample_limitとadd_left_right_image_flipsで調整できる。
データの増量では、基本的なData Augmentationが行われているため、学習用のデータは最小で良い。

実際、公式のサンプルコードでは、22枚のサンプル画像と矩形情報を学習用データセットとして、高い精度の顔検出器を作っている。

あまり画像を入れるとMemoryErrorの原因となる。
大体こんな感じで止まったら、Optionのパラメータ調整しなおすか、画像を減らすか、メモリを増やす必要がある。

Traceback (most recent call last):
  File "detector.py", line 104, in <module>
    boxes, images = make_train_data(rect_list)
  File "detector.py", line 70, in make_train_data
    images.append(io.imread(f_path))
  File "C:\Python27\lib\site-packages\skimage\io\_io.py", line 61, in imread
    img = call_plugin('imread', fname, plugin=plugin, **plugin_args)
  File "C:\Python27\lib\site-packages\skimage\io\manage_plugins.py", line 211, in call_plugin
    return func(*args, **kwargs)
  File "C:\Python27\lib\site-packages\skimage\io\_plugins\pil_plugin.py", line 37, in imread
    return pil_to_ndarray(im, dtype=dtype, img_num=img_num)
  File "C:\Python27\lib\site-packages\skimage\io\_plugins\pil_plugin.py", line 111, in pil_to_ndarray
    frame = np.array(frame, dtype=dtype)
MemoryError

dlibの公式Q&Aで「MemoryErrorって出るんだけど…」という質問に対して、作者が「Buy Memory!」と応えているくらいなので仕方ない。

感覚としては、32Gメモリ積んだマシンでも、100*100サイズの画像1000枚を、add_left_right_image_flips=true、upsample_limit=4とかで学習させたら落ちる。
CPUもフルに使うので最悪PCフリーズが有り得る。
学習データを減らすのが手っ取り早いが、対応できる環境が少なくなる。
マシンかパラメータでなんとかこうとかするのが良い。
(こういう点から、dlibの物体検出器学習クラスは背景や周りの環境が固定な場合超強いって感じする。)

64Gメモリ、16コアのCPUでも100*100の画像2000枚くらいが限界っぽい。
それ以上はパラメータ調整云々でもなんともならなかった。


学習用の矩形情報と画像情報はPythonコードで言うと以下のような形式で入力する。
boxes[n]とimages[n]が共通の情報となれば良い。

boxes_img1 = ([dlib.rectangle(left=329, top=78, right=437, bottom=186),
               dlib.rectangle(left=224, top=95, right=314, bottom=185),
               dlib.rectangle(left=125, top=65, right=214, bottom=155)])
boxes_img2 = ([dlib.rectangle(left=154, top=46, right=228, bottom=121),
               dlib.rectangle(left=266, top=280, right=328, bottom=342)])
boxes = [boxes_img1, boxes_img2]
images = [io.imread(dir_path + '/xxxxxx.jpg'),
          io.imread(dir_path + '/yyyyyy.jpg')]


学習に使うrect.txtは

x1 y1 x2 y2 file_name
x1 y1 x2 y2 file_name2

のような空白CSVっぽくなってる前提。
矩形が複数ある場合の1行は

x1 y1 x2 y2 x3 y3 x4 y4 file_name

といった形式で保存してあるものをパースしている。

いつかxmlにもする。
学習データ作って、xmlで学習させてる人は居たのでリンク貼っとく。



- 学習結果のsvmを使う -

前回の記事のdetector.runする部分を修正する。

- detector = dlib.get_frontal_face_detector()
+ detector = dlib.simple_object_detector("detector.svm")

- dets, scores, idx = detector.run(img_rgb, 0)
+ dets = detector(img_rgb, 0)

自前で学習した学習器はスコアや第二候補を返さないっぽい。



- テスト -

前回の記事Google Cloud Vision APIの記事で出したデータを元に学習させる。
例によって河村友歌ちゃんの顔画像でテストする。

f:id:vaaaaaanquish:20160902172432j:plain

はい、かわいい。


いつも顔検出ばかりやっていては仕方ないので、それっぽく猫の画像を学習させ適応する。
学習データは手動で矩形を出して、たった20枚作っただけ。

f:id:vaaaaaanquish:20160902172729j:plain

はい、かわいい。


- 考察 -

dlibの物体検出は内部でサンプリングもしてくれるので、正データとなる画像と矩形だけ集めれば良いし、精度も良いので結構良い。
パラメータも少なく物体検出できる方だと思う。
メモリとCPUはバカ食いするけど愛嬌がある。

v19.01現在で、追加学習のようなクラスはないため、既存のfrontal_faceのsvmファイルをsaveしてさらに学習とかはできない。
あと、メモリ少ないから学習データ小分けにして食わせようとかもできないのでつらい。
SVMなので仕方ない感じではあるが。

文中にも書いたけど、固定的な環境(監視カメラとか背景が固定とか)だと、手軽にかなり高い精度を実現できる。
それ以外ならOpenCVとかの検出器と組み合わせるか、CNNに突っ込んだ方が吉。


- おわりに -

dlib、Deep Learningとか強化学習とか新しい手法をガンガン積んでいってるし、期待したい。

まだまだかゆい所に手が届かないので「コントリビュータにさせてくれよ!」と思ったけどGithubリポジトリなかったのでつらい。

まあでも、やり取りするより1からオレオレで書いた方が早いなと思った。

ほんとそれなわかるアカデミア。



もろもろのコードはGithubリポジトリに入れといたんでよしなに。
github.com



- 追記 -

--09/04--


がんばります。