jieba源碼研讀筆記(四) - 正則表達式
前言
jieba包含的三大功能:分詞、詞性標注及關鍵詞提取都需要用到正則表達式。
筆者將jieba/__init__.py
,jieba/finalseg/__init__.py
及jieba/posseg/__init__.py
三個檔案裡出現的正則表達式集中在本篇介紹。
先介紹下筆者在學習正則表達式時用到的工具:
regex101:可以使用這個線上工具來測試regex的效果。
以及Python正則表達式的教學:
Python - Regular Expressions
還有一篇中文的:
正则表达式的功法大全,做NLP再也不怕搞不定字符串了
jieba/init.py
這裡定義了數個正則表達式,它們會在分詞及載入字典時發揮作用。
re_userdict
re_userdict
是在解析dict.txt
的內容時使用的。
以下是dict.txt
的部份內容:
AT&T 3 nz
B超 3 n
c# 3 nz
C# 3 nz
c++ 3 nz
C++ 3 nz
…
re_userdict = re.compile('^(.+?)( [0-9]+)?( [a-z]+)?$', re.U)
先來看一下這段敍述裡用到的正則表達式語句:
-
錨點
^
和$
:^
表示匹配字串的開頭,$
表示匹配字串的結尾 -
分組和捕獲
()
:()
會創造一個捕獲性分組,之後可以用re_userdict.match(line).groups()
來得到己匹配的分組 -
lazy匹配,
.+?
:.+
會貪心地(盡可能多地)匹配,加上?
之後變成lazy匹配。如果實際去測試移除?
之後的效果,會發現字串全部被分到第一個組別。 -
或運算符
[]
:如[0-9]
匹配0
到9
中的其中一個字 -
數量符
?
:匹配零次或一次?
前面的東西 -
re.compile(pattern, flags=0)
:
來自re.compile文檔:Compile a regular expression pattern into a regular expression object,
which can be used for matching using its match(), search() and other
methods, described below. The expression’s behaviour can be modified
by specifying a flags value.即
re.compile
會將pattern
編譯成正則表達式物件。我們之後就可以用它來match
或search
其它串。 -
re.U:
根據Python - Regular Expressions:Interprets letters according to the Unicode character set. This flag affects the behavior of \w, \W, \b, \B.
Make the \w, \W, \b, \B, \d, \D, \s and \S sequences dependent on the Unicode character properties database. Also enables non-ASCII matching for IGNORECASE.
re.U會根據Unicode字集來解釋字元。
注:Unicode與UTF-8的差異可以參考What’s the difference between Unicode and UTF-8? [duplicate]
回頭看一下這段敍述:
re_userdict = re.compile('^(.+?)( [0-9]+)?( [a-z]+)?$', re.U)
我們現在可以了解到,它會匹配一個字串,並將它分成三組。
第一組是配對一至多個任意字元,直到空白出現為止。
第二組是配對空白加上一至多個數字。
第三組是配對空白加上一至多個英文字母。
測試一下re_userdict
的效果:
re_userdict = re.compile('^(.+?)( [0-9]+)?( [a-z]+)?$', re.U)
re_userdict.match('AT&T 3 nz').groups()
Out[7]: ('AT&T', ' 3', ' nz')
順便測試一下如果第一組使用貪婪匹配會怎麼樣?
re_userdict_greedy = re.compile('^(.+)( [0-9]+)?( [a-z]+)?$', re.U)
re_userdict_greedy.match('AT&T 3 nz').groups()
Out[4]: ('AT&T 3 nz', None, None)
可以發現,字串全部被分到第一個組別,因為.+
的涵義本來就是匹配任意字元。
re_eng
# re.U: Unicode matching
re_eng = re.compile('[a-zA-Z0-9]', re.U)
re_eng對應到單個英文或數字。
re_han
re_han_default
及re_han_cut_all
會分別在不同分詞模式下被調用。
# \u4E00-\u9FD5a-zA-Z0-9+#&\._ : All non-space characters. Will be handled with re_han
# re_han_default = re.compile("([\u4E00-\u9FD5a-zA-Z0-9+#&\._%]+)", re.U)
# Adding "-" symbol in re_han_default
re_han_default = re.compile("([\u4E00-\u9FD5a-zA-Z0-9+#&\._%\-]+)", re.U)
re_han_cut_all = re.compile("([\u4E00-\u9FD5]+)", re.U)
先來看一下這段敍述裡用到的正則表達式語句:
\u4E00
表示的是一
這個字,\u9FD5
表示的是鿕
,[\u4E00-\u9FD5]
表示所有漢字- 在
[]
外,.
要用\
來跳脫:
在dot = re.compile('.+') #這裡的.是任意字元的意思 dot.match('abc') Out[60]: <_sre.SRE_Match object; span=(0, 3), match='abc'> dot = re.compile('\.+') #這裡的\.代表小數點本身 dot.match('abc') #輸出為None dot.match('...') Out[82]: <_sre.SRE_Match object; span=(0, 3), match='...'>
[]
內,.
失去它原有的特殊涵義:dot = re.compile('[.]+') #match小數點本身 dot.match('.') Out[71]: <_sre.SRE_Match object; span=(0, 1), match='.'> dot.match('abc') #輸出None #現在[.]只能match到小數點本身
- 在
[]
外,-
不需使用\
來跳脫:
在#在[]外,'-'代表減號本身 dash = re.compile('-+') dash.match('--') Out[66]: <_sre.SRE_Match object; span=(0, 2), match='--'>
[]
內,-
有特殊涵義,所以要用\
來跳脫:dot = re.compile('[a\-z]+', re.U) dot.match('a-z') Out[76]: <_sre.SRE_Match object; span=(0, 3), match='a-z'> dot = re.compile('[a-z]+', re.U) dot.match('a-z') Out[80]: <_sre.SRE_Match object; span=(0, 1), match='a'> #這時的-有特殊涵義,所以無法匹配減號,輸出為None
- 參考以下實驗:在
[]
內,所有特殊字元都會失去他們應有的意義。
具體可以參考re.compile文檔。mis = re.compile('[+#&._%-]+') mis.match('-#+.&-%_abc') #因為以上字元在[]中己無特殊涵義,所以可以被配對 Out[101]: <_sre.SRE_Match object; span=(0, 8), match='-#+.&-%_'> mis.match('abc') #輸出None,可以說明此處的.是與小數點而非任意字元配對
回頭看看這兩句正則表達式:
re_han_default = re.compile("([\u4E00-\u9FD5a-zA-Z0-9+#&\._%\-]+)", re.U)
re_han_cut_all = re.compile("([\u4E00-\u9FD5]+)", re.U)
re_han_cut_all
的作用是與一個或多個漢字配對。
re_han_default
的作用是與一個或多個漢字,英數字,+#&._%-等字元配對。
re_skip
# \r\n|\s : whitespace characters. Will not be handled.
re_skip_default = re.compile("(\r\n|\s)", re.U)
re_skip_cut_all = re.compile("[^a-zA-Z0-9+#\n]", re.U)
測試:
re_skip_default.split(" a bcd\r\nefg")
#用空白及換行符來分割字串
Out[108]: ['', ' ', '', ' ', 'a', ' ', '', ' ', 'bcd', '\r\n', 'efg']
re_skip_cut_all.split("aefawef wefawef 3242rf #$()#U) ")
#將能配對的及無法配對的分開
Out[110]: ['aefawef', 'wefawef', '3242rf', '#', '', '', '#U', '', '']
jieba/finalseg/init.py
re_han
#一個或多個漢字
re_han = re.compile("([\u4E00-\u9FD5]+)")
測試re_han
:
>>> s = "自然語言處理"
>>> re_han.match(s)
<_sre.SRE_Match object; span=(0, 6), match='自然語言處理'>
>>> re_han.match(s).group()
'自然語言處理'
>>> re_han.match(s).group(1)
'自然語言處理'
>>> re_han.match(s).group(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: no such group
注:要抓取re配對到的group,索引應該從group(1)
開始,參考Why regular expression’s “non-capturing” group is not working
re_skip
#[a-zA-Z0-9]+ : 一個或多個英數字
#\.\d+ : ".加上一個或多個數字"
#?: : 表示該group會被配對,但無法被抓取
#(?:\.\d+)? : 配對該group零次或一次
#%? : 配對%零次或一次
re_skip = re.compile("([a-zA-Z0-9]+(?:\.\d+)?%?)")
- (?: )
()
會創造一個捕獲性分組,而在組內加上?:
則會讓該分組無法捕獲。具體可以參考What is a non-capturing group? What does (?: ) do?
測試re_skip
:
>>> re_skip.match("asw42d.34243%").group(1)
'asw42d.34243%'
>>> re_skip.match("asw42d.34243%").group(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: no such group
為了測試?:
的效果,把它拿掉看看:
re_skip2 = re.compile("([a-zA-Z0-9]+(\.\d+)?%?)")
>>> re_skip2.match("asw42d.34243%").group(1)
'asw42d.34243%'
>>> re_skip2.match("asw42d.34243%").group(2)
'.34243'
>>> re_skip2.match("asw42d.34243%").group(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: no such group
如果在\.\d+
前沒加上?:
,那麼我們在用.group
時就會看到(\.\d+)
這個組中組,而這並不是我們所希望見到的結果。
jieba/posseg/init.py
#一個或多個漢字
re_han_detail = re.compile("([\u4E00-\u9FD5]+)")
#一個或多個 (.跟0-9)或英數字
re_skip_detail = re.compile("([\.0-9]+|[a-zA-Z0-9]+)")
#一個或多個漢字或英數字或+#&._
re_han_internal = re.compile("([\u4E00-\u9FD5a-zA-Z0-9+#&\._]+)")
#\s等於[ \t\n\r\f\v]:https://docs.python.org/3/library/re.html#re.compile
re_skip_internal = re.compile("(\r\n|\s)")
#一個或多個英數字
re_eng = re.compile("[a-zA-Z0-9]+")
#一個或多個小數點或數字
re_num = re.compile("[\.0-9]+")
#長度為1的英數字
re_eng1 = re.compile('^[a-zA-Z0-9]$', re.U)
參考連結
regex101
Python - Regular Expressions
正则表达式的功法大全,做NLP再也不怕搞不定字符串了
re.compile文檔
Python2 - re.U
What’s the difference between Unicode and UTF-8? [duplicate]
Why regular expression’s “non-capturing” group is not working
What is a non-capturing group? What does (?: ) do?