偶而會需要找內含字串 A 但不含字串 B 的字串, 若剛好得用 regexp 表示的話, 會有點麻煩 (像是用 Android logcat filter 的時候)。查了一下, 發現 regular expression 有個強大的語法叫作 lookaround, 用它可相對容易地達到此需求, 還可以應付各種情況。詳見 Regex Lookarounds: Lookahead and Lookbehind 的介紹。關鍵在於 lookaround 的語法只是從「目前的位置」往前或往後「看看是否符合目標 pattern」,不會實際占去符合的字串。看文件的例子會比較好理解。
以下是用 Python RE 比對「內含 abc 但 abc 之後不含 def 的字串」:
In [70]: re.search('(?!.*def)abc', 'abcdef')
(?!.*def)abc 的意思是每一個位置都做以下的事:
- 先往後找看看有沒有符合 .*def, 找不到才算成立。(?!...) 的意思是 negative lookahead
- negative lookahead 成立後, 看看目前位置是否能找到 abc
反之, 下面這個寫法是錯的, 比對 abcdef 仍會有結果:
In [69]: re.search('(?!def)abc', 'abcdef') Out[69]: <_sre.SRE_Match at 0x2f2fd98>
因為它的意思是在目前位置看看是否不符合 def, 於是一開始比對 abc 時就成立了, 然後再比對成功 abc, 於是回傳比對成功的結果。
(?!.*def)abc 看起來很美好, 但它無法避開 defabc, 也就是 abc 之前有 def 的情況。雖然有 lookbehind 的語法, 但它只能比對固定長度的 pattern, 無法應付 xxxdefxxxabc, 所以得換個方式表示。
regex - Regular expression to match string not containing a word? 說明如何用 regexp 表示不含目標字串的字串, 並有圖解說明運作的過程。了解之後, 可運用同樣的技巧表示「內含 abc 但 abc 之前不含 def 的字串」:
In [145]: re.search('^((?!def).)*abc', 'defabc')
- (?!def) 檢查目前位置是否不含 def
- (?!def). 注意多加了一個 '.', 表示檢查完後占去一個字元
- ((?!def).)* 比對 0 到多個符合 (?!def). 的字元
利用 regexp greedy 的特性, pattern 3 會盡可能占去符合這個的字元, 於是 ^((?!def.)*abc 就會比對出「內含 abc 但 abc 之前不含 def 的字串」。
再和一開始用的 lookahead 合在一起, 就能表示「內含 abc 但不含 def 的字串了」:
In [147]: re.search('^((?!def).)*(?!.*def)abc', 'abcdef') In [148]: re.search('^((?!def).)*(?!.*def)abc', 'defabc') In [149]: re.search('^((?!def).)*(?!.*def)abc', 'abc') Out[149]: <_sre.SRE_Match at 0x2f2de40> In [150]: re.search('^((?!def).)*(?!.*def)abc', 'xxxabcxdefxx') In [151]: re.search('^((?!def).)*(?!.*def)abc', 'xdefxxabcxxx') In [152]:就算一時之間無法消化也無所謂, 記得關鍵字 lookaround, lookahead, lookbehind, 之後比較方便找 regexp 進階用法。每次找 regexp 的說明, 都會學到新東西, 真是博大精深的表示法。
2014/07/07 更新
經 weiyu 提醒, 移動 abc 到中間效率會比較好, 以下是程式和測試結果:
$ cat a.py import re import time begin = time.time() pattern = re.compile('^((?!def).)*(?!.*def)abc') for i in xrange(1000000): pattern.search('xxxxxxabcxxxxxx') pattern.search('xxxdefxxxabcxxxxxx') pattern.search('xxxxxxabcxxxdefxxx') print time.time() - begin begin = time.time() pattern = re.compile('^((?!def).)*abc(?!.*def)') for i in xrange(1000000): pattern.search('xxxxxxabcxxxxxx') pattern.search('xxxdefxxxabcxxxxxx') pattern.search('xxxxxxabcxxxdefxxx') print time.time() - begin $ python a.py 4.64261507988 3.08146381378
沒有留言:
張貼留言