skip to Main Content
你的Python高效学习之道 025-5987-6503 contact@lessoncode.com

Python隐藏的正则表达式

Python标准库中有许多糟糕的模块,但Python的re模块并不是其中之一。 虽然它很旧,但还没有更新多年,它是所有动态语言中最好的之一。

我一直觉得有趣的是,Python是少数没有语言集成正则表达式支持的动态语言之一。 然而,尽管它缺乏语法和解释器支持,但它从纯API的角度来看,它使用更好的核心系统之一来弥补。 同时这是非常古怪的。 例如,解析器是用纯Python编写的,如果您在导入时尝试跟踪Python,则会产生一些奇怪的后果。 你会发现,90%的时间可能用在了re的支持模块上。

旧但久经考验

Python中的正则表达式模块现在已经很旧了,而且是标准库中的一个常量。忽略Python 3它自成立以来还没有真正发展,除了在一点上获得基本的unicode支持。直到这个日期它有一个破坏的成员枚举(看看什么dir()返回正则表达式模式对象)。

然而,它的一个很好的事情是旧的,它不会改变Python版本,是非常可靠的。由于正则表达式模块已更改,因此我不需要调整某些内容。鉴于我在Python中编写的正则表达式有多少是个好消息。

关于其设计的一个有趣的奇怪之处在于,其解析器和编译器是用Python编写的,但是匹配器是用C编写的。这意味着我们可以将解析器的内部结构传递到编译器中,以便完全绕过正则表达式解析,如果我们感觉到喜欢它。不是说这是文件。但它仍然有效。

然而,还有很多其他的事情没有或正式表达式系统的记录,所以我想举一些为什么Python中的Regex模块非常酷的例子。

迭代匹配

Python中正则表达式系统的最大特点是毫无疑问地在匹配和搜索之间进行了明确的区分。 没有其他正则表达式引擎做的事情。 特别是当您执行匹配时,您可以提供一个索引来抵消匹配,但匹配本身将被锚定到该位置。

特别是这意味着你可以这样做:

>>> pattern = re.compile(‘bar’)
>>> string = ‘foobar’
>>> pattern.match(string) is None
True
>>> pattern.match(string, 3)

这对于构建词法分析器非常有用,因为您可以继续使用特殊的^符号来表示整个字符串的一行的开始。 我们只需要增加索引以进一步匹配。 这也意味着我们不必自己分割字符串,从而节省了大量的内存分配和字符串复制(而不是Python在这方面特别好)。

除了匹配的Python可以搜索哪一种意思,它会跳过,直到找到一个匹配:

>>> pattern = re.compile(‘bar’)
>>> pattern.search(‘foobar’)

>>> _.start()
3

不匹配也匹配

一个特别的常见问题是,Python中缺少匹配是很昂贵的。 想像为wiki这样的wiki编写一个标记器,比如说语言(例如,markdown)。 在表示格式化的令牌之间,还有很多文本也需要处理。 所以当我们关心所有令牌之间的一些wiki语法时,我们有更多的令牌需要处理。 那么我们怎么跳过去呢?

一种方法是将一堆正则表达式编译成列表,然后逐个尝试。 如果没有比赛,我们跳过一个角色:

rules = [
(‘bold’, re.compile(r’\*\*’)),
(‘link’, re.compile(r’\[\[(.*?)\]\]’)),
]

def tokenize(string):
pos = 0
last_end = 0
while 1:
if pos >= len(string):
break
for tok, rule in rules:
match = rule.match(string, pos)
if match is not None:
start, end = match.span()
if start > last_end:
yield ‘text’, string[last_end:start] yield tok, match.group()
last_end = pos = match.end()
break
else:
pos += 1
if last_end < len(string):
yield ‘text’, string[last_end:]

