orangain flavor

じっくりコトコト煮込んだみかん2。知らないことを知りたい。

プログラミングしながら運動できるエアロバイク FitDesk X-2.0を買った

2016-01-16追記: 購入した当初は高いと思っていたサドルですが、座り方で多少改善されるようです。身長160cmの妻でも漕げています。

平日の家に帰ってご飯を食べてから寝るまでの時間はコードを書いていることが多いのですが、まったく運動しないことが気にかかっていました。 運動するためにジムに行くのは面倒だし、コードを書く時間を減らしたくはないので、運動しながらコードを書ける方法がないか考えていました。

そんなときに以下の記事を見て、運動しないとなぁと思っていると、「サイクリングデスク」という言葉が目に入りました。

「The Healthy Programmer」を読んで自宅を快適なプログラミング環境にする - 八発白中

 オフィスでは1種類以上のイスを使うこと -- 普通のオフィスチェア、バランスボール、可動式のイス、さらに言えばサイクリングデスク

サイクリングデスクってなんだ?と思ってググって先頭に出てきたもの*1は家庭用ではなかったですが、要するにエアロバイクにPCを置ける机がついていれば良いことに気付きました。

というわけでFitDesk X-2.0という製品を買いました。

以下のページのように普通のエアロバイクに机を足すのでも良いかもしれませんが、安定性やスペースを考えると、多少高くても最初から一体となっている製品を買うのが良いと判断しました。

組み立て

組み立てには大人の男性1人で2時間弱かかりました。 必要な工具は一緒に入っているので問題ありません。

組み立てマニュアルは中国の方が英語から日本語に訳したようで、若干元の英語を想像しながら読解する必要がありますが、なんとかなります。

地味に大変だったのはサドルに棒をくっつけるところです。角度が急な箇所のナットを閉める必要があり、ちょっとずつしか進まないため時間がかかりました。

サイズ的には、アメリカンサイズでかなり大きめです。私は身長170cmですが、サドルを一番下げて、机を一番手前に引いてちょうどいいぐらいです。 小柄な人は注意したほうがいいかもしれません。

また、Amazonのページには以下のように書かれていますが、この横と長さは床に接する部分のサイズです。

サイズ 横41cmx長さ71cmx高さ114cm

実際に組み立てると机の部分が本体より前にせり出すため、空間としては横53cmx長さ110cmぐらい必要です。

 サポート

いざ組み立ててしばらく漕いでみるとカタカタと異音がしました。 代理店のWebサイトからサポートに問い合わせてみると、迅速に本体を交換して頂けました。

Amazon.comでは$300以下で販売されている一方、日本では5万円を超えているのも気になっていましたが、サポートを考えると日本の代理店で買って良かったです。

感想

漕ぎながら作業できるのかは若干疑問もありましたが、慣れれば集中して作業できます。この記事も半分ぐらいは漕ぎながら書きました。 逆に疲れたことに気づかずに漕ぎ続けてしまう危険もあると思っていて、最初のうちは意識的に30分ぐらいで辞めるよう気をつけています。

何にせよ、日常生活に運動を取り入れられるのは嬉しいです。 エアロバイクは低めの負荷でスルスルと漕ぐのが良いらしいので、10段階中の下から3段目で漕いでいます。

肝心の机の使い心地は上々です。流石にエアロバイクと一体になっているだけあって安定しています。アームレストがついているので腕がつかれることもありません。机にはMacbook Pro 13 inch がちょうど良く収まります。

f:id:mi_kattun:20150222224712j:plain

ノートPCを置くとメーターは見えなくなるので全く使ってません。アームバンドも置いてあるだけです。机の下の引き出しも使ってないですが、滑りが悪く、使いやすくはなさそうです。

音はほとんど気になりませんが、床を保護するために以前Kinect用に買ったマットを敷いています。

ALINCO(アルインコ)  エクササイズ フロアマット 厚さ 9mm EXP150

ALINCO(アルインコ) エクササイズ フロアマット 厚さ 9mm EXP150

Re:VIEWとDockerとCircleCIで原稿を継続的インテグレーション

Re:VIEW で執筆する原稿を継続的インテグレーションしたかったので、以下の図のような仕組みを作りました。

f:id:mi_kattun:20150113001123p:plain

ローカル執筆環境の前提条件

  • Docker 1.3以降がインストールされている
  • Re:VIEW形式の原稿のフォルダがある

Re:VIEWやLaTeXはDockerイメージのものを使うので、インストール不要です。

私はMac OS X 10.9とBoot2docker 1.4.1で動かしましたが、WindowsLinuxでも動くのではないかと思います。

Re:VIEWをDockerで動かす

vvakameさんがDocker Hubにイメージを公開してくれていますので、これを使います。

https://registry.hub.docker.com/u/vvakame/review/

dockerコマンドさえ使える状態であれば、以下のようにしてサンプル書籍コンパイルしてbook.pdfを生成できます。

$ git clone -b docker-circleci https://github.com/orangain/review-sample-book
$ cd review-sample-book/src
$ docker run \
     --rm \
     -v $(pwd):/work \
     -v $(pwd)/.texmf-var:/root/.texmf-var \
     vvakame/review:latest /bin/sh -c "cd /work && review-pdfmaker config.yml"

初回実行時は2GB超のイメージをダウンロードするので、結構時間がかかりますが、2回目以降は数秒で終わります。

ホストのカレントディレクトリをコンテナ内の /work としてマウントしてその中で実行するので、コンテナ内であることをほとんど意識せずに使えます。

review-pdfmaker 実行の際に、コンテナ内の /root/.texmf-var フォルダにフォントのキャッシュ?(詳しくは知りません)が作られます。これをホスト側に永続化できるよう設定してやることで、2回目以降は高速にPDFを生成できます。

CircleCIでDockerを使う

CircleCI ではDockerを使えます。DockerネイティブなDrone.io のほうがやりやすいかもしれませんが、プライベートリポジトリでも1並列なら無料で使えるCircleCIを選択しました。(と思いましたが、Shippable などでも良かったかもしれません。いずれ試してみたいです。)

サンプル書籍では、以下のような circle.yml を置くことでビルドを自動化できました。

# Dockerを使う
machine:
  services:
    - docker

# Dockerイメージを毎回プルしなくても良いようにキャッシュする
# See: https://circleci.com/docs/docker#caching-docker-layers
dependencies:
  cache_directories:
    - "~/docker"
  override:
    - docker info
    - if [[ -e ~/docker/image.tar ]]; then docker load --input ~/docker/image.tar; fi
    - docker pull vvakame/review
    - mkdir -p ~/docker; docker save vvakame/review > ~/docker/image.tar

# 執筆環境で使うコマンドとは以下の2点が異なる
# 1. --rm オプションがエラーになるので使わない
#    See: https://github.com/docker/docker/issues/4897
# 2. .texmf-varフォルダをマウントしない
#    キャッシュするためにはdependenciesでキャッシュを作らないといけない
test:
  override:
    - cd src; docker run -v $(pwd):/work vvakame/review:latest /bin/sh -c "cd /work && review-pdfmaker config.yml"

# 生成したPDFを成果物として保存する
general:
  artifacts:
    - "src/*.pdf"

ジョブは以下の場所から閲覧できます。成果物は自分しか見えないようですが。

forkしたソースコードは以下の場所に置いてあります。

参考

2014年を振り返って

2014年を振り返って

2014年は対外的に評価される機会があって嬉しかったです。

MBSハッカソンは特にそうなんですが、新しい人との出会いがあり、これから楽しくなりそうです。

一方で個人で数ヶ月開発していたサービスはまだ世に出せていないので何とかしないといけません。

また、一番仲の良かった友人を亡くし、忘れられない年になりました。

 2015年に向けて

2015年に向けて面白い話を頂いていて、そのうちお知らせできるかもしれません。

最近はやりたいことの量の割に、使える時間が足りていないので、やりたいことを絞って一つ一つ実現していきたいです。

体力的にも色々ガタが来てる気がするので、健康にも気をつかって、使える時間を最大化していく必要があります。

特にPCを見続けると眼のリソースが消耗していると感じるので、ポッドキャストのように眼を使わずにできることを増やしていきたいです。

来年もよろしくお願いします。

Webサイトのクローラビリティをチェックする

これはクローラー/スクレイピング Advent Calendar 2014の18日目の記事です。

Webサイトをクローリング、スクレイピングしたいと思ったとき、はじめに何をするでしょうか?

私はとりあえずブラウザの開発者ツールでDOMを覗きますが、その後robots.txt利用規約をチェックします。

そういう作業を繰り返すうちに面倒になってきたので、URLを与えるだけで自動的にクローラビリティ(クロールしやすさ)をチェックするWebサービスをやっつけで作りました。

f:id:mi_kattun:20141219003040p:plain

Crawlability · Check the crawlability of web sites

適当にクロールしたいURLを入力してCheckボタンを押してみてください。

できること

クローラビリティとは言ってもまだコンセプトレベルで、以下のことができるだけです。

  • 指定したURLの情報を表示する
  • トップページの情報を表示する
  • robots.txtを表示する
  • よくあるXML SitemapのURLにアクセスして存在するか確かめる
  • 利用規約っぽいページにアクセスしてみる

もう少しいい感じに情報を取得できるようにしたいです。

中身

ソースコードGitHubに置いてあります。

https://github.com/capybala/crawlability

ちなみに内部では以前の記事で紹介したaiohttpを使っています。Python 3.3から使えるasyncioを使って並列にアクセスしています。

