write.kogu

Wikipediaから任意の記事を集めるサービスをGoogle App Engine/Go SEで作った

Google App Engine

2016年半ば現在のGoogle App Engine」という記事を書いてから約1年半、GAEも色々再評価されたり、国内での大きな採用事例も増えているようです。 私たちは相変わらず、このブログ同様個人のサービスは全て、Google App Engine/GoのStandard Environmentで作ってます。

今回も同じ構成で「Wikipediaから事件などの記事だけを集める」というサービスを作りました。

コンセプト

Wikipediaを読み物として楽しんでいる人は多いと思います。 特に事件や事故の記事は力の入ったものもたくさんあり、時間を忘れて読みふけってしまうことも。

一方で、オープンな百科事典を作ろうというWikipediaの記事には、「要出典」とか脚注とか編集機能とか、単なる読み物としては余計な要素が色々含まれています。 またカテゴライズも曖昧な基準で行われており、「このまま続けて暗殺事件をさくさく読みたいのに…」などと思うことがままあります。 他にも、統一的な編集が行われていない分、記事ごとの品質のばらつきが大きく、開いてみたらがっかりだったり。

そこで、事件に関する記事だけをWikipediaから転載し、読み物として余計な要素を切り落とし、読み応えを評価し、集めて分類するサービスを作りました。 まだまだ記事も250件弱、分類方法も安定していませんが、ひとまず公開しています。

当初は事件、そして事故の記事を集めていましたが、災害や戦争との厳密な区別も難しいため、対象を「事件・事故・災害・戦争」に変えました。

システムの構成

使用しているのはいつものようにGCP + Cloudflareです。

GCP

GCPでは、App Engine(GAE)と、Cloud Storage(GCS)、そしてCompute Engine(GCE)を主に使っています。

GAEはひたすらいつも通り。

これ以外は全部GAE標準の機能で、Task QueueとCronとMemcacheと、それにSearchを少し。 Servicesによる管理画面とフロントの分割やB/Gデプロイは、当たり前過ぎて単体の機能という意識もなくなってます。

GCEはWebページのサムネイル生成や、試している最中の形態素処理など、管理画面向けの機能で幾つか使っています。 しかも基本無料で使える最小インスタンス(f1-micro)は想像以上に働いてくれます。

ヘッドレスなWebページのレンダリングはメモリが厳しいかと思いましたが、用意されたUbuntuかDebianのイメージに、wkhtmltopdf(のwkhtmltoimage)、xvfb、日本語フォント、jpegoptim辺り入れて、Goで書いた小さなサーバーをSupervisorに任せるだけで、昔はどこかで地味にお金かかってた自由なWebページのキャプチャーサーバーが手に入ります。

Cloudflare

全体がほぼ静的コンテンツなので、相変わらずCloudflareはめいっぱい使っています。

GCPのエッジキャッシュも素晴らしいですが、キャッシュのパージができること、DDoSその他の攻撃へのある程度の防御になること、GAE側が落ちてもキャッシュを返してくれること(Allways On)などから、Cloudflareを使います。

以前は無料でとりあえずHTTPSに対応できたのも嬉しかったですが、GAE用の独自ドメインに自動でLet’s Encryptの証明書が適用・更新されるようになったため、恩恵は少なくなりました。 それでも、無料で結構な所まで制御可能なCDNとしてCloudflareはまだ手放せません。

他に、管理画面側はCSSのライブラリにCoreUIを使いました。 便利ではありますが、ベースとしているBootstrapがalphaからbetaへと替わるタイミングで色々影響を受け、ややめんどくさかったです。

フロント側はnormalize.cssだけ使い、グリッド含めライブラリは使わないでみました。 この程度のボリュームなら下手にライブラリを使わない方が早いかもと思いましたが、ブラウザやOSの互換など細かい点で、やっぱりグリッドぐらいは使うべきかと考え直しています。 それと、日本語の組版的な指定だけ、ベターなセットをまとめたライブラリが欲しくなりました。

Wikipediaからの転載

記事の取得

