Help us understand the problem. What is going on with this article?

Wikipediaのダンプからページを取り出す

Wikipedia は全ページのダンプを提供しています。巨大なデータですが圧縮したまま扱えるようにインデックスが用意されています。実際にデータを取り出してみます。

準備

ダンプデータについての説明は以下にあります。

ファイルサイズが巨大なため、解凍したXMLを通常のエディタやブラウザで開かないようにご注意ください。

Wikipedia 日本語版のデータは以下にあります。

記事執筆時点で入手可能な2020年5月1日版より、以下の2つのファイルを使用します。

  1. jawiki-20200501-pages-articles-multistream.xml.bz2 3.0 GB
  2. jawiki-20200501-pages-articles-multistream-index.txt.bz2 23.9 MB

1番目の XML が本体のデータです。圧縮済みでこのサイズですから展開するととんでもないサイズになりますが、圧縮したまま扱えるように配慮されているため今回は展開しません。

2番目のインデックスの方は展開します。107MB ほどになります。

仕様

以下の記事でダンプされた XML のタグの構造を調べています。

主要部分の構造は以下の通りです。1 つの項目が 1 つの page タグに格納されます。

<mediawiki>
    <siteinfo></siteinfo>
    <page></page>
    <page></page><page></page>
</mediawiki>

bz2 ファイルは単純に XML ファイル全体を圧縮したのではなく、100 項目ごとのブロックで構成されています。ブロックを取り出してピンポイントで展開できます。この構造をマルチストリームと呼びます。

siteinfo page × 100 page × 100

インデックスは行ごとに次の構造となっています。

bz2のオフセット:id:title

実際のデータを確認します。

$ head -n 5 jawiki-20200501-pages-articles-multistream-index.txt
690:1:Wikipedia:アップロードログ 2004年4月
690:2:Wikipedia:削除記録/過去ログ 2002年12月
690:5:アンパサンド
690:6:Wikipedia:Sandbox
690:10:言語

690 から始まるブロックの長さを知るには、次のブロックの開始位置を確認する必要があります。

$ head -n 101 jawiki-20200501-pages-articles-multistream-index.txt | tail -n 2
690:217:ミュージシャン一覧 (グループ)
814164:219:曲名一覧

1行1項目のため、行数を数えれば全体の項目数が分かります。約250万項目あります。

$ wc -l jawiki-20200501-pages-articles-multistream-index.txt
2495246 jawiki-20200501-pages-articles-multistream-index.txt

取り出し

実際に特定の項目を取り出してみます。「Qiita」を対象とします。

情報取得

「Qiita」を検索します。

$ grep Qiita jawiki-20200501-pages-articles-multistream-index.txt
2919984762:3691277:Qiita
3081398799:3921935:Template:Qiita tag
3081398799:3921945:Template:Qiita tag/doc

Template は無視して、最初の id=3691277 を対象とします。

基本的には 1 ブロック 100 項目ですが、中には例外があってずれているようなので、次のブロックの開始位置を手動で確認します。

2919984762:3691305:Category:ガボンの二国間関係
2920110520:3691306:Category:日本・カメルーン関係

必要な情報が揃いました。

id ブロック 次のブロック
3691277 2919984762 2920110520

Python

Python を起動します。

$ python
Python 3.8.2 (default, Apr  8 2020, 14:31:25)
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.

圧縮ファイルを開きます。

>>> f = open("jawiki-20200501-pages-articles-multistream.xml.bz2", "rb")

オフセットを指定して、Qiita の項目を含むブロックを取り出します。

>>> f.seek(2919984762)
2919984762
>>> block = f.read(2920110520 - 2919984762)

ブロックを展開して、文字列を取得します。

>>> import bz2
>>> data = bz2.decompress(block)
>>> xml = data.decode(encoding="utf-8")

内容を確認します。page タグが 100 個入っています。

>>> print(xml)
  <page>
    <title>Category:恵庭市長</title>
    <ns>14</ns>
    <id>3691165</id>
(略)

このままでは扱いにくいので、XML としてパースします。パースにはルート要素が必要なため、適当に追加します。

>>> import xml.etree.ElementTree as ET
>>> root = ET.fromstring("<root>" + xml + "</root>")

中身を確認します。ルートの下に page タグが 100 個入っています。

>>> len(root)
100
>>> [child.tag for child in root]
['page', 'page', (略), 'page']

id を指定して page を取得します。find の引数は XPath と呼ばれる記法です。

>>> page = root.find("page/[id='3691277']")

内容を確認します。

>>> page.find("title").text
'Qiita'
>>> page.find("revision/text").text[:50]
'{{Infobox Website\n|サイト名=Qiita\n|ロゴ=\n|スクリーンショット=\n|スク'

ファイルとして保存します。

>>> tree = ET.ElementTree(page)
>>> tree.write("Qiita.xml", encoding="utf-8")

次のようなファイルが得られます。

Qiita.xml
<page>
    <title>Qiita</title>
    <ns>0</ns>
    <id>3691277</id>
    <revision>
      <id>77245770</id>
      <parentid>75514051</parentid>
      <timestamp>2020-04-26T12:21:10Z</timestamp>
      <contributor>
        <username>Linuxmetel</username>
        <id>1613984</id>
      </contributor>
      <comment>Qiitaの論争についてと、LGTM ストックの説明の追加</comment>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text bytes="4507" xml:space="preserve">{{Infobox Website
|サイト名=Qiita
(略)
[[Category:日本のウェブサイト]]</text>
      <sha1>mtwuh9z42c7j6ku1irgizeww271k4dc</sha1>
    </revision>
  </page>

スクリプト

一連の流れをスクリプトにまとめました。

インデックスを SQLite に格納して使用します。

SQLite

スクリプトでインデックスを TSV に変換して、取り込み用 SQL を生成します。

python conv_index.py jawiki-20200501-pages-articles-multistream-index.txt

3つのファイルが生成されます。

  • jawiki-20200501-pages-articles-multistream-index-1.tsv
  • jawiki-20200501-pages-articles-multistream-index-2.tsv
  • jawiki-20200501-pages-articles-multistream-index.sql

SQLite に取り込みます。

sqlite3 jawiki.db ".read jawiki-20200501-pages-articles-multistream-index.sql"

これで準備が完了しました。

使い方

DB にはインデックスのみが格納されているため、同じディレクトリに xml.bz2 ファイルが必要です。DB に xml.bz2 のファイル名が記録されているためリネームしないでください。

DB と項目名を指定すると、結果が表示されます。デフォルトでは text タグの内容のみですが、-x を指定すれば page タグ内のすべてのタグが出力されます。

python mediawiki.py jawiki.db Qiita
python mediawiki.py -x jawiki.db Qiita

ファイルに出力できます。

python mediawiki.py -o Qiita.txt jawiki.db Qiita
python mediawiki.py -o Qiita.xml -x jawiki.db Qiita

mediawiki.py はライブラリとしても使えるように作られています。

import mediawiki
db = mediawiki.DB("jawiki.db")
print(db["Qiita"].text)

関連記事

マルチストリームと bz2 モジュールについての記事です。

多言語辞書 Wiktionary のダンプからデータを抽出する記事です。Wikipedia の関連プロジェクトでダンプの形式は同じです。

参考

Wikipedia のインデックスの仕様について参考にしました。

ElementTree XML API はドキュメントを参照しました。

SQLite の使い方は例文データを処理した時に調べました。

7shi
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした