言語処理100本ノック 2015「第3章: 正規表現」の22本目「カテゴリ名の抽出」記録です。
今回はキャプチャ対象外・非貪欲マッチを使います。少しずつ覚える内容が出てくるのがこの100本ノックのいい点ですね。
参考リンク
リンク | 備考 |
---|---|
022.カテゴリ名の抽出.ipynb | 回答プログラムのGitHubリンク |
素人の言語処理100本ノック:22 | 多くのソース部分のコピペ元 |
ゼロから覚えるPython正規表現の基本とTips | 当ノックで学習した内容を整理しました |
正規表現 HOWTO | Python公式の正規表現How To |
re --- 正規表現操作 | Python公式のreパッケージ説明 |
Help:早見表 | Wikipediaの代表的なマークアップの早見表 |
環境
種類 | バージョン | 内容 |
---|---|---|
OS | Ubuntu18.04.01 LTS | 仮想で動かしています |
pyenv | 1.2.15 | 複数Python環境を使うことがあるのでpyenv使っています |
Python | 3.6.9 | pyenv上でpython3.6.9を使っています 3.7や3.8系を使っていないことに深い理由はありません パッケージはvenvを使って管理しています |
上記環境で、以下のPython追加パッケージを使っています。通常のpipでインストールするだけです。
種類 | バージョン |
---|---|
pandas | 0.25.3 |
第3章: 正規表現
学習内容
Wikipediaのページのマークアップ記述に正規表現を適用することで,様々な情報・知識を取り出します.
正規表現, JSON, Wikipedia, InfoBox, ウェブサービス
ノック内容
Wikipediaの記事を以下のフォーマットで書き出したファイルjawiki-country.json.gzがある.
- 1行に1記事の情報がJSON形式で格納される
- 各行には記事名が"title"キーに,記事本文が"text"キーの辞書オブジェクトに格納され,そのオブジェクトがJSON形式で書き出される
- ファイル全体はgzipで圧縮される
以下の処理を行うプログラムを作成せよ.
22. カテゴリ名の抽出
記事のカテゴリ名を(行単位ではなく名前で)抽出せよ.
課題補足(「カテゴリ」について)
Help:早見表によると「カテゴリ」は[[Category:ヘルプ|はやみひよう]]
形式です。この形式での「ヘルプ
」部分を抽出します。
ファイル内では「カテゴリ」部分は以下のデータです。
[[Category:イギリス|*]]\n'
[[Category:英連邦王国|*]]\n'
[[Category:G8加盟国]]\n'
[[Category:欧州連合加盟国]]\n'
[[Category:海洋国家]]\n'
[[Category:君主国]]\n'
[[Category:島国|くれいとふりてん]]\n'
[[Category:1801年に設立された州・地域]]'
回答
回答プログラム 022.カテゴリ名の抽出.ipynb
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))
回答解説
今回のメインは以下の部分です。
print(re.findall(r'''
^ # 文字列の先頭(なくても結果は変わらないが入れておく)
\[\[Category: # 検索語句(\はエスケープ処理)
( # キャプチャ対象グループ化開始
.*? # 任意の文字列0文字以上を非貪欲マッチ
) # キャプチャ対象グループ化終了
(?: # キャプチャ対象外グループ化開始
\| # 検索語句'|'
.* # 任意の文字列0文字以上
)? # キャプチャ対象外グループ化終了(0/1回出現対象)
\]\] # 検索語句(\はエスケープ処理)
$ # 文字列の末尾(なくても結果は変わらないが入れておく)
''', wiki_body, re.MULTILINE+re.VERBOSE))
?:...
でキャプチャ対象外
(?:...)
をつけると検索結果文字列に含めずキャプチャ対象外となります。
今回は[[Category:ヘルプ|はやみひよう]]
形式の|はやみひょう
部分は取得したくないのでキャプチャ対象外としています。
以下の例では、4
の部分を正規表現パターンとはしていますが、結果には出力していません。
>>> re.findall(r'(.012)(?:4)', 'A0123 B0124 C0123')
['B012']
貪欲・非貪欲マッチ
検索結果対象文字列の長さをコントロールすることができます。**最大限の長さでマッチさせるのが貪欲マッチ(greedy match)で最小限の長さでマッチさせるのが非貪欲マッチ(non-greedy match)**です。デフォルトは貪欲マッチです。
今回は[[Category:ヘルプ|はやみひよう]]
形式で|はやみひよう
の部分を0/1回出現にしているので、非貪欲マッチにしないと、0回として|はやみひよう]]
まで取得してしまいます。
# 貪欲マッチ
>>> print(re.findall(r'.0.*2', 'A0123 B0123'))
['A0123 B012']
# 非貪欲マッチ(*の後に?)
>>> print(re.findall(r'.0.*?2', 'A0123 B0123'))
['A012', 'B012']
出力結果(実行結果)
プログラム実行すると以下の結果が出力されます。
['イギリス', '英連邦王国', 'G8加盟国', '欧州連合加盟国', '海洋国家', '君主国', '島国', '1801年に設立された州・地域']