言語処理100本ノック2020年版を解いてみた【第3章：正規表現 20〜24】

東北大乾・岡崎研（現乾・鈴木研）で作成された、新人研修の一つであるプログラミング基礎勉強会の教材『[言語処理100本ノック 2020年版](https://nlp100.github.io/ja/)』をPython（3.7）で解いた記事の第5弾です。

独学でPythonを勉強してきたので、間違いやもっと効率の良い方法があるかもしれません。
改善点を見つけた際はご指摘いただけると幸いです。

第3章からは合っているかどうかな部分が多くなってきているので、改善点だけでなく、合っているかどうかという点もぜひご指摘ください。

ソースコードは[GitHub](https://github.com/Soh1121/NLP100-2020)にも公開しています。

## 第3章: 正規表現

>Wikipediaの記事を以下のフォーマットで書き出したファイル[jawiki-country.json.gz](https://nlp100.github.io/data/jawiki-country.json.gz)がある．

> - 1行に1記事の情報がJSON形式で格納される

> - 各行には記事名が”title”キーに，記事本文が”text”キーの辞書オブジェクトに格納され，そのオブジェクトがJSON形式で書き出される

> - ファイル全体はgzipで圧縮される

>以下の処理を行うプログラムを作成せよ．

### 20. JSONデータの読み込み

> Wikipedia記事のJSONファイルを読み込み，「イギリス」に関する記事本文を表示せよ．問題21-29では，ここで抽出した記事本文に対して実行せよ．

```python:20.py
import pandas as pd

file_name = "jawiki-country.json.gz"
data_frame = pd.read_json(file_name, compression='infer', lines=True)
uk_text = data_frame.query('title == "イギリス"')['text'].values[0]
print(uk_text)
```

これまでの経験でpandasは非常に便利だと実感したので、pandasを使っています。
`pandas.read_json()`関数の第一引数にJSON形式の文字列かファイルパスを渡すと、`pandas.DataFrame`として読み込んでくれます。

今回読み込んでいるファイルは圧縮ファイルなので、オプションとして引数`compression`で`infer`を指定しています。
`compression='infer'`では`.gz`、`.bz2`、`.zip`、`.xz`に対応です。

`lines`の引数では1行ごとにJSONオブジェクトとして読み込みを行っています。

イギリスの記事を探すのはDataFrameの`query()`メソッドを使って行いました。
`query()`メソッドは指定した条件のデータ抽出を簡単に行えて便利です。
ここでは`title`という列名が`イギリス`であるものを抽出して`text`列の値を出力しています。

### 21. カテゴリ名を含む行を抽出

> 記事中でカテゴリ名を宣言している行を抽出せよ．

```python:21.py
import pandas as pd
import re

file_name = "jawiki-country.json.gz"
data_frame = pd.read_json(file_name, compression='infer', lines=True)
uk_text = data_frame.query('title == "イギリス"')['text'].values[0]

pattern = r'\[\[Category:(.*)\]\]'
for line in uk_text.split("\n"):
    result = re.match(pattern, line)
    if result is not None:
        print(line)
```

ここから正規表現を使っていきます。

`pattern`では`[[Category:〜]]`に該当する行を抽出するように指定しています。
Pythonの正規表現を用いたマッチングでは`re`モジュールをインポートします。
`re.match()`関数で第一引数にマッチさせたいパターンを、第二引数に調べたいテキストを指定してマッチングさせます。

マッチング結果が`None`でなければ条件に当てはまっているので、その行を出力という内容です。

### 22. カテゴリ名の抽出

> 記事のカテゴリ名を（行単位ではなく名前で）抽出せよ．

```python:22.py
import pandas as pd
import re

file_name = "jawiki-country.json.gz"
data_frame = pd.read_json(file_name, compression='infer', lines=True)
uk_text = data_frame.query('title == "イギリス"')['text'].values[0]

pattern = r'\[\[Category:(.*)\]\]'
remove_pattern = r'\|.*'
for line in uk_text.split("\n"):
    result = re.match(pattern, line)
    if result is None:
        continue
    ans = re.sub(remove_pattern, '', result.group(1))
    print(ans)
```

先程はカテゴリ名を行単位で出力していましたが、今回は名前のみの抽出という課題です。
各行に対して2つの作業を行っています。

まずは、「21.」同様`[[Category:〜]]`にマッチする部分があるかということ。
そして、マッチするものが合った場合は`|〜`の部分を取り除いたものを取得すること。

`result.group(1)`で`pattern = r'\[\[Category:(.*)\]\]'`のマッチした部分のうち、`(.*)`の部分が取得できます。
これを回答としてしまっても良いかも知れませんが、余計なものも中に含まれていました。
`[[Category:欧州連合加盟国|元]]`のようにパイプも入ったものがたまにあるんですね。
このような場合、`result.group(1)`のみでは`欧州連合加盟国|元`が取得されるので、カテゴリ名として不要なパイプ以降は取り除きたいと思います。

ということで、`re.sub()`メソッドで第一引数に置き換えたいパターン、第二引数に置き換える文字列、第三引数に対象の文字列を指定して置換を行って出力しています。

### 23. セクション構造

> 記事中に含まれるセクション名とそのレベル（例えば”== セクション名 ==”なら1）を表示せよ．

```python:23.py
import pandas as pd
import re

file_name = "jawiki-country.json.gz"
data_frame = pd.read_json(file_name, compression='infer', lines=True)
uk_text = data_frame.query('title == "イギリス"')['text'].values[0]

pattern = r'^(==*)(.+)=+$'
for line in uk_text.split("\n"):
    result = re.match(pattern, line)
    if result is None:
        continue
    print(result.group(2).strip(' ='), len(result.group(1)))
```

見出しは`== 見出し ==`のように行頭にイコールを2つ以上つけて表します。（参考：[マークアップ早見表](http://ja.wikipedia.org/wiki/Help:%E6%97%A9%E8%A6%8B%E8%A1%A8)）
イコールの数が見出しレベルを表しているので、見出しに対してマッチングを行い、行頭のイコールの数を`result.group(1)`で、見出し名を`result.group(2)`で抽出しています。

### 24. ファイル参照の抽出

> 記事から参照されているメディアファイルをすべて抜き出せ．

```python:24.py
import pandas as pd
import re

file_name = "jawiki-country.json.gz"
data_frame = pd.read_json(file_name, compression='infer', lines=True)
uk_text = data_frame.query('title == "イギリス"')['text'].values[0]

pattern = r'ファイル:(.+?)\|(thumb\|.*)+?'
for line in uk_text.split("\n"):
    result = re.finditer(pattern, line)
    if result is None:
        continue
    for match in result:
        print(match.group(1))
```

ファイルは`[[ファイル:Wikipedia-logo-v2-ja.png|thumb|説明文]]`のような形で表されています。
行頭にあるとは限らないので、「ファイル:」という文字列を手がかりにマッチングを行っています。

今回は1行に複数のファイルが含まれている場合も考えられるので、`re.finditer()`関数を用いています。
`finditer()`関数では連続的にマッチオブジェクトを返すことができます。
行内でマッチしたものを対象にメディアファイル名を抽出しています。

## まとめ

この記事では言語処理100本ノック2020年版 第3章: 正規表現の問題番号20〜24までを解いてみました。

学生の頃はRubyで正規表現を扱っていたのですが、Pythonの方がひとくせある印象を受けました。
最短マッチに毎度苦戦しましたが、やっているうちに徐々に慣れてくる感覚があったので、こういった丁寧な問題には感謝ですね。

まだまだ未熟なので、よりよい解答がありましたらぜひご指摘ください！！
よろしくお願いいたします。

## つづき

- [言語処理100本ノック2020年版を解いてみた【第3章：正規表現 25〜29】](https://qiita.com/Soh1121/items/6449cd67a7b77d8bd244)
- [言語処理100本ノック2020年版を解いてみた【第4章：形態素解析 30〜34】](https://qiita.com/Soh1121/items/dd769127d6e494459967)

## 前回まで

- [言語処理100本ノック2020年版を解いてみた【第1章：準備運動 00〜04】](https://qiita.com/Soh1121/items/bd970195f93a23540d4c)
- [言語処理100本ノック2020年版を解いてみた【第1章：準備運動 05〜09】](https://qiita.com/Soh1121/items/edd976bc58909dd79062)
- [言語処理100本ノック2020年版を解いてみた【第2章：UNIXコマンド 10〜14】](https://qiita.com/Soh1121/items/5d35e6718d3f17a9976e)
- [言語処理100本ノック2020年版を解いてみた【第2章：UNIXコマンド 15〜19】](https://qiita.com/Soh1121/items/c372616f2445e98e368d)