《Python核心编程(第3版)》——1.5 更长的正则表达式示例

1.5 更长的正则表达式示例

我们现在将浏览一个深入的示列,它以不同的方式使用正则表达式来操作字符串。首先是一些实际上生成用于操作随机数(但不是太随机)的代码。示例1-5展示了gendata.py,这是一个生成数据集的脚本。尽管该程序只是将简单地将生成的字符串集显示到标准输出,但是该输出可以很容易重定向到测试文件。

示例1-5 用于正则表达式练习的数据生成器(gendata.py)

该脚本为正则表达式练习创建随机数据,然后将生成的数据输出到屏幕。要将该程序移植到Python 3,仅需要将print语句修改为函数,将xrange()函数修改为range(),以及将sys.maxint修改为sys.maxsize。

该脚本生成拥有三个字段的字符串,由一对冒号或者一对双冒号分隔。第一个字段是随机(32位)整数,该整数将被转换为一个日期。下一个字段是一个随机生成的电子邮件地址。最后一个字段是一个由单横线(-)分隔的整数集。

运行这段代码,我们将获得以下输出(读者将会从此获益颇多),并将该输出在本地另存为redata.txt文件。

Thu Jul 22 19:21:19 2004::izsp@dicqdhytvhv.edu::1090549279-4-11
Sun Jul 13 22:42:11 2008::zqeu@dxaibjgkniy.com::1216014131-4-11
Sat May 5 16:36:23 1990::fclihw@alwdbzpsdg.edu::641950583-6-10
Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::1171590364-6-8
Thu Jun 26 19:08:59 2036::ugxfugt@jkhuqhs.net::2098145339-7-7
Tue Apr 10 01:04:45 2012::zkwaq@rpxwmtikse.com::1334045085-5-10

读者或者可能会辨别出来,但是来自该程序的输出是为正则表达式处理做准备的。后续将逐行解释,我们将实现一些正则表达式来操作这些数据,以及为本章末尾的练习留下很多内容。

逐行解释
第1~6行

在示例脚本中,需要使用多个模块。由于多种原因,尽管我们小心翼翼地避免使用from-import语句(例如,很容易判断一个函数来自哪个模块,以及可能导致本地模块冲突等),我们还是选择从这些模块中仅导入特定的属性,来帮助读者仅专注于那些属性,以及缩短每行代码的长度。

第8行

tlds是一组高级域名集合,当需要随机生成电子邮件地址时,就可以从中随机选出一个。

第10~12行

每次执行gendata.py,就会生成第5行和第10行之间的输出(该脚本对于所有需要随机整数的场景都使用random.randrange()函数)。对于每一行,我们选取所有可能范围(0~231–1 [sys.maxint])中的随机整数,然后使用time.ctime()函数将该整数转换为日期。Python中的系统时间和大多数基于POSIX的计算机一样,两者都使用从“epoch”至今的秒数,epoch是指1970年1月1日格林威治时间的午夜。如果我们选择一个32位整数,那么该整数将表示从epoch到最大可能时间(即epoch后的232秒)之间的某个时刻。

第13~16行

伪造邮件地址的登录名长度为4~7个字符(因此使用randrange(4,8))。为了将它们放在一起,需要随机选择4~7个小写字母,将所有字母逐个连接成一个字符串。random.choice()函数的功能就是接受一个序列,然后返回该序列中的一个随机元素。在该示例中,string.ascii_lowercase是字母表中拥有26个小写字母的序列集合。

我们决定伪造电子邮件地址的主域名长度不能多于12个字符,但是至少和登录名一样长。再一次使用随机的小写字母,逐个字母来组合这个名字。

第17~18行

该脚本的关键部分就是将所有随机数据放入输出行。先是数据字符串,然后是分隔符。然后将所有电子邮件地址通过登录名、“@”符号、域名和一个随机选择的高级域名组合在一起。在最终的双冒号之后,我们将使用用于表示初始时间的随机数字符串(日期字符串),后面跟着登录名和域名的长度,所有这些都由一个连字符分隔。

1.5.1 匹配字符串

对于后续的练习,为正则表达式创建宽松和约束性的版本。建议读者在一个简短的应用中测试这些正则表达式,该应用利用之前所展示的示例文件redata.txt(或者使用通过运行gendata.py生成的数据)。当做练习时,读者将需要再次使用该数据。

