検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
img_for_tre_tron

Tré Thộn を食べたことがありますか?
ベトナム・ビンズオン滞在中の方は是非注文して食べてみて!
絶対に美味しいです!
ホーチミン市内へも配達可能です。お問い合わせください。

Have you ever had "Tré Thộn" before?
If you're living at Bình Dương in Vietnam, you "must" try to order and eat it.
I'm sure you're very surprised how delicious it is!!
If you're in Hồ Chí Minh, you have a chance to get it too. Please call!!
>>

【 Python + Regular Expressions 】正規表現の 先読み ( look ahead; ?=expression ) 機能を使いこなそう! 投稿一覧へ戻る

Published 2020年9月10日21:37 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

例えばファイルから読み込んだテキストに含まれるセンテンスの数を数える場合、'.'、'?'、'!' いずれかのリテラル文字で終わる場所を 1 つのセンテンスの最後と判断し、non-greedy にマッチさせるパターンを作成、findall() から返ってきたマッチリストの長さを取得します。


マッチオブジェクトを作成する際に findall() に指定している DOTALL フラグは、'.' 特殊文字において「改行文字 ( \n )」も含めるように指示するためです。


これによってセンテンスが行を跨いでいても対応可能になります。


import re


string = """Here is a single sentence. Here is
another sentence, ending in a period.
And here is yet another. Is there any
questions? Good, go ahead!"""



pat = r'.*?[.?!]'


m_lst = re.findall(pat, string, flags=re.DOTALL)


print(len(m_lst))

# 5



これは期待通りに 5 を返してくれますが、もしテキストに「略字」や「小数点を含む数字」が含まれていた場合は期待通りには動いてくれません。


string = """I want to go U.S.A. It has
about 310.5 million people. 1, 2, 3, Go!"""



pat = r'.*?[.?!]'


m_lst = re.findall(pat, string, flags=re.DOTALL)


print(len(m_lst))

# 6  本当は 3 つのセンテンスしか含まれていないのにーー



regular-expression システムは、現在のポジションの直後にある文字集団がパターンとマッチするか、を判断する 先読み ( look-ahead ) 機能 を備えています。


先読み ( look ahead ) 構文の書式:

(?=パターン)



この look ahead パターン自体にマッチした文字集団は「消費」されません


すなわち、「先読み」されたパターン部分は、続くマッチ検索において再び「検索文字列の一部」として検索対象になる、ということです。


この機能を利用することで、上の例でうまくいかなかったセンテンス数の数え上げを期待通りに実行することが可能になります。


1: まず 1 つのセンテンスの先頭として「大文字 or 数字」を探します ( 数字で始まっているかもしれませんね。それにも対応しましょう )。


2: 続いて、non-greedy マッチとして [.!?] のいずれかのリテラル文字で終わるポジションまで読み進めます。


3: このとき、以下のいずれかの条件を満たしていればセンテンスの終端とみなします。

a) センテンス終了文字の直後はスペースであり、その直後に大文字 or 数字が続く

b) センテンス終了文字の直後は行末か文字列の終わりである


4: 3 における a も b も満たしていないセンテンス終端文字 ( 特にピリオド [.]) は、略字の一部、もしくは、小数点とみなします。



これら 1 から 4 の条件をみたすように、先読み構文を利用してパターンを構築しましょう。


string = """Here is a single sentence. Here is
another sentence, ending in a period.
And here is yet another. Is there any
questions? Good, go ahead! I want to
go U.S.A. It has about 310.5 million people. 1, 2, 3, Go!"""



pat = r'[A-Z0-9].*?[.?!](?=\s[A-Z0-9]|$)'


m_lst = re.findall(pat, string, flags=re.DOTALL | re.MULTILINE)


print(len(m_lst))

# 8



この例の string は 1 つ目と 2 つ目の string を合体させたものですからセンテンスは 8 つです。正解です!


また、ここでは findall() に渡すフラグとして re.MULTILINE を追加しています。


これによって、'$' 特殊文字が、「文字列の末尾」だけではなく「行末の改行文字 ( \n )」にもマッチする ようになります。


