Hatena::Grouprekken

murawaki の雑記

2010-12-31

Parsing Wikitext

Wikipedia のデータを自然言語処理で使う。そのために wiki format のテキストを整形して、用途に応じたデータを抽出する方法。

2012年2月18日追記: この情報は古くなっています。今風のスマートなやり方については、こちらのブログ記事を参照してください。

用途。元々の目的はテキスト抽出。他に、リンクの解析、カテゴリの利用とかにも使える。大量のデータをバッチで処理。ウェブサイトにアクセスしてたら怒られるので、XML dump を処理。

やること。MediaWikiwikiマークアップを処理してテキストを抽出。結論を先に言うと、mwlib という Python で書かれたライブラリを使う。ライブラリの出力は parse tree。tree を traverse するコードを自分で書いて望みのデータを得る。先に mwlib に至った経緯を少々述べる。

そもそも wiki は手軽さが売り。昔は処理する側も正規表現で適当に整形するだけでなんとかなった。最近は wiki の仕様が複雑化してそうもいかない。特に問題なのは template。template は複数の記事に共通するテキストを外部化したもの。記事中で template を利用するには {{Aimai}} のように {{ }} で囲む。この場合、Template:Aimai というページの中身が記事に流し込まれる。曖昧さ回避のためのページであるという説明文が記事に展開される。記事によって変化する部分をパラメータとして与えられる。{{otheruses|役職|その他|将軍 (曖昧さ回避)}} とすると、

この項目では、役職について記述しています。その他の用法については「将軍 (曖昧さ回避)」をご覧ください。

に展開される (実際には単なるテキストではなく表)。

明らかに処理が面倒。処理の都合として、dump の XML を前から順に読んで、article タグに囲まれた部分を処理したいわけだが、template が使われていたらランダムアクセスが必要になる。template の内容を取得しないといけないから。template 名をキーにして template の内容が取り出せるようなデータベースが必要。また、パラメータが入っていたら適切に展開しないといけない。これがかなり面倒そう。template の中身は一般編集者がいじらないこともあって、ほとんどプログラミング言語化している。

一つの対処法は template を無視するというもの。template の主な用途は、曖昧さ回避のようなメタな案内や、国の面積や人口などの定型情報が入った表など。たいていの場合、template を無視しても本文は残る。ただし、一部本文中に埋め込まれる template もある。例えば Lang-ja は日本語の表記や読みを入れる template。普通は本文に埋め込まれるので、単純に無視したら文が歯抜け状態になる。

というわけで、人が書いた wikitext の parser を探す。まず、Perl でないかと CPAN を探すが良いものが見たらない。parser と称しているパッケージが、単に dump の外側の XML を処理するだけで、wikitext はそのままにするものだったり。

結局 mwlib に落ち着く。Python で書かれている。現在もメンテされてる。時々解析をミスるがそれほど悪くない。

問題。ドキュメントがない。使い方がわからない。試行錯誤。メソッドを見てそれっぽいと思ったクラスのインスタンスを適当につっこむと、無茶苦茶深い階層から例外が投げられたりする。こんな複雑なライブラリが本当に必要なのかと思ったりすることも。とりあえずうまくいっているっぽい方法を載せる。

方法1: XML のままだと template へのアクセスが難しい。mwlib は (Python で書かれた) cdb に記事データを格納する機能がある。そこで、先に cdb に template 一覧を突っ込んでおいて、本処理では XML dump を前から処理。DumpParser というクラスを継承してやっつけ処理。BuildWiki は指定されたディレクトリに cdb のデータを吐く。

import sys
from mwlib.cdbwiki import BuildWiki
from mwlib.dumpparser import DumpParser

class TemplateOnlyDumpParser(DumpParser):
    def handlePageElement(self, pageElem):
        res = super(TemplateOnlyDumpParser, self).handlePageElement(pageElem)
        if res is not None and res.title.find("Template:", 0) == 0:
            return res
        else:
            return None

def main(dumpPath, dbDir):
    p = TemplateOnlyDumpParser(dumpPath)
    BuildWiki(p, outputdir=dbDir)()