在将正则表达式放入应用中之前,为了测试正则表达式,我们将导入re模块,然后将redata.txt中的一个示例行赋给字符串变量data。如下所示,这些语句在所有展示的示例中都是常量。

>>> import re
>>> data = 'Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::1171590364-6-8'

在第一个示例中,我们将创建一个正则表达式来提取(仅仅)数据文件redata.txt中每一行时间戳中一周的几天。我们将使用下面的正则表达式。

"^Mon|^Tue|^Wed|^Thu|^Fri|^Sat|^Sun"

该示例需要字符串以列出的7个字符串中的任意一个开头(“^”正则表达式中的脱字符)。如果我们将该正则表达式“翻译”成自然语言,读起来就会像这样:“字符串应当以“Mon”,“Tue”,. . . ,“Sat”或者“Sun”开头。

换句话说,如果按照如下所示的方式对日期字符串分组,我们就可以使用一个脱字符来替换所有脱字符。

"^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)"

括住字符串集的圆括号意思是:这些字符串中的一个将会有一次成功匹配。这是我们一开始就使用的“友好的”正则表达式版本,该版本并没有使用圆括号。如下所示,在这个修改过的正则表达式版本中,可以以子组的方式来访问匹配字符串。

>>> patt = '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)'
>>> m = re.match(patt, data)
>>> m.group()            # entire match
'Thu'
>>> m.group(1)           # subgroup 1
'Thu'
>>> m.groups()           # all subgroups
('Thu',)

我们在该示例所实现的这个特性可能看起来并不是革命性的,但是在下一个示例或者作为正则表达式的一部分提供额外数据来实现字符串匹配操作的任何地方,它确定有它的独到之处,即使这些字符并不是你所感兴趣字符的一部分。

以上两个正则表达式都是非常严格的,尤其是要求一个字符串集。这可能在一个国际化的环境中并不能良好地工作,因为所在的环境中会使用当地的日期和缩写。一个宽松的正则表达式将为:^\w{3}。该正则表达式仅仅需要一个以三个连续字母数字字符开头的字符串。再一次,将正则表达式转换为正常的自然语言:脱字符^表示“作为起始”,\w表示任意单个字母数字字符,{3}表示将会有3个连续的正则表达式副本,这里使用{3}来修饰正则表达式。再一次,如果想要分组,就必须使用圆括号,例如^(\w{3})。

>>> patt = '^(\w{3})'
>>> m = re.match(patt, data)
>>> if m is not None: m.group()
...
'Thu'
>>> m.group(1)
'Thu'

注意,正则表达式^(\w){3}是错误的。当{3}在圆括号中时,先匹配三个连续的字母数字字符,然后表示为一个分组。但是如果将{3}移到外部,它就等效于三个连续的单个字母数字字符。

>>> patt = '^(\w){3}'
>>> m = re.match(patt, data)
>>> if m is not None: m.group()
...
'Thu'
>>> m.group(1)
'u'

当我们访问子组1时,出现字母“u”的原因是子组1持续被下一个字符替换。换句话说,m.group(1)以字母“T”作为开始,然后变为“h”,最终被替换为“u”。这些是单个字母数字字符的三个独立(并且重叠)分组,与一个包含三个连续字母数字字符的单独分组相反。

在下一个(而且是最后)的示例中,我们将创建一个正则表达式来提取redata.txt每一行的末尾所发现的数字字段。

1.5.2 搜索与匹配……还有贪婪

然而,在创建任何正则表达式之前,我们就意识到这些整数数据项位于数据字符串的末尾。这就意味着我们需要选择使用搜索还是匹配。发起一个搜索将更易于理解,因为我们确切知道想要查找的内容(包含三个整数的数据集),所要查找的内容不是在字符串的起始部分,也不是整个字符串。如果我们想要实现匹配,就必须创建一个正则表达式来匹配整个行,然后使用子组来保存想要的数据。要展示它们之间的差别,就需要先执行搜索,然后实现匹配,以展示使用搜索更适合当前的需要。

因为我们想要寻找三个由连字符分隔的整数,所以可以创建自己的正则表达式来说明这一需求:\d+-\d+-\d+。该正则表达式的含义是,“任何数值的数字(至少一个)后面跟着一个连字符,然后是多个数字、另一个连字符,最后是一个数字集。”我们现在将使用search()来测试该正则表达式:

