Rust × ParolでWikipediaからコーパスを作成する話

# Rustの自然言語処理界隈

Rustで自然言語処理関連の作業をするときに，Pythonで言うところのNLTKが欲しくなりました．[crates.io](https://crates.io/)を探してみると，それらしい名前のクレートはたくさんあるものの，現時点でコレというものはなさそう...

今時点で特に欲しかったのはPOSタグをつけられる機能だったのですが，見つかりませんでした...
(もしあったら教えてほしい)

ないなら作ればいいじゃない．ということで，PythonのNLTKを参考にしてTaggerを作ろうと思いました．
いろいろ調べてみると，どうやらNLTKではPerceptronをベースにした機械学習モデルを使ってTaggerを実装している模様．
ならば，同じようなモデルを作って学習させればTagger完成ですね！ということで，NLTKで使われていたのではないかと思われるPenn Treebankデータセットを探しに行ったところ，実はPTBの全体はWeb上で配布されてはいないらしく，様々なライブラリで落とせるPTBデータセットはサンプリングされたものであるとのこと．

ないなら作ればいいじゃない．ということで，コーパスから作ることにしました．
Wikipediaのダンプをクリーニングして，[Stanford-CoreNLP](https://stanfordnlp.github.io/CoreNLP/)でタグ付して自前のデータセットを構築します．

Wikipediaからのデータセット構築は，一度準備しておくとこの先何かと便利なので，ここでやり方を確立してしまおうかなと．

# やりたいこと

というわけで，この記事ではWikipediaのダンプからプレーンテキストを抽出してコーパスを作成します．

Wikipediaのダンプは[こちら](https://dumps.wikimedia.org/enwiki/latest/)からダウンロード可能です．
使用するのは`nwiki-latest-pages-articles-multistream.xml.bz2`というファイルでWikipediaの記事がXML形式で100GBほど入っています．

このXMLをプレーンテキストにクリーニングしたいです．

以下はサンプル．

```xml
<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.11/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.11/ http://www.mediawiki.org/xml/export-0.11.xsd" version="0.11" xml:lang="en">
  <siteinfo>
    <sitename>Wikipedia</sitename>
    <dbname>enwiki</dbname>
    <base>https://en.wikipedia.org/wiki/Main_Page</base>
    <generator>MediaWiki 1.44.0-wmf.1</generator>
    <case>first-letter</case>
    <namespaces>
      <namespace key="-2" case="first-letter">Media</namespace>
      <namespace key="-1" case="first-letter">Special</namespace>
      <namespace key="0" case="first-letter" />
      <namespace key="1" case="first-letter">Talk</namespace>
      <namespace key="2" case="first-letter">User</namespace>
      <namespace key="3" case="first-letter">User talk</namespace>
      <namespace key="4" case="first-letter">Wikipedia</namespace>
      <namespace key="5" case="first-letter">Wikipedia talk</namespace>
      <namespace key="6" case="first-letter">File</namespace>
      <namespace key="7" case="first-letter">File talk</namespace>
      <namespace key="8" case="first-letter">MediaWiki</namespace>
      <namespace key="9" case="first-letter">MediaWiki talk</namespace>
      <namespace key="10" case="first-letter">Template</namespace>
      <namespace key="11" case="first-letter">Template talk</namespace>
      <namespace key="12" case="first-letter">Help</namespace>
      <namespace key="13" case="first-letter">Help talk</namespace>
      <namespace key="14" case="first-letter">Category</namespace>
      <namespace key="15" case="first-letter">Category talk</namespace>
      <namespace key="100" case="first-letter">Portal</namespace>
      <namespace key="101" case="first-letter">Portal talk</namespace>
      <namespace key="118" case="first-letter">Draft</namespace>
      <namespace key="119" case="first-letter">Draft talk</namespace>
      <namespace key="126" case="first-letter">MOS</namespace>
      <namespace key="127" case="first-letter">MOS talk</namespace>
      <namespace key="710" case="first-letter">TimedText</namespace>
      <namespace key="711" case="first-letter">TimedText talk</namespace>
      <namespace key="828" case="first-letter">Module</namespace>
      <namespace key="829" case="first-letter">Module talk</namespace>
    </namespaces>
  </siteinfo>
  <page>
    <title>AccessibleComputing</title>
    <ns>0</ns>
    <id>10</id>
    <redirect title="Computer accessibility" />
    <revision>
      <id>1219062925</id>
      <parentid>1219062840</parentid>
      <timestamp>2024-04-15T14:38:04Z</timestamp>
      <contributor>
        <username>Asparagusus</username>
        <id>43603280</id>
      </contributor>
      <comment>Restored revision 1002250816 by [[Special:Contributions/Elli|Elli]] ([[User talk:Elli|talk]]): Unexplained redirect breaking</comment>
      <origin>1219062925</origin>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text bytes="111" sha1="kmysdltgexdwkv2xsml3j44jb56dxvn" xml:space="preserve">#REDIRECT [[Computer accessibility]]

{{rcat shell|
{{R from move}}
{{R from CamelCase}}
{{R unprintworthy}}
}}</text>
      <sha1>kmysdltgexdwkv2xsml3j44jb56dxvn</sha1>
    </revision>
  </page>
  <page>
    <title>Anarchism</title>
    <ns>0</ns>
    <id>12</id>
    <revision>
      <id>1252883864</id>
      <parentid>1252800548</parentid>
      <timestamp>2024-10-23T10:20:03Z</timestamp>
      <contributor>
        <username>Monkbot</username>
        <id>20483999</id>
      </contributor>
      <minor />
      <comment>[[User:Monkbot/task 20|Task 20]]: replace {lang-??} templates with {langx|??} [[Wikipedia:Templates_for_discussion/Log/2024_September_27#Replace_and_delete_lang-??_templates|‹See Tfd›]] (Replaced 1);</comment>
      <origin>1252883864</origin>
      <model>wikitext</model>
      <format>text/x-wiki</format>
      <text bytes="111665" sha1="etj0tlwq449l52lm0y34n79o8jf9hby" xml:space="preserve">{{Short description|Political philosophy and movement}}
{{Other uses|Anarchy|Anarchism (disambiguation)|Anarchist (disambiguation)}}
{{Pp-semi-indef}}
{{Good article}}
{{Use British English|date=August 2021}}
{{Use dmy dates|date=October 2024}}
{{Use shortened footnotes|date=May 2023}}
{{Anarchism sidebar}}

'''Anarchism''' is a [[political philosophy]] and [[Political movement|movement]] that is against all forms of authority and seeks to abolish the institutions it claims maintain unnecessary coercion and [[Social hierarchy|hierarchy]], typically including the [[state (polity)|state]] and [[capitalism]]. Anarchism advocates for the replacement of the state with [[Stateless society|stateless societies]] and voluntary [[Free association (communism and anarchism)|free associations]]. A historically left-wing movement, anarchism is usually described as the [[libertarian]] wing of the [[socialist movement]] ([[libertarian socialism]]).

Although traces of anarchist ideas are found all throughout history, modern anarchism emerged from the [[Age of Enlightenment|Enlightenment]]. During the latter half of the 19th and the first decades of the 20th century, the anarchist movement flourished in most parts of the world and had a significant role in [[Labour movement|workers' struggles]] for [[emancipation]]. [[#Schools of thought|Various anarchist schools of thought]] formed during this period. Anarchists have taken part in [[List of revolutions and rebellions|several revolutions]], most notably in the [[Paris Commune]], the [[Russian Civil War]] and the [[Spanish Civil War]], whose end marked the end of the [[classical era of anarchism]]. In the last decades of the 20th and into the 21st century, the anarchist movement has been resurgent once more, growing in popularity and influence within [[anti-capitalist]], [[anti-war]] and [[anti-globalisation]] movements.

Anarchists employ [[diversity of tactics|diverse approaches]], which may be generally divided into [[revolutionary]] and [[evolutionary strategies]]; there is significant overlap between the two. Evolutionary methods try to simulate what an anarchist society might be like, but revolutionary tactics, which have historically taken a [[Violent extremism|violent]] turn, aim to overthrow authority and the state. Many facets of [[Civilization|human civilization]] have been influenced by anarchist theory, critique, and [[Praxis (process)|praxis]].
{{Toc limit|3}}
...
...
```

この中の`<text/>`タグに入っている文字列を取り出して，テキストをクリーニングします．
パッとみた感じ，

- {{XXX}}
- [[XXX]]
- \`\`\`XXX\`\`\`

などが含まれているので，これらをプレーンテキストにパースしたいです．

# 正規表現ではダメなのか？

最初に思いつくのは，正規表現を使ったやり方なのですが，残念ながら今回は正規表現ではほしい箇所を抽出できません．

例えばこんなやつ．

```text
{{XXX|[[YYY]]}}
```

Wikipediaのテキストはパイプ`|`を介して括弧が入れ子になっているようで，このような再起的な表現は単純な正規表現では表しきれません．

そこで，正規表現をパワーアップしたモノ (と言っていいかわかりませんが) である生成文法の構文を使ってパーサを実装します．

Rustには[Parol](https://github.com/jsinger67/parol)という非常に強力なパーサジェネレータがあり，これを使ってWikipedia専用のパーサを構築します．
~~Parolを使ってみたかっただけという説~~

# Parol

Parolは[ドキュメント](https://jsinger67.github.io/)が非常に丁寧なので，これに沿っていけば環境構築は難しくありません．

### インストール

```bash
> cargo install parol
```

### プロジェクトの作成

```bash
> parol new --bin --path ./wiki_parser
> tree wiki_parser
wiki_parser/
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── src
│   ├── main.rs
│   ├── wiki_parser_grammar.rs
│   ├── wiki_parser_grammar_trait.rs
│   └── wiki_parser_parser.rs
├── test.txt
└── wiki_parser.par
```

### サンプルのビルドと実行

```bash
> cargo build
> cargo run ./test.txt
Parsing took 88 milliseconds.
Success!
WikiParser { wiki_parser: Token { text: "Hello world!", token_type: 5, location: Location { start_line: 4, start_column: 5, end_line: 4, end_column: 17, length: 12, scanner_switch_pos: 0, offset: 74, file_name: "./test.txt" }, token_number: 2 } }
```

これで準備完了です．
あとは`wiki_parser.par`に文法を記述すればOK．

ちなみに，Parolは[Visual Studio Code向けの拡張機能](https://github.com/jsinger67/parol/tree/main/tools/parol-vscode)も実装されていて，これがまた非常に使いやすいので，Parolの文法を書くときはVSCodeがおすすめです．

# 実装

.parファイルの書き方については，正直よくわからないので，ParolのJSONパーサのサンプルを参考にさせてもらいました．

```text:json.par
Json: Value
    ;

Object
    : '{'^ Pair { ','^ Pair } '}'^
    | '{'^ '}'^
    ;

Pair: String ':'^ Value
    ;

Array
    : '['^ Value { ','^ Value } ']'^
    | '['^ ']'^
    ;

Value
    : String
    | Number
    | Object
    | Array
    | 'true'^
    | 'false'^
    | 'null'^
    ;

String
    : /"(\\.|[^"])*"/
    ;

Number
    : /-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][-+]?(0|[1-9][0-9]*)?)?/
    ;