aiohttpはHTTPクライアントだけでなく、HTTPサーバーの機能も実験的ながら付いているので、Webインターフェイスはこれで作ってみました。

クローラーをデーモンとして動かす ― Scrapyd

この記事はクローラー/スクレイピング Advent Calendar 2014の12日目の記事です。

ScrapyPythonにおけるクローリング・スクレイピングフレームワークとして有名ですが、Scrapydという興味深い機能があるので今日はこれを紹介します。

Scrapydはその名の通り、Scrapyのデーモンです。サーバーにおいてサービスとして動作し、Scrapyで作ったクローラーのジョブ管理ができます。

多くのページをクロールするクローラーにおいては、1回の実行時間が1日を超えることもしばしばあるので、ジョブ管理は重要です。このように運用を考慮した機能まで備えているのがScrapyの特徴的なところです。

Scrapydの概要

Scrapydは簡単なWebインターフェイスを提供しており、主にcurlを使ってAPIを呼び出します。

http://Scrapydをインストールしたホスト:6800 にアクセスすると、以下のようなシンプルな画面が表示されます。

f:id:mi_kattun:20141212224359p:plain

詳しくは後述しますが、Scrapyプロジェクトをデプロイしてある状態で以下のコマンドを実行すると、ジョブを実行できます。

curl http://Scrapydをインストールしたホスト:6800/schedule.json -d project=プロジェクト名 -d spider=スパイダー名

「Jobs」というリンクをクリックした先で実行中のジョブの状況を見られます。ジョブごとにログやスクレイプしたアイテムを見ることもできます。

f:id:mi_kattun:20141212224405p:plain

Scrapydのメリット

通常クローラーをデーモンとして動かすときは、Cronを使うと思います。ScrapydはCronと競合するツールではなく、Cronと組み合わせて使えるツールです。Scrapydを使うことで、Cron単体で使うときに比べて以下のメリットが得られます。

  • ジョブの実行状況を一覧で把握できる。
  • 同時に実行するジョブ数を制限できる。
  • Cronで定期的にジョブを実行するだけでなく、アドホックに実行できる。
  • HTTPのAPIで外部から簡単に操作できる。

Scrapydの使い方

以下のイメージでサーバーにデプロイします。

f:id:mi_kattun:20141212225022p:plain

Scrapydのインストール(サーバー側)

普通にpipでインストールしてsupervisordなどで管理することもできますが、Ubuntu向けにaptのパッケージが提供されています。せっかくなのでこれを使って楽をしてみましょう。

$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 627220E7
$ echo 'deb http://archive.scrapy.org/ubuntu scrapy main' | sudo tee /etc/apt/sources.list.d/scrapy.list
$ sudo apt-get update
$ sudo apt-get install scrapyd

探せばDockerイメージやDockerfileを公開している人もいるので、それを使ってもよいでしょう。

※プロジェクトでScrapy以外のサードパーティライブラリ(例えばDBへのアダプタ)を使っている場合は、Scrapydと同じPython環境にインストールする必要があります。グローバル環境が汚れるのが気になる場合は、Virtualenv内にScrapydをインストールしてsupervisordなどで管理するのが良いでしょう。

初期設定(クライアント側)

Scrapyプロジェクトのフォルダにある scrapy.cfg[deploy] セクションにあるurlをScrapydのURLに書き換えます。

# Automatically created by: scrapy startproject
#
# For more information about the [deploy] section see:
# http://doc.scrapy.org/en/latest/topics/scrapyd.html

[settings]
default = helloscrapy.settings

[deploy]
# ここのurlを書き換える
url = http://192.168.33.10:6800/
project = helloscrapy

ブラウザで http://Scrapydをインストールしたホスト:6800 にアクセスして上で紹介したスクリーンショットのような画面が表示されることを確認しましょう。

デプロイ(クライアント側)

Scrapyプロジェクトのディレクトリで、以下のコマンドを実行するだけで現在のプロジェクトがegg化され、Scrapydにデプロイされます。

$ scrapy deploy
Packing version 1418386468
Deploying to project "helloscrapy" in http://192.168.33.10:6800/addversion.json
Server response (200):
{"status": "ok", "project": "helloscrapy", "version": "1418386468", "spiders": 2}

ジョブの実行(クライアント側)

以下のようにしてAPIを叩くことで、クローラーが動き始めます。

$ curl http://Scrapydをインストールしたホスト:6800/schedule.json -d project=プロジェクト名 -d spider=スパイダー名
{"status": "ok", "jobid": "722f9a7281f811e4b62808002743bc76"}

HTTPのAPIを叩くだけなので、Cronからも簡単に起動できます。Cronでありがちな環境変数の問題にハマることも少ないでしょう。また、外部のホストからも簡単にトリガーできるので、私はCronではなくJenkinsを使って定期的にクローラーを起動しています。