>>> patt = '\d+-\d+-\d+'
>>> re.search(patt, data).group()     # entire match
'1171590364-6-8'

一个匹配尝试失败了,为什么呢?因为匹配从字符串的起始部分开始,需要被匹配的数值位于字符串的末尾。我们将不得不创建另一个正则表达式来匹配整个字符串。但是可以使用惰性匹配,即使用“.+”来表明一个任意字符集跟在我们真正感兴趣的部分之后。

patt = '.+\d+-\d+-\d+'
>>> re.match(patt, data).group()      # entire match
'Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::1171590364-6-8'

该正则表达式效果非常好,但是我们只想要末尾的数字字段,而并不是整个字符串,因此不得不使用圆括号对想要的内容进行分组。

>>> patt = '.+(\d+-\d+-\d+)'
>>> re.match(patt, data).group(1)     # subgroup 1
'4-6-8'

发生了什么?我们将提取1171590364-6-8,而不仅仅是4-6-8。第一个整数的其余部分在哪儿?问题在于正则表达式本质上实现贪婪匹配。这就意味着对于该通配符模式,将对正则表达式从左至右按顺序求值,而且试图获取匹配该模式的尽可能多的字符。在之前的示例中,使用“.+”获取从字符串起始位置开始的全部单个字符,包括所期望的第一个整数字段。\d+仅仅需要一个数字,因此将得到“4”,其中.+匹配了从字符串起始部分到所期望的第一个数字的全部内容:“Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::117159036”,如图1-2所示。

其中的一个方案是使用“非贪婪”操作符“?”。读者可以在“*”、“+”或者“?”之后使用该操作符。该操作符将要求正则表达式引擎匹配尽可能少的字符。因此,如果在“.+”之后放置一个“?”,我们将获得所期望的结果,如图1-3所示。

>>> patt = '.+?(\d+-\d+-\d+)'
>>> re.match(patt, data).group(1)     # subgroup 1
'1171590364-6-8'

另一个实际情况下更简单的方案,就是把“::”作为字段分隔符。读者可以仅仅使用正则字符串strip(':: ')方法获取所有的部分,然后使用strip('-')作为另一个横线分隔符,就能够获取最初想要查询的三个整数。现在,我们不想先选择该方案,因为这就是我们如何将字符串放在一起,以使用gendata.py作为开始!

最后一个示例:假定我们仅想取出三个整数字段中间的那个整数。如下所示,这就是实现的方法(使用一个搜索,这样就不必匹配整个字符串):-(\d+)-。尝试该模式,将得到以下内容。

>>> patt = '-(\d+)-'
>>> m = re.search(patt, data)
>>> m.group()            # entire match
'-6-'
>>> m.group(1)           # subgroup 1
'6'

本章几乎没有涉及正则表达式的强大功能,在有限的篇幅里面我们不可能做到。然而,我们希望已经向读者提供了足够有用的介绍性信息,使读者能够掌握这个强有力的工具,并融入到自己的编程技巧里面。建议读者阅读参考文档以获取在Python中如何使用正则表达式的更多细节。对于想要更深入研究正则表达式的读者,建议阅读由 Jeffrey E. F. Friedl.编写的Mastering Regular Expressions。

时间: 2024-11-10 00:55:44

《Python核心编程(第3版)》——1.5 更长的正则表达式示例的相关文章

《Python核心编程(第二版)》——导读

前 言 欢迎走进Python核心编程 我们很高兴能帮你尽快并尽可能深入地学习Python.掌握语法是本书的一个目标,不管怎样,我们都坚信,哪怕是一个初学者,只要他能掌握Python的运作机理,他就不再仅仅是用Python"编写",而是能开发出更高效的Python应用程序.但是你知道,并不是掌握了一门语言的语法就能让你立刻登堂入室. 在本书中,你能发现许多可以立即上手的例子.为了巩固基础,你还会在每章的末尾找到有趣又富有挑战性的习题.这些初级和中级水平的习题可以检验你的学习效果,并且提升

《Python核心编程(第二版)》——1.3 特点

1.3 特点 尽管Python已经流行了超过15年,但是一些人仍旧认为相对于通用软件开发产业而言,它还是个新丁.我们应当谨慎地使用"相对"这个词,因为"网络时代"的程序开发,几年看上去就像几十年. 当人们询问:"什么是Python?"的时候,很难用任何一个具象来描述它.人们更倾向于一口气不加思索地说出他们对Python的所有感觉,Python是____(请填写),这些特点究竟又是什么呢?为了让你能知其所以然,我们下面会对这些特点进行逐一地阐释.

