プログラミングしながら運動できるエアロバイク FitDesk X-2.0を買った
2016-01-16追記: 購入した当初は高いと思っていたサドルですが、座り方で多少改善されるようです。身長160cmの妻でも漕げています。
平日の家に帰ってご飯を食べてから寝るまでの時間はコードを書いていることが多いのですが、まったく運動しないことが気にかかっていました。 運動するためにジムに行くのは面倒だし、コードを書く時間を減らしたくはないので、運動しながらコードを書ける方法がないか考えていました。
そんなときに以下の記事を見て、運動しないとなぁと思っていると、「サイクリングデスク」という言葉が目に入りました。
「The Healthy Programmer」を読んで自宅を快適なプログラミング環境にする - 八発白中
オフィスでは1種類以上のイスを使うこと -- 普通のオフィスチェア、バランスボール、可動式のイス、さらに言えばサイクリングデスク
サイクリングデスクってなんだ?と思ってググって先頭に出てきたもの*1は家庭用ではなかったですが、要するにエアロバイクにPCを置ける机がついていれば良いことに気付きました。
というわけでFitDesk X-2.0という製品を買いました。
FitDesk X-2.0 ジャパンモデル(フィットデスクX-2.0 ジャパンモデル )エアロバイク、パソコン
- 出版社/メーカー: FitDesk
- メディア: スポーツ用品
- この商品を含むブログを見る
以下のページのように普通のエアロバイクに机を足すのでも良いかもしれませんが、安定性やスペースを考えると、多少高くても最初から一体となっている製品を買うのが良いと判断しました。
組み立て
組み立てには大人の男性1人で2時間弱かかりました。 必要な工具は一緒に入っているので問題ありません。
組み立てマニュアルは中国の方が英語から日本語に訳したようで、若干元の英語を想像しながら読解する必要がありますが、なんとかなります。
地味に大変だったのはサドルに棒をくっつけるところです。角度が急な箇所のナットを閉める必要があり、ちょっとずつしか進まないため時間がかかりました。
サイズ的には、アメリカンサイズでかなり大きめです。私は身長170cmですが、サドルを一番下げて、机を一番手前に引いてちょうどいいぐらいです。 小柄な人は注意したほうがいいかもしれません。
また、Amazonのページには以下のように書かれていますが、この横と長さは床に接する部分のサイズです。
サイズ 横41cmx長さ71cmx高さ114cm
実際に組み立てると机の部分が本体より前にせり出すため、空間としては横53cmx長さ110cmぐらい必要です。
サポート
いざ組み立ててしばらく漕いでみるとカタカタと異音がしました。 代理店のWebサイトからサポートに問い合わせてみると、迅速に本体を交換して頂けました。
Amazon.comでは$300以下で販売されている一方、日本では5万円を超えているのも気になっていましたが、サポートを考えると日本の代理店で買って良かったです。
感想
漕ぎながら作業できるのかは若干疑問もありましたが、慣れれば集中して作業できます。この記事も半分ぐらいは漕ぎながら書きました。 逆に疲れたことに気づかずに漕ぎ続けてしまう危険もあると思っていて、最初のうちは意識的に30分ぐらいで辞めるよう気をつけています。
何にせよ、日常生活に運動を取り入れられるのは嬉しいです。 エアロバイクは低めの負荷でスルスルと漕ぐのが良いらしいので、10段階中の下から3段目で漕いでいます。
肝心の机の使い心地は上々です。流石にエアロバイクと一体になっているだけあって安定しています。アームレストがついているので腕がつかれることもありません。机にはMacbook Pro 13 inch がちょうど良く収まります。
ノートPCを置くとメーターは見えなくなるので全く使ってません。アームバンドも置いてあるだけです。机の下の引き出しも使ってないですが、滑りが悪く、使いやすくはなさそうです。
音はほとんど気になりませんが、床を保護するために以前Kinect用に買ったマットを敷いています。
ALINCO(アルインコ) エクササイズ フロアマット 厚さ 9mm EXP150
- 出版社/メーカー: ALINCO(アルインコ)
- 発売日: 2012/04/05
- メディア: スポーツ用品
- 購入: 17人 クリック: 35回
- この商品を含むブログ (2件) を見る
Re:VIEWとDockerとCircleCIで原稿を継続的インテグレーション
Re:VIEW で執筆する原稿を継続的インテグレーションしたかったので、以下の図のような仕組みを作りました。
ローカル執筆環境の前提条件
- Docker 1.3以降がインストールされている
- Re:VIEW形式の原稿のフォルダがある
Re:VIEWやLaTeXはDockerイメージのものを使うので、インストール不要です。
私はMac OS X 10.9とBoot2docker 1.4.1で動かしましたが、WindowsやLinuxでも動くのではないかと思います。
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したソースコードは以下の場所に置いてあります。
参考
- Configuring CircleCI - CircleCI
https://circleci.com/docs/configuration - Continuous Integration and Delivery with Docker - CircleCI
https://circleci.com/docs/docker - vvakame/docker-review
https://github.com/vvakame/docker-review - [ReVIEW Tips] DockerでRe:VIEW - Qiita
http://qiita.com/takahashim/items/406421d515ef1d4f1189 - Can't destroy container on btrfs: Failed to destroy btrfs snapshot: device or resource busy · Issue #4897 · docker/docker
https://github.com/docker/docker/issues/4897 - boot2dockerでのVolume問題が解決しそう | SOTA
http://deeeet.com/writing/2014/10/08/boot2docker-guest-additions/
2014年を振り返って
2014年を振り返って
2014年は対外的に評価される機会があって嬉しかったです。
- IBM Bluemix Challengeで賞をもらった
- IBM developerWorks 日本語版 : IBM Bluemix Challenge: 概要
- Webページには載ってませんが、美術館を探したり記録をメモできるモバイルアプリを作って、審査員特別賞を頂きました。
- MBSハッカソンで最優秀賞をもらった
- Hack On Air ~ MBSハッカソン〜 公式サイト
- iPhoneを手に動かして描いた軌跡を可視化するアプリを作って、最優秀賞を頂きました。
MBSハッカソンは特にそうなんですが、新しい人との出会いがあり、これから楽しくなりそうです。
一方で個人で数ヶ月開発していたサービスはまだ世に出せていないので何とかしないといけません。
また、一番仲の良かった友人を亡くし、忘れられない年になりました。
2015年に向けて
2015年に向けて面白い話を頂いていて、そのうちお知らせできるかもしれません。
最近はやりたいことの量の割に、使える時間が足りていないので、やりたいことを絞って一つ一つ実現していきたいです。
体力的にも色々ガタが来てる気がするので、健康にも気をつかって、使える時間を最大化していく必要があります。
特にPCを見続けると眼のリソースが消耗していると感じるので、ポッドキャストのように眼を使わずにできることを増やしていきたいです。
来年もよろしくお願いします。
Webサイトのクローラビリティをチェックする
これはクローラー/スクレイピング Advent Calendar 2014の18日目の記事です。
Webサイトをクローリング、スクレイピングしたいと思ったとき、はじめに何をするでしょうか?
私はとりあえずブラウザの開発者ツールでDOMを覗きますが、その後robots.txtや利用規約をチェックします。
そういう作業を繰り返すうちに面倒になってきたので、URLを与えるだけで自動的にクローラビリティ(クロールしやすさ)をチェックするWebサービスをやっつけで作りました。
Crawlability · Check the crawlability of web sites
適当にクロールしたいURLを入力してCheckボタンを押してみてください。
できること
クローラビリティとは言ってもまだコンセプトレベルで、以下のことができるだけです。
- 指定したURLの情報を表示する
- トップページの情報を表示する
- robots.txtを表示する
- よくあるXML SitemapのURLにアクセスして存在するか確かめる
- 利用規約っぽいページにアクセスしてみる
もう少しいい感じに情報を取得できるようにしたいです。
中身
https://github.com/capybala/crawlability
ちなみに内部では以前の記事で紹介したaiohttpを使っています。Python 3.3から使えるasyncioを使って並列にアクセスしています。
aiohttpはHTTPクライアントだけでなく、HTTPサーバーの機能も実験的ながら付いているので、Webインターフェイスはこれで作ってみました。
クローラーをデーモンとして動かす ― Scrapyd
この記事はクローラー/スクレイピング Advent Calendar 2014の12日目の記事です。
ScrapyはPythonにおけるクローリング・スクレイピングのフレームワークとして有名ですが、Scrapydという興味深い機能があるので今日はこれを紹介します。
Scrapydはその名の通り、Scrapyのデーモンです。サーバーにおいてサービスとして動作し、Scrapyで作ったクローラーのジョブ管理ができます。
多くのページをクロールするクローラーにおいては、1回の実行時間が1日を超えることもしばしばあるので、ジョブ管理は重要です。このように運用を考慮した機能まで備えているのがScrapyの特徴的なところです。
Scrapydの概要
Scrapydは簡単なWebインターフェイスを提供しており、主にcurlを使ってAPIを呼び出します。
http://Scrapydをインストールしたホスト:6800
にアクセスすると、以下のようなシンプルな画面が表示されます。
詳しくは後述しますが、Scrapyプロジェクトをデプロイしてある状態で以下のコマンドを実行すると、ジョブを実行できます。
curl http://Scrapydをインストールしたホスト:6800/schedule.json -d project=プロジェクト名 -d spider=スパイダー名
「Jobs」というリンクをクリックした先で実行中のジョブの状況を見られます。ジョブごとにログやスクレイプしたアイテムを見ることもできます。
Scrapydのメリット
通常クローラーをデーモンとして動かすときは、Cronを使うと思います。ScrapydはCronと競合するツールではなく、Cronと組み合わせて使えるツールです。Scrapydを使うことで、Cron単体で使うときに比べて以下のメリットが得られます。
Scrapydの使い方
以下のイメージでサーバーにデプロイします。
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クローリング&スクレイピング」という本を書きました!
Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-
- 作者: 加藤耕太
- 出版社/メーカー: 技術評論社
- 発売日: 2016/12/16
- メディア: 大型本
- この商品を含むブログを見る
これはクローラー/スクレイピング 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
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
クロール先のサーバーに負荷をかけ過ぎないように、間隔を自動的に調整したりエラー時に自動でリトライしてくれます。内部では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
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
SeleniumのPythonバインディングです。FirefoxやChromeなどのブラウザを自動操作したり、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
SeleniumのFirefox, 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
今回調べて初めて知ったライブラリもありました。他にももっと良いものがありましたらぜひ教えて下さい。
#isucon 2014予選の延長戦をやってみた
予選の時間内では足りてないことばかりだったので、もう少し試行錯誤することにしました。
#isucon 2014の予選をほぼ一人で戦うハメになった話 - orangain flavor
目標は50000点、できれば60000点出したい。
予選終了時
最終提出スコア: 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 FILE
でvclを検証すれば良いことを知りました。
バックエンドとして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の最新版を使うと良いと教わったので、置き換えてみました。
@orangain gunicorn の worker は何を使われてます? meinheld の github 最新版を使ってもらえると性能上がるかも。
— INADA Naoki (@methane) 2014年10月4日
スコアは若干上がり、 53872。
まとめ
最終的なスコアは 53872 となりました。これ以上はどうすればスコアが上がるか思いつきません。
とりあえずの目標としてた50000は超えましたが、60000には届きませんでした。Pythonのマルチプロセスモデルだとこの辺が限界なんでしょうか。。
ちなみにワークロードやGunicornのワーカー数はいじってもスコアが上がらなかったので、変えてません。
クーポンのおかげで、あまりお金をかけずに勉強できました。AWS様ありがとうございます。
参考サイト
- Gunicorn
- Varnish
- Redis
- Bottle
- PyPy
おまけ
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 氏に教えて頂きました。ほんとうに感謝です。
@orangain あと、PyPyを使う場合は JIT のウォームアップのオーバーヘッドが大きいので、プロセス数を絞った上で、 init で wrk か何かで温めてやる必要があります。
— INADA Naoki (@methane) 2014年10月5日
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言語力を高めたいですが。