WikipediaはMediaWikiという、Wikipediaのために作られたWikiのシステムを用いて構築されています。 ただしMediaWikiは任意のWikiとしても利用できるようWikiとしての基本的な基盤を提供し、Wikipediaなら百科事典用の、Wiktionaryなら辞書用の、Wikibooksなら教科書用の、といった各Wiki固有の要求は、テンプレートや名前空間といった個別用の機能で実装されています。こうした構成のため、WikipediaをAPI等から使いたい場合、実際にはMediaWikiを利用することになります。

さてMediaWiki用の記事は、MediaWiki記法という、独自のマークダウンな記法で書かれています。 レンダリングされたHTMLのスクレイピングをしなくとも、MediaWikiに用意されているAPIを使用すれば、このMediaWiki記法のソースが取得できます。

MediaWikiのAPI

APIの利用は、基本的に次に挙げるページに準拠しても問題ありません。 ただし、formatversion=2を付けないと、恐ろしく扱いにくいフォーマットで返される場合があります。

APIは幅広い機能を提供していますが、今回のサービスでは以下の用途でのみAPIを使用します。

  • 記事のソース取得
  • 記事のリビジョン間チェック
  • 画像等の実ファイル取得

どれもWikipediaに無用な負荷を掛けないよう、実行間隔など注意します。 幸いGAEのTask Queueは、タスクのキューイングと実行間隔の制御などがお手軽なので、こういった配慮も、キューの調整で対応できます。

APIの返すJSON

MediaWiki APIはレスポンスにXMLやYAMLも選べますが、特に理由が無いのでJSONにします。

GoでのJSONはLLほど手軽ではありませんが、構造が安定したレスポンスに対応するだけなら、JSON-to-Goであっという間です。 サンプルとなるJSONのレスポンスを貼るだけで、ほぼそのまま使えるstructが手に入ります。

今回の場合は、記事情報とファイル情報のレスポンスに対応した型を用意しました。 それぞれ小さなメソッドは幾つか生やしましたが、不可欠なものでもありません。

当面GAE以外で使う用途もありませんが、念のためhttpパッケージとGAEのurlfetchパッケージに場合分けし、小さなMediaWiki API用のパッケージとしました。

記事の整形とMarkdownへの変換

Wikiepdiaの記事はMediaWiki記法で書かれています。 初めは、この記法をPEGで解析してASTを作り、不要な要素を取り除いてからMarkdownにパースして、などと考えていました。

GoのPEGとしては、pointlander/pegmna/pigeonが実用十分。 yhirose/go-pegはPackrat未実装で実験目的なものだけど、他のプロダクトも参考にしてて使いやすい分もったいない、という印象でした。 迷った結果 pointlander/peg を使うことに決めました。が、最終的にPEGでのAST生成そのものを諦めました。 最大の理由は、実際の記事で使われているMediaWiki記法の表記の幅の大きさと、Templateの多用です。

表記の幅の大きさは、単なるミスの場合もあれば、意図的に見た目の調整のために行われている場合もあります。 たとえば行頭に”:“が使われていればddタグとしてレンダリングされますが、これがインデントのために用いられたりしています。 また単純に記法のルールしたがっていなくても、多くの執筆者は見た目で判断しています。 こういった誤った意味付けがされた文書をASTに持っていっても、結局修正の大変な構文木が手に入るだけです。

Templateはその名の通り部分的なテンプレートを表現する仕組ですが、用途がかなり広いです。 これ標準文法に入れたら?と思うようなものまでテンプレートで実現されています。 Wiki用のマークダウンな記法としてのMediaWiki記法と、百科事典であるWikipediaという個別実装の関係を考えると仕方ないのですが、テンプレートで解決してる幅が大きいです。そのためMediaWiki記法のパーサを書いただけでは、Wikipedia全てを取り扱うのに全然足りません。

こういった状況から、結局PEGによる構文解析器の実装やASTへの変換は諦めました。 最終的に、APIで取得したMediaWiki記法のソースは、確実な下処理だけGoで行って、あとはブラウザ上のMarkdownエディタで、整形用のJavaScriptをぽちぽち半自動的で適用することにしました。

Editor.mdに整形用のスクリプトを追加した編集画面 by kogu Public Domain

整形は、脚注や不要なTemplateを除去したり、MediaWiki記法 → Markdown記法の単純な置換をしたりといったものです。 ソースのばらつきが大きいため、念のため結果を見ながら順番に適用させています。

