自動タグ付け機能でも作ってみる - 6: Wikipediaの出力を形態素解析してみる

前提

先の構成図 参照。

MorphoAnalyzerはできたので、Wikipediaの記事をパースしてみる。

準備

WikiExtractor を使うと、 Wikipediaからダウンロードした全記事分のデータを展開してPlaintextにしてくれる。

参考: 【Python】日本語Wikipediaのダンプデータから本文を抽出する - プログラムは、用いる言葉の選択で決まる に詳しい。

WikiExtractorを使うと、最終的には次のようなファイルが大量に作られる。

<doc id="..." url="...">プレーンテキスト...</doc>
<doc id="..." url="...">プレーンテキスト...</doc>
<doc id="..." url="...">プレーンテキスト...</doc>
...

XMLの様でXMLでない、ちょっとXMLなファイルである。

WikiExtractor の出力を<root>で包む

XMLはルートとなるオブジェクトが無いと成立しないので、まずはユルいTextIOのラッパーでも書いてみる。

from typing import Any, IO, Optional, TextIO
from io import TextIOBase, StringIO


def wrap_root(stream: TextIO) -> IO[Any]:
    class wrapper(TextIOBase):

        def __init__(self, base: TextIO) -> None:
            self.head = True
            self.base = base
            self.tail = True

        def read(self, size: Optional[int]=-1) -> str:
            if self.head:
                self.head = False
                return '<root>'
            if size is None:
                body = self.base.read(-1)
            else:
                body = self.base.read(size)
            if len(body) > 0:
                return body
            if self.tail:
                self.tail = False
                return '</root>'
            return ''

    return wrapper(stream)  # type: ignore


if __name__ == '__main__':
    source = StringIO('''
<doc id="..." url="...">プレーンテキスト...</doc>
<doc id="..." url="...">プレーンテキスト...</doc>
<doc id="..." url="...">プレーンテキスト...</doc>
''')

    import sys

    wrapped = wrap_root(source)
    text = wrapped.read()
    while len(text) > 0:
        sys.stdout.write(text)
        text = wrapped.read()
$ python -m gen_tags.xml_wrap_root
<root>
<doc id="..." url="...">プレーンテキスト...</doc>
<doc id="..." url="...">プレーンテキスト...</doc>
<doc id="..." url="...">プレーンテキスト...</doc>
</root>% 

できてる。

XMLParserを作ってみる

1つ1つのファイルもそう小さくはないので、一度にパースするよりは iterparse したいところ。 ということで、ザックリ書くと、こう。

from xml.etree.ElementTree import iterparse
from typing import Any, TextIO

from .text_storage import TextStorage
from .wrap_root import wrap_root
from .parser import Parser


class XMLParser(Parser):
    def __init__(self, path: str, storage: TextStorage) -> None:
        super().__init__()
        self.path = path
        self.storage = storage

    def parse(self, stream: TextIO) -> None:
        for _, element in iterparse(wrap_root(stream)):
            self.storage.push(self.path, element.text)

    def __enter__(self) -> Any:
        return self

    def __exit__(self, type: str, value: str, traceback: str) -> Any:
        pass


if __name__ == '__main__':
    from .word import Word
    from .word_storage import WordStorage
    from .morpho_analyzer import MorphoAnalyzer

    class WordPrinter(WordStorage):
        def push(self, path: str, word: Word) -> None:
            print('{}: {}({})'.format(path, word.word, word.feature_id))

    doc = "./extracted/AA/wiki_00"
    with open(doc, "r") as f:
        ana = XMLParser(doc, MorphoAnalyzer(WordPrinter()))
        ana.parse(f)
na.parse(f)

ところがどっこい、こいつを走らせると…

