Wikipediaのdumpからinfoboxの内容や文章を取ってくる方法

こんにちは!

Link-Uの町屋敷です。

今回はWikipediaの本文を収集する方法と特定のInfoboxを収集する方法を書いていきます。

Wikipediaから文章を取ってくる

Wikipediaの文章を取ってくる方法は主に以下の2つです。

  1. MediaWikiのAPIを使う
  2. 同じくMediaWikiが提供しているXML形式のダンプファイルを使う。

APIを使う方法のほうが簡単ですが、Wikipediaはクロールが禁止されているので、データセットの作成には方法2を使わざるを得ません。チャットボットとかなら1で良いでしょう。

Wikipedia Extractorを使う

今回はWikipediaExtracctorを使って本文を取得します。

まず、最新のMediaWikiが提供しているXML形式のダンプファイルの中から、

jawiki-latest-pages-articles.xml.bz2

をダウンロード

次に適当なフォルダに移動して

git clone git@github.com:attardi/wikiextractor.git
cd wikiextractor
python python setup.py install

でwikiextractorをインストール出来ます。

自分はminicondaの仮想環境に入ってからやりました。

早速本文を抽出してみましょう。

WikiExtractor.py --json --keep_tables -s --lists -o ../whole_data/ ~/Downloads/jawiki-latest-pages-articles.xml

コマンド説明
--json        : 出力をjsonにする
--keep_tables : 文章内の表を出力するようにする
-s            : セクション情報も出力するようにする
--list        : 文章内のリストを出力するようにする
-o            : 出力ファルダ名

最後のパスがさっきダウンロードしたxmlの入力ファイルです。

このフォイルはダウンロード時bz2という謎の形式で圧縮されています。

wikiextractorで使う分には圧縮されたままで良いのですが、後で中身を見る必要が出てくるので先に解凍しちゃいましょう。linuxなら

bzip2 -d jawiki-latest-pages-articles.xml.bz2 

で解凍できます。

Windowsの場合はLhaplusを使うと良いでしょう。(なぜか7zipだと失敗した)

少し話がそれましたが、先程のスクリプトを実行するとわらわらとファイルが生成されて、

中を見ると文章が取得できているのでめでたしめでたし。

と、思いきやよく見るInfoboxが取得できていない。

Infoboxとはなんぞやと言う事ですが、要はWikipedia見てると右側にわりといる情報が書かれた表です。

これ機械学習だと正解ラベルになるようなかなり重要なこと書かれてるのになんで無いの?

最初–keep_tablesのオプション使っていなくて絶対コレじゃんと思ってやってみたが増えたのは文章の途中に出てくる表だけだった。残念。

XMLから特定のInfoboxの情報を取得する

Infoboxの情報がどうしてもほしいのでネットで色々検索したが、出てくるのはAPIを使ったものばかり。

WikipediaのXMLをJSONに変換するツールなども試してみたがうまく動かず、

そんなこんなで1日くらいあがいても良いライブラリは見つからなかった。

こうなったら自分で作るしか無い

というわけで、XMLから特定のInfoboxの情報を取得してjsonで保存するスクリプトをPython3で作成した。

通常の文章はさっきほど抽出したものがあるので、必要なInfoboxが存在する項目だけタイトルとタイトルIDとセットで保存して、使うときに結合させるようにする。

どうせやるなら一緒に本文も保存したら良いじゃないかってなりそうだが、xml本文中にはいらないものが大量に含まれていて、消す作業が面倒だったので保留。

幸いあがいてる途中に参考になるサイトを見つけたので、大体はこれに沿ってやります。

プログラム全文はページの一番下。特に特別なパッケージは必要ない

大事なところだけ解説すると、

PythonでXMLからタグを取ってくる方法は、普通は

import xml.etree.ElementTree as ET
tree = ET.parse('country_data.xml')
root = tree.getroot()

こんな感じだが、今回はxmlファイルが馬鹿でかいのでこれを行うとメモリが死ぬ可能性がある。

なので、イテレータを用いて処理する。

for event, elem in etree.iterparse(pathWikiXML, events=('start', 'end')): 

処理は上のタグから一つずつ進行していく、例えば

<a>
    <b>0</b>
    <c>
      <d>70334050</d>
    </c>
</a>

このようなタグが来た時は、elem.tagにはa->b->b->c->d->d->c->aの順番でデータが入り、

開きタグなのか閉じタグなのかは,eventを見て判断する。

elem.textにそのタグに囲まれた部分の文章がひるようだ、

ここで、実際のxmlがどんな形式ななっているか確認しましょう。

less ~/Downloads/jawiki-latest-pages-articles.xml

巨大なファイルなので通常のテキストエディタでは開きません。

どうやら<page>~</page>でwikipediaの各ページが表現されていて、

<title>にタイトル

<id>にページ番号

<text>にメインの文章が書かれているようだ。

ただ、Infoboxは少々面倒で、特定のタグに囲われているわけでもなく、<text></text>の中に