Webブラウザ内での編集機能には、pandao/editor.mdというtextarea置き換え型のMarkdownエディターを利用しています。 もう3年メンテされておらずバグも細々ありますが、このwrite.koguで利用しているSimpleMDEには無い利点が幾つかあったので採用しました。 これはいずれCodeMirror等をベースに自作のものに置き換える予定です。

画像の取得

Wikipediaの記事のソース内では、画像は直接パスを表記せず、抽象化されて表現されています。 画像を特定する識別子と、表示サイズや配置などレンダラへの指示だけが書かれており、画像ファイル自体への参照はソースに含まれていません。 ページのレンダリング時に、画像サーバーへレンダリング指定を渡し、その時点ではじめて返される画像のパス(指定サイズより元画像が大きければサムネイル化された画像のパス)を受けています。

今回はAPIを使ってそのパスを取得し、GCSへの保存までをまとめて行うことにしました。 ひとつの記事に数10件の画像が使われている場合もありますが、ここでもGAEのTask Queueにまかせて、十分な間隔を空けて処理を行っています。

GCSへの保存は、同じGCPのプロジェクトなので簡単です。 urlfetchで指定の画像を取得し、storageパッケージを使ってクライアントを作り、バケットを開いてオブジェクトにバッファから流し込むだけで済みます。

記事のライセンス

Wikpediaの記事は、当初はGFDLで公開されていましたが、現在はCC-BY SA 3.0が推奨されています。 これらの記事を転載するincidents.koguも、全ての記事にCC-BY SA 3.0が継承されます。

incidents.koguでは、全ての記事に転載元とした記事名とURL、またそのリビジョンへのリンクを付記することにしました。 記事のEntityには元のリビジョンやソースも全部gzipで圧縮して残し、最新のリビジョンと比較したりに使用しています。

読み物としての編集や分類

積極的な編集対象

Wikipediaはオープンな百科事典であり、編集の統制はかなり弱いため、記事ごとの品質に大きなばらつきがあります。 かなり有名な事件の記事でも、てにをはレベルでぐちゃぐちゃだったり、推敲の跡が残っていたりします。 また海外の題材の場合、英語版Wikipediaからの翻訳を元にしていることも多く、ほとんど直訳レベルのものもあります。 他にも事実関係が入り乱れていたり、ある事件の一部として必要な記述が、他の人物のページに分散していたりもします。

こういった問題は読み物としてより良く修正しく方針で、たとえば「ガーフィールド大統領暗殺事件」は、かなりの手直しと画像の追加などを行っています。

悩ましいのは、百科事典としては有用でも、読み物としては冗長な記述です。 たとえば「三河島事故」は死者160名の大規模な列車事故の記事ですが、関係する車輌が型名で呼ばれています。 事故の記録や調べ物の下地としては素晴らしいですが、列車に詳しくないと、恐ろしく読みにくいです。 このような場合は、なるべく元の記事が持っている情報量を減らさず、且つ読みやすくなるよう編集したいところです。

現在は記事数を優先しているため全然追いついてないですが、出来れば全ての記事で加筆修正を行い、元の記事に還元したいと考えています。

分類

分類は、このサービスに限らずいつも悩みます。 「事件」「事故」「災害」「戦争」というカテゴリ分けすら、明確にならない場合もあります(エミュー戦争は災害?戦争?事件?等々)。 かと言って、Wikipediaのように全てをタグ式に分類するのも、読みたい記事を探すには不便が多いです。

迷いながら、現時点では次の分類を使っています。

  • カテゴリー(事件・事故・災害・戦争から複数選択可)
  • キーワード(粒度も名寄せも統制が難しい)
  • 国・地域(現存国家と、過去の国家、地域等、粒度も基準も難しい)
  • 年代(長期に渡るケースや不正確な年代の扱いが難しい)

他に、「読み応え」という5段階の分類も設けています。 これは飽くまで主観で、文章量や表現、図版の充実などをもとに読み応えを判断したものです。

今後の方針

記事数の充実が当面の目標ですが、たとえば江戸時代の事件・事故・災害・戦争に関する記事だけで、300件ぐらい積み残してます。 できれば知名度の高い事件を優先的に登録して、地域をまたいだ事件の年表や、時間をまたいだ事件の地図など出したいなと気長に考えています。

広告