这不是一个特别美丽的解决方案,也不是很快。我们拥有的错配越多,我们得到的速度越慢,因为我们只提前一个角色,而这个循环在解释Py​​thon中。在处理这个问题的时候,我们也是非常僵硬的。对于每个令牌,我们只得到匹配的文本,所以如果涉及到组,我们将不得不扩展这个代码。

那么有更好的方法呢?如果我们可以向正则表达式引擎指出我们希望它扫描几个正则表达式?

这是有趣的地方。从本质上讲,当我们使用子模式编写正则表达式时,我们会做些什么:(a | b)。这将搜索a或b。所以我们可以从我们所有的表达式中构建一个很好的正则表达式,然后匹配。这样做的缺点是我们最终会与所有涉及的团体混淆。
输入扫描器

这是事情变得有趣的地方。在过去15年左右的时间里,正则表达式引擎中有一个完全没有文档的功能:扫描仪。扫描仪是底层SRE模式对象的属性,引擎在找到匹配后的下一个对象时保持匹配。甚至存在一个re.Scanner类(也是未记录的),它建立在SRE模式扫描器之上,这使得它具有稍高的级别接口。

在re模块中存在的扫描器并不是非常有用,因为使“不匹配”部分更快,但是查看其源代码可以显示它的实现方式:在SRE原语之上。

它的工作原理是它接受正则表达式和回调元组的列表。对于每个匹配,它使用匹配对象调用回调,然后从其中构建结果列表。当我们看看它是如何实现的,它会在内部手动创建SRE模式和子模式对象。 (基本上它构建一个更大的正则表达式,而不需要解析它)。有了这个知识,我们可以扩展这个:

from sre_parse import Pattern, SubPattern, parse
from sre_compile import compile as sre_compile
from sre_constants import BRANCH, SUBPATTERN

class Scanner(object):

def __init__(self, rules, flags=0):
pattern = Pattern()
pattern.flags = flags
pattern.groups = len(rules) + 1

self.rules = [name for name, _ in rules] self._scanner = sre_compile(SubPattern(pattern, [
(BRANCH, (None, [SubPattern(pattern, [
(SUBPATTERN, (group, parse(regex, flags, pattern))),
]) for group, (_, regex) in enumerate(rules, 1)]))
])).scanner

def scan(self, string, skip=False):
sc = self._scanner(string)

match = None
for match in iter(sc.search if skip else sc.match, None):
yield self.rules[match.lastindex – 1], match

if not skip and not match or match.end() < len(string):
raise EOFError(match.end())

那么我们该怎么用呢?

scanner = Scanner([
(‘whitespace’, r’\s+’),
(‘plus’, r’\+’),
(‘minus’, r’\-‘),
(‘mult’, r’\*’),
(‘div’, r’/’),
(‘num’, r’\d+’),
(‘paren_open’, r’\(‘),
(‘paren_close’, r’\)’),
])

for token, match in scanner.scan(‘(1 + 2) * 3’):
print (token, match.group())

在这种形式下,它会引发一个EOFError,以防万一它不能使用某种东西,但是如果你通过skip = True,那么它会跳过不可修改的部分,这是完美的,用于构建诸如wiki语法词法的东西。
用漏洞扫描

当我们跳过,我们可以使用match.start()和match.end()来找出我们跳过的部分。 所以这里第一个例子调整到:

scanner = Scanner([
(‘bold’, r’\*\*’),
(‘link’, r’\[\[(.*?)\]\]’),
])

def tokenize(string):
pos = 0
for rule, match in self.scan(string, skip=True):
hole = string[pos:match.start()] if hole:
yield ‘text’, hole
yield rule, match.group()
pos = match.end()
hole = string[pos:] if hole:
yield ‘text’, hole

修复组

一个令人讨厌的事情是,我们的组索引不是我们自己的正则表达式的本地化,而是组合索引。 这意味着如果您有一个规则(a | b),并且要通过索引访问该组,则它将是错误的。 这将需要一个额外的工程,一个类包装SRE匹配对象与一个自定义的,调整索引和组名称。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

Back To Top