{{東京都の特別区
|画像 = [[File:Sensoji 2012.JPG|200px]]
|画像の説明 = [[浅草寺]]境内
|区旗 = [[File:Flag of Taito, Tokyo.svg|100px]]
|区旗の説明 = 台東[[市町村旗|区旗]]&lt;div style=&quot;font-size:smaller&quot;&gt;[[1965年]][[6月4日]]制定
|区章 = [[File:東京都台東区区章.svg|75px]]
|区章の説明 = 台東[[市町村章|区章]]&lt;div style=&quot;font-size:smaller&quot;&gt;[[1951年]][[4月18日]]制定&lt;ref&gt;「東京都台東区紋章制定について」昭和26年4月18日台東区告示第47号&lt;/ref&gt;
|自治体名 = 台東区
|コード = 13106-7
|隣接自治体 = [[千代田区]]、[[中央区 (東京都)|中央区]]、[[文京区]]、[[墨田区]]、[[荒川区]]
|木 = [[サクラ]]
|花 = [[アサガオ]]
|郵便番号 = 110-8615
|所在地 = 台東区[[東上野]]四丁目5番6号&lt;br /&gt;&lt;small&gt;{{ウィキ座標度分秒|35|42|45.4|N|139|46|47.9|E|region:JP-13_type:adm3rd|display=inline,title}}&lt;/small&gt;&lt;br /&gt;[[File:Taito Ward Office.JPG|250px|台東区役所庁舎(東上野四丁目)]]
|外部リンク = [http://www.city.taito.lg.jp/ 台東区]
|位置画像 = {{基礎自治体位置図|13|106}}
|特記事項 =}}

突然現れるだけで文章からの抽出が必要。

どうやら<text>中で{{}}で表されているものは複数あるらしく、単純に{{}}に囲われた部分を抽出してもうまく行かない。

そこでInfoboxの名前を使う。名前は{{のすぐ後に書いているやつで、lessでそのInfoboxが使われている項目を検索して直接確認するか、Wikipediaの基本情報テンプレートに書いてあるTemplate:Infobox ~ の項目を確認する。

                    elem.text = re.sub('{{[F|f]lagicon.*?}}', '', elem.text)
                    infobox = re.findall('{{{0}n.*?|.*?}}'.format(INFOBOX_SEARCH_WORD), elem.text, re.MULTILINE | re.DOTALL)

ここがその抽出部分で、INFOBOX_SEARCH_WORDにInfoboxの名前を入れるとその名前に対応するInfoboxを抽出できる。ただし、{{}}が入れ子になっているとバグるので、re.subで事前にいらないものを消している。

reの代わりにregexを使うと入れ子でも処理できるようだが、今回選んだInfoboxではこれで大丈夫だったので保留。

試しに上のWikipedia画面のキャプチャで囲んでいた漫画のInfobox(Infobox名 animanga/Manga)で試してみた結果がこちら。

ちゃんと取得できていることが確認できた。

参考サイト

https://www.heatonresearch.com/2017/03/03/python-basic-wikipedia-parsing.html

プログラム全文

import xml.etree.ElementTree as etree
import time
import os
import json
import re
import collections as cl

PATH_WIKI_XML = '/home/machiyahiki/Downloads/'
FILENAME_WIKI = 'jawiki-latest-pages-articles.xml'
JSON_SAVE_DIR = '.'
INFOBOX_SEARCH_WORD = 'Infobox animanga/Manga'

ENCODING = "utf-8"

def strip_tag_name(t):
    idx = t.rfind("}")
    if idx != -1:
        t = t[idx + 1:]
    return t

pathWikiXML = os.path.join(PATH_WIKI_XML, FILENAME_WIKI)

totalCount = 0
articleCount = 0
redirectCount = 0
templateCount = 0
title = None
start_time = time.time()
dict_array = []

with open('{0}//wiki_infobox_{1}.json'.format(JSON_SAVE_DIR, re.sub('[ |//]', '_',  INFOBOX_SEARCH_WORD)),'w', encoding='utf_8') as jf:
    for event, elem in etree.iterparse(pathWikiXML, events=('start', 'end')):
        tname = strip_tag_name(elem.tag)
    
        if event == 'start':
            if tname == 'page':
                inrevision = False
                findinfobox = False
                data_dict = cl.OrderedDict()
            elif tname == 'revision':
                # Do not pick up on revision id's
                inrevision = True
            if tname == 'title':
                data_dict['title'] = elem.text
            elif tname == 'id' and not inrevision:
                data_dict['id'] = elem.text
        else:
            if tname == 'text':
                if elem.text:
                    elem.text = re.sub('{{[U|u]nicode.*?}}', '', elem.text)
                    elem.text = re.sub('{{[F|f]lagicon.*?}}', '', elem.text)
                    infobox = re.findall('{{{0}n.*?|.*?}}'.format(INFOBOX_SEARCH_WORD), elem.text, re.MULTILINE | re.DOTALL)
                    if infobox:
                        findinfobox = True
                        data_dict['infobox'] = infobox
            if tname == 'page':
                if findinfobox:
                    if data_dict['title']: #タイトル名に{{Unicode}}があるとnullが入る、後で修可
                        if 'プロジェクト:' in data_dict['title']: #プロジェクト:から始まる項目は無視
                            continue
                        if 'Template:Infobox' in data_dict['title']: #Template:Infoboxから始まる項目も無視
                            continue
                    dict_array.append(data_dict)
                    
                
        if len(dict_array) > 1 and (len(dict_array) % 10000) == 0:
            print("{:,}".format(len(dict_array)))

        
        elem.clear()
    json.dump(dict_array, jf, ensure_ascii=False)
    
    
elapsed_time = time.time() - start_time

print("Total pages: {:,}".format(len(dict_array)))
print("Elapsed Time: {:,}".format(elapsed_time))