まとめ

いかがでしたでしょうか。若干使いにくいところもあるScrapydですが、クローラーの実行状況をまとめて管理できるので見通しが良くなります。Scrapydを使って快適なScrapy運用ライフを送りましょう!

Scrapydの機能はそんなに多くないですが、紹介しきれてはいないので詳しくはドキュメントをご覧ください。

http://scrapyd.readthedocs.org/en/latest/

今回使用したScrapyのバージョンは0.24ですが、Scrapydのドキュメントには古い記述が混ざっているようなのでお気をつけ下さい。

Pythonでクローリング・スクレイピングに使えるライブラリいろいろ

2016-12-09追記

Pythonクローリング&スクレイピング」という本を書きました!


これはクローラー/スクレイピング Advent Calendar 2014の7日目の記事です。

Pythonでクローリング・スクレイピングするにあたって、いろいろなライブラリがあるので一覧でまとめてみます。

以下の4つのカテゴリにわけて紹介します。

  • Webページを取得する
  • Webページからデータを抜き出す
  • Webページの自動操作
  • 総合的なフレームワーク

なんでこれが載ってないの?この説明はおかしい!などありましたらお気軽にお知らせください。なお、この記事はいろいろなライブラリを紹介することを目的にしているので、各ライブラリの細かい説明は他に譲ります。

サンプルで使用するPythonのバージョンは特に断りのない限り3.4で、一部3.xに対応していないライブラリは2.7を使います。

Webページを取得する

urllib.request

21.6. urllib.request — URL を開くための拡張可能なライブラリ — Python 3.4.2 ドキュメント

標準ライブラリです。Python 2.xではurllib2に相当します。

>>> from urllib.request import urlopen
>>> f = urlopen('http://qiita.com/advent-calendar/2014')
>>> f.code
200
>>> f.getheader('content-type')
'text/html; charset=utf-8'
>>> f.info().get_content_charset()
'utf-8'
>>> f.read()
b'<!DOCTYPE html><html xmlns:og="http://ogp.me/ns#"><head><meta charset="UTF-8" /><title>2014\xe5\xb9\xb4\xe3\x81\xaeAdvent Calendar\xe4\xb8\x80\xe8\xa6\xa7 - Qiita</title><meta charset="UTF-8" />...

requests

Requests: HTTP for Humans — Requests 2.5.0 documentation

HTTP for Humans、人間のためのHTTP Clientです。このサンプルではurllibとの違いがわかりにくいですが、Basic認証を使うなどHTTPヘッダーを扱うときには簡単さが際立ちます。

pip install requests
>>> import requests
>>> r = requests.get('http://qiita.com/advent-calendar/2014')
>>> r.status_code
200
>>> r.headers['content-type']
'text/html; charset=utf-8'
>>> r.encoding
'utf-8'
>>> r.text
'<!DOCTYPE html><html xmlns:og="http://ogp.me/ns#"><head><meta charset="UTF-8" /><title>2014年のAdvent Calendar一覧 - Qiita</title><meta charset="UTF-8" />...

aiohttp

KeepSafe/aiohttp

Python 3.3から使用可能なasyncioを使って非同期にページを取得できます。多数のWebサイトを高速にクロールしたいときに力を発揮するでしょう。

pip install aiohttp
>>> import asyncio
>>> import aiohttp
>>> def get_body(url):
...     response = yield from aiohttp.request('GET', url)
...     return (yield from response.text())
...
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(get_body('http://qiita.com/advent-calendar/2014'))
'<!DOCTYPE html><html xmlns:og="http://ogp.me/ns#"><head><meta charset="UTF-8" /><title>2014年のAdvent Calendar一覧 - Qiita</title><meta charset="UTF-8" />...

scrapelib

sunlightlabs/scrapelib

クロール先のサーバーに負荷をかけ過ぎないように、間隔を自動的に調整したりエラー時に自動でリトライしてくれます。内部ではrequestsを使っています。

pip install scrapelib
>>> import scrapelib
>>> s = scrapelib.Scraper(requests_per_minute=10)
>>> r = s.get('http://qiita.com/advent-calendar/2014')
>>> type(r)
<class 'requests.models.Response'>
>>> r.status_code
200
>>> while True:
...     r = s.get('http://qiita.com/advent-calendar/2014')
...     r.status_code
... 

200
200
200
...

Webページからデータを抜き出す

html.parser

20.2. html.parser— HTML および XHTML のシンプルなパーサー — Python 3.4.2 ドキュメント

標準ライブラリです。SAX形式のイベント駆動型のAPIなので、あまり複雑な処理には向いていませんが、開始タグだけが必要な時なら手軽に使えます。

