Pythonクローリング&スクレイピングの増補改訂版が出版されます
Pythonクローリング&スクレイピングはおかげさまでご好評いただき、この度、増補改訂版を出版する運びとなりました。紙版は本日8/10発売で、電子書籍版は既に発売中です。
Pythonクローリング&スクレイピング[増補改訂版] -データ収集・解析のための実践開発ガイド
- 作者: 加藤耕太
- 出版社/メーカー: 技術評論社
- 発売日: 2019/08/10
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
クローリングやスクレイピングを扱う書籍は、対象として利用しているWebサイトの変化によって、サンプルコードが動かなくなってしまう宿命があります。初版を執筆した際は、なるべく考え方を伝えるようにすることで、たとえサンプルが動かなくなったとしても役立つ内容にすることを心がけました。
ですが、書いてあるコードがそのまま動くに越したことはありません。今回改訂の機会をいただいたことで、読者の皆様に学びやすい形でお届けできるのを嬉しく思います。
増補改訂版の主な変更点
初版で評価いただいた良い点は残しつつ、全体的に内容をアップデートしています。主な変更点は次のとおりです。
早めにサードパーティライブラリを使う構成に変更
初版では、2章で標準ライブラリのみを使い、3章からサードパーティライブラリを使う構成でした。増補改訂版では、主要なサードパーティライブラリであるRequestsとlxmlを2章から使うように変更しました。これによって、実践的な手法を素早く学べるようになりました。
最新のPython 3.7に対応
初版ではPython 3.4/3.5を使っていましたが、増補改訂版ではPython 3.7を使います。これによって、f-stringsやdictの並び順保持、asyncio.runなどの便利な機能を使えるようになりました。
また、サンプルコードでは積極的に型ヒントを使うようにしました。型ヒントは好みが分かれるかもしれませんが、関数の入出力がわかりやすくなることを重視してつけました。好みでない場合、型ヒントは省略しても問題なく動きます。
Headless Chromeやpyppeteerに対応
初版から2年ほどの間に、ヘッドレスブラウザー周りは大きな変化がありました。PhantomJSがメンテナンスされなくなり、ChromeやFirefoxがヘッドレスモードをサポートしました。増補改訂版でもHeadless Chromeを使うよう変更し、PuppeteerをPythonにポートしたpyppeteerも解説しました。
運用をアップデート
7章で扱う運用もいろいろとアップデートしました。
- Linux OSをUbuntu 14.04 → 18.04に変更し、systemdを前提とした解説に変更
- Scrapy Cloudの解説を追加
- クローリングとスクレイピングの分離ではキューとしてAmazon SQSを使用
- サーバーレスなクローラーの運用についても軽く解説
さいごに
今回、改訂の機会を得られたのは、初版を読んでくださった読者の皆様と、初版の出版に関わってくださった皆様のおかげです。改めてお礼申し上げます。また、増補改訂版のレビュアーの皆様には数多くの指摘をいただき、よりわかりやすく正確な内容へと洗練させることができました。本当にありがとうございました。
増補改訂版をよろしくお願いいたします。
Pythonクローリング&スクレイピング[増補改訂版] -データ収集・解析のための実践開発ガイド
- 作者: 加藤耕太
- 出版社/メーカー: 技術評論社
- 発売日: 2019/08/10
- メディア: 単行本(ソフトカバー)
- この商品を含むブログを見る
Pythonクローリング&スクレイピングの韓国語版が出版されました
表紙のクモは光沢処理されてて綺麗です。
https://wikibook.co.kr/python-for-web-scraping/
原書では日本のWebサイトを多く扱っているので、翻訳して大丈夫なのかと勝手に心配していましたが、韓国のサイトに置き換えられていました。他にも自然言語処理でMeCabを使っている箇所がKoNLPyという韓国語のライブラリに置き換えられているなど、韓国語の読者にとってわかりやすい仕上がりになっているようです。
執筆していた頃は翻訳されるなどとは思ってもみませんでしたが、訳書を通してさらに多くの読者に役立ててもらえたら嬉しいです。
2018年を振り返って
主に子育てで時間の取れない生活が続いており、1つ前の記事が2017年の振り返りなのは非常に良くないですが、少なくとも1年に1回は書けてよかったです。
2018年を振り返って
今年は転職したのが大きなイベントでした。まあ楽しくやってますということで、勤務先のブログに記事を書きました。
個人開発では、eBook-1をIT技術書の横断検索サイトにリニューアルしたり、僕の考えた最強のランキングを簡単に作れるBOKURANを開発したりしてました。
昨年の振り返りで書いてたSpotlight Englishは、転職後にAirPodsを買ったこともあり、よく聴けてます。
2019年に向けて
とりあえず現在仕掛かり中のお仕事が終われば落ち着くと思うので、楽しく過ごせればと思います。
加齢とともに夜遅くまで起きているのが難しくなってきているので、時間を有効活用できるよういろいろ改善したいです。
勤務先では、チームには馴染めているので、来年はチームを超えてインパクトを与えていきたいです。
来年もよろしくお願いします。
2017年を振り返って
年末にCivilization Revolution 2にハマってしまい時間がなくなりました。
2017年を振り返って
2016年末に出版されたPythonクローリング&スクレイピングが高い評価をいただけていて、とてもありがたく思っています。
また、書籍に関連してコミュニティでの発表機会や、お仕事の話などもいただけて良い経験になりました。
2016年の振り返りに書いていたとおり、2017年は比較的開発や勉強に時間を使えて充実していました。
- Glance NewsというAndroidアプリを公開しました - orangain flavor
- 将棋の定跡を勉強するためのアプリ「Kifu Notebook」を作った - orangain flavor
プライベートでは2人目の子供が産まれてから疲れやすくなっており、夜はスマホのゲームをしてMP回復して終わるみたいな日が増えてしまっていますが、調子のいい時間を大切にしていきたいです。
2018年に向けて
2018年は変化の年にしたいと思います。
最近は、Twitterで勧められているのを見かけたSpotlight Englishを通勤時間帯に歩きながら聴いているので、これは継続していきたいです。
今後ともよろしくお願いいたします。
【1万部突破】Pythonクローリング&スクレイピングの発売から約1年
先月ツイートしましたが、Pythonクローリング&スクレイピングは第5刷となり、累計発行部数が1万部を突破しました。
【1万部突破!】Pythonクローリング&スクレイピングの増刷(第5刷)が決まり、発売1年足らずで累計発行部数が1万部を突破しました!読者の皆様、書評を書いてくださった皆様、レビュワーをはじめとする関係者の皆様に改めてお礼申し上げます。今後ともよろしくお願いします。 https://t.co/jrJxo9iCuC
— かと (@orangain) 2017年11月10日
評価
1万部突破にあたって http://scraping-book.com/ を更新する際に、ブログでの書評をまとめたのですが、とても良い評価をいただけていて嬉しく思います。
Amazonのカスタマーレビューは13件も書いていただき、★4.3と高い水準が継続していて本当にありがたいです。
「Rubyによるクローラー開発技法」のヒットやデータサイエンスにおけるPython人気の高まりという良いタイミングで企画をいただけたことに加え、締め切り前にScrapyがPython 3に対応したのも幸運でした。
また、書籍が良い内容になるには、レビュアーとして参加いただいた皆様の協力も不可欠でした。関係者の皆様にお礼を申し上げます。
振り返りの意味も込めて、この1年での本書を取り巻く状況の変化のうち、主なものを述べていきます。
クロール対象のサイトの変化
クローラー本の宿命ですが、クロール対象のサイトの変化により、書籍で掲載しているサンプルのコードが動かなくなってしまうことがあります。
把握している範囲の変更は、以下のサポートページに補足情報として掲載しています。
サポートページ:Pythonクローリング&スクレイピング ―データ収集・解析のための実践開発ガイド―:|技術評論社
gihyo.jpでCDNが導入されたことで、2章で使っているurllibのUser-Agentに対して403を返すようになってしまったのは結構初期段階なので辛いですが、代わりにサンプルサイトをご利用ください。
今のところ把握している範囲では、サイトが消えてなくなったりはしていないので、まだマシだとは思っています。
PhantomJSのメンテナー引退
書籍ではJavaScriptを使ったページへの対応として、PhantomJSを使っていましたが、今後はChromeやFirefoxのヘッドレスモードが主流になりそうです。
以下の記事は、PhantomJSの代わりにHeadless Chromeを使って本書のサンプルコードを動かしてみたものです。
SPAの採用が進むにつれてJavaScriptを使ったページへの対応は重要になっていくので、もっとページを割いても良かったかなと思います。
AWS LambdaのPython 3対応
発売当時のAWS LambdaはPython 3系に対応していなかったこともあり、クローラーの運用としてLambdaは紹介しませんでしたが、2017年4月にLambdaがPython 3に対応しました。
Lambdaを使えばサーバー管理が不要になるので、可能な場合には積極的に使っていきたいサービスです。以下の記事ではLambdaに加えてAWS Fargateを使い、EC2を管理せずにクローラーを運用できることを示しています。
その他
他にも書籍ではページ数の都合で書けなかったことを補足的に記事にしておきました。
今後ともよろしくお願いいたします。
AWS FargateとLambdaでサーバーレスなクローラー運用
これはWebスクレイピング Advent Calendar 2017の7日目の記事です。こんな感じでAWS FargateとAWS Lambdaを使ってサーバーレス(EC2レス)なクローラーを作ります。
この記事はFargateでのクローリング処理にフォーカスしており、クロールしたHTMLをS3に保存するところまでを主に解説します。Lambdaの方はおまけ程度の扱いで、スクレイピングしたデータの扱い(データベースへの格納など)はスコープ外です。
長くなったので目次です。
背景
クローラーを稼働させるのにサーバー管理をしたくないという思いがあります。かと言ってScrapinghubのようなSaaSは、ロケーション・実装の自由度やサービス継続性の面から採用しづらいこともあります。
サーバーレスと言えばLambdaが代表格です。Lambdaでクローラーを作ろうとする取り組みは過去にもあります。
ただ、クローラー*1の処理のうち、スクレイピング処理はいいとしても、クローリング処理はあまりLambda向きのワークロードではないと考えています。
クロール対象のサーバー負荷軽減のためにクロール間隔を空けることを考えると、Lambdaのスケーラビリティはあまりメリットになりません。Lambdaをn秒間隔で呼び出してくれるような仕組みはない*2ので、クロール間隔を空けるためにはLambdaの実行中にSleepする必要があります。
LambdaはCPUを使っていなくても関数の実行時間で料金が決まるので、Sleepしている間やWebページのダウンロードを待っている間も課金対象です。このクローラーの性質は、まさに以下の記事に書かれているLambdaに不向きなアプリケーションの性質に合致します。
- 1: Lambda functionの実行時間のうち、ネットワークI/O時間が支配的である
- 2: Lambda functionの実行終了を同期的に待たなければならない
- 3: 複数のレコードをLambda functionの引数に渡すことができない*3
コスト効率の悪いLambdaアプリケーションの性質に関する考察 - ゆううきブログ
さらに、Scrapyのような1プロセス内での非同期実行を前提とした開発効率の良いフレームワークがある中、細切れのLambda関数を書いていくのはダルいという思いもあります。
AWS Fargateの登場
そんなわけで2017年11月にこのAdvent Calendarに登録した時はクローリング処理をサーバーレス(EC2レス)で実行するためにAWS Batchを使うことを考えていました。
しかし先日のAWS re:Invent 2017でAWS Fargateが颯爽と登場したので、早速試してみます。AWS FargateはDockerクラスターを構築するサービスであるAmazon ECSにおいて、EC2を管理することなくクラスターを使えるサービス(クラスターの一形態)です。
クローラーの場合、FargateにしたところでLambdaのコストモデルの問題が解消するわけではありませんが、プロセスの実行が長時間に渡ることを前提としたScrapyのようなフレームワークを使いやすいというメリットがあります。
LambdaでもFargateでも、クロール対象サイトを複数にして同時にクロールすればネットワークI/Oを多重化できます。ですが、サイトごとのクロール間隔の調整はそれなりに面倒な処理なので、Fargateを使うことで既存のフレームワークで提供されている仕組みに乗っかれるのはメリットです。
クローラーの構成
冒頭の構成図を再掲します。
Scrapyはクローリングしながらスクレイピングするアーキテクチャですが、以前も書いたように、これはあまり良いアーキテクチャではないように思います。そこで、クローリングとスクレイピングを分離し、Scrapyではクローリングのみを行い、HTMLを丸ごとAmazon S3に保存します。
S3を間に挟むとスクレイピングに失敗したときや追加の情報を取得したくなったときでも、相手のサーバーに負荷を与えずにやり直しできて安心です*4。実運用ではS3のライフサイクルポリシーを使って1ヶ月後にオブジェクトを消すなどすると、再実行可能性とデータ量の抑制を両立できます。
やってみる
クローリング処理はPython 3.6とScrapyで作成します。途中でPython 2系では動作しないライブラリを使います。
これから作成するScrapyのSpiderのソースコードは以下の場所に置いてあります。
以下は前提とします。
- AWSのアカウントを持っていること
- 適当なS3バケットを持っていること
- ローカル開発環境において、そのS3バケットへの書き込み権限とAmazon ECRへの書き込み権限を持つユーザーでAWS CLIを設定済みであること
- ローカル開発環境ではPython 3の仮想環境内で作業していること
1. ScrapyのプロジェクトでSpiderを作る
まずはローカル開発環境でScrapyのSpiderを作ります。この記事では、Pythonクローリング&スクレイピングのサンプルファイルの中にある、6.7節の食べログのSpiderを改変して使います。
DOWNLOAD_DELAY
などの設定は適切にしておきましょう。
2. Scrapy S3 Pipelineをインストールする
Scrapyでクローリングのみを行い、HTMLを丸ごとS3に保存するのはよくあるパターンなので、Scrapy S3 Pipelineというライブラリにしてみました。これをインストールします。
(venv) $ pip install scrapy-s3pipeline
Scrapy S3 Pipelineはスクレイピングして得られたItemをJSONLines形式でS3に保存するItem Pipelineです。Scrapyの通常のFeed ExportsもS3への保存に対応していますが、S3ではファイルへの追記ができない都合上、Spiderの実行が終わってから大きな1つのファイルをS3に保存します。一方Scrapy S3 Pipelineは、アイテムをチャンクごと(デフォルトで100アイテムごと)に区切ってS3に保存します。これによって、S3のイベント通知を受けてLambdaでスクレイピングする構成を取りやすくなります。
また、Gzip圧縮をサポートしているのも特徴です。同じようなHTMLが複数含まれるファイルは圧縮率が高くなるので、スクレイピング再実行のためにHTMLを残しておきたい場合には効果的です。
3. Scrapy S3 Pipelineをプロジェクトに追加する
プロジェクトのsettings.py
に以下の設定を追加します。S3PIPELINE_URL
のバケット名 (scraping-book
の部分) はご自身の所有するS3バケットに変更してください。
ITEM_PIPELINES = { 's3pipeline.S3Pipeline': 100, } S3PIPELINE_URL = 's3://scraping-book/{name}/{time}/items.{chunk:07d}.jl.gz'
なおS3PIPELINE_URL
内の変数は以下のように置き換えられます。
{name}
: Spider名{time}
: Spiderの開始時刻{chunk}
: チャンクに含まれるアイテムの開始番号(0, 100, 200, ...)。上記ではFormat Stringを使ってゼロパディングしている。
Spiderのコールバックメソッドではスクレイピング処理を行わず、単にs3pipeline.Page
オブジェクトをyieldするのみとします。Page
クラスはurl
, body
, crawled_at
の3つのフィールドを持ち、Page.from_response(response)
とするだけでこれらの値が埋められます。
# ... from s3pipeline import Page # ... class TabelogSpider(CrawlSpider): # ... def parse(self, response): yield Page.from_response(response)
ここまでできたら以下のコマンドを実行し、問題なくクローラーが動作していればOKです。
(venv) $ scrapy crawl tabelog
正常に動作していれば以下のようにS3にオブジェクトが作成されるはずです。
4. ScrapyのプロジェクトをDockerizeする
作成したSpiderをDockerコンテナ内で実行できるようにします。
以下の内容でDockerfile
を作成します。
FROM python:3.6.3 WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["scrapy", "crawl", "tabelog"]
.dockerignore
ファイルも作成しておきます。
venv/ .scrapy/ .git/
以下のコマンドで、Dockerイメージをビルドして実行できればOKです。
$ docker build -t serverless-crawler . $ docker run --rm -it serverless-crawler
通常Dockerコンテナ内ではAWSのクレデンシャルを取得できないので、botocore.exceptions.NoCredentialsError: Unable to locate credentials
などのエラーが発生するはずです。Fargateでの実行時にはIAM Roleで取得できるようになるので特に問題ありません。Ctrl-Cを2回押してSpiderの実行を終了しておきます。
5. Amazon ECRにリポジトリを作成する
さて、ここからはAWSのマネジメントコンソールで作業します。2017年12月時点ではAWS Fargateは北部バージニアリージョン(us-east-1)のみに対応しているので、以降の手順ではすべて北部バージニアリージョンを使います。
Amazon ECSの管理画面の「リポジトリ」から serverless-crawler
という名前のリポジトリを作ります。
6. DockerイメージをAmazon ECRにpushする
リポジトリ作成時に手順が表示されるはずですが、以下のようにログインして先ほどビルドしたDockerイメージをAmazon ECRにpushします。
ログイン:
$ $(aws ecr get-login --no-include-email --region us-east-1) Login Succeeded
タグ付けとプッシュ(<AWS ID>
はご自身のIDに置き換えてください):
$ docker tag serverless-crawler:latest <AWS ID>.dkr.ecr.us-east-1.amazonaws.com/serverless-crawler:latest $ docker push <AWS ID>.dkr.ecr.us-east-1.amazonaws.com/serverless-crawler:latest
7. IAM Roleを作成する
ECSのタスクからS3にPUTできるよう、EC2 Container Service Task (ecs-tasks.amazonaws.com
) をTrusted EntityとするIAM Roleを作成しておきます。 ecsTaskServerlessCrawler
という名前のロールを作りました。
本来はポリシーを書いて必要最低限の権限のみを割り当てるべきですが、手抜きしてAWS管理ポリシーAmazonS3FullAccess
を割り当てました。
8. ECSクラスターを作成する
ここからいよいよAmazon ECSの管理画面での作業です。「クラスター」→「クラスターの作成」から、Networking Only (Powered by AWS Fargate) のクラスターを作成します。default
という名前にしました。
「Getting Started with Amazon Elastic Container Service (Amazon ECS) using Fargate」というウィザードもありますが、今回は使いません。このウィザードを使うとECSのサービスも一緒に作成されますが、バッチ処理のように実行完了したら終了したいタスクはクラスターとタスク定義だけあれば良く、サービスは不要なためです。
9. ECSタスク定義を作成する
「タスク定義」→「新しいタスク定義の作成」から、実行するタスクの雛形であるタスク定義を作成します。
- ステップ 1: Select launch type compatibility
- Launch Type Conmpatibility: Fargate
- ステップ 2: Configure task and container definitions
- タスク定義名:
serverless-crawler
- タスクロール:
ecsTaskServerlessCrawler
(7で作成したもの) - Task execution role:
ecsTaskExecutionRole
(初回に自動作成されるはず) - Task memory (GB): 0.5GB
- Task CPU (vCPU): 0.25 vCPU
- コンテナ定義
- コンテナ名:
serverless-crawler
- イメージ:
<AWS ID>.dkr.ecr.us-east-1.amazonaws.com/serverless-crawler:latest
(6でpushしたもの) - メモリ制限: ソフト制限 128MB
- ※環境変数など必要なものがあればここで追加する
- コンテナ名:
- タスク定義名:
10. タスクを実行する
クラスターdefault
の「タスク」→「新しいタスクの実行」か、タスク定義serverless-crawler
の「アクション」→「タスクの実行」からタスクを実行します。
Launch typeでFARGATEを選択し、適切なVPCとサブネットを選択して実行します。この時点でタスクのロールやコンテナの環境変数などを上書きすることも可能です。
実行開始して1分ほど待つとRUNNING状態になり、ログを確認できるようになります。 ただ、ECSの管理画面に表示されるログは見づらいので、CloudWatch Logsで見たほうがわかりやすいです。
5分ほどでSpiderの実行は終了し、S3にアイテムが生成されているのが確認できます。
おまけ: Lambda関数を作成する
スクレイピング処理は、こんな感じのLambda関数(Python 3.6)を作成し、S3へのオブジェクトの作成をトリガーとして呼び出されるようにします。Lambdaに関しては世の中に多くの情報があるのでここでは詳しく解説しませんが、以下の点には気をつけてください。
- サードパーティライブラリ(この例ではlxmlとcssselect)は一緒にパッケージングする。
- S3からファイルを取得できるようIAM Roleを設定する。
- 同じファイルへのイベントが複数来ることがあるので、処理がべき等になるようにする。
- LambdaをVPC内で実行する場合はLambdaからS3にアクセスできるようVPCエンドポイントを作るなどする。
import io import json import gzip import boto3 import lxml.html def lambda_handler(event, context): """ Lambdaから呼び出されるエントリーポイント """ for record in event['Records']: bucket_name = record['s3']['bucket']['name'] object_key = record['s3']['object']['key'] process_object(bucket_name, object_key) def process_object(bucket_name, object_key): """ 1つのS3オブジェクトを処理する。 """ s3 = boto3.client('s3') for page in read_pages(s3, bucket_name, object_key): scrape_from_page(page) def read_pages(s3, bucket_name, object_key): """ S3オブジェクトからページをyieldするジェネレーター """ use_gzip = object_key.endswith('.gz') bio = io.BytesIO() s3.download_fileobj(bucket_name, object_key, bio) bio.seek(0) f = gzip.GzipFile(mode='rb', fileobj=bio) if use_gzip else bio for line in f: page = json.loads(line) yield page def scrape_from_page(page): """ 1つのページからlxmlなどを使ってスクレイピングする。 """ root = lxml.html.fromstring(page['body']) restaurant = { 'url': page['url'], 'name': root.cssselect('.display-name').text_content().strip(), } # スクレイピングしたデータのDBへの保存などを行う
まとめ
AWS Fargateを使って、EC2の管理をすることなくDocker化されたバッチ処理(クローラー)を実行できました。バッチ処理に合わせてクラスタを起動・終了しなくてもクラスタ実行時間のみが課金対象となるので楽です。Lambdaのような制限も少なく、既存の処理も移行しやすそうです。
タスクの起動に時間がかかるのはやや気になりますが、ある程度長時間実行するクローラーであれば影響は少ないでしょう。今回はDebianベースのDockerイメージを使ったので、Alpineベースのイメージにするなど、イメージサイズを減らすことで改善するかもしれません。
他にもやり残した点として、クローラーのグレースフルな停止があります。ScrapyのクローラーをDocker化するとコンテナの停止時(マネジメントコンソールからECSのタスクを停止した時)にSIGTERMが送られて一瞬で終了してしまうので、SIGINTによるグレースフルなクローラーの停止に対応させたいです。
Pythonクローリング&スクレイピングはおかげさまで1万部を超えるヒットとなりました。出版当時の2016年12月はLambdaのPython 3対応もなかった時代で、7章の運用ではオーソドックスにEC2内のCronでプロセスを実行していましたが、Fargateの登場によってEC2なしで運用できるようになりました。Fargateが東京リージョンに来るのを楽しみにしています。
言語実装パターンを読んだ
言語実装パターン ―コンパイラ技術によるテキスト処理から言語実装まで
- 作者: Terence Parr,中田育男,伊藤真浩
- 出版社/メーカー: オライリージャパン
- 発売日: 2011/12/24
- メディア: 大型本
- 購入: 5人 クリック: 333回
- この商品を含むブログ (13件) を見る
「言語実装」と聞くとコンパイラの本かと思いますが、本書は「言語アプリケーション」を実装するための本です。言語アプリケーションには設定ファイルの読み取り、データの読み取り、コード生成、コード変換器、インタプリタなど、入力ファイルを処理・解析したり、変換したりするアプリケーションが含まれます。
このようなアプリケーションはだいたい同じような構成になり、以下のような要素を組み合わせて実装することになりますが、各要素の実装パターンが数種類紹介されていて、メリット・デメリットを考慮して適切なものを選択できるようになっています。
(私が知らないだけの可能性は高いですが)このような知見がまとまった書籍は他にないような気がします。言語の変換を行う際に、実装の指針となりとても役立ちました。
ANTLR 4の学習
言語アプリケーションの実装において、すべてを手で実装するのは現実的ではなく、特に構文解析にはパーサージェネレーター(あるいはパーサーコンビネーター)を使うのが一般的です。本書では著者のTerence Parrさんが開発しているパーサージェネレーターのANTLRを使います。
原著が2009年に執筆された本書ではANTLR 3が使われていますが、現在一般的なのは2013年に公開されたANTLR 4です。ANTLR 4はフルスクラッチで書き直され、考え方が大きく変わっています。木構造を生成する文法が削除されるなど、文法定義も互換性がありません。
このため、ANTLR 4で書籍内のサンプルコードを動かすことはできません。おとなしくANTLR 3で動かせばいいのですが、せっかくだし最新のバージョンを使いたかったのでANTLR 4の勉強もはじめました。
この本はANTLR自体を解説した本ではありません。ANTLR 4のリファレンスはGitHubで公開されていますし、文法定義の説明もWebに多くありますが、生成されたパーサーの使い方に関する情報があまり見当たらないように思いました。
ANTLR 4の学習には、(英語ですが)同じくTerence Parrさんが書かれたThe Definitive ANTLR 4 Referenceを読むのがオススメです。タイトルにReferenceとありますが、リファレンスは一部だけで、ANTLR 4の使い方が丁寧に解説されています。
The Definitive ANTLR 4 Reference by Terence Parr | The Pragmatic Bookshelf
以降では、主に参考にした箇所についてコメントします。
中間表現の木構造のパターン
4章「中間形式木の構築」に登場するパターンは、中身は簡単なことなのに難しい名前がついていてわかりにくく感じたので、5章「木の走査と書換え」に登場するパターンとの関連性と合わせて図示してみました。
ちなみにANTLR 3では文法定義にアクションとして抽象構文木を生成するコードを書いたり、木文法で訪問器を生成したりしていたようですが、ANTLR 4が生成するパーサーでは構文解析木が自動的に構築されるように変更されました。外部訪問器の基底クラスも生成されるので、抽象構文木は必要なら外部訪問器を使って自分で生成します。
この方が文法定義がスッキリして分かりやすいコードになるので、良い変更だと思います。
ちなみにパターン15「木パターン照合器」についても、ANTLR 3にあった木書換えの文法はなくなったので、ANTLR 4では代わりに ParseTreePatternを使うようです*1。
記号表のパターン
6章「プログラム記号の記録と識別」と7章「データ集合体のための記号表管理」に登場する記号表は以下のライブラリとして利用でき、助けられました。
コード変換のパターン
11章「コンピュータ言語の変換」に登場するパターン29「構文主導変換器」とパターン30「規則方式変換器」の違いがちょっとわかりづらかったです。構文ベースで変換するのが前者で、規則ベースで変換するのが後者ということだと思います。しかし、パターン30の例は変換規則DSLとしてANTLRの文法定義を使い、アクションでprint文を使って出力するというもので、これはパターン29でもあるのではないかと感じました。
パターン31「目的構成体ごとに固有の生成器クラス」と12章「テンプレートを使ったDSL生成」との関連もよくわからなかったです。パターン31の最後に「toString()の中で文字列を生成する代わりにテンプレートを使うこともできます。しかしながら、その場合は、特性の生成器クラスはやめてテンプレートだけを使うようにしたほうが良いでしょう。」という説明がありますが、12章はパターン31をより高度にしたものという理解で良いのでしょうか。12章をパターン32として、11章のパターンと比較する説明があると良かったと思います。
まとめ
わかりにくいと書いたところもありますが、私の読み込みが足りないだけとも言えます。全体的に見れば最初に書いたように実装の指針となる良書でした。もし叶うならば、ANTLR 4で書き直されたバージョンを読んでみたいです。
*1:まだ試してないので試したい。