言語処理100本ノック-22:カテゴリ名の抽出

[言語処理100本ノック 2015](http://www.cl.ecei.tohoku.ac.jp/nlp100/)[「第3章: 正規表現」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#ch3)の[22本目「カテゴリ名の抽出」](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec22)記録です。
今回は**キャプチャ対象外・非貪欲マッチ**を使います。少しずつ覚える内容が出てくるのがこの100本ノックのいい点ですね。

# 参考リンク

|リンク|備考|
|:--|:--|
|[022.カテゴリ名の抽出.ipynb](https://github.com/YoheiFukuhara/nlp100/blob/master/03.%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE/022.%E3%82%AB%E3%83%86%E3%82%B4%E3%83%AA%E5%90%8D%E3%81%AE%E6%8A%BD%E5%87%BA.ipynb)|回答プログラムのGitHubリンク|
|[素人の言語処理100本ノック:22](http://qiita.com/segavvy/items/3e7de80f88c2c1096bef)|多くのソース部分のコピペ元|
|[ゼロから覚える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で圧縮される

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

### [22. カテゴリ名の抽出](http://www.cl.ecei.tohoku.ac.jp/nlp100/#sec22)
> 記事のカテゴリ名を（行単位ではなく名前で）抽出せよ．

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

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

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

```python
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で複数行に対して検索
# 非貪欲マッチにすることで、短い文字列を検索
print(re.findall(r'''
                  ^                  # 文字列の先頭(なくても結果は変わらないが入れておく)
                  \[\[Category:      # 検索語句(\はエスケープ処理)
                  (                  # キャプチャ対象グループ化開始
                  .*?                # 任意の文字列0文字以上を非貪欲マッチ
                  )                  # キャプチャ対象グループ化終了
                  (?:                # キャプチャ対象外グループ化開始
                  \|                 # 検索語句'|'
                  .*                 # 任意の文字列0文字以上
                  )?                 # キャプチャ対象外グループ化終了(0/1回出現対象)
                  \]\]               # 検索語句(\はエスケープ処理)
                  $                  # 文字列の末尾(なくても結果は変わらないが入れておく)
                  ''', wiki_body, re.MULTILINE+re.VERBOSE))
```

## 回答解説
今回のメインは以下の部分です。

```python:
print(re.findall(r'''
                  ^                  # 文字列の先頭(なくても結果は変わらないが入れておく)
                  \[\[Category:      # 検索語句(\はエスケープ処理)
                  (                  # キャプチャ対象グループ化開始
                  .*?                # 任意の文字列0文字以上を非貪欲マッチ
                  )                  # キャプチャ対象グループ化終了
                  (?:                # キャプチャ対象外グループ化開始
                  \|                 # 検索語句'|'
                  .*                 # 任意の文字列0文字以上
                  )?                 # キャプチャ対象外グループ化終了(0/1回出現対象)
                  \]\]               # 検索語句(\はエスケープ処理)
                  $                  # 文字列の末尾(なくても結果は変わらないが入れておく)
                  ''', wiki_body, re.MULTILINE+re.VERBOSE))
```

### [`?:...`でキャプチャ対象外](https://qiita.com/FukuharaYohei/items/459f27f0d7bbba551af7#%E3%82%AD%E3%83%A3%E3%83%97%E3%83%81%E3%83%A3%E5%AF%BE%E8%B1%A1%E5%A4%96)
`(?:...)`をつけると**検索結果文字列に含めず**キャプチャ対象外となります。
今回は`[[Category:ヘルプ|はやみひよう]]`形式の`|はやみひょう`部分は取得したくないのでキャプチャ対象外としています。
以下の例では、`4`の部分を正規表現パターンとはしていますが、結果には出力していません。

```python
>>> re.findall(r'(.012)(?:4)', 'A0123 B0124 C0123')
['B012']
```

### 貪欲・非貪欲マッチ
**検索結果対象文字列の長さをコントロール**することができます。**最大限の長さでマッチさせるのが貪欲マッチ(greedy match)で最小限の長さでマッチさせるのが非貪欲マッチ(non-greedy match)**です。デフォルトは貪欲マッチです。
今回は`[[Category:ヘルプ|はやみひよう]]`形式で`|はやみひよう`の部分を0/1回出現にしているので、非貪欲マッチにしないと、0回として`|はやみひよう]]`まで取得してしまいます。

```python
# 貪欲マッチ
>>> print(re.findall(r'.0.*2',  'A0123 B0123'))
['A0123 B012']

# 非貪欲マッチ(*の後に?)
>>> print(re.findall(r'.0.*?2', 'A0123 B0123'))
['A012', 'B012']
```

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

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