《Python核心编程(第二版)》——第1部分 Python核心 第1章 欢迎来到Python世界 1.1 什么是Python

第1部分 Python核心 第1章 欢迎来到Python世界 本章主题 什么是Python Python的起源 Python的特点 下载Python 安装Python 运行Python Python文档 比较Python(与其他语言的比较) 其他实现 开篇将介绍一些Python的背景知识,包括什么是Python.Python的起源和它的一些关键特性.一旦你来了兴致,我们就会向你介绍怎样获得Python,以及如何在你的系统上安装并运行它.本章最后的练习将会帮助你非常自如地使用Python,包括使用

《Python核心编程(第二版)》——1.5 运行Python

1.5 运行Python 有三种不同的办法来启动Python.最简单的方式就是交互式的启动解释器,每次输入一行Python代码来执行.另外一种启动Python的方法是运行Python脚本.这样会调用相关的脚本解释器.最后一种办法就是用集成开发环境中的图形用户界面运行Python.集成开发环境通常整合了其他的工具,例如集成的调试器.文本编辑器,而且支持各种像CVS这样的源代码版本控制工具. 1.5.1 命令行上的交互式解释器 在命令行上启动解释器,你马上就可以开始编写Python代码.在Unix,

《Python核心编程(第二版)》——1.4 下载和安装Python

1.4 下载和安装Python 得到所有Python相关软件最直接的方法就是去访问它的网站(http://python.org ).为了方便读者,你也可以访问本书的网站(http://corepython.com )并点击左侧的"Download Python"链接--我们在表格中罗列了当前针对大多数平台的Python版本,当然,这还是主要集中在"三巨头"身上:Unix,Win32和MacOS X. 正如我们在前面1.3.5小节中提到的,Python可应用的平台非常

《Python核心编程(第二版)》——1.7 比较Python(Python与其他语言的比较)

1.7 比较Python(Python与其他语言的比较) Python已经和很多语言比较过了.一个原因就是Python提供了很多其他语言拥有的特性,另外一个原因就是Python本身也是由诸多其他语言发展而来的,包括ABC.Modula-3.C.C++.Algol-68.SmallTalk.Unix shell和其他的脚本语言,等等.Python就是"浓缩的精华":Van Rossum研究过很多语言,从中吸收了许多觉得不错的特性,并将它们溶于一炉. 然而,往往因为Python是一门解释型

《Python核心编程(第二版)》——1.2 起源

1.2 起源 Guido van Rossum于1989年底始创了Python,那时,他还在荷兰的CWI(Centrum voor Wiskun- de en Informatica,国家数学和计算机科学研究院).1991年初,Python发布了第一个公开发行版.这一切究竟是如何开始的呢?像C.C++.Lisp.Java和Perl一样,Python来自于某个研究项目,项目中的那些程序员利用手边现有的工具辛苦地工作着,他们设想并开发出了更好的解决办法. 那时van Rossum是一位研究人员,对解

《Python核心编程(第二版)》——1.6 Python文档

1.6 Python文档 Python文档可以在很多地方找到,最便捷的方式就是从Python网站查看在线文档.如果你没上网,并且使用的是Win32系统,那么在C:Python2xDoc目录下会找到一个名为Python2x.chm的离线帮助文档.它使用IE接口,所以你实际上是使用网页浏览器来查看文档.其他的离线文档包括PDF和PostScript(PS)文件.最后,如果你下载了Python发行版,你会得到LaTeX格式的源文件. 在本书的网站中,我们创建了一个包括绝大多数Python版本的文档,只

《Python核心编程(第二版)》—— 1.8 其他实现

1.8 其他实现 标准版本的Python是用C来编译的,又被称为Cpython.除此之外,还有一些其他的Python实现.我们将在下面讲述些实现,除了本书中提到的这些实现以外,下面的网址还有更多的实现版本. http://python.org/dev/implementations.html Java 我们在上一节中曾经提到,还有一个可以用的 Python解释器是完全由Java写成的,名为Jython.尽管两种解释器之间存在一些细微的差别,但是它们非常接近,而且启动环境也完全相同.那Jython