做NLP离不开数据处理,做数据处理怎么可以不会正则表达式呢?本文涵盖了正则表达式的几个核心概念和常用函数,适合对正则表达式有基本了解的同学阅读,作为一个笔记。
1.概念
字符组(character class) 字符组的内容是在同一个位置能够匹配的若干字符,它的意思是“或”,用[···]表示。
多选分支(alternative) 假如Bob和Robert是两个表达式,但Bob | Robert能够同时匹配其中任意一个的正则表达式。在这样的组合中,子表达式称为“多选分支”。
反向引用(back reference) 表达式在匹配时,表达式引擎会将小括号()包含的表达式所匹配到的字符串记录下来,叫做捕获。在获取匹配结果的时候,小括号包含的表达式所匹配到的字符串可以单独获取。 \1 引用第1对括号内匹配到的字符串,\2引用第2对括号内匹配到的字符串,以此类推。 如果一对括号内包含另一对括号,则外层的括号先排序号。换句话说,哪一对的左括号(在前,那这一对为先。
匹配 一个正则表达式“匹配”一个字符串,其实是指这个正则表达式在字符串中找到匹配文本。严格地说,正则表达式a不能匹配cat,但是能匹配cat中的a。
子表达式(group) 子表达式是指整个表达式中的一部分,通常指括号内的表达式,或者是由|分隔的多选分支。量词的作用对象是它们之前紧邻的子表达式。如果量词之前紧邻的是一个括号包围的子表达式,整个子表达式都被视作一个单元。
非捕获型括号 (···)用来分组和捕获,而(?:···)表示只分组不捕获。例如
if ($input =~ m/^([-+]?[0-9]+(?:\.[0-9]*)?)([CF])$/)
即使[CF]两端的括号的确是排在第三位,它匹配的文本也会保存到$2中,因为(?:···)不会影响捕获计数。
环视(lookaround) 环视结构不匹配任何字符,只匹配文本中的特定位置,这一点与单词分界符\b、锚点^和$相似。肯定型顺序环视(positive lookahead)用(?=···)表示,例如(?=\d)表示如果当前位置右边的字符是数字则匹配成功。肯定型逆序环视(?<=\d)表示如果当前位置的左边有一位数字则匹配成功(也就是说,紧跟在数字后面的位置)。环视不占用字符。
2.特殊字符
3. Python re
3.1 pattern对象
正则表达式编译成pattern对象,pattern对象执行匹配和替代等操作。
>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')
re.compile()还接收flag参数,比如忽略大小写。
>>> p = re.compile('ab*', re.IGNORECASE)
\在Python和正则表达式中都是转义符,这就决定了如果想要在正则表达式中使用\作为普通字符,得先在Python中“转义”一次,再在正则表达式中“转义”一次。比如,想要匹配LaTeX中的\section,需要一个匹\section的正则表达式,也就是\\section。但直接把\\section放入re.compile()是不行的,因为Python遇到\还要“转义”一次,使Python代码中\\section这个字符串的正则表达式意义是\section,而不是想要的\\section意义。所以re.complie()真正需要的参数是\\\\section。要避免这种麻烦的写法,可以在正则表达式前加一个r。比如r"\\section"的正则表达式意义就是\section。
3.2 pattern对象的方法
regex.search() 扫描整个字符串找到第一个匹配的位置。如果有匹配返回match对象,如果没有返回None。可选参数pos和endpos表示期望匹配的字符范围,左闭右开。rx.search(s,0,50)从0开始扫描,扫描长度为50。
>>> pattern = re.compile("d")
>>> pattern.search("dog") # Match at index 0
<_sre.SRE_Match object; span=(0, 1), match='d'>
regex.match() 从头开始匹配零个或多个字符。如果匹配返回match对象,否则匹配None。可选参数pos和endpos作用同search。
regex.findall() 作用同re.findall(pattern,s,flags=0)。返回字符串中所有不重复(不重复指的是位置不重复)的匹配,以string列表的形式。字符串从左到右扫描,返回的匹配顺序如被发现的顺序。 此外,也有pos和endpos参数。
regex.findite() 作用同re.finditer(pattern, string, flags=0)。返回一个迭代器,产生所有不重复的match对象。此外,也有pos和endpos参数。
regex.sub(repl,string,count=0) 同re.sub(pattern, repl, string, count=0, flags=0)。返回将匹配到的substring替换成repl后的字符串。如果没有匹配,字符串不变。
repl可以包含反向引用,包含反向引用的时候需要以r'···'包围。
result = re.sub('abc', '', input) # Delete pattern abc
result = re.sub('abc', 'def', input) # Replace pattern abc -> def
result = re.sub(r'\s+', ' ', input) # Eliminate duplicate whitespaces
result = re.sub('abc(def)ghi', r'\1', input) # Replace a string with a part of itself
result = re.sub("(\d+) (\w+)", r"\2 \1") #exchange digits and word
如果repl中有转义符,转义符保持python中的转义功能。
>>>result = re.sub(r'def\s+([a-zA-Z_][a-zA-Z_0-9]*)\s*\(\s*\):',
... r'static PyObject*\npy_\1(void)\n{',
... 'def myfunc():')
>>>print (result)
static PyObject*
py_myfunc(void)
{
#两个\n都保留了换行意义。
match对象
match对象的布尔值总是真。
match = re.search(pattern, string)
if match:
process(match)
match.group([group1, ...]) group对应括号括起来的子表达式。这个方法返回一个或多个子表达式的匹配。参数是子表达式的序号。如果参数只有一个,就返回一个字符串,这时候也可以写成match[gid]。如果多个,就返回一个元组,元组元素对应每个序号的子表达式匹配的文本。如果没有参数,或参数中包含0,那么就返回整个匹配的文本。如果一个子表达式有多个匹配,返回最后一个匹配。
>>> m = re.match(r"(\w+) (\w+)", "Isaac Newton, physicist")
>>> m.group(0) # The entire match
'Isaac Newton'
>>> m.group(1) # The first parenthesized subgroup.
'Isaac'
>>> m.group(2) # The second parenthesized subgroup.
'Newton'
>>> m.group(1, 2) # Multiple arguments give us a tuple.
('Isaac', 'Newton')
>>> m = re.match(r"(..)+", "a1b2c3") # Matches 3 times.
>>> m.group(1) # Returns only the last match.
'c3'
回到regex.sub()。repl除了是字符串,还可以是函数。如果repl是函数,它的输入是match对象,返回一个最终想替换的字符串。
>>> def dashrepl(matchobj):
... if matchobj.group(0) == '-': return ' '
... else: return '-'
>>> re.sub('-{1,2}', dashrepl, 'pro----gram-files')
'pro--gram files'
#-{1,2}匹配一个或两个-,尽量匹配两个。如果匹配的是两个-,就替换成一个-。如果匹配的是一个-,就替换成空格。
match.start() 返回所匹配文本的起始位置。
match.end() 返回所匹配文本的结束位置。
match.span() 返回(start, end)的元组。
>>>import re
>>>p = re.compile('[a-z]+')
>>>m = p.match('tempo')
>>>>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)
3.3 编译标志位
re.compile()可以传入flags参数,一定程度上改变正则表达式的工作方式。flag可以用全称也可以用简称。多个flags之间用|并列。
flags = 0
if multiline:
flags = re.M
if dotall:
flags |= re.S
if verbose:
flags |= re.X
if ignorecase:
flags |= re.I
if uni_code:
flags |= re.U
regex = re.compile(r'Test Pattern', flags)
3.4 贪婪和非贪婪
a*会匹配尽可能多的字符,这是贪婪的。
>>> s = '
Title'>>> print(re.match('<.*>', s).group())
Title想要非贪婪匹配,在量词后面加?匹配尽可能少的字符:*?,??,{m,n}?。
>>> print(re.match('<.*?>', s).group())
3.5 使用re.VERBOSE
re.VERBOSE标志位会增加正则表达式的可读性。使用re.VERBOSE时,不出现在字符组中的空格会被忽略,还可以使用#注释。
pat = re.compile(r"""\s* # Skip leading whitespace(?P[^:]+) # Header name\s* : # Whitespace, and a colon(?P.*?) # The header's value -- *? used to# lose the following trailing whitespace\s*$ # Trailing whitespace to end-of-line""", re.VERBOSE)
#对比
pat = re.compile(r"\s*(?P[^:]+)\s*:(?P.*?)\s*$")
参考资料
《精通正则表达式》