Python 2.xではHTMLParserというモジュール名でした。

>>> from html.parser import HTMLParser
>>> from urllib.request import urlopen
>>> class MyHTMLParser(HTMLParser):
...     def handle_starttag(self, tag, attrs):
...         if tag == 'a':
...             print(dict(attrs).get('href'))
...
>>> f = urlopen('http://qiita.com/advent-calendar/2014')
>>> parser = MyHTMLParser()
>>> parser.feed(f.read().decode('utf-8'))
/
/login?redirect_to=%2Fadvent-calendar%2F2014
/signup?redirect_to=%2Fadvent-calendar%2F2014
/advent-calendar
/advent-calendar/2014
/advent-calendar/2014/new
/advent-calendar/2014/muda/feed
/advent-calendar/2014/muda
/advent-calendar/2014/softlayer2/feed
...

lxml

lxml - Processing XML and HTML with Python

libxml2とlibxsltのPythonバインディングです。処理の高速さと機能の豊富さでは他の追随を許しません。

pip install lxml
>>> import lxml.html
>>> root = lxml.html.parse('http://qiita.com/advent-calendar/2014').getroot()
>>> root.cssselect('title')[0]
<Element title at 0x10b391c78>
>>> root.cssselect('title')[0].text
'2014年のAdvent Calendar一覧 - Qiita'
>>> for a in root.xpath('//a'):
...     print(a.get('href'))
...
/
/login?redirect_to=%2Fadvent-calendar%2F2014
/signup?redirect_to=%2Fadvent-calendar%2F2014
/advent-calendar
/advent-calendar/2014
/advent-calendar/2014/new
/advent-calendar/2014/muda/feed
/advent-calendar/2014/muda
/advent-calendar/2014/softlayer2/feed
...

BeautifulSoup4

Beautiful Soup: We called him Tortoise because he taught us.

3まではPure Pythonのため処理の遅さが弱点でしたが、4からはパーサーを選択できるので、lxmlを使えば高速に処理できます。標準ライブラリのパーサーを使えばC拡張を使えない環境でも役立ちます。

日本語ドキュメントが存在するのも、とっつきやすいでしょう。

pip install beautifulsoup4
>>> from urllib.request import urlopen
>>> from bs4 import BeautifulSoup
>>> f = urlopen('http://qiita.com/advent-calendar/2014')
>>> soup = BeautifulSoup(f)
>>> soup.title
<title>2014年のAdvent Calendar一覧 - Qiita</title>
>>> soup.title.string
'2014年のAdvent Calendar一覧 - Qiita'
>>> for a in soup.find_all('a'):
...     print(a.get('href'))
...
/
/login?redirect_to=%2Fadvent-calendar%2F2014
/signup?redirect_to=%2Fadvent-calendar%2F2014
/advent-calendar
/advent-calendar/2014
/advent-calendar/2014/new
/advent-calendar/2014/muda/feed
/advent-calendar/2014/muda
/advent-calendar/2014/softlayer2/feed
...

pyquery

pyquery 1.2.9 : Python Package Index

jQueryライクなAPIを提供するので、jQueryに慣れている人には馴染みやすいでしょう。内部ではlxmlが使われます。

pip install pyquery
>>> from pyquery import PyQuery as pq
>>> d = pq(url='http://qiita.com/advent-calendar/2014')
>>> d('title')
[<title>]
>>> d('title').text()
'2014年のAdvent Calendar一覧 - Qiita'
>>> for a in d('a').items():
...     print(a.attr('href'))
...
/
/login?redirect_to=%2Fadvent-calendar%2F2014
/signup?redirect_to=%2Fadvent-calendar%2F2014
/advent-calendar
/advent-calendar/2014
/advent-calendar/2014/new
/advent-calendar/2014/muda/feed
/advent-calendar/2014/muda
/advent-calendar/2014/softlayer2/feed

feedparser

feedparser 5.1.3 : Python Package Index

RSSなどのフィードをパースするための定番ライブラリです。フィードの種類に依らずに同じ書き方ができるため、標準ライブラリのxml.etree.ElementTreeなんかでパースするより簡単です。

pip install feedparser
>>> import feedparser
>>> d = feedparser.parse('http://qiita.com/advent-calendar/2014/crawler/feed')
>>> d.feed.title
'クローラー/スクレイピング Advent Calendarの投稿 - Qiita'
>>> for entry in d.entries:
...     print(entry.link)
...
http://blog.takuros.net/entry/2014/12/06/235232
http://blog.takuros.net/entry/2014/12/05/061034
http://happyou-info.hatenablog.com/entry/2014/12/04/005504
http://qiita.com/nezuq/items/3cc9772118ad112c18dc
http://blog.takuros.net/entry/2014/12/02/234959

Webページの自動操作

Mechanize

mechanize