```

たったこれだけで，JSONがパースできてしまうんですね．
スゴイ．

Wikipediaパーサの場合も似たような作りになりそうです．
ただし，Wikipediaのテキストでは文法表現において`{{`や`[[`のような二重括弧が使われているのですが，実はRustで正規表現を扱うクレート [regex](https://crates.io/crates/regex) の実装の関係でこれを扱うことができません．
詳細は割愛しますが，regexではパフォーマンス重視のため正規表現における先読みや後読みが実装されていないためです．

そこで，`{{`や`[[`といった二重括弧系は前処理で一重に置換してしまおうと思います．

さて，wiki_parserの実装です．以下のようになりました．
あらかじめ複雑な表現は置換しているので，文法自体は非常にシンプルです．

```text
WikiParser: Value { Value }
    ;

Link: '[' String { '|' Value { Value } } ']'
    ;

MagicWord
    : '{' String { '|' Value { Value } } '}'
    ;

Value
    : Link
    | MagicWord
    | String
    ;

String
    : /[^\[\]\{\}\|]*/
    ;
```

.parファイルは以下のような記法になっているようです．
- `''`: 文字列
- `{}`: 0回以上の繰り返し
- `|`: OR条件
- `()` グルーピング
- `/''/`: 正規表現 (内部はregexクレート)

## テスト

Wikipediaのダンプから最初の方の記事のテキストをとってきてテストしてみます．
二重括弧などはすでに置換済みです．

```text
{Short description|Political philosophy and movement}
{Other uses|Anarchy|Anarchism (disambiguation)|Anarchist (disambiguation)}
{Pp-semi-indef}
{Good article}
{Use British English|date=August 2021}
{Use dmy dates|date=October 2024}
{Use shortened footnotes|date=May 2023}
{Anarchism sidebar}

'''Anarchism''' is a [political philosophy] and [Political movement|movement] that is against all for    ms of authority and seeks to abolish the institutions it claims maintain unnecessary coercion and [So    cial hierarchy|hierarchy], typically including the [state (polity)|state] and [capitalism]. Anarchism     advocates for the replacement of the state with [Stateless society|stateless societies] and voluntar    y [Free association (communism and anarchism)|free associations]. A historically left-wing movement,     anarchism is usually described as the [libertarian] wing of the [socialist movement] ([libertarian so    cialism]).

Although traces of anarchist ideas are found all throughout history, modern anarchism emerged from th    e [Age of Enlightenment|Enlightenment]. During the latter half of the 19th and the first decades of t    he 20th century, the anarchist movement flourished in most parts of the world and had a significant r    ole in [Labour movement|workers' struggles] for [emancipation]. [#Schools of thought|Various anarchis    t schools of thought] formed during this period. Anarchists have taken part in [List of revolutions a    nd rebellions|several revolutions], most notably in the [Paris Commune], the [Russian Civil War] and     the [Spanish Civil War], whose end marked the end of the [classical era of anarchism]. In the last de    cades of the 20th and into the 21st century, the anarchist movement has been resurgent once more, gro    wing in popularity and influence within [anti-capitalist], [anti-war] and [anti-globalisation] moveme    nts.

Anarchists employ [diversity of tactics|diverse approaches], which may be generally divided into [rev    olutionary] and [evolutionary strategies]; there is significant overlap between the two. Evolutionary     methods try to simulate what an anarchist society might be like, but revolutionary tactics, which ha    ve historically taken a [Violent extremism|violent] turn, aim to overthrow authority and the state. M    any facets of [Civilization|human civilization] have been influenced by anarchist theory, critique, a    nd [Praxis (process)|praxis].
{Toc limit|3}

== Etymology, terminology, and definition ==
{Main|Definition of anarchism and libertarianism}
{See also|Glossary of anarchism}
[File:WilhelmWeitling.jpg|thumb|[Wilhelm Weitling] is an example of a writer who added to anarchist t    heory without using the exact term.{Sfn|Carlson|1972|pp=22–23}]
```

実行結果がこちら．

```text
Parsing took 113 milliseconds.
Success!

WikiParser {
    value: MagicWord(ValueMagicWord {
        magic_word: MagicWord { 
            l_brace: Token {
                text: "{",
                token_type: 8,
                location: Location {
                    start_line: 1,
                    start_column: 1,
                    end_line: 1,
                    end_column: 2,
                    length: 1, 
                    scanner_switch_pos: 0,
                    offset: 1,
                    file_name: "./wiki_sample.txt"
                },
                token_number: 0
            }, string: String
...
...
```

長いので一部しか掲載できませんが，パース成功です．
これで，パース結果からほしいテキストだけを抽出することができるようになりました！

あとは，それ用のプログラムを組んで100GBのテキストを流し込むだけ...！
それはまた別のどこかで．