言語処理100本ノック-21:カテゴリ名を含む行を抽出

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第3章: 正規表現」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch3)の[21本目「カテゴリ名を含む行を抽出」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec2)記録です。
前回は準備事項で、今回からが正規表現の実践です。今までググっては思い出していた基本内容をたくさん使っています。具体的には、**raw文字列・re.VERBOSE・re.MULTILINE・トリプルクォート**など基本だらけ。

# 参考リンク

|リンク|備考|
|:--|:--|
|[021.カテゴリ名を含む行を抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/03.%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE/021.%E3%82%AB%E3%83%86%E3%82%B4%E3%83%AA%E5%90%8D%E3%82%92%E5%90%AB%E3%82%80%E8%A1%8C%E3%82%92%E6%8A%BD%E5%87%BA.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:21](http://qiita.com/segavvy/items/73f1b91ff75529ae3b8d)|多くのソース部分のコピペ元|
|[ゼロから覚えるPython正規表現の基本とTips](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7)|当ノックで学習した内容を整理しました|
|[正規表現 HOWTO](https://docs.python.org/ja/3/howto/regex.html#regex-howto)|Python公式の正規表現How To|
|[re --- 正規表現操作](https://docs.python.org/ja/3/library/re.html)|Python公式のreパッケージ説明|
|[Help:早見表](https://ja.wikipedia.org/wiki/Help:%E6%97%A9%E8%A6%8B%E8%A1%A8)|Wikipediaの代表的なマークアップの早見表|

# 環境

|種類|バージョン|内容|
|:--|:--|:--|
|OS|Ubuntu18.04.01 LTS|仮想で動かしています|
|pyenv|1.2.15|複数Python環境を使うことがあるのでpyenv使っています|
|Python|3.6.9|pyenv上でpython3.6.9を使っています<br>3.7や3.8系を使っていないことに深い理由はありません<br>パッケージはvenvを使って管理しています|

上記環境で、以下のPython追加パッケージを使っています。通常のpipでインストールするだけです。

|種類|バージョン|
|:--|:--|
|pandas|0.25.3|

# [第3章: 正規表現](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch3)
## 学習内容
> Wikipediaのページのマークアップ記述に正規表現を適用することで，様々な情報・知識を取り出します．
>> 正規表現, JSON, Wikipedia, InfoBox, ウェブサービス

## ノック内容
> Wikipediaの記事を以下のフォーマットで書き出したファイル[jawiki-country.json.gz](http://www.cl.ecei.tohoku.ac.jp/nlp100/data/jawiki-country.json.gz)がある．

> - 1行に1記事の情報がJSON形式で格納される
> - 各行には記事名が"title"キーに，記事本文が"text"キーの辞書オブジェクトに格納され，そのオブジェクトがJSON形式で書き出される
> - ファイル全体はgzipで圧縮される

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

### [21. カテゴリ名を含む行を抽出](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec21)
> 記事中でカテゴリ名を宣言している行を抽出せよ．

### 課題補足(「カテゴリ名」について)
[Help:早見表](https://ja.wikipedia.org/wiki/Help:%E6%97%A9%E8%A6%8B%E8%A1%A8)によると「カテゴリ名」は`[[Category:ヘルプ|はやみひよう]]`形式です。
ファイル内の以下の部分を正規表現で抽出します。

```:ファイル内の「カテゴリ名」部分抜粋
[[Category:イギリス|*]]\n'
[[Category:英連邦王国|*]]\n'
[[Category:G8加盟国]]\n'
[[Category:欧州連合加盟国]]\n'
[[Category:海洋国家]]\n'
[[Category:君主国]]\n'
[[Category:島国|くれいとふりてん]]\n'
[[Category:1801年に設立された州・地域]]'
```

# 回答
## 回答プログラム [021.カテゴリ名を含む行を抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/03.%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE/021.%E3%82%AB%E3%83%86%E3%82%B4%E3%83%AA%E5%90%8D%E3%82%92%E5%90%AB%E3%82%80%E8%A1%8C%E3%82%92%E6%8A%BD%E5%87%BA.ipynb)

```python
from pprint import pprint
import re

import pandas as pd

def extract_by_title(title):
    df_wiki = pd.read_json('jawiki-country.json', lines=True)
    return df_wiki[(df_wiki['title'] == title)]['text'].values[0]

wiki_body = extract_by_title('イギリス')

# rを先頭にするとraw string でエスケープシーケンス無視
# 3重クォートで途中改行無視
# re.VERBOSEオプションを使うことによって、空白とコメント無視
# re.MULTILINEで複数行に対して検索
pprint(re.findall(r'''
                     ^                  # 文字列の先頭(なくても結果は変わらないが入れておく)
                     (                  # グループ化開始
                     .*                 # 任意の文字列0文字以上
                     \[\[Category:      # 検索語句(\はエスケープ処理)
                     .*                 # 任意の文字列0文字以上
                     \]\]               # 検索語句(\はエスケープ処理)
                     .*                 # 任意の文字列0文字以上
                     )                  # グループ化終了
                     $                  # 文字列の末尾(なくても結果は変わらないが入れておく)
                     ''', wiki_body, re.MULTILINE+re.VERBOSE))
```



## 回答解説
今回のノックの本題は以下の箇所です。

```python
pprint(re.findall(r'''
                     ^                  # 文字列の先頭(なくても結果は変わらないが入れておく)
                     (                  # グループ化開始
                     .*                 # 任意の文字列0文字以上
                     \[\[Category:      # 検索語句(\はエスケープ処理)
                     .*                 # 任意の文字列0文字以上
                     \]\]               # 検索語句(\はエスケープ処理)
                     .*                 # 任意の文字列0文字以上
                     )                  # グループ化終了
                     $                  # 文字列の末尾(なくても結果は変わらないが入れておく)
                     ''', wiki_body, re.MULTILINE+re.VERBOSE))
```

### [`findall`関数](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#findall)で検索結果をすべて取得
**パターンにマッチした全ての文字列をリスト形式で返す**のが`findall`関数です。
以下の例では、`ly`が末尾につく副詞の単語をすべて抽出しています([`\w`は「英数文字と下線」](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#%E7%89%B9%E6%AE%8A%E6%96%87%E5%AD%97))。

```python:findall例
>>> text = "He was carefully disguised but captured quickly by police."
>>> re.findall(r"\w+ly", text)
['carefully', 'quickly']
```

### [raw文字列(raw string)](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#raw%E6%96%87%E5%AD%97%E5%88%97%E3%81%A7%E3%82%A8%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%97%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B9%E7%84%A1%E5%8A%B9)でエスケープシーケンス無効
引用記号の前に`r`をつけることでraw文字列(raw string)とします。raw文字列を使うことでエスケープシーケンスを無効にできます。**正規表現パターンにエスケープシーケンスあると読みにくいので、raw文字列にして無効化**します。

```python:raw文字列print出力例
>>> print('a\tb\nA\tB')
a   b
A   B

>>> print(r'a\tb\nA\tB')
a\tb\nA\tB
```

### [トリプルクォート](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#%E3%83%88%E3%83%AA%E3%83%97%E3%83%AB%E3%82%AF%E3%82%A9%E3%83%BC%E3%83%88%E3%81%A8reverbose%E3%81%A7%E6%94%B9%E8%A1%8C%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88%E7%A9%BA%E7%99%BD%E7%84%A1%E8%A6%96)で改行使用
`'''`トリプルクォート(`"""`でも可能)で囲むことにより、正規表現パターン中に改行を使うことができます(改行がなくても問題なし)。**改行することにより正規表現パターンを読みやすくします。**

```python:トリプルクォート使用例
a = re.compile(r'''\d +
                   \.  
                   \d *''')
```

### [`re.VERBOSE`](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#%E3%83%88%E3%83%AA%E3%83%97%E3%83%AB%E3%82%AF%E3%82%A9%E3%83%BC%E3%83%88%E3%81%A8reverbose%E3%81%A7%E6%94%B9%E8%A1%8C%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88%E7%A9%BA%E7%99%BD%E7%84%A1%E8%A6%96)でコメント・空白使用
`re.VERBOSE`をパラメータ`flags`に渡すことで、正規表現パターン中にコメント・空白を使うことができます(使わなくても問題なし)。**コメントと空白を挟むことで正規表現パターンを読みやすくします**。トリプルクォートと併せて使う可読性向上方法です。

```python:トリプルクォート使用例
a = re.compile(r'''\d +  # the integral part
                   \.    # the decimal point
                   \d *  # some fractional digits''', re.VERBOSE)
```

### [`re.MULTILINE`](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#multiline)で複数行検索
複数行に対してそれぞれ検索したい場合に使います。

```python:re.MULTILINE使用例
string = r'''\
行頭 1st line
行頭 2nd line'''

# 複数行が検索対象
print(re.findall(r'^行頭.*', string, re.MULTILINE))
# ['行頭 1st line', '行頭 2nd line']

# 1行目のみが検索対象
print(re.findall(r'^行頭.*', string))
# ['行頭 1st line']
```

### 出力結果(実行結果)
プログラム実行すると以下の結果が出力されます。

```bash:出力結果
['[[Category:イギリス|*]]',
 '[[Category:英連邦王国|*]]',
 '[[Category:G8加盟国]]',
 '[[Category:欧州連合加盟国]]',
 '[[Category:海洋国家]]',
 '[[Category:君主国]]',
 '[[Category:島国|くれいとふりてん]]',
 '[[Category:1801年に設立された州・地域]]']
```