PerlのWWW:MechanizeのPython版です。ログインが必要なページのスクレイピングに向いています。最終更新は2011年で、Python 3に対応していません。以下のサンプルはPython 2.7で動かしたものです。6日目のdkfjさんの記事と同様にAmazonアソシエイトから売上を取得するサンプルです。

pip install mechanize
pip install lxml  # lxmlはサンプルで使用しているだけで必須ではありません
>>> import mechanize
>>> import lxml.html
>>> br = mechanize.Browser()
>>> br.addheaders = [('User-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36')]
>>> br.open('https://affiliate.amazon.co.jp')
<response_seek_wrapper at 0x1020b95f0 whose wrapped object = <closeable_response at 0x1020bd878 whose fp = <socket._fileobject object at 0x100d1a350>>>
>>> print(br.title())
Amazonアソシエイト(アフィリエイト)プログラムに参加しよう!
>>> br.select_form(name='sign_in')
>>> br['username'] = 'YOUR_EMAIL'
>>> br['password'] = 'YOUR_PASSWORD'
>>> response = br.submit()
>>> print(br.title())
Amazon アソシエイト(アフィリエイト) - ホーム
>>> root = lxml.html.parse(response).getroot()
>>> print(root.cssselect('#mini-report .line-item-total .data')[0].text)
¥0

selenium

selenium 2.44.0 : Python Package Index

SeleniumPythonバインディングです。FirefoxChromeなどのブラウザを自動操作したり、PhantomJSのようなヘッドレスブラウザを使うことができます。JavaScriptを使ったページにも対応できる点が強みです。

pip install selenium
>>> from selenium import webdriver
>>> driver = webdriver.Firefox()
>>> driver.get('https://affiliate.amazon.co.jp')
>>> driver.title
'Amazonアソシエイト(アフィリエイト)プログラムに参加しよう!'
>>> driver.find_element_by_name('username').send_keys('YOUR_EMAIL')
>>> driver.find_element_by_name('password').send_keys('YOUR_PASSWORD')
>>> driver.find_element_by_name('password').submit()
>>> driver.title
'Amazon アソシエイト(アフィリエイト) - ホーム'
>>> driver.find_element_by_css_selector('#mini-report .line-item-total .data').text
'¥0'

Splinter

Splinter — Splinter 0.7.0 documentation

SeleniumFirefox, Chrome, Remote, PhantomJSの各種ドライバーに加えて、zope.testbrowserなどもラップしており目的に応じて使い分けられます。シンプルで使いやすいAPIが特徴です。

pip install splinter

デフォルトではFirefox WebDriverが使われますが、以下のサンプルではPhantomJS WebDriverを使います。*1

PhantomJSがインストールされていない場合はインストールします。Mac以外の方はググってください。。

brew install phantomjs
>>> from splinter import Browser
>>> browser = Browser('phantomjs', user_agent='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.71 Safari/537.36')
>>> browser.visit('https://affiliate.amazon.co.jp')
>>> browser.title.decode('utf-8')
'Amazonアソシエイト(アフィリエイト)プログラムに参加しよう!'
>>> browser.fill('username', 'YOUR_EMAIL')
>>> browser.fill('password', 'YOUR_PASSWORD')
>>> browser.find_by_value('サインイン').click()
>>> browser.title.decode('utf-8')
'Amazon アソシエイト(アフィリエイト) - ホーム'
>>> browser.find_by_css('#mini-report .line-item-total .data').text
'¥0'

総合的なフレームワーク

Scrapy

Scrapy | A Fast and Powerful Scraping and Web Crawling Framework

Pythonが誇るスクレイピングフレームワークです。Python 3への対応作業は進められていますが、現在はPython 2.7のみ対応です。

pip install scrapy

他のライブラリと違いフレームワークなので、インタラクティブシェルを使わずにファイルを作成します。

advent_spider.py

from scrapy import Spider, Item, Field

class AdventCalendar(Item):
    title = Field()

class AdventCalendarSpider(Spider):
    name = 'advent_spider'
    start_urls = ['http://qiita.com/advent-calendar/2014']

    def parse(self, response):
        return [AdventCalendar(title=e.extract()) for e in response.css('td.adventCalendar_calendarList_calendarTitle a:nth-child(2)::text')]
