RequestsとBeautiful Soupでのスクレイピング時に文字化けを減らす
多様なWebサイトからスクレイピングする際、Webサイトによっては文字化けが発生することがあります。 RequestsとBeautiful Soupを組み合わせる場合に、なるべく文字化けを減らす方法を解説します。
Beautiful Soupはパーサーを選択できますが、ここではhtml.parserに絞って解説します*1。
結論
以下の2点を守ると概ね幸せです。 Content-Typeヘッダーのエンコーディングを参照するコードは下の方に掲載しています。
1. Chardetをインストールしておく。
$ pip install chardet
2. RequestsのResponseオブジェクトをr
としたとき、BeautifulSoupのコンストラクターには(r.text
ではなく)r.content
を渡す。
import requests from bs4 import BeautifulSoup r = requests.get(url) soup = BeautifulSoup(r.content, 'html.parser')
環境
- Python 3.6.0
- Requests 2.12.4
- Beautiful Soup 4.5.3 (パーサーはhtml.parserを使用)
- Chardet 2.3.0
Webページのエンコーディング
Webページのエンコーディングの指定・推定方法は以下の3つがあります。どれも間違っていることがあるので、これらを組み合わせて正しそうなエンコーディングを選択します。
- HTTPレスポンスのContent-Typeヘッダーのcharsetで指定されたエンコーディング
- この記事ではContent-Typeのエンコーディングと呼ぶ
- 正しくなかったり、charsetが指定されてなかったりすることがある(特に静的ページの場合)
- HTMLの<meta>タグまたはXMLのXML宣言で指定されたエンコーディング
- この記事では<meta>タグのエンコーディングと呼ぶ
- 概ね正しいことが多い
- HTTPレスポンスのバイト列から推定されたエンコーディング
- この記事では推定されたエンコーディングと呼ぶ
- ある程度の長さがあれば概ね正しく推定できる
- 上記2つに比べて処理に時間がかかる
Requestsとエンコーディング
RequestsではResponseオブジェクトをr
とすると次のようになります。
r.encoding
: Content-Typeのエンコーディングr.apparent_encoding
: 推定されたエンコーディング *2r.content
: bytes型のレスポンスボディr.text
:r.encoding
でデコードされたstr型のレスポンスボディ
以下の点は注意が必要です。
- Requestsは<meta>タグのエンコーディングは見ない(HTTPのライブラリなので)
- Content-Typeにtextが含まれていてcharsetがない場合、
r.encoding
は'ISO-8859-1'
となる *3*4- この場合、日本語のサイトでは
r.text
が文字化けする
- この場合、日本語のサイトでは
Beautiful Soupとエンコーディング
コンストラクターBeautifulSoup()
の第1引数には、HTMLの文字列をstr型またはbytes型で指定できます。
str型のHTML文字列を渡した場合は、Beautiful Soup側ではエンコーディングに関しては特に何もしません。
bytes型のHTML文字列を渡した場合は、Beautiful Soup側でstr型にデコードされます。以下のエンコーディングによるデコードを順に試して、正しくデコードできたものが使われます*5。
どうしたら文字化けしないのか
ここまで見てきたように、Requestsのr.encoding
やr.text
をそのまま使うと、文字化けしやすくなります。
基本的な戦略としては、以下の2点を守るのがオススメです。
- Chardetをインストールしておく
BeautifulSoup()
にr.content
を渡してBeautiful Soup側でデコードする
大体OKなのでオススメなコード
記事冒頭にも書きましたが、Chardetをインストールした上で、次のようにすると大体の場合文字化けを回避できます。 シンプルなのでオススメです。
import requests from bs4 import BeautifulSoup r = requests.get(url) soup = BeautifulSoup(r.content, 'html.parser')
このコードでは次の順でエンコーディングを見ます。
Content-Typeヘッダーの指定を尊重したい場合のコード
Content-Typeヘッダーの指定を尊重したい場合は、次のようにできます。
r.encoding
が'ISO-8859-1'
の場合は無視することで、Content-Typeヘッダーにcharsetが指定されていない時に文字化けするのを回避できます。ただし、Content-Typeヘッダーが間違っていた場合には文字化けすることがあります*6。
import requests from bs4 import BeautifulSoup r = requests.get(url) content_type_encoding = r.encoding if r.encoding != 'ISO-8859-1' else None soup = BeautifulSoup(r.content, 'html.parser', from_encoding=content_type_encoding)
このコードでは次の順でエンコーディングを見ます。
まとめ
なるべく文字化けに遭遇することなくスクレイピングしたいですね。
Pythonクローリング&スクレイピングではライブラリを個別に紹介していて、組み合わせたときの話はあまり詳しく書いていませんでした。普段はBeautiful Soupよりlxml推しですが、Beautiful Soupを使う機会があったのでまとめておきました。
*1:軽く試した範囲では、lxmlはhtml.parserと同様の結果になり、html5libは推定されたエンコーディングの判別に失敗することがありました。html5libは先頭100バイトしかChardetに渡さないようです。html5libを使う場合は、「Content-Typeヘッダーの指定を尊重したい場合のコード」を使ったほうが良いかもしれません。
*4:ISO-8859-1はRFC 2616で定められたデフォルト値です。RFC 7231ではこのデフォルト値はなくなったので、3.0に向けての議論はありますが、Kenneth Reitz氏はあまり乗り気ではなさそうです。
*5:詳しくはbs4/builder/_htmlparser.pyやbs4/dammit.pyを参照
*6:from_encodingに指定したエンコーディングで正しくデコードできない場合は無視されるので、文字化けしないこともあります。
2016年を振り返って
2016年を振り返って
今年はやはり書籍「Pythonクローリング&スクレイピング」を出版できたのが大きかったです。お陰様で良い評価をいただけているようで、ありがたいことです。
2013年の振り返りの時点で、やりたいことの1つとして「ブログよりも長いまとまった記事を執筆する」を挙げており、なんとなく書籍の執筆とかやってみたいとは思ってました。ですが、こんなにも都合よく機会が来るとは思いもしませんでした。
コミュニティ活動や発表をしてたわけでもなく、ただブログを書いていただけで、面識のない編集者からオファーを頂けたのは、話すより書くほうが好きな自分にとっては幸せでした。2年かかりましたが、自分の実績として残るものができて嬉しいです。
今年からできた神戸Pythonの会にも参加しています。Pythonについてオフラインで話せる機会・人が増えて、毎回楽しいです。
あとは、今年からボードゲームをよくやるようになりました。カタンやパンデミックから始めてどんどんハマっていきました。月に1回ほど集まって友人・知人とプレイしています。いつも気付くと夕方になっていて、世の中にある楽しいゲームの多さにただ驚くばかりです。ボードゲームの楽しさをまだうまく言語化できていませんが、結局のところ、自分なんかよりずっと賢いゲームデザイナーの手のひらの上で懸命に頭を使って踊るのが楽しいのではないかと思っています。
各方面で多くの方のお世話になりました。ありがとうございました。
2017年に向けて
長い間本を書いていて、他のことに使える時間が少なかったので、今後はいろいろ作りかけのものを完成させて公開したいです。2015年の振り返りにも同じことが書いてあり、成長していない感じが辛いですが、来年こそは進められるはずです。
書籍の執筆という大きな仕事も落ち着いたので、今後どのように生きていくかじっくりと考えたいと思います。
今後ともよろしくお願いします。
PhantomJSとか使わずに簡単なJavaScriptを処理してスクレイピング
この記事はクローラー/Webスクレイピング Advent Calendar 2016 16日目の記事です。
JavaScriptが使われているWebページからスクレイピングする場合、PhantomJSなどのヘッドレスブラウザーを使うのが一般的です。 ただ、ちょっとしたJavaScriptを解釈できれば十分な場合、オーバーキルなこともあります。
この記事では、PhantomJSとかを使わずに簡単なJavaScriptを処理する方法を解説します。
どんな場合に役立つの?
NHKニュースのWebサイトを題材として取り上げます。
http://www3.nhk.or.jp/news/html/20161215/k10010807361000.html
NHK NEWS WEBのRSSではニュースのタイトルとURL、概要しか提供されていません。画像のURLやニュースの本文を取得したい場合は、スクレイピングが必要です。
普通にスクレイピングしてもいいですが、ソースを見るとJavaScriptで__DetailProp__
という変数が定義されています。この変数の値のオブジェクトをパースできればXPathやCSSセレクターでパースするより楽ですし、変更に強くなりそうです。
<script type="text/javascript"> var __DetailProp__ = { cate: '5', date: '12月15日 4時04分', datetime: '2016-12-15T04:04', //id: 'k10010807361000', //video: 'k10010807361_201612150440_201612150458.mp4', video: 'https:\/\/www3.nhk.or.jp\/news\/html\/20161215\/movie\/k10010807361_201612150440_201612150458.html', duration: 87, img: 'html\/20161215\/K10010807361_1612150440_1612150458_01_03.jpg', title: '\u7C73\uFF26\uFF32\uFF22 \u8FFD\u52A0\u5229\u4E0A\u3052\u6C7A\u5B9A \uFF11\u5E74\u3076\u308A', summary: '\u30A2\u30E1\u30EA\u30AB\u306E\u4E2D\u592E\u9280\u884C\u306B\u5F53\u305F\u308B\uFF26\uFF32\uFF22\uFF1D\u9023\u90A6\u6E96\u5099\u5236\u5EA6\u7406\u4E8B\u4F1A\u306F\u3001\uFF11\uFF14\u65E5\u307E\u3067\u958B\u3044\u305F\u91D1\u878D\u653F\u7B56\u3092\u6C7A\u3081\u308B\u4F1A\u5408\u3067\u3001\u30A2\u30E1\u30EA\u30AB\u7D4C\u6E08\u306F\u62E1\u5927\u3057\u3066\u3044...', more: '\uFF26\uFF32\uFF22\u306F\uFF11\uFF14\u65E5\u307E\u3067\u306E\uFF12\u65E5\u9593\u3001\u30EF\u30B7\u30F3\u30C8\u30F3\u3067\u91D1\u878D\u653F\u7B56\u3092\u6C7A\u3081\u308B\u516C\u958B\u5E02\u5834\u59D4\u54E1\u4F1A\u3092\u958B\u304D\u307E\u3057\u305F\u3002<br /><br />\u7D42\u4E86\u5F8C\u767A\u8868\u3055\u308C\u305F\u58F0\u660E\u306B\u3088\u308A\u307E\u3059\u3068\u3001\uFF26\uFF32\uFF22\u306F\u3001\u30A2\u30E1\u30EA\u30AB\u7D4C\u6E08\u306B\u3064\u3044\u3066\u3001\u96C7...', body: [{ title: '\u4ECA\u5F8C\u306E\u91D1\u5229\u306E\u898B\u901A\u3057\u306F', img: '', align: '', text: '\uFF26\uFF32\uFF22\u306F\uFF11\uFF14\u65E5\u3001\u30A4\u30A8\u30EC\u30F3\u8B70\u9577\u306A\u3069\u91D1\u878D\u653F\u7B56\u3092\u6C7A\u3081\u308B\u4F1A\u5408\u306E\u53C2\u52A0\u8005\u304C\u4E88\u6E2C\u3057\u305F\u3001\u4ECA\u5F8C\u306E\u91D1\u5229\u306E\u898B\u901A\u3057\u3092\u516C\u8868\u3057\u307E\u3057\u305F\u3002<br /><br />\u305D\u308C\u306B\u3088\u308A\u307E\u3059\u3068\u3001\u6765\u5E74\u306E\u5229\u4E0A\u3052\u306E\u56DE\u6570\u306E\u4E2D\u5FC3\u7684\u306A\u60F3\u5B9A\u306F\uFF13\u56DE\u3068\u3001\uFF19\u6708\u306E\u6BB5\u968E\u306E\uFF12\u56DE\u3088\u308A\u3082\u5897\u3084\u3057\u3066\u3044\u3066\u3001\u30C8\u30E9\u30F3\u30D7\u6C0F\u306E\u7D4C\u6E08\u653F\u7B56\u306B...', textPos: '' },{ title: '\u300C\u30BC\u30ED\u91D1\u5229\u653F\u7B56\u89E3\u9664\u300D\u304B\u3089\u306E\uFF11\u5E74\u306F', img: '', align: '', text: '\uFF26\uFF32\uFF22\u306F\u53BB\u5E74\uFF11\uFF12\u6708\u3001\u3044\u308F\u3086\u308B\u30EA\u30FC\u30DE\u30F3\u30B7\u30E7\u30C3\u30AF\u306E\u3042\u3068\u5C0E\u5165\u3057\u3066\u3044\u305F\u300C\u30BC\u30ED\u91D1\u5229\u653F\u7B56\u300D\u3092\u3001\u96C7\u7528\u306E\u6539\u5584\u306A\u3069\u3092\u7406\u7531\u306B\u89E3\u9664\u3059\u308B\u3053\u3068\u3092...', textPos: '' }] }; </script>
JavaScript以外の言語でこのオブジェクトをパースするのは少し面倒なので、JavaScriptエンジンを使います。
組み込みJavaScriptエンジン:Duktape
この程度のオブジェクトをパースするのであれば、ブラウザーの機能は不要でJavaScriptエンジンが動けば十分です。 幸い、いろいろな言語から使える組み込みのJavaScriptエンジンはいくつかあり、ここではDuktapeを取り上げます。
真面目に比較検討したわけではないですが、ポータビリティの高さとフットプリントの小ささが特徴のようです。 いろいろな言語のバインディングがあります。
PythonでもPyPIをduktapeで検索すると、以下のライブラリが見つかります。
ここでは一番よくメンテナンスされている感じのdukpyを使用します*1。
動かしてみる
環境の準備
Python 3.5.1を使い、pipでdukpyをインストールします。バージョン0.0.6を使います。
(venv) $ pip install dukpy
ソースコード
以下のようなコードでスクレイピングできます。script要素内のJavaScriptのコードを取得し、dukpy.evaljs()
で実行します。
nhknews.py
import urllib import re import dukpy # Webページを取得する。 f = urllib.request.urlopen('http://www3.nhk.or.jp/news/html/20161215/k10010807361000.html') # HTMLのボディをUnicode文字列にデコードする。 html = f.read().decode('utf-8') # 正規表現でscript要素の中身を取得する。 # re.DOTALLで正規表現中の.が改行にもマッチするようになる。 m = re.search(r'<script type="text/javascript">(.*?)</script>', html, re.DOTALL) # 正規表現でキャプチャしたscript要素の中身を取得する。 script = m.group(1) # dukpyでJavaScriptを実行し、変数__DetailProp__の値を取得する。 js_obj = dukpy.evaljs([script, '__DetailProp__']) # 取得できた値(タイトル、画像のURL、もっと読むの中身)を表示する。 print(js_obj['title']) # タイトル print(js_obj['img']) # 画像のURL(/news/からの相対URL) print(js_obj['more']) # もっと読むの中身
実行
実行すると次のようになり、JavaScriptの変数に格納されていた値を取得できていることがわかります。
(venv) $ python nhknews.py 米FRB 追加利上げ決定 1年ぶり html/20161215/K10010807361_1612150440_1612150458_01_03.jpg FRBは14日までの2日間、ワシントンで金融政策を決める公開市場委員会を開きました。<br /><br />終了後発表された声明によりますと、FRBは、アメリカ経済について、雇用の伸びなどを背景に緩やかなペースで拡大しているとして、全会一致で政策金利を引き上げることを決めました。<br /><br />具体的には、0.25%から0.5%の範囲となっている今の政策金利を、0.25%引き上げて、0.5%から0.75%の範囲にします。<br /><br />FRBが利上げに踏み切るのは、去年12月にゼロ金利政策を解除して以来、1年ぶりとなります。...
解説
dukpy.evaljs()
関数には、JavaScriptのコードを文字列、または文字列のリストとして渡します。文字列のリストを渡した場合は、同じコンテキストで順に実行されます*2。戻り値は、最後に評価された値です。
今回の場合、script要素の中身はvar
文だけなので、そのままだと評価される値はundefined
(PythonではNone
)となります。このため、リストの2つ目の要素として変数名を与えることで、変数__DetailProp__
を評価し、その値を得ます。
JavaScriptのオブジェクトはPythonのdict
にいい感じに変換されます。
dukpyの詳しい使い方はドキュメントを参照してください。
まとめ
Duktapeを使うといろいろな言語から簡単にJavaScriptのコードを実行できます。 Ajaxのようなブラウザーに依存する機能は使えませんが、PlainなJavaScriptを実行できれば十分な場合には便利です。
正直JavaScriptの変数をサーバーサイドで書き出すのはバッドプラクティス*3なので、使える場面は少ないかもしれません。 しかし、こういう手段も知っておくと役に立つ日が来るかもしれません。
宣伝
Pythonクローリング&スクレイピングという本を書きました。紙版・電子書籍版共にちょうど本日発売でした。 この本の中では、オーソドックスにSeleniumとPhantomJSを使ってJavaScriptを使ったページに対応する方法を解説しています。
*1:これはProjects using Duktape (alphabetical order)からリンクされているkovidgoyal/dukpyとは別物です。kovidgoyal/dukpyはPyPIには登録されていないようです。
*2:リストの要素をセミコロン区切りで接続した文字列を実行するのと同等の結果になります。
*3:参考: HTMLのscriptタグ内に出力されるJavaScriptのエスケープ処理に起因するXSSがとても多い件について - 金利0無利息キャッシング – キャッシングできます - subtech
書籍を書きながらOSSに貢献した話
Pythonクローリング&スクレイピングでは、クローリング・スクレイピングやデータ解析のための様々なライブラリを紹介しています。 書籍でOSSのライブラリを紹介すると、そのライブラリに貢献する機会やインセンティブが生まれると考えています。
書籍で紹介すると貢献したくなる
書籍で解説するには、ドキュメントやソースコードを読んでライブラリの正確な挙動を知る必要があります。 ドキュメントが曖昧な場合、そうして得られた知識でドキュメント改善のための指摘や提案ができるようになります。
書籍で紹介するライブラリで変更の大きな新バージョンが公開される場合、Alpha版やBeta版、RC (Release Candidate) 版で検証して、書籍内のコードが正しく動くことを確認する必要があります。動かない場合は自分で直すなり、早めに直るよう報告するなりしたくなるでしょう。
ライブラリの挙動が不自然な場合、「〜については注意が必要です」のように注記するよりも、いっそ改善してしまったほうが解説がシンプルになることもあります。特に1つのライブラリだけが書籍で扱う実行環境から外れてしまうような場合は、そのライブラリに対応してもらえたら解説が楽になります*1。
Pythonクローリング&スクレイピングの場合、全編でPython 3を使っていますが、執筆を始めた時点ではPython 2.7のみに対応しているライブラリもありました。Python 3に対応してもらえるようPull Requestを送ったものもあります。
貢献したライブラリなど
せっかくなので、貢献したライブラリをまとめておきます。 並び順はPythonクローリング&スクレイピング(以降で書籍内と言った場合はこの本を指します。)での登場順です。
Beautiful Soup
Beautiful Soupはスクレイピングのための有名なライブラリです。
CSSセレクターで,
の挙動がおかしかったのを修正しました。
WikiExtractor
WikiExtractorはWikipediaのダンプからテキストだけを抽出するコマンドです。 Python 2.xにしか対応してなかったので、Python 3にも対応させました。
python-amazon-simple-product-api
python-amazon-simple-product-apiはAmazonのProduct Advertising APIのラッパーです。 インストール時に依存ライブラリが正しくインストールされるようにしました。
- Add python-dateutil to install_requires in setup() by orangain · Pull Request #66 · yoavaviram/python-amazon-simple-product-api
- Fix installation instruction of dateutil by orangain · Pull Request #67 · yoavaviram/python-amazon-simple-product-api
- Test case TestAmazonCart.test_cart_modify fails · Issue #85 · yoavaviram/python-amazon-simple-product-api
- Support Python3 by orangain · Pull Request #86 · yoavaviram/python-amazon-simple-product-api
PDFMiner.six
PDFMiner.sixはPDFからテキストやオブジェクトを抽出するライブラリです。 インストールが簡単になるようにしたり、コマンドの実行結果がおかしかった箇所を修正しました。
- Ensure to install required libraries on installation by orangain · Pull Request #7 · goulu/pdfminer
- Include compiled cmap resources to simplify installation for CJK languages by orangain · Pull Request #13 · goulu/pdfminer
- Close device to write footer of xml/html files by orangain · Pull Request #14 · goulu/pdfminer
- Ensure that command line tools use LF line endings to work on Linux/OS X by orangain · Pull Request #17 · goulu/pdfminer
RoboBrowser
RoboBrowserはWebページの自動操作のためのライブラリです。 chardetというライブラリがインストールされていない環境でも文字化けが起きにくくする修正をマージしてほしかったですが、未反応のままです。
MechanicalSoup
MechanicalSoupもRoboBrowserと同様の自動操作のためのライブラリです。書籍内では名前が登場するのみです。 ドキュメントを修正しました。
BigQuery-Python
BigQuery-PythonはPythonからGoogle BigQueryを扱うクライアントです。
credentials.json
というファイルを置いた場合に、コードが簡潔になるようにしました。
Scrapy
Scrapyはクローリング・スクレイピングのためのフレームワークです。 ドキュメントのわかりにくい箇所を修正したり、Python 3に対応したVer. 1.1 RC1が出た時に問題を報告・修正したりしました。 1個目のドキュメントの目次を見やすくしたPull Requestはお気に入りです。
- [MRG+1] DOC: Add captions to toctrees which appear in sidebar by orangain · Pull Request #1638 · scrapy/scrapy
- [MRG+1] DOC: Update MetaRefreshMiddlware's setting variables by orangain · Pull Request #1642 · scrapy/scrapy
- DOC: Add AjaxCrawlMiddleware to DOWNLOADER_MIDDLEWARES_BASE by orangain · Pull Request #1643 · scrapy/scrapy
- BlogSpider on scrapy.org does not crawl archive pages now · Issue #1763 · scrapy/scrapy
- [MRG+1] PY3: Fix SitemapSpider to extract sitemap urls from robots.txt properly by orangain · Pull Request #1767 · scrapy/scrapy
- [MRG+1] PY3: Fix TypeError when outputting to stdout by orangain · Pull Request #1769 · scrapy/scrapy
- [MRG+1] PY3: Implement some attributes of WrappedRequest required in Python 3 by orangain · Pull Request #1771 · scrapy/scrapy
OpenCV
OpenCVはコンピュータービジョンのためのライブラリです。 Debian上でPython 3からOpenCVを使えるようにするpython3-opencvパッケージを実現するパッチを投稿しましたが、特に進展なしでした。
このため、Ubuntu 14.04と16.04向けにpython3-opencvパッケージを含むPPA (Personal Package Archives) を作成しました。
rq
rqはRedisをバックエンドとした軽量なメッセージキューを実現するライブラリです。 ドキュメントが古い箇所やサンプルコードがPython 3で動かない箇所などを修正しました。
- docs: Use rq command instead of rqworker/rqinfo by orangain · Pull Request #647 · nvie/rq
- Accept byte strings as the first argument of Worker() in Python 2 by orangain · Pull Request #650 · nvie/rq
- Update outdated sample codes in README.md by orangain · Pull Request #651 · nvie/rq
- docs: Make sample code of workers compatible with Python 3 by orangain · Pull Request #652 · nvie/rq
ウォッチしてたけど進まなかった問題
以下は特にできることがないのでウォッチしてましたが、残念ながら特に進展のなかった問題です。
MeCabのPython 3対応
MeCab公式のPythonバインディングでPython 3をサポートするPull Requestがあり、ウォッチしてましたがマージされていません。書籍内では代わりにmecab-python3を紹介しています。
Ubuntu 16.04のVagrant Box
Pythonとは直接関係ないですが、Ubuntu 16.04のVagrant Box (ubuntu/xenial64) がVagrant向けに設定されてなく、まともに使えないという問題があります。 このため、書籍内で使用する環境はUbuntu 14.04のままです*2。
- Bug #1569237 “vagrant xenial box is not provided with vagrant/va...” : Bugs : cloud-images
- Bug #1581347 “Configure the xenial vagrant box the same way as t...” : Bugs : cloud-images
- Bug #1567259 “unable to run multiple vagrant instances of ubuntu...” : Bugs : cloud-images
まとめ
1つ1つは簡単なものばかりですが、少しでも世界を良くする手伝いができて良かったです。
Beta版やRC版を出しているにも関わらず正式版リリース後にバグ報告が来るという話をたまに聞きます。書籍を執筆することで、正式版リリース前に検証するインセンティブが働くのは非常に良いことだと思います。書籍を書く機会はあまりないので、もう少しカジュアルにこの効果を活用できないかと考えています(小さ目の電子書籍を書くとか?)。
また、個人で開発しているライブラリはドキュメントが不十分だったり、言語のツールチェインとの連携がスムーズでなかったり*3するので、貢献すると喜ばれるでしょう。
GitHubのおかげでOSSに貢献しやすい環境が整っているので、ぜひ気がついたことはどんどん報告・修正していきましょう。
おまけ:英語でのIssue/Pull Requestのコツ
英語のIssueやPull Requestでうまく伝わらないと感じることがあるかもしれません。 コツとしては、一定のフォーマットの小見出し*4に沿って書き、なるべく長い文章を書かないようにすることだと思います。
IssueであればEnvironmentとHow to Reproduceを示し、Expected BehaviorとActual Resultを、実際のコマンドの実行結果やスクリーンショットなどを示してわかりやすく対比させます。文章を読まなくてもわかるようにすると、英語の説明の拙さを補えます。
Pull Requestの場合はまず、変更を小さくして解決したい問題にフォーカスするのが重要です。 Issueと同様に、BeforeとAfterをわかりやすく対比させると良いでしょう。 変更の動機を説明するのが難しい場合は、まず問題を明確にするためのIssueを作成し、それをFixする形でPull Requestを作成するとシンプルに説明できるようになることもあります。
参考: 英語圏の開発者に初めてバグレポートを出す時の5つのポイント【連載:コピペで使えるIT英語tips】 - エンジニアtype
関連
Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-
- 作者: 加藤耕太
- 出版社/メーカー: 技術評論社
- 発売日: 2016/12/16
- メディア: 大型本
- この商品を含むブログを見る
Pythonクローリング&スクレイピングの見本が届きました
昨日Pythonクローリング&スクレイピングの見本が届きました。 表紙はきれいな青色と印象的なタイポグラフィで気に入ってます。
横から見ると、実際のWebサイトを対象としてデータを収集・解析する5章と、Scrapyを扱う6章に多くのページを割いていることがわかります。
どのページを開いても自分の書いた文章がでてくるのは初めての経験で、若干戸惑います。
400ページに渡る原稿はとても1人では作り上げられませんでした。最初にお話をいただいた時から、長きに渡ってサポートしてくださった技術評論社の野田さんのお陰です。作業の見積もりが下手でご迷惑をお掛けすることも多かったですが、無事完成して良かったです。ありがとうございました。
また、いろいろな方にレビューをお願いしました。お会いしたことのない方、昔一緒にコードを書いたけど長らく会ってない方、よく会っている方、最近知り合った方など様々ですが、ぜひレビューしていただきたいと思った方ばかりです。 レビュアーの皆様のお陰で、よりわかりやすく正確な内容になりました。本当にありがとうございました。
レビューをお願いする際は引き受けていただけるか不安もありましたが、お声がけした方には全員快諾していただけてとても幸せでした。いい歳した大人の台詞ではありませんが、ちょっとの勇気を出してお願いすることの大切さを学びました。
発売日は12/16ですが、都内の一部の大型書店では先行販売も始まっています。
12/9先行販売『Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド』インプレス(978-4-7741-8367-1)加藤耕太 著 入荷◆「Python」棚にて展開中! pic.twitter.com/sExaFDRd05
— 書泉ブックタワーコンピュータ書売り場 (@shosen_bt_pc) 2016年12月9日
【4階PC】コンピュータ書売場に、先行テスト販売書籍が入荷致しました。『Python クローリング&スレイピング』(本体3200円+税) 4階棚F15にございます。是非お早目にチェックを!JH
— 紀伊國屋書店 新宿本店 (@KinoShinjuku) 2016年12月10日
12/10先行販売:ISBN978-4-7741-8367-1 技術評論社『Pythonクローリング&スクレイピング データ収集・解析のための実践開発ガイド』加藤耕太 著 10冊入荷
— ジュンク堂書店池袋本店/PC書 (@junkudo_ike_pc) 2016年12月10日
電子書籍も着々と準備中です。ぜひ多くの皆様に読んでいただけたら幸いです。
Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-
- 作者: 加藤耕太
- 出版社/メーカー: 技術評論社
- 発売日: 2016/12/16
- メディア: 大型本
- この商品を含むブログ (3件) を見る
書籍執筆でお世話になったツール 〜Re:VIEW, textlint, prh, goemon, GitHub, CircleCI〜
「Pythonクローリング&スクレイピング」という書籍を執筆しました。執筆にあたって色々なツール・サービスのお世話になったので、記録を残しておきます。
大まかな流れは、以前の記事に書いたとおりです。ツールの選定は2015年1月頃(textlintとprhは2016年1月頃)に行ったので、現在では状況が変わっているものもあるかもしれません。ご了承ください。
目次
Re:VIEW
原稿の形式は著者の自由とのことでしたので、Re:VIEWで書きました。
Re:VIEW - Digital Publishing System for Books and eBooks
採用理由
技術評論社さんではWordやMarkdownが多いということでしたが、WordはGitでバージョン管理しづらいし、Markdownは表現力が乏しいのが気になるところでした*1。
SphinxやAsciiDocなども検討しましたが、Sphinxは書籍向けを想定したものではなさそうなこと、AsciiDocよりもRe:VIEWのほうが日本の出版社で実績がありそうだったことから、Re:VIEWを採用しました。あとなんか面白そうでした。
良いところ
1つのソースからPDF、ePubを作ることができ、書籍の表現に必要な要素(画像・表・リストの参照、別の章・節・項の参照や、リード文、キーワードなど)が充実しているのが良かったです。あと表の記法がMarkdownより圧倒的に書きやすいのが好きです。
review-ext.rbというファイルにRubyのコードを書くことで、挙動を簡単にカスタマイズできるのも便利でした。
振り返って
結果的にRe:VIEWという選択が正しかったのかよくわかりません。Re:VIEWが生成する成果物は最終的な成果物としては使われていないからです。
紙面のデータはRe:VIEWで出力したPDFとは別にDTPで作成したものが使われています。DTPの際もRe:VIEWで出力したInDesign XMLがうまく処理できないということで、Re:VIEWのフォーマットから無理矢理Markdownに変換してもらいました*2。
電子書籍についても、紙版をベースにPDF版を作成する都合上、ePub版だけRe:VIEWで生成したものを使うと確認コストが上がるということで、ePUB版もPDF版をベースに作られています。
結局、Re:VIEWで生成したPDFは紙面のイメージを掴むためと、レビュアーの皆様に読んでいただくためだけに使用しました。PDFで思い通りの見た目にするためにLaTeXとの格闘に時間を費やしましたが、LaTeXで詰まると本当に辛いので、細かい見た目の問題はもっと割り切れば良かったです。
もちろんこの辺はRe:VIEWの経験が豊富な出版社さんであれば違うと思います。書籍制作のフローをよくわかっていない著者が最適なツールを選ぶのは難しいなという感想です。
実装を見る機会も多かったですが、実装やドキュメントから意図が読み取れず、疑問に思ったところがいくつかありました。
- 章への参照は番号や見出しを表示するかどうかに応じて
@<chap>{}
@<title>{}
@<chapref>{}
と3種類あるのに、節や項への参照は@<hd>{}
しかないのはなぜなんだろう。 //tsize
や//latextsize
はPDFで表の見た目を整えるのに必須なのにマニュアルに載ってないのはなぜなんだろう。他のBuilderでも使えるようになり、ドキュメントにも記載されていました。//info
とかが一部のBuilderでしか実装されていないのはなぜなんだろう。リスト(ソースコード)は外部ファイルにわけるべきなんだろうか。#@mapfileの使い方がよくわからない。プリプロセッサのドキュメントが追加されていました。
作ったものとか
書いている途中にvim-reviewのforkとDash用のdocsetを作りました。
- orangain/vim-review: syntax and helpers for ReVIEW text format.
- Re:VIEWのDash用Docsetを作った - orangain flavor
textlint
原稿の校正にはtextlintを使用しました。
採用理由
RedPenも検討しましたが、設定をXMLで書いたりプラグインをJavaで書いたりするのは辛そうとなことと、(当時調べた限りは)すぐに使える表記ゆれ修正の辞書が見つからなかったことからtextlintを採用しました。あとなんか面白そうでした。
良いところ
textlintはRe:VIEWなどの構文を解析してマークアップを除いた文章だけを対象として校正できるので、誤判定の少なさがポイントです。その分エディタで保存時に実行すると若干時間がかかります。Vimのsyntasticで使えるようになっていて良かったです。
fixableと呼ばれる誤りを自動で修正可能なルールもあり、一括で修正できるのは最高でした。あとは作者のazuさんのレスポンスの速さと開発スピードに驚きました。
振り返って
textlintを導入したのが原稿が一旦書き上がったぐらいのタイミングだったので、十分に効果を発揮することはできなかったように思います。後述のprhを使った表記ゆれのチェックが主で、他のルールは参考程度という感じになってしまいました。プログラムを書く時に開発初期からCIを取り入れたほうがいいのと同じように、執筆初期からtextlintを導入していれば最初からより良い文章を書けたと思います。
textlintはコンテキストによって異なる規則を適用できます。例えば本文は「ですます調」で書くが、箇条書きは「である調」で書くなどです。これは便利ですが、例えば画像のキャプションは何調で書くべきなのか、ソースコード中のコメントは何調で書くべきなのかなど、なかなか難しいです。コンテキストに応じた場合分けがルール内で定義されていると、それを上書きできなくて困ることもありましたが、どこで定義すべきなのかは自分の中でも答えが出ていません。
作ったものとか
Re:VIEWのプラグインがなかったので、自分で作成しました。 パーサーを雑に書けるのはRe:VIEWの設計思想の良い点だと思います*3。
prh
表記ゆれを修正するためにprhを使いました。 prhコマンドを直接使用したわけではなく、textlintのルール textlint-rule-prh 経由で使用しました。
採用理由
手軽に表記ゆれのチェック・修正をしたくて使いました。 Re:VIEW形式に対応できそうなツールは他に見つかりませんでした。
良いところ
ルールベースで表記を統一できて非常に便利でした。textlint-rule-prhはfixableなルールなので、Re:VIEWの文法に合わせて必要な箇所だけを自動で修正できました。
振り返って
WEB+DB PRESSのルールを使用しましたが、カタカナ末尾の長音については、基本的に長音をつけるという表記を採用し、元のルールと反対にしたので書き直すのが大変でした。(prhに限らず)この辺をうまくどちらか選べる仕組みがあるといい気がしています。だいたい語尾の形で場合分けすることになるので、カタカナ語を英語表記に変換する辞書があればできそう。
goemon
Re:VIEW形式のファイルをブラウザでリアルタイムプレビューするために、goemonを使いました。 geomonはGoで書かれたLiveReload的なツールです。
採用理由
LiveReload的なツールは色々なものがありますが、原稿を書くのに特定の言語ランタイムに依存するのも変な気がしたのでGoemonを使いました。あとREADMEの画像がカッコよかったです。
良いところ
言語ランタイムがなくても動いて、YAMLの設定ファイルもわかりやすくて便利でした。
振り返って
OS Xだと変更を監視するファイル数が多すぎて困るという問題がありました。.gitignoreを参照して無視するみたいなことをやりたかったですが、あまり綺麗に実装できなかったので適当に手元でごまかして対処しました。最近この問題に対応するプルリクエストがマージされていました。
Re:VIEWのようなマークアップ言語ではありがちですが、慣れるに従ってリアルタイムプレビューは不要になるので、後半はあまり使いませんでした。
GitHub
編集者さんとの原稿のやり取りや課題の管理、情報共有はGitHubのプライベートリポジトリを使いました。
採用理由
筆者・編集者ともに普段から使っていて、自然と使うことになりました。
良いところ
あえて書くまでもない気もしますが、GitとGitHubは普段から使い慣れてて便利でした。GitHubのプルリクエストベースのフローが使えるかは編集者さん個人に依存すると思うので、ありがたかったです。
振り返って
最初に一通りの原稿を書く(脱稿)までは1章ごとにプルリクエストを作成していました。GitHubには1ファイルあたり1,000行以上のdiffを表示できないという制限があり、インラインでコメントできなくて困ることがありました。今見たら制限が緩和されていました。
仕方ないのでRe:VIEWのreファイルをch05a.re
, ch05b.re
, ... のように複数に分けて対応しました。最初に作成した目次に合わせて節または項単位で書いていければ1ファイルが肥大化することもなかったと思いますが、最初は全体像が見えていなかったのでそのような書き方はできませんでした。
GitとGitHubは便利ですが、原稿の執筆に向いているかと言われるとちょっと違うような気がします。Gitは行単位で差分を管理しますが、原稿はソースコードに比べて1行が長くなりがちです。私は1行に1文を書くスタイルで書いていました。
1行が長くなると、修正の際に2つの異なるプルリクエストで同じ行を変更することが起こりえます。 プルリクエストAでは文の最初の方を修正し、プルリクエストBでは文の最後の方を修正するといった具合です。 すると後にマージされるプルリクエストは見事にコンフリクトします。
なので、Wordの校閲ツールのように、1行の中でも一部だけを修正して順次マージしていけるツールがオンラインで使えればそのほうがやりやすかったと思います。もちろん他のサービス・ツールとの連携しやすさもGitやGitHubの重要なポイントなので、総合的に判断する必要はありますが。
あとは、レビュー時には指摘事項をGitHubにIssueとして作成してもらいました。これはレビュアーの皆様にとってはやや面倒だったかとは思いますが、挙げてもらう側としてはその後の議論や追跡が楽で助かりました。ちょうどIssue Templateが使えるようになって良かったです。
最終的に300のIssueと200のプルリクエストが作られました。
CircleCI
継続的インテグレーションにはCircleCIを使いました。
採用理由
ある程度の使用経験があったことと、プライベートリポジトリでも1並列は無料で使えて助かることが採用理由です。
良いところ
CircleCIではRe:VIEWのビルドと、インタラクティブシェルやソースコードのテストを行いました。クローリングの書籍なので、クロール対象とするWebサイトの構造が変化したときに素早く気づけて良かったです。
振り返って
CircleCIのビルドでDocker Hub上のDockerイメージを使う場合、毎回docker pull
を実行することになります。大きなイメージ*4だと時間がかかるので、docker load
とdocker save
でキャッシュを作ることもできますが、それでもイメージの復元には時間がかかります。
この辺の時間を短くできて透過的にキャッシュされるような仕組みがあると嬉しいです。
インタラクティブシェルのテストはdoctestで行い、ソースコードのテストはユニットテストを書きました。テストが書きづらいコード(認証が必要、費用がかかる、Python以外のコマンドが関係するなど)は自動テストを書けず、エラーの発見が遅れてしまうこともありました。
成功したり失敗したりするテストケース*5もあり、ビルドの失敗に鈍感になってしまったのは反省点です。Gitのpushをトリガーとして開始するテストしか実行していませんでしたが、原稿の変更がない時期はテストが実行されないのも良くなかったです。ビルドに時間がかかる原因にもなるので、pushをトリガーとするテストは最小限にして、残りはNightly Buildなどで定期的に実行すればよかったです。
textlintは途中から導入したこともあり、textlintの指摘をビルド失敗にするとずっと成功しなさそうだったので、チェックを実行してCircleCI上で結果を見られるようにするのみとしました。
まとめ
改めて振り返ると色々なツール・サービスのお世話になりました。先人の積み上げてきたものに感謝します。
特にRe:VIEW, textlint, prhについては、使っていく中で気づいた点をIssueで報告したりプルリクエストを送ったりと、少しは貢献できたかと思います。今後も発展していくことを陰ながら応援しています。
Pythonクローリング&スクレイピング -データ収集・解析のための実践開発ガイド-
- 作者: 加藤耕太
- 出版社/メーカー: 技術評論社
- 発売日: 2016/12/16
- メディア: 大型本
- この商品を含むブログ (3件) を見る