プログラミングと絵と音楽

コンピューター科学を専攻し、絵と音楽を趣味とするエンジニアのブログです。

質問応答システムの実装と考察:学習用の文章データを解析する

質問応答システムを実装した過程を書き留めています。

最初の記事は次のものになります。こちらから順を追えます。
GitHubのリンクも載せているのでコードを参照できます。

tfull.hatenablog.jp

ファイルからDBに記事を読み込ませる

Wikipediaのダンプファイルを取得し、DBにタイトルと記事の内容のみを取り出して挿入しました。

ダンプファイルはXMLファイルで、xml.etree.ElementTree を使うとパースできます。

Wikipediaの全データを読み込むにはメモリが足りなかったので、1万件ごとに分割したXMLファイルを順に読み込んでいっています。

import xml.etree.ElementTree

def load_xml(path):
    with open(path, "r") as f:
        parser = xml.etree.ElementTree.XMLParser()

        for line in f:
            parser.feed(line)

        return parser.close()

Wikipediaをパースして、中のデータにアクセスします。トップのタグ(xml)は何の名前でも良いのですが、次のような形式になっています。

<xml>
    <page>
        <title>広島県</title>
        <revision>
            <text>広島県は、瀬戸内海に面する件である。</text>
        </revision>
    </page>
    <page>
        <title>...</title>
        <redirect title="..." />
    </page>
</xml>

pageという項目が並んでいて、中に記事のtitleと、文章あるいはリダイレクション(別のページに飛ばす設定)が存在します。

# path: ファイルのパス

# まずXMLをパースする
wikipedia = load_xml(path)

# 各ページに分割してリストにする。
pages = wikipedia.findall("page"):

# 各ページに対して
for page in pages:
    # タイトルへアクセス
    title = page.find("title").text
    # リダイレクションへアクセス(なければ None)
    redirect = page.find("redirect")
    if redirect is not None:
        redirect = redirect.text
    # テキスト(リダイレクトがない場合)
    revision = page.find("revision")
    text = revision.find("text").text

リダイレクトに関しては、使うことがありそうなので、記事とは別のテーブルに保存します。

Wikipedia記法の記事から、平の文章を抽出する。

さて、記事の文字列は取得できたのですが、今の段階では、別記事へのリンクや、テキストの装飾など、Wiki形式の特殊な記法が混じっているので、それからプレーンな文章を抽出する必要があります。

別の方のプログラムで、 Wikipedia Extractor というものがあります。

GitHub - attardi/wikiextractor: A tool for extracting plain text from Wikipedia dumps

上記、試しに使ってみましたが、今回の目的に対しては自分自身で自由に実装したかったのと、昔にも抽出プログラムを記述したことがあったので、自前で実装することにしました。

私が GitHub に上げているコードは常に改良していくつもりですが、そのなかで、テキスト抽出に関するいくつか例を提示します。

厳密にやるのであれば構文解析(tokenizer, parser)を使うべきですが、それだけでとてつもない時間がかかるため、暫定的に正規表現で文字列の置換を行う方法を実行します。

# 正規表現ライブラリ
import re

強調の排除

例:文字が'''強調'''されています。 → 文字が強調されています。

ダッシュ記号が2つ以上連続している部分を単純に消します。

RE_PRIME = re.compile(r"\'{2,}")
text = re.sub(RE_PRIME, "", text)

付帯情報の除去

例:人物名{{誕生|...年,出身|...}} → 人物名
画像の情報など、文章になっていないものを除去します。

RE_BRACKET = re.compile(r"\{[^\{]*?\}") # {から}まで、途中に{を含まない
for i in range(5):
    text = re.sub(RE_BRACKET, "", text)

入れ子になっている場合 {{... {{ ... }} ... }} もあるので、最も内側のものから反応し、複数回繰り返して全部除去するようにしています。

こちらに関しては、現在は除去という対処法をしていますが、重要な情報もよく含まれていることが多いように思いましたので、以降の実装では適宜抽出することになりそうです。

リンクから語句の抽出

例:[[五百円硬貨|500円]] → 五百円硬貨

正規表現だけおいておきます。

RE_LINK_I     = re.compile(r"\[\[[^\[]*?\]\]")
RE_LINK_S = re.compile(r"\[\[(.*?)\|(.*?)\]\]")

おそらく画像系で、[[...|right|350px|thumb| [[ ... ]] ]] みたいな記法を発見したので、こちらは改良の余地があります。

平の文章もDBに保存

これで、構文解析済みの文章データが生成されたので、DBに保存できます。次のようにデータを保存しました。

  • entries:titleとWikipedia記法の文章を入れておく
  • redirections:語句とentriesのidを対応させる。その語句はentries.idを持つentryへリンクされる。
  • plain_texts:entries.idと解析済み文章を保存する。


面倒かつ大事なデータの前処理が終了しました。

これで、文章を学習させたりすることができます。