$ scrapy runspider advent_spider.py
...
2014-12-07 22:47:47+0900 [advent_spider] DEBUG: Crawled (200) <GET http://qiita.com/advent-calendar/2014> (referer: None)
2014-12-07 22:47:47+0900 [advent_spider] DEBUG: Scraped from <200 http://qiita.com/advent-calendar/2014>
{'title': u'1\u5186\u306b\u3082\u306a\u3089\u306a\u3044\u7121\u99c4\u306a\u6280\u8853'}
2014-12-07 22:47:47+0900 [advent_spider] DEBUG: Scraped from <200 http://qiita.com/advent-calendar/2014>
{'title': u'1\u5206\u3067\u5b9f\u73fe\u3067\u304d\u308b\u6709\u7528\u306a\u6280\u8853'}
2014-12-07 22:47:47+0900 [advent_spider] DEBUG: Scraped from <200 http://qiita.com/advent-calendar/2014>
{'title': u'2\u679a\u76ee SoftLayer '}
2014-12-07 22:47:47+0900 [advent_spider] DEBUG: Scraped from <200 http://qiita.com/advent-calendar/2014>
{'title': u'Abby'}
2014-12-07 22:47:47+0900 [advent_spider] DEBUG: Scraped from <200 http://qiita.com/advent-calendar/2014>
{'title': u'Adobe'}
2014-12-07 22:47:47+0900 [advent_spider] DEBUG: Scraped from <200 http://qiita.com/advent-calendar/2014>
{'title': u'AngularJS'}
...

このサンプルはフレームワークとしての通常の使い方ではないので、以前書いた記事もどうぞ。

PythonとかScrapyとか使ってクローリングやスクレイピングするノウハウを公開してみる! - orangain flavor

まとめ

いかがでしたでしょうか。いろいろな選択肢を知り、適切なものを選ぶ助けになれば幸いです。

複数のライブラリを紹介した3つのカテゴリにおいて1つを選ぶとしたら、個人的には以下のものが定番かと思います。

  • Webページを取得する:requests
  • Webページからデータを抜き出す:lxml
  • Webページの自動操作:selenium

今回調べて初めて知ったライブラリもありました。他にももっと良いものがありましたらぜひ教えて下さい。

*1:PhantomJSを使うことでFirefoxを使うよりもコードが複雑になり、seleniumのサンプルよりも若干複雑に見えますが、これに他意はありません。本来であればseleniumでもヘッドレスのPhantomJSを使いたかったのですが、seleniumでUser-agentヘッダを変更するのが面倒すぎたので仕方なくFirefoxを使いました。

#isucon 2014予選の延長戦をやってみた

予選の時間内では足りてないことばかりだったので、もう少し試行錯誤することにしました。

#isucon 2014の予選をほぼ一人で戦うハメになった話 - orangain flavor

目標は50000点、できれば60000点出したい。

予選終了時

  • Python実装
  • DBはRedisのみを使う
  • Cookieがないときだけnginxで静的ファイルを返す
  • Gunicornを使ったマルチプロセスモデル
  • ワーカー数10、ワークロード10

最終提出スコア: 32710

細々とした改良

  • nginxの設定を追加。
  • redis-pyのパーサーをhiredisに置き換え。
  • テンプレートエンジンを使わないよう変更。
  • アプリを見なおして、RedisのRead/Write数を削減。

スコアはあまり上がらず 32912

Gunicornのワーカーをmeinheldに置き換え

前回のエントリのコメントで id:methane 氏に教えていただいたmeinheldに置き換え、nginx―Gunicorn間でKeepAliveを有効にしました。PyPI最新の0.5.6です。

スコアは5000ほど上がって、 37223

副作用として、nginxでCookieがないときだけ静的なindex.htmlを返すことができなくなりました。

nginxでの良い設定方法がわからず、こんな感じで無理矢理やっていたのですが、meinheldを使うと400 Bad Requestが返ってくるようになりました。

    location = / {
      if ($http_cookie !~* "session") {
        root /home/isucon/webapp/public/;
        rewrite ^.* index.html break;
      }
      proxy_pass http://app;
      proxy_set_header Host $http_host;
      proxy_http_version 1.1;
      proxy_set_header Connection "";
    }
    location / {
      proxy_pass http://app;
      proxy_set_header Host $host;
      proxy_http_version 1.1;
      proxy_set_header Connection "";
    }

原因不明ですが、Varnishを使うともっと簡単に設定できるようになると聞いていたので、この時点では深追いしませんでした。

Varnish導入

Varnishを導入しました。yumで4.0.xをインストールしたつもりが、Amazon Linuxだと3.0.xしかインストールできずしばらくハマりました。

また、daemonの起動に失敗してもどこにもログが吐かれないのでだいぶ悩みました。 varnishd -C -f FILEvclを検証すれば良いことを知りました。

バックエンドとしてUnix Domain Socketに対応していないようなので、元の通りGunicornをTCP 8080で動かすようにしました。

静的ファイルはnginx、アプリはgunicornと振り分けるよう設定しました。 

スコアはさほど変わらず 37632

エラーページを静的に返すように

Varnishで簡単にキャッシュできるようになったので、ログイン失敗時に /?err=*** のページにリダイレクトするようにしました。

これは結構効いて、スコアは 43715 に。Varnish便利。