そのため、センテンスがぴったり行末で終わっていても、この look ahead (先読み) パターンがマッチするんです。


さて、パターン文字列が少し複雑ですね。1 つ 1 つ見ていきましょう。


1) [A-Z0-9]: サブ文字列 (センテンス) の先頭は 'A' から 'Z' までの大文字か '0' から '9' までの数字のどれか 1 文字でなければなりません。


2) .*?: 0 個以上の任意の文字にマッチします。re.DOTALL フラグを指定していますから、この '.' 特殊文字は改行文字を含む全ての文字にマッチします。'?'、'*'、'+' といった繰り返し修飾子 ( quantifier ) 直後に ? 特殊文字を続けることで non-greedy マッチを要求しています。つまり、一番短いマッチで終了する、ということです。


3) [.?!]: サブ文字列 (センテンス) の終端文字を指定しています。上のパターンで non-greedy マッチを選択していますから、regular-expression システムはこれらの文字のいずれかを見つけた段階で停止します。


4) (?=\s[A-Z0-9]|$): これが look ahead のパートです。たとえここまでのパターンがマッチしていたとしても、そのマッチの後にこのパターンが来ていなければダメですよ、ということです。すなわち、1 つのスペースがありその後ろにアルファベットの大文字か数字が続くか、もしくは、行末、または、文字列の最後であること、というのがサブ文字列がセンテンスである条件です。




くどいですが、1) から 3) がマッチしていたとしても、4) の look ahead 条件が満たされなければ regular-expression システムはマッチを返さず、4) の look ahead の部分から再度マッチを探し始めます。


look ahead パターンはマッチには含まれませんし、消費もされませんからね。


ここで、look ahead シンタックスを採用したパターンを利用してマッチしたサブ文字列を確認してみましょう。


string = """I want to go U.S.A. It has
about 310.5 million people. 1, 2, 3, Go!"""



pat = r'[A-Z0-9].*?[.?!](?=\s[A-Z0-9]|$)'


m_lst = re.findall(pat, string, flags=re.DOTALL | re.MULTILINE)


for i in m_lst:
print(f"-> {i}")

# -> I want to go U.S.A.
# -> It has
# about 310.5 million people.
# -> 1, 2, 3, Go!



マッチしたサブ文字列には改行文字 ( \n ) がそのまま含まれている場合もありますから 2 行にわたって出力されているものもありますが、結果は期待通り 3 つのセンテンスが取得できていますね。


改行が気になる方は、サブ文字列に含まれる改行文字をスペースに置き換えるような正規表現を適用するなりしてみてください!
この記事に興味のある方は次の記事にも関心を持っているようです...
- People who read this article may also be interested in following articles ... -
【Python 雑談・雑学 + coding challenge】文字列中の数字を抜き出して桁区切りをつけよう! 正規表現 (regular expression ) を使うと「えっ!?」っていうくらい簡単ですょ。lookahead と negative lookahead を使います。
【 Python + Regular Expressions 】複数のマッチパターンを look ahead (先読み) シンタックスを利用して 1 つのマッチパターンにまとめて処理しよう! re.match() の呼び出しも 1 回で済んじゃいます!
【 Effective Python, 2nd Edition 】keyword-only arguments (キーワード指定のみ引数) と positional-only arguments (位置指定のみ引数) を使いこなして、コードの読解性と将来的な拡張性を高めよう!
Python coding challenge [解説編]: Basics of Graph Theory Part.3 - Representation and Manipulation of Trees (グラフ理論の基礎総復習 その3 - ツリーの表現と操作) 🔒
Python coding challenge [解説編]: Basics of Graph Theory Part.2 - Representation and Manipulation of Graphs (グラフ理論の基礎総復習 その2 - グラフの表現と操作)
【Python 雑談・雑学 + coding challenge】Unicode の正規化処理 ( normalization ) を利用して、diacritical marks ( 発音区別符号 ) を取り除こう! テキスト解析の前処理としても重要です!
Python で学ぶ architecture patterns - DDD (domain driven design) - Repository pattern の巻