第一次接触正则表达式是刚毕业那会儿。当时我在写一个DICOM图片浏览器。
DICOM图像中的各种信息(比如:图像宽度、高度、编码类型、像素数据、成像时间等)分散存储在不同的item中。通常,一个item由一个预定义的tag、数据类型、数据长度、数据域这几部分组成。一些item还可以嵌套包含子item。DICOM图像一个有趣的地方就是,并不是每个图像文件都包含的item集合都是相同的(DICOM标准中指定了一些DICOM图像中必须要包含的item,而另一些则是可选的)。而且每个item的中包含的数据长度也不确定。这就导致了一个问题,即某个特定的item在不同的图像文件中的起始偏移量不是确定的。当我们想要从DICOM图片中获取一项信息(比如:图像的尺寸)时,我们必须要从图像头部遍历每个item,直到找到表示图像尺寸的item为止。
嗯。有点像单链表,而且链表中的节点还可能是一棵树。
为了快速定位某个item的位置,我在解析图像时,将每个item的起始位置都记录下来,并存储到一个csv文件中(那时,我还没接触过XML)。这个csv文件中每行记录的信息看起来是这样的:
item Offset, item Tag, item Data Type, item Data Length
而在读取这个csv文件时,我想使用C++标准库中的正则表达式库<regex>,将以逗号分隔开的各列信息捕获到不同的group中。于是,我就在网上找到了《正则表达式30分钟入门》。这份文档对我的帮助很大,衷心感谢该文的作者。
不过,这份文档中主要讲的是.NET中正则表达式引擎能够处理的正则表达式格式。后来,我又用Python中的正则模块做过一些文本处理,发现Python中的正则和.NET中正则在语法上有些许差异。
实际上,我没接触过C#。我只是根据自己从Python文档中记录的re模块的信息和《正则表达式30分钟入门》中透露的知识来对比一下,并做一个简单总结。由于我不懂.NET,所以本文更侧重于Python。本文也不是一个完整的对比总结。而且,也避免不了错误(特别是针对.NET部分)。欢迎各位指正。
第一,Python中和.NET中named group写法的不同:
在《正则表达式30分钟入门》中提到一个named group写法如下:
(?<group_name>)
在Python中,应该这样写:
(?P<group_name>)
第二,Python的re模块不支持对捕获到的group的压栈和出栈操作。Python 3.3 re模块文档中未提到这种用法,我的尝试也告诉我Python目前不支持这种用法。
第三,对于分支条件中命名组的处理,两者行为也不一样。比如我要匹配一个C语言中的变量名或者一个整数。
在.NET中,(使用《正则表达式30分钟入门》中介绍的正则表达式工具测试通过):
(?<VAR_OR_NUM>[A-Za-z_][A-Za-z0-9_]*)|(?<VAR_OR_NUM>[0-9]+)
而在Python中,等价的写法:
(?P<VAR_OR_NUM>[A-Za-z_][A-Za-z0-9_]*)|(?P<VAR_OR_NUM>[0-9]+)
会有如下错误提示:sre_constants.error: redefinition of group name 'VAR_OR_NUM' as group 2; was group 1
实际上,找到可替代的写法很容易:
(?P<VAR_OR_NUM>(?:[A-Za-z_][A-Za-z0-9_]*)|(?:[0-9]+))
这里只是为了指出Python中不接受这种语法才那样写的。然后,说一下Python中编译正则时DEBUG标志的用处。
在Python中compile一个存在语法错误的正则表达式时,我们能够得到详细的错误提示。比如,上面的“redefinition of group name”。但是一个能够compile的正则表达式不代表它就是按照我们的意图工作的。Python中re模块的DEBUG flag可以帮助我们快速检查自己所写的正则中的错误。
看下面的例子,我想要写一个匹配由一对引号(双引号或单引号)引起来的字符串:
>>> import re
>>> s = """(["']).*\1"""
>>> p = re.compile(s)
>>> p.search(r'"abc"') == None
True
嗯,怎么回事?没有提示语法错误啊?但却匹配不到。让我们使用DEBUG flag看一下Python是怎么解析这个正则表达式的:
>>> p = re.compile(s, re.DEBUG)
subpattern 1
in
literal 34
literal 39
max_repeat 0 65535
any None
literal 1
注意最后一行,\1我明明想表示之前匹配到的引号,而这里竟被解析成了字符1(literal 1)。再看一下正则,哦,原来如此!由于我忘记在定义s的时候在字符串前加r,所以Python把\1当作转义处理了。如果不通过使用re.DEBUG看解析过程,要发现这个错误有时还真难。
>>> s = r"""(["']).*\1"""
>>> p = re.compile(s, re.DEBUG)
subpattern 1
in
literal 34
literal 39
max_repeat 0 65535
any None
groupref 1
>>> p.search('"abc"') == None
False
这下,成功匹配。当然,下面这种写法也可以:
s = """(["']).*\\1"""
第四,Python中的正则不支持shell BRE中的character class。
在学习Shell中grep工具的用法的时候,书中讲到了character class的用法,比如:
[[:alpha:]]表示字母字母的集合(等价于[A-Za-z]),[[:dight:]]表示数字的集合(等价于[0-9])。还有诸如[:alnum:]、[:blank:]、[:cntrl:]、[:graph:]、[:lower:]、[:print:]、[:punct:]、[:space:]、[:upper:]、[:xdigit:]等。当然,这些character class只有位于[]中时才有效,比如:
[[:dight:]]。
而单独的[:dight:]是不起作用的。书中还提到一个例子:
([[:alpha:]_][[:alnum:]_]*) = \1 匹配简易C/C++赋值语句(实际上,以我对后向引用的了解,这个好像只能匹配诸如a = a、b = b,而不能匹配a = b,不过尚待验证,因为还不明grep工作原理)。
在Python中,该正则会怎么被解析呢?
>>> import re
>>> re.compile(r'([[:alpha:]][[:alnum:]_]*) = \1', re.DEBUG)
subpattern 1
in
literal 91
literal 58
literal 97
literal 108
literal 112
literal 104
literal 97
literal 58
literal 93
in
literal 91
literal 58
literal 97
literal 108
literal 110
literal 117
literal 109
literal 58
literal 95
max_repeat 0 65535
literal 93
literal 32
literal 61
literal 32
groupref 1
<_sre.SRE_Pattern object at 0x2180c20>
可以看到,Python中的正则表达式引擎无法识别这种character class,而是按照字符字面量来解析的。不过,使用DEBUG flag的时候也有不太好的地方。我们可以看到每个literal后面打印的都是字符的ASCII码,而非字符本身。
接下来我介绍下自己写的一个小脚本。该脚本会把re模块compile正则的过程输出,不过它会把literal及range后的ASCII转换为相应的字符输出。
如果,我想修改在使用DEBUG flag时re.compile的打印结果,那么,第一种方案是修改re的源码。这不太好。
第二种,也就是目前这种,“迂回”地使用一下管道。
Python解释器支持通过-c命令来运行一段代码,比如:
python3 -c "print('Hello, world!')"
就直接为我们打印了print('Hello, world!')这句代码执行结果。那么利用同样的方法,
python3 -c 'import re; re.compile(r"[a-z]", re.DEBUG)'
这样,解析正则的过程就会在终端打印出来。
我们要在打印前先把这些literal后面的ASCII码转换成对应的ASCII字符,就需要用到管道了。看下面这个脚本(不妨叫做pyreparser.py):
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# By mayadong7349 2014-01-11 18:37
def parse_re(str_re = ''):
from os import name as os_name
from re import compile as re_compile
from subprocess import check_output
args = ['python' if os_name == 'nt' else 'python3', '-c',
'import re; re.compile(r"{0}", re.DEBUG)'.format(str_re)]
# Potential exception: subprocess.CalledProcessError
parse_ret = check_output(args, universal_newlines = True)
######################################################################
literal_group = 'LITERAL'
# The RE below is not so good since we will lost string 'literal' in the parse result.
# literal_re = r'[\t ]*literal[\t ]+(?P<{0}>[0-9]+)'.format(literal_group)
# However, positive look-behind assertion requires fixed-width pattern. So RE below isn't right.
# literal_re = r'(?<=[\t ]*literal[\t ]+)(?P<{0}>[0-9]+)'.format(literal_group)
# This one is simple but enough for use.
literal_re = r'(?<=literal )(?P<{0}>[0-9]+)'.format(literal_group)
literal_pat = re_compile(literal_re)
repl_callback = lambda match_ret: \
chr(int(match_ret.group(literal_group))) + \
' ({0:#x})'.format(int(match_ret.group(literal_group)))
if literal_pat.search(parse_ret):
parse_ret = literal_pat.sub(repl_callback, parse_ret)
######################################################################
range_down_group = 'RANGE_DOWN'
range_up_group = 'RANGE_UP'
range_re = r'(?<=range )\((?P<{0}>[0-9]+), (?P<{1}>[0-9]+)\)' \
.format(range_down_group, range_up_group)
range_pat = re_compile(range_re)
def range_repl_callback(match_ret):
down = int(match_ret.group(range_down_group))
up = int(match_ret.group(range_up_group))
return '({0}, {1}) ({2:#x}, {3:#x})' \
.format(chr(down), chr(up), down, up)
if range_pat.search(parse_ret):
parse_ret = range_pat.sub(range_repl_callback, parse_ret)
return parse_ret
if __name__ == '__main__':
from sys import argv
if len(argv) >= 2:
print(parse_re(argv[1]), end = '')
else:
print(parse_re(r'([[:alpha:]][[:alnum:]_]*) = \1'), end = '')
print(parse_re(r'[A-Za-z][0-9]'), end = '')
嗯。就是从管道中读取解析的结果,然后做我们的替换工作,然后,再输出出来。这里,管道做了一下中转站。第五,从这个脚本中的注释中,你也看到了Python中positive look-behind assertion(在《正则表达式30分钟入门》中,这叫做“零宽度正回顾后发断言”)只允许固定长度的pattern,而.NET中好像没这个限制。
看起来怪怪的,不是吗?如果你想到了好的方法,不妨告诉我吧。
2014-02-08
zotin大哥的另一种实现:《改进了一个Python程序》。小伙伴们赶快去学习吧!