$ python -m gen_tags.xml_parser
Traceback (most recent call last):
  File "/Users/yamada/.pyenv/versions/3.6.5/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/Users/yamada/.pyenv/versions/3.6.5/Python.framework/Versions/3.6/lib/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/Users/yamada/go/src/github.com/kyoh86/gen-tags/gen_tags/xml_parser.py", line 42, in <module>
    ana.parse(f)
  File "/Users/yamada/go/src/github.com/kyoh86/gen-tags/gen_tags/xml_parser.py", line 20, in parse
    for _, element in iterparse(wrap_root(stream)):
  File "/Users/yamada/.pyenv/versions/3.6.5/Python.framework/Versions/3.6/lib/python3.6/xml/etree/ElementTree.py", line 1221, in iterator
    yield from pullparser.read_events()
  File "/Users/yamada/.pyenv/versions/3.6.5/Python.framework/Versions/3.6/lib/python3.6/xml/etree/ElementTree.py", line 1296, in read_events
    raise event
  File "/Users/yamada/.pyenv/versions/3.6.5/Python.framework/Versions/3.6/lib/python3.6/xml/etree/ElementTree.py", line 1268, in feed
    self._parser.feed(data)
xml.etree.ElementTree.ParseError: not well-formed (invalid token): line 4, column 11

not well-formed (invalid token) と言われてしまった。 試しに中身を見てみると、

<doc id="5" url="https://ja.wikipedia.org/wiki?curid=5" title="アンパサンド">
アンパサンド

アンパサンド (, &) とは「…と…」を意味する記号である。ラテン語の ...

& が実体参照になってない!!!!

そう、所詮は「XMLの様な何か」でしかないこのファイル、XMLに則ったエスケープ処理なんてされていない。 こんなのもうバグでしか無いと思うんだけど… ということで、普通に WikiExtractor を使うだけだと、パース不可能な意味のない出力を作ってくれる。 作者いわく「これはXMLではない。Plaintextだ」ということなので、<doc>という謎表記についての釈明が待たれる。

参考

WikiExtractor -> Plaintext

ということで、正しくplaintextだけ取り出す方法は、次の通り。

$ WikiExtractor.py -o - --json jawiki-latest-pages-articles.xml.bz2 | \
    jq -r .text | \
    grep -ve '^$' | \
    split -a 3 -l 3000 - /var/wikipedia/ja/wiki_

--json オプションでJSONとして、-o - オプションで標準出力へ出力させる。 そして、jq でテキストだけを取り出し、 grep で空行を取り除き、 split で3000行ごと(約1MB程度)に分割する。 split は好みの問題かもしれないので、なくても良い。

TextParserを作る

XMLではないなにかをパースしてもしょうがないので、こうして得られたPlaintextを対象に、 TextParserを作る。

TextParserには何も難しいポイントはない。受け取ったStreamから1行ずつ MorphoAnalyzer (TextStorage) に渡してやればいいだけだ。

from typing import Any, TextIO

from .text_storage import TextStorage
from .parser import Parser


class TextParser(Parser):
    def __init__(self, path: str, storage: TextStorage) -> None:
        super().__init__()
        self.path = path
        self.storage = storage

    def parse(self, stream: TextIO) -> None:
        for line in stream:
            self.storage.push(self.path, line)

    def __enter__(self) -> Any:
        return self

    def __exit__(self, type: str, value: str, traceback: str) -> Any:
        pass


if __name__ == '__main__':
    from .word import Word
    from .word_storage import WordStorage
    from .morpho_analyzer import MorphoAnalyzer

    class WordPrinter(WordStorage):
        def push(self, path: str, word: Word) -> None:
            print('{}: {}({})'.format(path, word.word, word.feature_id))

    doc = "/var/wikipedia/ja/wiki_aaa"
    with open(doc, "r") as f:
        ana = TextParser(doc, MorphoAnalyzer(WordPrinter()))
        ana.parse(f)

動かしてみると、どうやら今度は正しく処理できていそう。

$ python -m gen_tags.text_parser
/var/wikipedia/ja/wiki_aaa: アンパサンド(45)
/var/wikipedia/ja/wiki_aaa: アンパサンド(38)
/var/wikipedia/ja/wiki_aaa: (,&)(36)
/var/wikipedia/ja/wiki_aaa: と(14)
/var/wikipedia/ja/wiki_aaa: は(16)
/var/wikipedia/ja/wiki_aaa: 「(5)
/var/wikipedia/ja/wiki_aaa: …(4)
/var/wikipedia/ja/wiki_aaa: と(14)
/var/wikipedia/ja/wiki_aaa: …(4)
/var/wikipedia/ja/wiki_aaa: 」(6)
...

次はいよいよ KeywordRanker (WordStorage) を実装していくことになりそう。