データをインメモリに持つように

ユーザー情報とBAN/Lockの情報をメモリに保持するようにしました。マルチプロセスなので、結局Redisにもアクセスする必要があり、苦労した割に効果は薄かったです。

スコアは上がらず、というかむしろ下がり 42533

FlaskからBottleに書き換え

プロファイリング結果を見るとFlaskで時間を食っていたので、さらにmicroなフレームワーク Bottle に置き換えました。

これは結構効果があり、スコアは 50386

Sessionを暗号化されていないCookieに置き換え

正直どうなのかという話ですが、Session Cookieの暗号化と検証を捨て、ユーザー名と最終ログイン情報をCookieに直接格納しました。

このとき、Cookieの値に空白が含まれるとベンチマーカーが意図したとおりに返してくれない問題に若干苦しみました。

スコアは若干上がり 51076

RedisからのReadをパイプライン化

attempt_loginの条件分岐の中で、細切れでRedisからReadしていたのを、パイプラインで一括取得するようにしました。

これによりRedisへのアクセスは、1リクエストあたり最大Read1回、Write1回の計2回で済むようになりました。

スコア: 52705

1回休み

Redisの設定を見ると、いつの間にかRedisのappendonlyが無効になっており、永続化されてない状態になっていました。

有効にしてスコアを測り直すと 51246 でした。

PyPyにしてみる

PyPyを使ったら速くなったりしないかなーと思って試してみますが、スコアは 46607 に下がったので却下。

PyPyに単純に置き換えただけで、標準以外のライブラリも普通に動くのだなと感動しました。

2014-10-05 23:34追記: 後ほどちゃんとPyPyに合わせてチューニングしたらCPythonよりも高いスコアがでました。詳しくは文末の追記を参照。

Varnishのチューニング

VarnishのCPU利用率が高いのでチューニングしてみました。

  • VARNISH_MIN_THREADS=200 → 50
  • -p thread_pools=4
  • -p session_linger=100

いまいちよくわからないもののスコアは若干上がり 53016

最新のmeinheldに置き換え

GitHubの最新版を使うと良いと教わったので、置き換えてみました。

スコアは若干上がり、 53872

まとめ

最終的なスコアは 53872 となりました。これ以上はどうすればスコアが上がるか思いつきません。

とりあえずの目標としてた50000は超えましたが、60000には届きませんでした。Pythonのマルチプロセスモデルだとこの辺が限界なんでしょうか。。

ちなみにワークロードやGunicornのワーカー数はいじってもスコアが上がらなかったので、変えてません。 

ソースコードと設定はGitHubで公開しています。

クーポンのおかげで、あまりお金をかけずに勉強できました。AWS様ありがとうございます。

f:id:mi_kattun:20141005123906p:plain

参考サイト

おまけ

GOGC=offでベンチマーク実行

$ GOGC=off ~/benchmarker bench --workload 10

スコア: 79324

静的ファイルを返さないように

<link>タグと<img>タグをdocument.writeで書きだすようにしました。

$ GOGC=off ~/benchmarker bench --workload 10

スコア: 180084

orangain/isucon2014-qualifier-python at no-static

2014-10-05 23:34追記:PyPyをちゃんと導入してみた

例によって id:methane 氏に教えて頂きました。ほんとうに感謝です。

PyPyへの最適化

Tornado 3.2をインストールし、GunicornのワーカーをTornadoWorkerに置き換えました。またC拡張であるhiredisもアンインストールしました。

さらにinitスクリプトの中で、JITのウォームアップとして25秒間ベンチマークを走らせました。

ベンチマーク実行中は、GunicornのCPU消費がCPythonに比べて格段に減り、ワーカー数を減らすほどスコアが上がるようになりました。ワーカー数1、ワークロード14で 54736 まで上がりました。

1プロセスへの最適化

1プロセスを前提とすれば、メモリにRedisと同じ内容を保持し、RedisへのReadアクセスを完全に無くすことができます。Readしないように書き換えたところ、同じくワーカー数1、ワークロード14で 55955 まで上がりました。

orangain/isucon2014-qualifier-python at pypy

PyPyを使ってみて

PyPyは今回初めてまともに使いましたが、サードパーティのライブラリを含めてエラーなどは全く無く、単純に置き換えただけ*1で正常に動作し、正直驚きました。

Amazon Linuxへのインストールこそ非公式配布のバイナリを使いましたが、非常に安定している印象を受けました*2

PyPyを使えば、Pythonでも1プロセスで高速に処理するGo言語のような戦い方ができることを確認できて良かったです。それでも来年はGoで出られる程度にはGo言語力を高めたいですが。

*1:もちろんサードパーティライブラリのインストールは必要ですが。

*2:ちなみに使用したPyPyのバージョンはCPython 2.7互換のPyPy 2.4です。