cdb の使い方が分からなかったが、WikiDB を nuwiki に adapt したら使えた。

import sys
import mwlib.parser.nodes
from mwlib.cdbwiki import WikiDB
from mwlib.uparser import parseString
from mwlib import nuwiki

class MediaWikiWikiSegmenter(object):
    def traverse(self, node, output, depth):
        ...省略...

def parse(utext, templdb):
    segmenter = MediaWikiWikiSegmenter()

    db = nuwiki.adapt(WikiDB(templdb, lang="ja"))
    tree = parseString(title=u"日本語", raw=utext, wikidb=db)
    output = segmenter.traverse(tree, [], 0)

MediaWikiWikiSegmenter で parse tree を traverse。tree の仕様についてもドキュメントがない。mwlib.parser.nodes の中身を見ながらコードを書く。例えば、Category が取得したいなら、isinstance(node, mwlib.parser.nodes.CategoryLink) としてチェック。

template から半構造化データを取り出したいという用途の場合はどうしたらいいのか分からない。template 処理は複雑で、適当なところで intercept する方法があるのか不明。

方法2: 最近は XML dump 全体をいったん cdb に変換している。上記のスクリプトで TemplateOnlyDumpParser を呼ぶかわりに DumpParser を呼べばいい。先に、cdb から記事名一覧だけを出力しておいて、その結果をもとに並列処理している。

import sys, getopt
from mwlib.cdbwiki import WikiDB
from mwlib.uparser import parseString
from mwlib import nuwiki

def main(titleStream, contentdbPath):
    segmenter = MediaWikiWikiSegmenter()
    contentdb = nuwiki.adapt(WikiDB(contentdbPath, lang="ja"))
    for line in titleStream:
        title = line.rstrip("\n").decode("utf-8")
        text = contentdb.reader[title]

        # ignore exceptions; keep going
        try:
            tree = parseString(title=u"日本語", raw=text, wikidb=contentdb)
        except Exception as detail:
            print >>sys.stderr, "failed to parse %s: %s" % (title.encode("utf-8"), detail)
            continue

        output = segmenter.traverse(tree, [], 0)

並列処理をして気付いたこと。wikipedia の記事は id が小さいほど古い。一般に古い記事ほど発展して長くなる。並列化のために id で分割したら、若い番号の処理だけやたら時間がかかってバランスが悪い。

追記: つい先日WP2TXT という Ruby で書かれたライブラリの存在を知った。こちらは試していない。

2012年5月14日追記: lang はある時期までは dummy だったような記憶があるが、少なくとも現在 (version 0.13.7) はちゃんと使われている。例えば、ローカライズされた名前空間の解決に使われている。wiki の断片

[[File:XYZ.jpg|thumb|right]]

は画像であって普通のリンクではない。それは名前空間 File に属していることから分かる。この File を日本語化して、例えば、「ファイル」とした場合、mwlib に「ファイル」が File に対応することを教えないといけない。そこで lang が使われている。

mwlib は lang=XY にしたがって、mwlib/siteinfo/siteinfo-XY.json というファイルを参照する。この JSON ファイルに名前空間やその他の情報が記述されている。英語や日本語の Wikipedia はあらかじめ用意されているが、ロシア語やモンゴル語は用意されていない。でも作るのは簡単。

python mwlib/siteinfo/fetch_siteinfo.py ru

でロシア語版が作成される。

2014年2月26日追記: mwlib 0.15 を触ってみたら CDB 機能がなくなっている。履歴をたどると2012年7月に削除されていた。探すと、mwlib.cdb 0.1.0 という別のパッケージの形で公開されていた。mwlib.cdb 付属の mw-buildcdb で引き続き CDB が構築できる。

使うときには注意が必要。本体側では CDB のサポートはやめたことになっている。だから、

from mwlib import wiki
env = wiki.makewiki('somewhere/wikiconf.txt')

によるロードはもはやできない。かわりに

from mwlib.cdb.cdbwiki import WikiDB
wikidb = WikiDB('somewhere', lang='ja')

として直接 WikiDB を呼び出す。