Python核心技术实战详解

978-7-115-51286-4
作者: 张洪朋王卫军
译者:
编辑: 张涛
分类: Python

图书目录:

详情

张洪朋, 西南交通大学计算机硕士,山东天易信息技术有限公司,6年Python开发经验,精通Python设计模式,擅长自动化运维平台开发。并且拥有5年以上PHP开发经验,熟悉关系型数据库,精通Linux 操作系统。现在负责企业级Python运维和架构工作,业余时间喜欢创作和技术分享。张洪朋,西南理工大学计算机硕士,山东天易信息技术有限公司,6年Python开发经验,精通Python设计模式,擅长自动化运维平台开发。并且拥有5年以上PHP开发经验,熟悉关系型数据库,精通Linux 操作系统。现在负责企业级Python运维和架构工作,业余时间喜欢创作和技术分享。


图书摘要

版权信息

书名:Python核心技术实战详解

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

编  著 张洪朋 王卫军

责任编辑 张 涛

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


本书逐层深入地介绍了Python的核心开发技术,并通过具体实例演练了各个知识点的使用流程。全书共10章,分别讲解了正则表达式、网络编程、多线程开发、Tkinter图形化界面开发、数据持久化、Pygame游戏开发、数据可视化、Django Web开发、Flask Web开发和网络爬虫开发等知识。全书简洁而不失技术深度,内容丰富,以通俗易懂的文字介绍了复杂的案例,易于阅读。

本书适用于已经了解Python基础语法的读者,以及希望进一步提高自己Python开发水平的读者,也可以作为大专院校相关专业的师生用书和培训学校的专业性教材。


Python的应用越来越广泛,为了帮助程序员掌握高级的开发技术并提高编程效率,本书特意挑选了程序员常用的Python核心功能模块进行讲解,如网络编程、多线程开发、图形化界面开发、数据可视化、Web开发和爬虫开发等。

全书共10章,主要内容包括如下。

第1章详细讲解正则表达式和常用的函数。

第2章主要讲解了套接字编程、socketserver编程、使用select模块实现I/O多路复用、使用urllib包、使用HTTP包、解析XML、解析JSON数据等。

第3章介绍了_thread 模块、threading模块、进程库multiprocessing、线程优先级队列模块queue、使用subprocess模块创建进程等。

第4章讲解了Tkinter开发基础、Tkinter组件开发、Tkinter事件处理、通过Tkinter实现对话框效果,并结合开发资源管理器实例介绍了Tkinte的具体用法等。

第5章介绍了SQLite3数据库、MySQL数据库、MariaDB数据库、MongoDB数据库、Python和ORM等开发知识。

第6章介绍了Pygame框架的核心模块、经典游戏实战、传统贪吃蛇游戏和AI贪吃蛇游戏开发技术等。

第7章讲解了Matplotlib、Pygal库、Pandas库、NumPy库等,帮助读者轻松完成图表的绘制。

第8章介绍了Django及其常用的命令,并通过几个综合案例,具体讲解了Django的使用,如博客系统、新闻聚合系统和在线商城系统开发。

第9章讲解了Flask开发基础、表单操作和具体案例,并通过应用把Flask的知识串联起来,达到学以致用的目标。

第 10 章介绍了网络爬虫的基础知识、如何开发网络爬虫应用程序、如何使用爬虫框架Scrapy、如何抓取知乎数据并分析、如何实现爬虫模块等。

本书对Python的核心知识进行了深入剖析,循序渐进地讲解了核心功能模块的开发技术,帮助读者快速步入Python开发高手之列。

本书深入讲解了10个不同的主题模块,每一个主题涵盖了特定应用开发领域。在书中不仅给出了案例讲解,还包含了更多的拓展知识,能够帮助读者使用Python 开发各种类型的应用程序。

为了方便给读者答疑,特提供了网站论坛等支持,并且随时在线与读者互动,让大家在互学互帮中形成一个良好的学习编程的氛围。

本书的学习论坛是toppr网站(后缀名为.net)。

本书读者对象包括程序开发人员、软件测试人员和Web开发人员。

张洪朋编写了本书第1章和第3~9章,用友网络科技股份有限公司的王卫军编写了本书的第2章和第10章。在编写本书过程中,十分感谢家人给予的巨大支持。限于作者水平,书中纰漏之处在所难免,请读者提出意见或建议,以便我们对内容修订并使之日臻完善。编辑联系邮箱是zhangtao@ptpress.com.cn。

最后感谢您购买本书,希望本书能成为您编程路上的领航者,祝您阅读快乐!

作者


本书由异步社区出品,社区(https://www.epubit.com/)为您提供后续服务。

在异步社区本书页面中单击 ,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您所在的学校、培训机构或企业想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


正则表达式(Regular Expression,RE)是计算机科学中的一个重要概念。正则表达式描述了一种字符串匹配的模式,可以用来判断一个字符串是否含有某个子字符串,再将匹配的子字符串进行替换,或者从某个字符串中取出符合某个条件的子串等。本章将详细讲解在Python程序中使用正则表达式的知识,为读者进行本书后面知识的学习打下坚实的基础。

正则表达式(Regular Expression)是一种文本模式,包括普通字符(例如,字母a~z )和特殊字符(称为元字符)。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。正则表达式虽是复杂的,但它也是强大的。在当今市面中,绝大多数开发语言都支持利用正则表达式进行字符串操作,如C++、Java、C#、PHP和Python等。

正则表达式是由普通字符以及特殊字符组成的文字模式。模式描述在搜索文本时要匹配的一个或多个字符串。作为一个模板,正则表达式将某种字符模式与所搜索的字符串进行匹配。本节将详细讲解正则表达式的基本语法知识。

正则表达式包含文本和特殊字符的字符串,该字符串描述了一种可以识别各种字符串的模式。对于通用文本来说,用于正则表达式的字母表是所有大小写字母及数字的集合。普通字符包括没有显式指定为元字符的所有可打印和不可打印字符,这包括所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

下面介绍的正则表达式都是普通字符,它们仅用一个简单的字符串就能构造成一个匹配字符串的模式:该字符串由正则表达式定义。表1-1所示为几个正则表达式和它们所匹配的字符串。

表1-1 普通字符正则表达式和它们匹配的字符串

正则表达式模式

匹配的字符串

foo

foo

Python

Python

abc123

abc123

表1-1中的第一个正则表达式模式是“foo”,该模式没有使用任何特殊符号去匹配其他符号,只匹配所描述的内容。所以,能够匹配这个模式的只有包含“foo”的字符串。同理,对于字符串“Python”和“abc123”,也一样。正则表达式的强大之处在于引入特殊字符来定义字符集、匹配子组和重复模式。正是由于这些特殊符号,使得正则表达式可以匹配字符串集合,而不只是单个字符串。

由此可见,普通字符表达式属于最简单的正则表达式形式。

非打印字符也可以是正则表达式的组成部分,表1-2列出了表示非打印字符的转义序列。

表1-2 非打印字符的转义序列

字符

描述

\cx

匹配由x指明的控制字符,例如,\cM匹配一个Control-M或回车符。x的值必须为A~Z或a~z中之一;否则将c视为一个原义的 'c' 字符

\f

匹配一个换页符,等价于 \x0c和\cL

\n

匹配一个换行符,等价于 \x0a和\cJ

\r

匹配一个回车符,等价于 \x0d和\cM

\s

匹配任何空白字符,包括空格、制表符、换页符等,等价于 [ \f\n\r\t\v]

\S

匹配任何非空白字符,等价于 [^ \f\n\r\t\v]

\t

匹配一个制表符,等价于 \x09和\cI

\v

匹配一个垂直制表符,等价于 \x0b和\cK

所谓特殊字符,就是一些有特殊含义的字符,如“*.txt”中的星号,就表示任何字符串的意思。如果要查找文件名中有“*”的文件,则需要对“*”进行转义,即在其前加一个“\”,即“\*.txt”。如果要匹配这些特殊字符,则必须首先对字符进行转义,即将“\”放在它们的前面。表1-3列出了正则表达式中的特殊字符。

表1-3 正则表达式中的特殊字符

特殊字符

描述

$

匹配输入字符串的结尾位置,如果设置了RegExp对象的Multiline属性,则 $ 也匹配 '\n' 或 '\r'。要匹配 $ 字符本身,请使用 \$

( )

标记一个子表达式的开始和结束位置。可以获取子表达式供以后使用。要匹配这些字符,请使用“\(”和“\)”

*

匹配前面的子表达式零次或多次。要匹配“*”字符,请使用“\*”

+

匹配前面的子表达式一次或多次。要匹配“+”字符,请使用“\+”

匹配除换行符\n之外的任何单字符。要匹配“.”字符,请使用“\.”

[

标记一个中括号表达式的开始。要匹配“[”,请使用“\[”

?

匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配“?”字符,请使用“\?”

\

将下一个字符标记为特殊字符或原义字符或向后引用或八进制转义符。例如,'n' 匹配字符 'n','\n' 匹配换行符,序列 '\\' 匹配 "\",而 '\(' 则匹配 "("

^

匹配输入字符串的开始位置。要匹配“^”字符本身,请使用“\^”

{

标记限定符表达式的开始。要匹配“{”,可使用“\{”

|

指明两项之间的一个选择。要匹配“|”,可使用“\|”

literal

匹配文本字符串的字面值

1.使用择一匹配符号匹配多个正则表达式模式

竖线“|”表示择一匹配的管道符号,也就是键盘上的竖线,表示一个“从多个模式中选择其一”的操作,用于分隔不同的正则表达式。例如,在表1-4中,左边是一些运用择一匹配的模式,右边是左边相应模式所能够匹配的字符。

表1-4 “从多个模式中选择其一”的操作

正则表达式模式

匹配的字符串

at| home

at、home

r2d2 | c3po

r2d2、c3po

bat | bet | bit

bat、bet、bit

有了这个符号,就能够增强正则表达式的灵活性,使得正则表达式能够匹配多个字符串而不只是一个字符串。择一匹配有时也称并(union)或者逻辑或(logical OR)。

2.匹配单个任意字符

点符号“.”可以匹配除了换行符“\n”以外的任何字符(Python正则表达式有一个编译标记S或者DOTALL,该标记是个例外,它使点号能够匹配换行符)。无论是一个字母、数字、空格(并不包括“\n”换行符)、可打印字符、不可打印字符,还是一个符号,使用点号都能够匹配它们,例如表1-5中的演示信息。

表1-5 匹配单个任意字符的演示信息

正则表达式模式

匹配的字符串

f.o

匹配在字母“f”和“o”之间的任意一个字符,例如fao、f9o、f#o等

..

任意两个字符

.end

匹配在字符串end之前的任意一个字符

注意:要显式匹配一个句点本身,必须使用反斜线进行转义,即“\.”。

3.从字符串起始或者结尾以及单词边界匹配

一些符号和相关的特殊字符用于在字符串的起始和结尾部分指定所搜索的模式。如果要匹配字符串的开始位置,就必须使用脱字符“^”或者特殊字符“\A”(反斜线和大写字母A)。后者主要用于那些没有脱字符的键盘(例如,某些国际键盘)。同样,美元符号“$”或“\Z”将用于匹配字符串的末尾位置。

注意:在本书讲解与字符串中模式相关的正则表达式时,会用术语“匹配”(matching)进行剖析。在Python术语中,主要有两种方法完成模式匹配。

我们按照Python如何完成模式匹配的方式来区分“搜索”和“匹配”。

使用这些符号的模式与其他大多数模式是不同的,因为这些模式指定了位置或方位。表1-6展示了一些表示“边界绑定”的正则表达式搜索模式。

表1-6 “边界绑定”的正则表达式搜索模式

正则表达式模式

匹配的字符串

^From

任何以From起始的字符串

/bin/tcsh$

任何以/bin/tcsh结尾的字符串

^Subject: hi$

任何由单个字符串Subject: hi构成的字符串

如果要依次匹配这些字符中的任何一个或者全部,就必须使用反斜线进行转义。例如,要匹配任何以美元符号结尾的字符串,一个可行的正则表达式方案就是使用模式“.*\$$”。

特殊字符\b和\B可以用来匹配字符边界。而两者的区别在于\b将用于匹配一个单词的边界,这意味着这个模式必须位于单词的起始部分,不管该单词前面(单词位于字符串中间)是否有其他任何字符,都认为这个单词位于行首。同样,\B将匹配出现在一个单词中间的模式(即不是单词边界)。表1-7是一些实例。

表1-7 匹配字符边界的实例

正则表达式模式

匹配的字符串

the

任何包含the的字符串

\bthe

任何以the开始的字符串

\bthe\b

仅匹配单词the

\Bthe

任何包含但并不以the起始的字符串

4.创建字符集

尽管句点可以用于匹配任意符号,但是在某些时候,可能想要匹配某些特定字符。正因如此,发明了中括号。该正则表达式能够匹配一对中括号中包含的任何字符,表1-8列出了一些实例。

表1-8 创建字符集的实例

正则表达式模式

匹配的字符串

b[aeiu]t

bat、bet、bit、but

[cr][23][dp][o2]

一个包含4个字符的字符串,第一个字符是“c”或“r”,然后是“2”或“3”,接着是“d”或“p”,最后要么是“o”要么是“2”。例如,c2do、r3p2、r2d2、c3po等

在“[cr][23][dp][o2]”这个正则表达式中,如果仅允许“r2d2”或者“c3po”作为有效字符串,就需要更严格限定的正则表达式。因为中括号仅表示逻辑或的功能,所以使用中括号并不能满足这一要求。唯一的方案就是使用择一匹配,例如“r2d2|c3po”。然而,对于单个字符的正则表达式来说,使用择一匹配和字符集是等效的。例如,我们以正则表达式“ab”作为开始。该正则表达式只匹配包含字母“a”且后面跟着字母“b”的字符串。如果要匹配一个字母的字符串,例如要么匹配“a”,要么匹配“b”,就可以使用正则表达式[ab],因为此时字母“a”和字母“b”是相互独立的字符串。我们也可以选择正则表达式“a|b”。然而,如果要匹配满足模式“ab”后面跟着“cd”的字符串,就不能使用中括号,因为字符集的方法只适用于单字符的情况。这种情况下,唯一的方法就是使用“ab|cd”,这与刚才提到的r2d2/c3po问题是相同的。

5.使用闭包操作符实现存在性和频数匹配

下面开始介绍最常用的正则表达式符号,即特殊符号*、+和“ ?”,所有这些都可以用于匹配一个、多个或者没有出现的字符串模式。具体说明如下所示。

注意:在表1-3中曾使用问号(重载),这意味着要么匹配零次,要么匹配一次,或者其他含义:如果问号紧跟在任何使用闭合操作符的匹配后面,则它将直接要求正则表达式引擎匹配尽可能少的次数。尽可能少的次数是指,当模式匹配使用分组操作符时,正则表达式引擎将试图“吸收”匹配该模式的尽可能多的字符。这通常称为贪婪匹配。问号要求正则表达式引擎去“偷懒”,如果可能,就在当前的正则表达式中尽可能少地匹配字符,留下尽可能多的字符给后面的模式(如果存在)。

使用闭包操作符演示存在性和频数匹配的具体实例如表1-9所示。

表1-9 使用闭包操作符演示存在性和频数匹配的具体实例

正则表达式模式

匹配的字符串

[dn]ot?

字母“d”或者“n”,后面跟着一个“o”,然后最多一个“t”,例如,do、no、dot、not

0?[1-9]

任何数值数字,它可能前置一个“0”,例如,匹配一系列数(表示1~9月的数值),不管是一个还是两个数字

[0-9]{15,16}

匹配15或者16个数字(如信用卡号码)

</?[^>]+>

匹配全部有效的(和无效的)HTML标签

[KQRBNP][a-h][1-8]-[a-h][1-8]

在“长代数”标记法中,表示国际象棋合法的棋盘移动(仅移动,不包括吃子和将军)。即“K”“Q”“R”“B”“N”或“P”等字母后面加上“a1”~“h8”的棋盘坐标。前面的坐标表示从哪里开始走棋,后面的坐标表示走到哪个位置(棋格)上

6.表示字符集的特殊字符

一些特殊字符能够表示字符集。与使用“0~9”这个范围表示的十进制数相比,可以简单地使用\d表示匹配任何十进制数字。另一个特殊字符(\w)能够用于表示全部字母数字的字符集,相当于[A-Za-z0-9_]的缩写形式,\s可以用来表示空格字符。这些特殊字符的大写版本表示不匹配。例如,\D表示任何非十进制数(与[^0-9]相同)。使用这些缩写,可以表示一些更复杂的实例,如表1-10所示。

表1-10 表示字符集的特殊字符

正则表达式模式

匹配的字符串

\w+-\d+

一个由字母数字组成的字符串和一串由一个连字符分隔的数字

[A-Za-z]\w*

第一个字符是字母,其余字符(如果存在)可以是字母或者数字(几乎等价于Python中的有效标识符)

\d{3}-\d{3}-\d{4}

美国电话号码的格式,前面是区号前缀,如800-555-1212

\w+@\w+.com

以XXX@YYY.com格式表示的简单电子邮件地址

在Python程序中,有时可能会对之前匹配成功的数据更感兴趣。我们不仅想要知道整个字符串是否匹配指定的标准,而且想要知道能否提取任何已经成功匹配的特定字符串或者子字符串。要提取成功匹配的特定字符串成子字符串,只要用一对小括号包裹任何正则表达式即可。

当使用正则表达式时,一对小括号可以实现以下任意一个(或者两个)功能。

对正则表达式进行分组的一个很好的示例是,有两个不同的正则表达式而且想用它们来比较同一个字符串。另一个原因是,将正则表达式分组可以在整个正则表达式中使用重复操作符(而不是一个单独的字符或者字符集)。

使用小括号进行分组的一个坏处就是,匹配模式的子字符串可以保存起来供后续使用。这些子组能够被同一次的匹配或者搜索重复调用,或者提取出来用于后续处理。

为什么匹配子组这么重要?主要原因是在很多时候除了进行匹配操作外,还需要提取所匹配的模式。例如,如果匹配模式是\w+-\d+,但是想要分别保存第一部分的字母和第二部分的数字,该如何实现?这样做的原因是,对于任何成功的匹配,可能想要看到这些匹配正则表达式模式的字符串究竟是什么。

如果为两个子模式都加上小括号,如(\w+)-(\d+),就能够分别访问每一个匹配子组。我们更倾向于使用子组,这是因为择一匹配首先通过编写代码来判断是否匹配,然后执行另一个单独的程序(该程序也需要另外创建),以表示整个匹配仅用于提取两个部分。表1-11展示了使用小括号实现指定分组的说明。

表1-11 使用小括号实现指定分组的说明

正则表达式模式

匹配的字符串

\d+(\.\d*)?

表示简单浮点数的字符串。也就是说,任何十进制数字后面可以接一个小数点和零个或者多个十进制数字,如“0.004”“2”“75.”等

(Mr?s?.)?[A-Z][a-z]*[A-Za-z-]+

名字和姓氏,以及对名字的限制(如果有,首字母必须大写,后续字母小写),全名前可以有可选的“Mr.”“Mrs.”“Ms.”或者“M.”,姓氏没有限制,可以有多个单词、横线以及大写字母

限定符用来指定正则表达式的一个给定组件必须出现多少次才匹配,有“*”“+”“?”以及{n}、{n,}、{n,m}这6种。正则表达式中的限定符信息如表1-12所示。

表1-12 正则表达式中的限定符信息

字符

描述

*

匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。“*”等价于{0,}

+

匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配"z"。“+”等价于 {1,}

?

匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 中的"do"。“?”等价于 {0,1}

{n}

n是一个非负整数,能匹配确定的n次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个o

{n,}

n是一个非负整数,至少匹配n次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有o。'o{1,}' 等价于 'o+','o{0,}' 则等价于 'o*'

{n,m}

mn均为非负整数,其中nm。最少匹配n次且最多匹配m次。例如,"o{1,3}" 将匹配 "fooooood" 中的前3个o。'o{0,1}' 等价于 'o?'。请注意,在逗号和两个数之间不能有空格

举个例子,本书页数很多,所以全书由很多章(一级目录)组成,每一章下面又分了节(二级目录),每一节下面又分了小节(三级目录)。因为本书内容多,有些章节编号超过9节,所以我们需要采用一种方式来处理两位或三位的章节编号。这时可以通过限定符来实现。下面的正则表达式匹配编号为任何位数的章节标题。

/Chapter [1-9][0-9]*/

请注意,限定符出现在范围表达式之后,所以它应用于整个范围表达式。在本例中,只指定从0~9的数字(包括0和9)。

这里没有使用“+”限定符,因为在第二个位置或后面的位置不一定需要一个数字。也没有使用“?”字符,因为它将章节编号限制到两位数。我们需要至少匹配 Chapter和空格字符后面的一个数字。如果我们知道本书最多有99章,则可以使用下面的表达式来至少指定一位,至多指定两位数字。

/Chapter [0-9]{1,2}/

上面的表达式的缺点是,大于99的章号仍只匹配开头两位数字。另一个缺点是Chapter 0也将匹配。下面是只匹配两位数字的更好的表达式。

/Chapter [1-9][0-9]?/

或:

/Chapter [1-9][0-9]{0,1}/

其实“*”“+”和“?”限定符都是贪婪的,所以它们会尽可能多地匹配文字,在它们的后面加上一个“?”就可以实现非贪婪或最小匹配。例如,我们可能搜索HTML文档,目的是查找括在H1标记内的章节标题。假设这个要搜索的文本是通过如下形式存在的。

<H1>Chapter 1 – Introduction to Regular Expressions</H1>

下面的表达式可以匹配从开始H1标记的小于(<)符号到关闭H1标记的大于(>)符号之间的所有内容。

/<.*>/

如果只需要匹配开始H1标记,则下面的“非贪心”表达式只会匹配<H1>。

/<.*?>/

通过在“*”“+”和“?”限定符之后放置“?”,该表达式从“贪心”表达式转换为“非贪心”表达式或者最小匹配。

定位符能够将正则表达式固定到行首或行尾。另外,它还可以帮助我们创建这样的正则表达式,这些正则表达式出现在一个单词内、一个单词的开头或者一个单词的结尾。定位符用来描述字符串或单词的边界,^和$分别指字符串的开始与结束,\b用于描述单词的前边界或后边界,\B表示非单词边界。常用的正则表达式定位符如表1-13所示。

表1-13 常用的正则表达式定位符

字符

描述

^

匹配输入字符串开始的位置。如果设置了RegExp对象的Multiline属性,^ 还会与\n或\r之后的位置匹配

$

匹配输入字符串结尾的位置。如果设置了RegExp对象的Multiline属性,$ 还会与\n或\r之前的位置匹配

\b

匹配一个字边界,即字与空格间的位置

\B

匹配非字边界

如果要在搜索章节标题时使用定位点,下面的正则表达式匹配一个章节标题,该标题只包含两个尾随数字,并且出现在行首。

/^Chapter [1-9][0-9]{0,1}/

真正的章节标题不仅出现在行的开始处,还是该行中仅有的文本。它既出现在行首又出现在同一行的结尾。下面的表达式能确保指定的匹配只匹配章节而不匹配交叉引用。通过创建只匹配一行文本的开始和结尾的正则表达式,就可做到这一点。

/^Chapter [1-9][0-9]{0,1}$/

匹配字边界稍有不同,但向正则表达式添加了很重要的功能。字边界是单词和空格之间的位置,非字边界是任何其他位置。下面的表达式匹配单词Chapter的开头3个字符,因为这3个字符出现在字边界后面。

/\bCha/

\b字符的位置是非常重要的。如果它位于要匹配的字符串的开始,则它在单词的开始处查找匹配项。如果它位于字符串的结尾,则它在单词的结尾处查找匹配项。例如,下面的表达式匹配单词Chapter中的字符串ter,因为它出现在字边界的前面。

/ter\b/

下面的表达式匹配Chapter中的字符串apt,但是不匹配aptitude中的字符串apt。

/\Bapt/

字符串apt出现在单词Chapter中的非字边界处,但出现在单词aptitude中的字边界处。对于\B非字边界运算符,位置并不重要,因为匹配不关心究竟是单词开头还是结尾。

除单字符之外,字符集还支持匹配指定的字符范围。方括号中两个符号中间用连字符(-)连接,用于指定一个字符的范围。例如,A-Z、a-z和0-9分别用于表示大写字母、小写字母与数字。这是一个按照字母顺序的范围,所以不能将它们仅限定于字母和十进制数字上。另外,如果脱字符(^)紧跟在左方括号后面,这个符号就表示不匹配给定字符集中的任何一个字符。具体实例如表1-14所示。

表1-14 限定范围和否定的实例

正则表达式模式

匹配的字符串

z.[0-9]

字母“z”后面首先跟着任何一个字符,然后跟着一个数字

[r-u][env-y][us]

字母“r”“s”“t”或者“u”后面首先跟着“e”“n”“v”“w”“x”或者“y”,然后跟着“u”或者“s”

[^aeiou]

一个非元音字符

[^\t\n]

不匹配制表符或者\n

["-a]

在一个ASCII系统中,所有字符都位于“"”和“a”之间,即范围是34~97

正则表达式从左到右进行计算,并遵循优先级顺序,这与算术表达式类似。相同优先级的从左到右进行运算,不同优先级的运算先高后低。表1-15从最高到最低说明了各种正则表达式中运算符的优先级顺序。

表1-15 正则表达式中运算符的优先级

运算符

描述

\

转义符

()、(?:)、(?=)、[]

小括号和方括号

*、+、?、{n}、{n,}、{n,m}

限定符

^、$、\任何元字符、任何字符

定位点和序列(即位置和顺序)

|

替换,或操作字符具有高于替换运算符的优先级,使得"m|food"匹配"m"或"food"。若要匹配"mood"或"food",请使用括号创建子表达式,从而产生"(m|f)ood"

扩展表示法以问号(?…)开始,通常用于在判断匹配之前提供标记,实现一个前视(或者后视)匹配或者条件检查。表1-16展示了扩展表示法的基本用法。

表1-16 扩展表示法的基本用法

正则表达式模式

匹配的字符串

(?:\w+.)*

以句点结尾的字符串,如“google.”“twitter.”“facebook.”,但是这些匹配的字符串不会保存下来供后续使用

(?#comment)

此处并不进行,只是作为注释

(?=.com)

字符串后面跟着“.com”才进行匹配,并不使用任何目标字符串

(?!.net)

字符串后面不跟着“.net”才进行匹配

(?<=800-)

字符串之前为“800-”才进行匹配,假定为电话号码,同样并不使用任何输入字符串

(?<!192\.168\.)

字符串之前不是“192.168.”才进行匹配,假定用于过滤掉一组C类IP地址

(?(1)yx)

如果一个匹配组1(\1)存在,就与y匹配;否则,就与x匹配

在Python中,可使用re模块提供的内置标准库函数来处理正则表达式。在这个模块中,既可以直接匹配正则表达式的基本函数,也可以通过编译正则表达式对象,并使用其方法来使用正则表达式。本节将详细讲解使用re模块的基本知识。

表1-17列出了 re模块中常用的内置库函数,它们中的大多数函数与已经编译的正则表达式对象(regex object)和正则表达式匹配对象(regex match object)的方法同名并且具有相同的功能。

表1-17 re模块中常用的内置库函数

函数

描述

compile(pattern,flags = 0)

使用任何可选的标记来编译正则表达式的模式,并返回一个正则表达式对象

match(pattern,string,flags=0)

尝试使用带有可选标记的正则表达式的模式来匹配字符串。如果匹配成功,就返回匹配对象;如果失败,就返回None

search(pattern,string,flags=0)

使用可选标记搜索字符串中第一次出现的正则表达式模式。如果匹配成功,则返回匹配对象;如果失败,则返回None

findall(pattern,string [, flags] )

查找字符串中所有(非重复)出现的正则表达式模式,并返回一个匹配列表

finditer(pattern,string [, flags] )

与函数findall()相同,但返回的不是一个列表,而是一个迭代器。对于每次匹配,迭代器都返回一个匹配对象

split(pattern,string,maxsplit=0)

根据正则表达式的模式分隔符,split函数将字符串分割为列表,然后返回成功匹配的列表,最多分割maxsplit次(默认分割所有匹配成功的位置)

sub(pattern,repl,string,count=0)

使用repl替换所有正则表达式的模式在字符串中出现的位置,除非定义count,否则将替换所有出现的位置(另见subn()函数,该函数返回替换操作的数目)

purge()

清除隐式编译的正则表达式模式

group(num=0)

返回整个匹配对象,或者编号为num的特定子组

groups(default=None)

返回一个包含所有匹配子组的元组(如果没有成功匹配,则返回一个空元组)

groupdict(default=None)

返回一个包含所有匹配的命名子组的字典,所有的子组名称作为字典的键(如果没有成功匹配,则返回一个空字典)

表1-18列出了 re模块中常用的属性信息。

表1-18 re模块中常用的属性信息

属性

说明

re.I、re.IGNORECASE

不区分大小写的匹配

re.L、re.LOCALE

根据所使用的本地语言环境,通过\w、\W、\b、\B、\s、\S实现匹配

re.M、re.MULTILINE

^和$分别匹配目标字符串中行的起始与结尾,而不是严格匹配整个字符串本身的起始和结尾

re.S、re.DOTALL

“.”(点号)通常匹配除\n(换行符)之外的所有单个字符;该标记表示“.”(点号)能够匹配全部字符

re.X、re.VERBOSE

通过反斜线转义后,所有空格加上#(以及在该行中的所有后续文字)的内容都被忽略

在Python程序中,函数compile()的功能是编译正则表达式。使用函数compile()的语法如下所示。

compile(source, filename, mode[, flags[, dont_inherit]])

通过使用上述格式,能够将source编译为代码或者抽象语法树对象。代码对象能够通过exec语句来执行或者使用eval()进行求值。各个参数的具体说明如下所示。

下面的实例文件compilechuli.py演示了使用函数compile()将正则表达式的字符串形式编译为Pattern实例的过程。

源码路径:daima\1\1-2\compilechuli.py

import re
pattern = re.compile('[a-zA-Z]')
result = pattern.findall('as3SiOPdj#@23awe')
print(result)

在上述代码中,先使用函数re.compile()将正则表达式的字符串形式编译为Pattern实例,然后使用Pattern实例处理文本并获得匹配结果(一个匹配的实例),最后使用Match实例获得信息,进行其他操作。执行后输出:

['a', 's', 'S', 'i', 'O', 'P', 'd', 'j', 'a', 'w', 'e']

在Python程序中,函数match()的功能是在字符串中匹配正则表达式,如果匹配成功,则返回MatchObject对象实例。使用函数match()的语法格式如下所示。

re.match(pattern, string, flags=0)

各参数的具体说明如下。

表1-19 参数flags的选项

参数

意义

re.I

忽略大小写

re.L

根据本地设置而更改\w、\W、\b、\B、\s以及\S的匹配内容

re.M

多行匹配模式

re.S

如果使“.”元字符,也匹配换行符

re.U

匹配Unicode字符

re.X

忽略模式中的空格,并且可以使用“#”注释

注意:在Python中有多个内置模块,在每个内置模块中都会有很多内置函数。在平常描述时通常会直接使用函数名,而不会用模块前缀的方式。但是在程序中,必须使用函数的完整形式。例如,在上面的内容中,函数match()是内置模块re.的成员,所以在平常可以直接使用函数名的形式。但是在编写程序时,必须用完整的形式,即re.match。在本书的正文描述中将采用简写函数名的形式,在代码中将采用模块前缀的形式。

匹配成功后,函数re.match()会返回一个匹配的对象,否则返回None。可以使用函数group(num) 或函数groups()来获取匹配的表达式,如表1-20所示。

表1-20 使用函数group(num)或函数groups()获取匹配的表达式

匹配对象方法

描述

group(num=0)

匹配整个表达式的字符串,可以一次向group()输入多个组号,这种情况下它将返回一个包含那些组所对应值的元组

groups()

返回一个包含所有组中字符串的元组

下面是如何运用match()(以及group())的一个示例。

>>> m = re.match('foo', 'foo') #模式匹配字符串
>>> if m is not None: #如果匹配成功,就输出匹配内容
... m.group()
...
'foo'

模式“foo”完全匹配字符串“foo”,也能够确认m是交互式解释器中匹配对象的实例。

>>> m #确认返回的匹配对象
<re.MatchObject instance at 80ebf48>

下面是一个失败的匹配示例,它会返回None。

>>> m = re.match('foo', 'bar')#模式并不能匹配字符串
>>> if m is not None: m.group() #单行版本的if 语句
...
>>>

因为上面的匹配失败,所以m被赋值为None,而且以此方法构建的if 语句没有指明任何操作。对于剩余的示例,为了简洁起见,可省去if 语句块。但是在实际操作中,最好不要省去,以避免AttributeError异常(None是返回的错误值,该值并没有group()属性[方法])。

只要模式从字符串的起始部分开始匹配,即使字符串比模式长,匹配也能够成功。例如,模式“foo”将在字符串“food on the table”中找到一个匹配,因为它是从字符串的起始部分进行匹配的。

>>> m = re.match('foo', 'food on the table') # 匹配成功
>>>m.group()
'foo'

此时可以看到,尽管字符串比模式要长,但从字符串的起始部分开始匹配就会成功。子串“foo”是从那个比较长的字符串中抽取出来的匹配部分,甚至可以充分利用Python原生的面向对象特性,忽略保存中间过程产生的结果。

>>>re.match('foo', 'food on the table').group()
'foo'

注意:在上述演示实例中,如果匹配失败,将会抛出AttributeError异常。

下面的实例文件sou.py演示了使用函数match()进行匹配的过程。

源码路径:daima\1\1-2\sou.py

import re                                       #导入模块re
print(re.match('www', 'www.example.com').span())#在起始位置匹配
print(re.match('net', 'www.example.com'))       #不在起始位置匹配

执行后会输出:

(0, 3)
None

在正则表达式应用中,经常会看到关于电子邮件地址格式的正则表达式“\w+@\w+.com”,通常用于匹配这个正则表达式所允许的更多邮件地址。为了支持在域名前添加主机名称,如www.example.com,仅允许example.com作为整个域名,必须修改现有的正则表达式。为了表示主机名是可选的,需要创建一个模式来匹配主机名(后面跟着一个句点),使用问号( ?)操作符来表示该模式出现零次或一次,然后按照如下实例文件duoge.py所示的方式将可选的正则表达式插入之前的正则表达式\w+@(\w+\.)?\w+\.com中。

源码路径:daima\1\1-2\duoge.py

1  import re
2  patt = '\w+@(\w+\.)?\w+\.com'
3  print(re.match(patt, 'guan@example.com').group())
4  print(re.match(patt, 'guan@www.example.com').group())
5
6  patt = '\w+@(\w+\.)*\w+\.com'
7  print(re.match(patt, 'guan@www.xxx.yyy.zzz.com').group())

从上述实例代码可以看出,表达式“\w+@(\w+\.)?\w+\.com”允许在“.com”前面有一个或者两个名称。第6行代码允许任意数量的中间子域名存在,读者需要注意这里的细节变化,此处将前面的“?”形式修改为“\w+@(\w+\.)*\w+.com”。这样仅使用字母数字字符的方式,并不能匹配组成电子邮件地址的全部可能字符。上面的正则表达式不能匹配诸如.com的域名,或者使用非单词“\W”字符组成的域名。

下面的实例文件tiqu.py演示了使用函数group()提取字母和数字字符的过程。

源码路径:daima\1\1-2\tiqu.py

import re
m = re.match('(\w\w\w)-(\d\d\d)', 'abc-123')
print(m.group())#完整匹配
print(m.group(1))#子组1
print(m.group(2))#子组2
print(m.groups())#全部子组

在上述代码中,使用正则表达式能够提取字母、数字字符串和数字,方法是使用函数group()访问每个独立的子组,以及使用函数groups()获取一个包含所有匹配子组的元组。执行后会输出:

abc-123
abc
123
('abc', '123')

在Python程序中,函数search()的功能是扫描整个字符串并返回第一个成功的匹配。事实上,搜索模式出现在一个字符串中间部分的概率要远大于出现在字符串起始部分的概率。现在是将函数search()派上用场的时候。函数search()的工作方式与函数match()的几乎完全一致,不同之处在于,函数search()会用它的字符串参数在任意位置搜索给定正则表达式模式第一次出现的匹配情况。如果搜索到成功的匹配,就会返回一个匹配对象;否则,返回None。

使用函数search()的语法格式如下所示。

re.search(pattern, string, flags=0)

匹配成功后,re.match()方法会返回一个匹配的对象;否则,返回None。可以使用函数group(num) 或函数groups()函数来获取匹配表达式。

下面的实例文件ser.py演示了使用函数search()进行匹配的过程。

源码路径:daima\1\1-2\ser.py

import re                              #导入模块re
print(re.search('www', '').span())     #在起始位置匹配
print(re.search('net', '').span())     #不在起始位置匹配

执行后会输出:

(0, 3)
(10, 13)

下面的实例文件说明了函数match()和search()之间的差别,功能是使用字符串“foo”去匹配“seafood”。

源码路径:daima\1\1-2\sousuo.py

import re                            #导入模块re
m = re.match('foo', 'seafood')       #匹配失败
if m is not None: m.group()
m = re.search('foo', 'seafood')      #使用 search() 代替
if m is not None: m.group()

在上述代码中,当使用函数match()从字符串的起始部分进行匹配时,会失败。也就是说,模式中的“f”将匹配到字符串的首字母“s”上,这样的匹配肯定是失败的。然而,字符串“foo”确实出现在“seafood”之中(某个位置)。要想让Python得到肯定的结果,需要使用函数search(),而不是尝试匹配。函数search()不但会搜索模式在字符串中第一次出现的位置,而且严格地从左到右搜索字符串。

在Python程序中,函数findall()的功能是在字符串中查找所有符合正则表达式的字符串,并返回这些字符串的列表。如果在正则表达式中使用了组,则返回一个元组。函数 re.match()和函数re.search()的作用基本一样。不同的是,函数re.match()只从字符串中第一个字符开始匹配,而函数re.search()则搜索整个字符串。

使用函数findall()的语法格式如下所示。

re.findall(pattern, string, flags=0)

下面的实例文件fi.py演示了使用函数findall()进行匹配的过程。

源码路径:daima\1\1-2\fi.py

import re             #导入模块re
#定义一个要操作的字符串变量s
s = "adfad asdfasdf asdfas asdfawef asd adsfas "
reObj1 = re.compile('((\w+)\s+\w+)')#将正则表达式的字符串形式编译为Pattern实例
print(reObj1.findall(s))#第1次调用函数findall()
reObj2 = re.compile('(\w+)\s+\w+')#将正则表达式的字符串形式编译为Pattern实例
print(reObj2.findall(s))  #第2次调用函数findall()
reObj3 = re.compile('\w+\s+\w+')  #将正则表达式的字符串形式编译为Pattern实例
print(reObj3.findall(s))  #第3次调用函数findall()

因为函数findall()返回的总是正则表达式在字符串中所有匹配结果的列表,所以此处主要讨论列表中“结果”的展现方式,即findall返回列表中每个元素包含的信息。上述代码调用了三次函数findall(),具体说明如下所示。

执行后会输出:

[('adfad asdfasdf', 'adfad'), ('asdfas asdfawef', 'asdfas'), ('asd adsfas', 'asd')]
['adfad', 'asdfas', 'asd']
['adfad asdfasdf', 'asdfas asdfawef', 'asd adsfas']

下面的实例文件wangzhi.py演示了使用函数findall()提取网址和锚文本的过程。

源码路径:daima\1\1-2\wangzhi.py

import re
relink = '<a href="(.*)">(.*)</a>'
info = '<a href="http://www.baidu.com">baidu</a>'
cinfo = re.findall(relink,info)
print(cinfo)

在Python程序中,两个函数/方法可用于实现搜索和替换功能,这两个函数分别是sub()和subn()。两者几乎一样,都用于将某字符串中所有匹配正则表达式的部分进行某种形式的替换。用来替换的部分通常是一个字符串,也可能是一个函数,该函数返回一个用来替换的字符串。函数subn()和函数sub()的用法类似,但是函数subn() 可以返回一个替换总次数,替换后的字符串和表示替换总次数的数字一起作为一个拥有两个元素的元组返回。

在Python程序中,函数sub()和函数subn()的语法格式分别如下。

re.sub( pattern, repl, string[, count])
re.subn( pattern, repl, string[, count])

各个参数的具体说明如下。

下面的实例文件subbb.py演示了使用函数sub()实现替换功能的过程。

源码路径:daima\1\1-2\subbb.py

import re                             #导入模块re
print(re.sub('[abc]', 'o', 'Mark'))   #找出字母a、b或者 c
print(re.sub('[abc]', 'o', 'rock'))   #将"rock"变成"rook"
print(re.sub('[abc]', 'o', 'caps'))   #将caps 变成oops

在上述实例代码中,首先在“Mark”中找出字母a、b或者 c,并以字母“o”替换,Mark就变成了Mork。然后将“rock”变成“rook”。重点看最后一行代码,有的读者可能认为可以将caps变成oaps,但事实并非如此。函数re.sub()能够替换所有的匹配项,并且不只是第一个匹配项,因此正则表达式将会把caps变成oops,这里c和a都被转换为o。执行后会输出:

Mork
rook
oops

下面的实例文件sub.py演示了使用函数sub()和subn()实现替换功能的过程。

源码路径:daima\1\1-2\sub.py

import re
print(re.sub('X', 'Mr.Guan', 'attn: X\n\nDear X,\n'))
print(re.subn('X', 'Mr.Guan', 'attn: X\n\nDear X,\n'))
print(re.sub('X', 'Mr.Guan', 'attn: X\n\nDear X,\n'))
print(re.sub('[ae]', 'X', 'abcdef'))
print(re.subn('[ae]', 'X', 'abcdef'))

执行后会输出:

attn: Mr.Guan

Dear Mr.Guan,

('attn: Mr.Guan\n\nDear Mr.Guan,\n', 2)
attn: Mr.Guan

Dear Mr.Guan,

XbcdXf
('XbcdXf', 2)

在Python程序中,模块re与正则表达式中的对象函数split()的工作方式是类似的,但是与分割一个固定字符串相比,它们基于正则表达式分隔字符串。如果不想为每次出现的模式都分割字符串,就可以通过为参数max设定一个值(非零)的方式来指定最大分割次数。如果给定的分隔符不使用特殊符号来匹配多重模式的正则表达式,那么re模块中的函数split()与str模块中的函数split()的工作方式相同。下面的演示过程则基于单引号进行分割。

>>> re.split(':', 'str1:str2:str3')
['str1', 'str2', 'str3']

下面的实例文件fi1.py演示了使用函数split()分割字符串的过程。

源码路径:daima\1\1-2\fi1.py

import re
DATA = (
'MMMMM View, CA 88888',
'SSSSS, CA',
'LLL AAAAA, 99999',
'CCCCCC 99999',
'PPPP AAAA CA',
)
for datum in DATA:
  print(re.split(', |(?= (?:\d{5}|[A-Z]{2})) ', datum))

上面的正则表达式拥有一个简单的组件,使用split语句基于逗号分割字符串。更重要的部分是最后的正则表达式,可以通过该正则表达式预览扩展符号。在普通的英文字符串中,如果空格紧跟在5个数字或者两个大写字母之后,就用split()函数分割该字符串。执行后会输出:

['MMMMM View', 'CA', '88888']
['SSSSS', 'CA']
['LLL', 'AAAAA', '99999']
['CCCCCC', '99999']
['PPPP', 'AAAA', 'CA']

在Python程序中,正则表达式支持大量的扩展符号。例如,通过使用 (?iLmsux) 系列选项,可以直接在正则表达式里指定一个或者多个标记,而不是通过函数compile()或者其他re模块函数。下面的实例文件kuozhan1.py演示了使用扩展符号处理字符串的过程。

源码路径:daima\1\1-2\kuozhan1.py

import re
print(re.findall(r'(?i)yes', 'yes? Yes. YES!!'))
print(re.findall(r'(?i)th\w+', 'The Guanxijing way is thh this.'))
print(re.findall(r'(?im)(^th[\w ]+)', """This line is the first,another line,that line, it's the best"""))
①print(re.findall(r'th.+', '''
... The first line
... the second line
... the third line
②... '''))

③print(re.findall(r'(?s)th.+', '''
... The first line
... the second line
... the third line
... ''')
④)

⑤print(re.findall(r'http://(?:\w+\.)*(\w+\.com)', 'http://www.example.com/ http: //www.example.com/ http://www.example.com/'))

print(re.sub(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})','(\g<areacode>)   
\g<prefix>-xxxx', '(800) 0531-88888888'))

在①前面的两行代码中是不区分大小写的。①~②使用“多行”的方式在目标字符串中实现跨行搜索,而不必将整个字符串视为单个实体。注意,此时忽略了实例“the”,因为它们并不出现在各自的行首。

③~④使用re.S/DOTALL标记设置的点号(.)来表示符号“\n”,反之,该标记通常用于表示除“\n”之外的全部字符。

⑤使用问号(?)对部分正则表达式进行分组,而不会保存该分组(用于后续的检索)。当不想保存以后永远不会使用的多余匹配时,问号(?)符号就会非常有用。

执行后会输出:

['yes', 'Yes', 'YES']
['The', 'thh', 'this']
['This line is the first']
['the second line', 'the third line']
['the second line\n... the third line\n... ']
['example.com', 'example.com', 'example.com']
(800) 0531-88888888

在Python程序中,Pattern对象是一个编译好的正则表达式,通过Pattern提供的一系列方法可以对文本进行匹配查找。Pattern不能直接实例化,必须使用函数re.compile()进行构造。在Pattern对象中,提供了如下4个可读属性来获取表达式的相关信息。

下面的实例文件subbb.py演示了使用Pattern对象的函数compile()进行处理的过程。

源码路径:daima\1\1-3\subbb.py

import re                             #导入模块
#下面使用函数re.compile()进行构造
p = re.compile(r'(\w+) (\w+)(?P<sign>.*)', re.DOTALL)
print ("p.pattern:", p.pattern)       #表达式字符串
print ("p.flags:", p.flags)           #编译匹配模式
print ("p.groups:", p.groups)         #分组的数量
print ("p.groupindex:", p.groupindex) #别名和编号字典

执行后会输出:

p.pattern: (\w+) (\w+)(?P<sign>.*)
p.flags: 48
p.groups: 3
p.groupindex: {'sign': 3}

在Python程序中,模式字符串使用如下特殊的语法来表示一个正则表达式。

表1-21列出了正则表达式模式语法中的特殊元素。如果使用模式的同时提供了可选的标志参数,则某些模式元素的含义会发生改变。

表1-21 正则表达式模式语法中的特殊元素

模式

描述

^

匹配字符串的开头

$

匹配字符串的末尾

匹配任意字符,除换行符之外,当指定re.DOTALL标记时,则可以匹配包括换行符的任意字符

[...]

用来表示一组字符,单独列出,如[amk] 匹配 'a' 'm'或'k'

[^...]

不在[]中的字符,如[^abc] 匹配除a、b、c之外的字符

re*

匹配0个或多个表达式

re+

匹配1个或多个表达式

re?

匹配0个或1个由前面的正则表达式定义的片段,非贪婪方式

re{ n}

精确匹配前面的n个表达式

re{ n, m}

匹配nm次由前面的正则表达式定义的片段,贪婪方式

a | b

匹配ab

(re)

匹配括号内的表达式,也表示一个组

(?imx)

正则表达式包含3种可选标志——I、m或x。只影响括号中的区域

(?-imx)

正则表达式关闭I、m或x可选标志。只影响括号中的区域

(?: re)

类似于(...),但是不表示一组

(?imx: re)

在括号中使用I、m或x可选标志

(?-imx: re)

在括号中不使用I、m或x可选标志

(?#...)

注释

(?= re)

前向肯定界定符。如果所含正则表达式以 ... 表示,则在当前位置匹配时成功;否则,失败。一旦所含表达式已经使用,那么模式的剩余部分还要尝试匹配界定符的右边

(?! re)

前向否定界定符。与肯定界定符相反,当所含表达式不在字符串当前位置匹配时成功

(?> re)

匹配的独立模式,省去回溯

\w

匹配字母数字

\W

匹配非字母数字

\s

匹配任意空白字符,等价于 [\t\n\r\f\v]

\S

匹配任意非空白字符

\d

匹配任意数字,等价于[0~9]

\D

匹配任意非数字

\A

匹配字符串开始

\Z

匹配字符串结束,如果存在换行,则只匹配到换行前的结束字符串

\z

匹配字符串结束

\G

设置匹配必须出现在上一个匹配结束的地方

\b

匹配一个单词边界,也就是指单词和空格间的位置。例如,'er\b' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'

\B

匹配非单词边界。'er\B' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'

\n、\t

分别用于匹配一个换行符和一个制表符

\1...\9

匹配第n个分组的子表达式

\10

如果正在被匹配,则匹配第n个分组的子表达式;否则,指八进制字符码的表达式

表1-22列出了在Python程序中使用常用正则表达式的实例。

表1-22 使用常用正则表达式的实例

实例

描述

python

匹配 "python"

[Pp]ython

匹配 "Python" 或 "python"

rub[ye]

匹配 "ruby" 或 "rube"

[aeiou]

匹配中括号内的任意一个字母

[0-9]

匹配任何数字,类似于 [0123456789]

[a-z]

匹配任何小写字母

[A-Z]

匹配任何大写字母

[a-zA-Z0-9]

匹配任何字母及数字

[^aeiou]

匹配除a、e、i、o和u字母以外的所有字符

[^0-9]

匹配除数字之外的字符

.

匹配除 "\n" 之外的任何单个字符。要匹配包括 '\n' 在内的任何字符,使用像 '[.\n]' 的模式

\d

匹配一个数字字符,等价于 [0~9]

\D

匹配一个非数字字符,等价于 [^0~9]

\s

匹配任何空白字符,包括空格、制表符、换页符等,等价于 [ \f\n\r\t\v]

\S

匹配任何非空白字符,等价于 [^ \f\n\r\t\v]

\w

匹配包括下画线的任何单词字符,等价于'[A-Za-z0-9_]'

\W

匹配任何非单词字符,等价于 '[^A-Za-z0-9_]'

下面的实例文件cao.py演示了使用正则表达式统计指定文件中函数和变量的过程。Python语法规定,函数定义必须以“def”开头,为了降低实例的难度,在此假设Python函数的编写规范是在关键字“def”后跟一个空格,然后是函数名和参数。没有考虑使用多个空格的情况。在Python程序中,因为变量一般不需要事先声明,可以直接赋值,所以要想统计文件中的变量信息,需要先处理变量直接赋值的情况,然后通过匹配单词后接“=”的情况查找变量名。为了简化起见,仅考虑比较规范整洁的写法,即规定变量名与“=”之间有一个空格。另外,添加了一种在for循环语句中直接使用的变量类型,在实例中特别处理了for循环的情况。在这里,实例并没有处理变量名重复的情况。

源码路径:daima\1\1-4\cao.py

import re                              #导入模块re
import sys                             #导入模块sys
def TongjiFunc(s):                     #定义函数TongjiFunc()统计函数
      r = re.compile(r'''              #调用函数compile()
              (?<=def\s)               #设置前面必须有函数标志def,并且紧跟一个空格
              \w+                      #匹配函数名
              \(.*?\)                  #匹配参数
              (?=:)                    #后面紧跟一个冒号“:”
              ''',re.X | re.U)         #设置编译选项,忽略模式中的注释
      return r.findall(s)
def TongjiVar(s):                      #定义函数TongjiVar()统计变量
      vars = []                        #定义存储变量的列表
      r = re.compile(r'''
              \b                       #匹配单词
              \w+                      #匹配变量名
              (?=\s=)                  #处理特殊情况—— 变量赋值
              '',re.X | re.U)          #设置编译选项,忽略模式中的注释
      vars.extend(r.findall(s))
      r = re.compile(r'''
              (?<=for\s)               #处理for语句中的变量
              \w+                      #匹配变量名
              \s                       #匹配空格
              (?=in)                   #匹配in关键字
              ''',re.X | re.U)         #设置编译选项,忽略模式中的注释
      vars.extend(r.findall(s))
      return vars
if len(sys.argv) == 1:                 #判断是否输入了命令,如果没有输入,必须输入要处理的文件
      sour = input('亲,请输入文件路径')  #提示输入要处理文件的路径
else:                                  #如果输入了命令
      sour = sys.argv[1]               #获取命令行参数
file = open(sour,encoding="utf-8")     #打开文件
s = file.readlines()                   #以行读取文件的内容
file.close()                           #关闭文件
print('********************************')
print('文件',sour,'中存在的函数有:')
print('********************************')
i = 0                                  #i表示函数所在的行号
for line in s:   #循环遍历文件的内容,匹配里面的函数,输出函数所在的行号,输出函数的原型
      i = i + 1
      function = TongjiFunc(line)      #调用统计函数的函数
      if len(function) == 1:           #如果行数是1
              print('Line: ',i,'\t',function[0])
print('********************************')
print('文件',sour,'存在的变量有:')
print('********************************')
i = 0                                  #i表示变量所在的行号
for line in s:     #循环遍历文件的各行,匹配里面的变量,输出变量所在的行号,输出变量名
      i = i + 1
      var = TongjiVar(line)            #调用统计变量的函数
      if len(var) == 1:
              print('Line: ',i,'\t',var[0])

在上述实例代码中,首先定义了一个能够获取文件中函数名的函数TongjiFunc(s),然后定义了一个能够获取变量名的函数TongjiVar(s),最后分别调用它们执行查找和获取操作。执行文件cao.py后的效果如图1-1所示。

图1-1 执行文件cao.py后的效果

下面的实例gaoji1.py演示了如何创建一个生成数据集的脚本。

源码路径:daima\1\1-4\gaoji1.py

from random import randrange, choice
from string import ascii_lowercase as lc
from sys import maxsize
from time import ctime

①tlds = ( 'com', 'edu', 'net', 'org', 'gov' )

for i in range(randrange(5, 11)):
      dtint = randrange(maxsize)         #选择日期
      dtstr = ctime(dtint)               #日期字符串
②    llen = randrange(4, 7)             #登录名比较短
      login = ''.join(choice(lc) for j in range(llen))
      dlen = randrange(llen, 13)         #域比较长
      dom = ''.join(choice(lc) for j in range(dlen))
      print('%s::%s@%s.%s::%d-%d-%d' % (dtstr, login,dom, choice(tlds), dtint, llen, dlen))

上述代码是一个生成数据集的脚本,能够简单地将生成的字符串集显示到标准输出中,该输出很容易重定向到测试文件。上述脚本能够生成拥有3个字段的字符串,由一对冒号(:)或者一对双冒号(::)分隔。其中第一个字段是随机(32位)整数,该整数将被转换为一个日期;第2个字段是一个随机生成的电子邮件地址;第3个字段是一个由单横线(-)分隔的整数集。

其中tlds是由一组高级域名格式组合而成的,当需要随机生成电子邮件地址时,可以从中随机选出一个。每当执行文件gendata.py,就会生成for语句之前的输出(该脚本对于所有需要随机整数的场景都使用函数random.randrange())。对于每一行,选取范围(0~231−1)中所有可能的随机整数,然后使用函数time.ctime()将该整数转换为日期。在Python程序中,系统时间和大多数基于POSIX的计算机一样,两者都使用从“epoch”到今天的秒数,其中epoch是指1970年1月1日格林尼治时间的午夜。如果选择一个32位整数,那么该整数将表示从epoch到最大可能时间(即epoch后的232s)之间的某个时刻。①~②的功能是构造登录名长度是4~7个字符的邮件地址,这是通过使用randrange(4,8)实现的。为了将它们放在一起,需要随机选择4~7个小写字母,将所有字母逐个连接成一个字符串。函数random.choice()的功能就是接受一个序列,然后返回该序列中的一个随机元素。在上述代码中,string.ascii_lowercase是字母表里拥有26个小写字母的序列集合。

注意:不能将限定符与定位点一起使用。

在Python程序中,因为在换行符或者字边界的前面或后面不能有一个以上空的位置,所以不允许诸如“^*”之类的表达式。如果要匹配一行文本开始处的字符,请在正则表达式的开始使用^字符。不要将^的这种用法与中括号表达式内的用法混淆。如果要匹配一行文本结束处的字符,请在正则表达式的结束处使用“$”字符。


互联网改变了人们的生活方式,生活在当今社会中的人们已经越来越离不开网络。相对于其他语言,Python在网络通信方面的优点特别突出。本章将详细讲解使用Python开发网络项目的基本知识,为读者进行本书后面知识的学习打下基础。

应用程序通常通过“套接字”(socket)向网络发出请求或者应答网络请求,使主机间或者计算机上的进程间可以通信。Python提供了两种访问网络服务的功能。其中低级别的网络服务通过套接字实现,它提供了标准的BSD套接字API,可以访问底层操作系统套接字接口的全部方法。而高级别的网络服务通过模块SocketServer实现,它提供了服务器中心类,可以简化网络服务器的开发。本节将讲解通过socket对象实现网络编程的知识。

在Python程序中,Socket库针对服务器端和客户端进行打开、读写和关闭操作。在Socket库中,用于创建socket对象的内置成员如下所示。

(1)函数socket.socket()

在Python标准库中,通过使用socket模块提供的socket对象,可以在计算机网络中建立相互通信的服务器与客户端。在服务器端需要建立一个socket对象,并等待客户端的连接。客户端使用socket对象与服务器端进行连接,一旦连接成功,客户端和服务器端就可以进行通信了。

在Python的socket对象中,函数socket()能够创建套接字对象。此函数是套接字网络编程的基础对象,具体语法格式如下。

socket.socket(family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None)

在Python程序中,为了创建TCP/IP套接字,可以用下面的代码调用函数socket.socket()。

tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

同理,在创建UDP/IP套接字时需要执行如下代码。

udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

因为有很多socket模块属性,所以此时可以使用“from module import ”导入方式,但这只是其中的一个例外。如果使用“from socket import ”导入方式,那么就把socket属性导入命名空间中。虽然这看起来有些麻烦,但是通过这种方式能够大大减少代码,例如下面的代码。

tcpSock = socket(AF_INET, SOCK_STREAM)

一旦有了socket对象,使用socket对象的方法就可以进行进一步的交互。

(2)函数socket.socketpair([family[, type[, proto]]])

函数socket.socketpair()的功能是使用所给的地址族、套接字类型和协议号创建一对已连接的socket对象地址列表,类型(type)和协议号(proto)的含义与前面socket()函数的相同。

(3)函数socket.create_connection(address[, timeout[, source_address]])

其功能是连接到互联网上侦听的TCP服务地址(二元组),形式是(host,port),分别表示主机和端口,并返回套接字对象。这使得编写与IPv4和IPv6兼容的客户端变得容易。传递可选参数timeout将在尝试连接之前设置套接字实例的超时。如果未提供超时,则使用getdefaulttimeout()返回的全局默认超时设置。如果提供了参数source_address,则这个参数必须是一个二元组。如果主机或端口分别为''或0,则使用操作系统的默认值。

(4)函数socket.fromfd(fd, family, type, proto=0)

其功能是复制文件描述符fd(由文件对象的fileno()方法返回的整数),并从结果中构建一个套接字对象。参数family、type和proto的具体含义与socket()函数相同。文件描述符应该指向一个套接字,但是当文件描述符无效时,就不会被检查出来,在对象的后续操作中可能失败。虽然函数socket.fromfd()很少用到,但是可用于获取或设置作为标准输入或输出传递给程序的套接字选项(例如,由UNIX inet守护程序启动的服务器)。

(5)函数socket.fromshare(data)

该函数的功能是实例化从函数socket.share()获取的数据,假设套接字处于阻塞模式。

下面的实例文件jiandanfuwu.py演示了创建一个简单套接字服务器的过程。

源码路径:daima\2\2-1\jiandanfuwu.py

import socket
sk = socket.socket()
sk.bind(("127.0.0.1",8080))
sk.listen(5)
conn,address = sk.accept()
sk.sendall(bytes("Hello world",encoding="utf-8"))

下面的实例文件jiandankehu.py演示了创建一个简单套接字客户端的过程。

源码路径:daima\2\2-1\jiandankehu.py

import socket

obj = socket.socket()
obj.connect(("127.0.0.1",8080))

ret = str(obj.recv(1024),encoding="utf-8")
print(ret)

在Socket库中,socket对象提供了表2-1所示的内置函数。

表2-1 socket对象的内置函数

函数 功能
服务器端套接字函数
bind() 用于绑定地址(host,port)到套接字,在AF_INET下,以元组(host, port)的形式表示地址
listen() 用于开始TCP监听。backlog指定在拒绝连接之前,操作系统可以挂起的最大连接数量。该值至少为1,大部分应用程序设为5即可
accept() 被动接受TCP客户端连接,(阻塞式)等待连接的到来
客户端套接字函数
connect() 主动初始化TCP服务器连接,一般address是形式为(hostname, port)的二元组,如果连接出错,则返回socket.error错误
connect_ex() connect()函数的扩展版本,在出错时返回出错码,而不是抛出异常
公共用途的套接字函数
recv() 接收TCP数据,数据以字符串形式返回,bufsize指定要接收的最大数据量。flags提供有关消息的其他信息,通常可以忽略
send() 发送TCP数据,将字符串中的数据发送到连接的套接字。返回值是要发送的字节数量,该数量可能小于字符串的长度
sendall() 完整发送TCP数据。将字符串中的数据发送到连接的套接字,但在返回之前会尝试发送所有数据。若成功,则返回None;若失败,则抛出异常
recvform() 接收UDP数据,与recv()的功能类似,但返回值是(data, address)。其中data是包含接收数据的字符串,address是发送数据的套接字地址
公共用途的套接字函数
sendto() 发送UDP数据,将数据发送到套接字,address是形式为(ipaddr,port)的元组,用于指定远程地址。返回值是发送的字节数
close() 关闭套接字
getpeername() 返回连接套接字的远程地址。返回值通常是元组(ipaddr, port)
getsockname() 返回套接字自己的地址。返回值通常是元组(ipaddr, port)
setsockopt(level, optname,value) 设置给定套接字选项的值
getsockopt(level, optname[,buflen]) 返回套接字选项的值
settimeout(timeout) 设置套接字操作的超时时间,timeout是一个浮点数,单位是秒。值为None表示没有超时时间。一般,超时时间应该在刚创建套接字时设置,因为它们可能用于连接的操作(如connect())
gettimeout() 返回当前超时时间的值,单位是秒,如果没有设置超时时间,则返回None
fileno() 返回套接字的文件描述符
setblocking(flag) 如果flag为0,则将套接字设为非阻塞模式;否则,将套接字设为阻塞模式(默认值)。在非阻塞模式下,如果调用recv()没有发现任何数据,或send()调用无法立即发送数据,那么将引起socket.error异常
makefile() 创建一个与套接字相关联的文件

除上述内置函数之外,在socket模块中还提供很多与网络应用开发相关的属性和异常。表2-2列出了一些比较常用的属性和异常。

表2-2 socket模块的常见属性和异常

属性和异常 描述
属性
AF_UNIX、AF_INET、AF_INET6、AF_NETLINK、AF_TIPC Python中支持的套接字地址
SOCK_STREAM、SOCK_DGRAM 套接字类型(TCP=流,UDP=数据报)
socket.AF_UNIX 只能够在单一的UNIX系统进程间通信
socket.AF_INET 服务器之间的网络通信
socket.AF_INET6 IPv6
socket.SOCK_STREAM 流式套接字,是为TCP服务的
socket.SOCK_DGRAM 数据报式套接字,是为UDP服务的
socket.SOCK_RAW 原始套接字,普通的套接字无法处理ICMP、IGMP等网络报文,而SOCK_RAW可以。另外SOCK_RAW也可以处理特殊的IPv4报文。利用原始套接字,可以通过IP_HDRINCL套接字选项由用户构造IP头
socket.SOCK_SEQPACKET 可靠的连续数据包服务
has_ipv6 表示是否支持IPv6的布尔标记
异常
error 套接字错误
herror 主机和地址错误
gaierror 地址错误
timeout 超时时间

在Python程序中创建TCP服务器时,创建通用TCP服务器的一般演示代码如下。需要记住的是,这仅是设计服务器的一种方式。一旦熟悉了服务器设计,可以修改下面的代码来操作服务器。

ss = socket()                       #创建服务器套接字
ss.bind()                           #绑定套接字与地址
ss.listen()                         #监听连接
inf_loop:                           #服务器无限循环
    cs = ss.accept()                #接受客户端连接
comm_loop:                          #通信循环
        cs.recv()/cs.send()         #对话(接收/发送)
    cs.close()                      #关闭客户端套接字
ss.close()                          #关闭服务器套接字#(可选)

在Python程序中,所有套接字都是通过socket.socket()函数创建的。因为服务器需要占用一个端口并等待客户端的请求,所以它们必须绑定到一个本地地址。因为TCP是一种面向连接的通信系统,所以在TCP服务器开始操作之前,必须安装一些基础设施。特别地,TCP服务器必须监听(传入)的连接。一旦这个安装过程完成,服务器就可以开始它的无限循环。在调用accept()函数之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接。默认情况下,accept()函数是阻塞的,这说明执行的操作会被暂停,直到一个连接到达为止。一旦服务器接受了一个连接,就会利用accept()方法返回一个独立的客户端套接字,用来与即将到来的消息进行交换。

下面的实例代码演示了通过套接字建立TCP“客户端/服务器”连接的过程,这是一个可靠的、相互通信的“客户端/服务器”。

实例文件ser.py的功能是以TCP连接方式建立一个服务器端程序,该程序能够将收到的信息直接发回到客户端。文件ser.py的具体实现代码如下。

源码路径:daima\2\2-1\ser.py

import socket                  #导入socket模块
HOST = ''                      #定义变量HOST的初始值
PORT = 10000                   #定义变量PORT的初始值
#创建socket对象s,参数分别表示地址和协议类型
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))           #将套接字与地址绑定
s.listen(1)                    #监听连接
conn, addr = s.accept()        #接受客户端连接
print('客户端地址', addr)        #显示客户端地址
while True:                    #连接成功后
    data = conn.recv(1024)     #实现对话操作(接收/发送)
    print("获取信息:",data.decode('utf-8'))#显示获取的信息
    if not data:               #如果没有数据
        break                  #终止循环
    conn.sendall(data)         #发送数据信息
conn.close()                   #关闭连接

在上述实例代码中,建立TCP连接之后使用while语句多次与客户端进行数据交换,直到收到的数据为空时才会终止服务器的运行。因为这只是一个服务器端程序,所以运行之后程序不会立即返回交互信息,还要等待和客户端建立连接,等与客户端建立连接后才能看到具体的交互效果。

实例文件cli.py的功能是建立客户端程序,在此需要创建一个socket实例,然后调用这个socket实例的connect()函数来连接服务器端。connect()函数的语法格式如下所示。

connect (address)

参数“address”通常也是一个元组(由一个主机名/IP地址、端口构成),如果要连接本地计算机,则主机名可直接使用“localhost”,connect()函数能够将套接字连接到远程地址为“address”的计算机。

实例文件cli.py的具体实现代码如下。

源码路径:daima\2\2-1\cli.py

import socket                        #导入socket模块
HOST = 'localhost'                   #定义变量HOST的初始值
PORT = 10000                         #定义变量PORT的初始值
#创建socket对象s,参数分别表示地址和协议类型
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))              #建立和服务器端的连接
data = "你好!"                       #设置数据变量
while data:
    s.sendall(data.encode('utf-8'))  #发送数据"你好"
    data = s.recv(512)               #实现对话操作(接收/发送)
    print("获取服务器信息:\n",data.decode('utf-8')) #显示接收到的服务器信息
    data = input('请输入信息:\n')                  #信息输入
s.close()                                         #关闭连接

上述代码使用套接字以TCP连接方式建立一个简单的客户端程序,基本功能是将从键盘录入的信息发送给服务器,并从服务器接收信息。因为服务器端是建立在本地“localhost”的10000端口上,所以上述代码作为客户端程序,连接的就是本地“localhost”的10000端口。当连接成功之后,向服务器端发送一条默认的信息“你好!”,再将从键盘录入的信息发送给服务器端,直到录入空信息(按Enter键)时才退出while循环,关闭套接字连接。先运行ser.py服务器端程序,然后运行cli.py客户端程序,除发送一条默认的信息之外,从键盘中录入的信息都会发送给服务器,服务器收到信息后显示并再次转发回客户端进行显示。执行效果如图2-1所示。

图2-1 执行文件cli.py后的效果

再看下面的这个实例,功能是建立一个TCP“客户端/服务器”模式的机器人聊天程序。

在这个实例中,看服务器端的实例文件jiqirenser.py。具体实现代码如下。

源码路径:daima\2\2-1\jiqirenser.py

import  socketserver
class Myserver(socketserver.BaseRequestHandler):
    def handle(self):

        conn = self.request
        conn.sendall(bytes("你好,我是机器人",encoding="utf-8"))
        while True:
            ret_bytes = conn.recv(1024)
            ret_str = str(ret_bytes,encoding="utf-8")
            if ret_str == "q":
                break
            conn.sendall(bytes(ret_str+"你好我好大家好",encoding="utf-8"))

if __name__ == "__main__":
    server = socketserver.ThreadingTCPServer(("127.0.0.1",8000),Myserver)
    server.serve_forever()

在这个实例中,看客户端的实例文件jiqirencli.py。具体实现代码如下。

源码路径:daima\2\2-1\jiqirencli.py

import socket

obj = socket.socket()

obj.connect(("127.0.0.1",8000))

ret_bytes = obj.recv(1024)
ret_str = str(ret_bytes,encoding="utf-8")
print(ret_str)

while True:
    inp = input("请问您有什么问题? \n >>>")
    if inp == "q":
        obj.sendall(bytes(inp,encoding="utf-8"))
        break
    else:
        obj.sendall(bytes(inp, encoding="utf-8"))
        ret_bytes = obj.recv(1024)
        ret_str = str(ret_bytes,encoding="utf-8")
        print(ret_str)

执行后的效果如图2-2所示。

图2-2 执行文件jiqirencli.py后的效果

再看下面的这个实例,功能是实现文件上传。

在这个实例中,看服务器端的实例文件shangchuanfuwu.py。具体实现代码如下。

源码路径:daima\2\2-1\shangchuanfuwu.py

import socket
sk = socket.socket()
sk.bind(("127.0.0.1",8000))
sk.listen(5)

while True:
    conn,address = sk.accept()
    conn.sendall(bytes("欢迎光临我爱我家",encoding="utf-8"))

    size = conn.recv(1024)
    size_str = str(size,encoding="utf-8")
    file_size = int(size_str)

    conn.sendall(bytes("开始传送", encoding="utf-8"))

    has_size = 0
    f = open("123.jpg","wb")
    while True:
        if file_size == has_size:
            break
        date = conn.recv(1024)
        f.write(date)
        has_size += len(date)
    f.close()

在这个实例中,看客户端的实例文件shangchuancli.py。具体实现代码如下。

源码路径:daima\2\2-1\shangchuancli.py

import socket
import os

obj = socket.socket()

obj.connect(("127.0.0.1",8000))

ret_bytes = obj.recv(1024)
ret_str = str(ret_bytes,encoding="utf-8")
print(ret_str)

size = os.stat("yan.jpg").st_size
obj.sendall(bytes(str(size),encoding="utf-8"))

obj.recv(1024)

with open("yan.jpg","rb") as f:
    for line in f:
        obj.sendall(line)

在Python程序中,当使用套接字应用传输层的UDP建立客户端/服务器端程序时,整个实现过程要比使用TCP简单一点。基于UDP的服务器端与客户端在进行数据传送时,不先建立连接,而直接进行数据传送。

在socket对象中,使用方法recvfrom()接收数据。具体语法格式如下。

recvfrom(bufsize[,flags])    #bufsize用于指定缓冲区大小

方法recvfrom()主要用来从socket接收数据,可以连接UDP。

在socket对象中,使用方法sendto()发送数据。具体语法格式如下。

sendto (bytes, address)

参数“bytes”表示要发送的数据;参数“address”表示发送信息的目标地址。由目标IP地址和端口构成的元组,主要用来通过UDP将数据发送到指定的服务器端。

在Python程序中,UDP服务器不需要TCP服务器那么多的设置,因为它们不是面向连接的。除等待传入的连接之外,几乎不需要做其他工作。下面是一段通用的UDP服务器端代码。

ss = socket()                        #创建服务器套接字
ss.bind()                            #绑定服务器套接字
infloop:                             #服务器无限循环
    cs = ss.recvfrom()/ss.sendto()   #实现对话操作(接收/发送)
ss.close()                           #关闭服务器套接字

从上述演示代码中可以看到,除创建套接字并将它绑定到本地地址(主机名/端口号对)之外,并没有额外的工作。无限循环包含接收客户端消息、打上时间戳并返回消息,然后回到等待另一条消息的状态。注意,close()调用是可选的,并且由于无限循环的缘故,它并不会被调用,但它应该是优雅或智能退出方案的一部分。

注意:UDP和TCP服务器之间的另一个显著差异是,因为数据报套接字是无连接的,所以就没有为了成功通信而使一个客户端连接到一个独立的套接字的“转换”操作。这些服务器仅接收消息并有可能恢复数据。

下面的实例代码中演示了使用套接字建立UDP“客户端/服务器端”连接的过程,UDP是一个不可靠的、相互通信的“客户端/服务器端”。

实例文件serudp.py的功能是使用UDP连接方式建立一个服务器端程序,将收到的信息直接发回客户端。文件serudp.py的具体实现代码如下。

源码路径:daima\2\2-1\serudp.py

import socket                              #导入socket模块
HOST = ''                                  #定义变量HOST的初始值
PORT = 10000                               #定义变量PORT的初始值
#创建socket对象s,参数分别表示地址和协议类型
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind((HOST, PORT))                       #将套接字与地址绑定
data = True                                #设置变量data的初始值
while data:                                #如果有数据
    data,address = s.recvfrom(1024)        #实现对话操作(接收/发送)
    if data==b'zaijian':                   #当接收的数据是zaijian时
        break                              #停止循环
    print('接收信息:',data.decode('utf-8')) #显示接收到的信息
    s.sendto(data,address)                 #发送信息
s.close()                                  #关闭连接

在上述实例代码中,建立UDP连接之后,使用while语句多次与客户端进行数据交换。上述服务器程序建立在本机的10000端口上,当收到“zaijian”信息时退出while循环,关闭服务器。

实例文件cliudp.py的具体实现代码如下。

源码路径:daima\2\2-1\cliudp.py

import socket                                       #导入socket模块
HOST = 'localhost'                                  #定义变量HOST的初始值
PORT = 10000                                        #定义变量PORT的初始值
#创建socket对象s,参数分别表示地址和协议类型
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
data = "你好!"                                      #定义变量data的初始值
while data:                                         #如果有data数据
    s.sendto(data.encode('utf-8'),(HOST,PORT))      #发送数据信息
    if data=='zaijian':                             #如果data的值是'zaijian'
        break                                       #停止循环
    data,addr = s.recvfrom(512)                     #读取数据信息
    print("从服务器接收信息:\n",data.decode('utf-8')) #显示从服务器端接收的信息
    data = input('输入信息:\n')                      #信息输入
s.close()                                           #关闭连接

上述代码使用套接字以UDP连接方式建立了一个简单的客户端程序,当在客户端创建套接字后,会直接向服务器端(本机的10000端口)发送数据,而没有进行连接。当用户键入“zaijian”时退出while循环,关闭本程序。运行效果与TCP服务器端和客户端实例基本相同。执行效果如图2-3所示。

图2-3 执行文件cliudp.py后的效果

Python提供了高级的网络服务器模块socketserver,在里面提供了服务器中心类,用于简化网络服务器的开发步骤。本节将详细讲解使用socketserver对象实现网络编程的知识。

socketserver是Python标准库中的一个高级模块,在Python 3以前的版本中被命名为socketserver,推出socketserver的目的是简化程序代码。

在Python程序中,虽然使用前面介绍的socket模块可以创建服务器,但是开发者要对网络连接等进行管理和编程。为了更加方便地创建网络服务器,在Python标准库中提供了一个创建网络服务器的模块socketserver。socketserver框架将处理请求划分为两部分,分别对应服务器类和请求处理类。服务器类处理通信问题,请求处理类处理数据交换或传送。这样,更加容易进行网络编程和程序的扩展。同时,该模块还支持快速的多线程或多进程的服务器编程。

在socketserver模块中,使用的服务器类主要有TCPServer、UDPServer、ThreadingTCPServer、ThreadingUDPServer、ForkingTCPServer、ForkingUDPServer等。

接下来将详细讲解socketserver模块的具体构成。

在模块socketserver中,包含如下几个基本类。

1)类socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)

类TCPServer是一个基础的网络同步TCP服务器类,能够使用TCP在客户端和服务器之间提供连续的数据流。如果bind_and_activate为true,构造函数将自动尝试调用server_bind()和server_activate(),其他参数会被传递到BaseServer基类。

2)类socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)

类UDPServer是一个基础的网络同步UDP服务器类,用于实现在传输过程中可能不按顺序到达或丢失时的数据包处理,参数含义与TCPServer相同。

3)类socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate= True) 和类socketserver.UnixDatagramServer(server_address, RequestHandlerClass, bind_and_activate= True)

基于文件的基础同步TCP/UDP服务器,虽与前面的TCP和UDP类类似,但使用的是UNIX域套接字,只能在UNIX平台上使用。参数含义与TCPServer相同。

4)类BaseServer

类BaseServer包含核心服务器功能和混合类的钩子,仅用于推导,而不会创建类的实例。可以用TCPServer或UDPServer创建类的实例。

在模块socketserver中,构成类的继承关系如下所示。

+------------+
| BaseServer |
+------------+
      |
      v
+-----------+        +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+        +------------------+
      |
      v
+-----------+        +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+        +--------------------+

注意:类UnixDatagramServer源自类UDPServer,而不是来自UnixStreamServer。IP地址和UNIX流服务器之间的唯一区别是它们的地址描述族。

除上述基本的类之外,在socketserver模块中还包含其他功能类,具体说明如表2-3所示。

表2-3 socketserver模块中的其他功能类

功能

ForkingMixIn/ThreadingMixIn

具有核心复制或线程功能;只用于在混合类与一个服务器类配合时实现一些异步性;不能直接实例化这个类

ForkingTCPServer/ForkingUDPServer

用于ForkingMixIn和TCPServer/UDPServer的组合

ThreadingTCPServer/ThreadingUDPServer

用于ThreadingMixIn和TCPServer/UDPServer的组合

BaseRequestHandler

包含处理服务请求的核心功能;仅用于推导,而无法创建类的实例;可以使用StreamRequestHandler或DatagramRequestHandler创建类的实例

StreamRequestHandler/DatagramRequestHandler

用于实现TCP/UDP服务器的服务处理器

上述服务器的构造方法参数主要如下。

1.对象BaseServer(server_address, RequestHandlerClass)

类BaseServer是模块中所有服务器对象的超类,虽然在里面定义了接口和函数,但是大多数情况下都不会实现函数,具体实现是在子类中完成的。参数server_address与RequestHandlerClass存储在相应的server_address和RequestHandlerClass属性中。

类BaseServer包含如下内置成员。

1)fileno():返回服务器监听套接字的整数文件描述符。通常传递给select.select(), 以允许一个进程监视多个服务器。

2)handle_request():处理单个请求。处理顺序为get_request()、verify_request()、process_request()。如果用户提供的handle()方法抛出异常,则将调用服务器的handle_error()方法。如果在self.timeout内没有收到请求,则将调用handle_timeout()并返回handle_request()。

3)serve_forever(poll_interval=0.5):处理请求,直到出现明确的shutdown()请求。每poll_interval秒轮询一次shutdown。忽略self.timeout。如果要完成周期性的任务,建议把该方法放置在其他线程中。

4)service_actions():在serve_forever()循环中调用,此函数可以被子类或mixin类覆盖,以执行特定于服务的操作,例如清除操作。

5)shutdown():告诉serve_forever()循环停止并等待它停止。

6)server_close():清理服务器,可能会被覆盖。

7)address_family:服务器套接字所属的协议族,常见示例有socket.AF_INET和socket.AF_UNIX。

8)RequestHandlerClass:用户提供的请求处理类,这个类为每个请求创建实例。

9)server_address:服务器侦听的地址,格式根据协议族地址的不同而不同。对于Internet协议来说,这是一个包含提供地址的字符串和整数端口号的元组('127.0.0.1',80)。

10)socket:服务器将侦听的套接字对象。

2.请求处理对象BaseRequestHandler

BaseRequestHandler是所有请求处理程序对象的超类,具体的请求处理程序子类必须定义新的handle()方法,并且可以覆盖任何其他方法,为每个请求创建子类的新实例。BaseRequestHandler类包含如下成员。

BaseRequestHandler子类会覆盖setup()方法和finish()方法,并提供self.rfile和self.wfile属性。这样可以分别读取或写入self.rfile和self.wfile属性,以获取请求数据或将数据返回给客户端。

下面的实例代码演示了使用socketserver建立TCP“客户端/服务器端”程序的过程。本实例使用socketserver创建了一个可靠的、相互通信的“客户端/服务器端”。

实例文件socketserverser.py的功能是使用socketserver模块创建基于TCP的服务器端程序,该程序能够将收到的信息直接发回到客户端。文件socketserverser.py的具体实现代码如下。

源码路径:daima\2\2-2\socketserverser.py

#定义类StreamRequestHandler的子类MyTcpHandler
class MyTcpHandler(socketserver.StreamRequestHandler):
    def handle(self):                              #定义函数handle()
while True:
            data = self.request.recv(1024)         #返回接收到的数据
if not data:
                Server.shutdown()                  #关闭连接
                break                              #停止循环
            print('接收信息:',data.decode('utf-8')) #显示接收的信息
            self.request.send(data)                #发送信息
return
#定义类TCPServer的对象实例
Server = socketserver.TCPServer((HOST,PORT),MyTcpHandler)
Server.serve_forever()                             #循环并等待它停止

在上述实例代码中,自定义了一个继承自StreamRequestHandler的处理程序类,并覆盖了方法handle()以实现数据处理。然后直接实例化了类TCPServer,调用方法serve_forever()启动服务器。

客户端实例文件socketservercli.py的代码和daima\2\2-1\cli.py的代码相同,本实例的最终执行效果如图2-4所示。

图2-4 执行文件socketserverser.py后的效果

在ThreadingTCPServer实现的套接字服务器内部,会为每一个客户端创建一个“线程”,该线程用来和客户端进行交互。在Python程序中,使用ThreadingTCPServer的步骤如下。

1)创建一个继承自SocketServer.BaseRequestHandler的类。

2)在类中定义一个名为handle()的方法。

3)启动ThreadingTCPServer。

下面的实例代码演示了使用ThreadingTCPServer创建“客户端/服务器端”通信程序的过程。

实例文件ser.py的功能是使用socketserver模块创建服务器端程序,该程序能够将收到的信息直接发回到客户端。文件ser.py的具体实现代码如下。

源码路径:daima\2\2-2\ser.py

import  socketserver

class Myserver(socketserver.BaseRequestHandler):

    def handle(self):

        conn = self.request
        conn.sendall(bytes("你好,我是机器人",encoding="utf-8"))
        while True:
            ret_bytes = conn.recv(1024)
            ret_str = str(ret_bytes,encoding="utf-8")
            if ret_str == "q":
                break
            conn.sendall(bytes(ret_str+"你好我好大家好",encoding="utf-8"))

if __name__ == "__main__":
    server = socketserver.ThreadingTCPServer(("127.0.0.1",8000),Myserver)
    server.serve_forever()

实例文件cli.py的功能是使用socketserver模块创建客户端程序,该程序能够接收服务器端发送的信息。文件cli.py的具体实现代码如下。

源码路径:daima\2\2-2\cli.py

import socket
obj = socket.socket()
obj.connect(("127.0.0.1",8000))
ret_bytes = obj.recv(1024)
ret_str = str(ret_bytes,encoding="utf-8")
print(ret_str)

while True:
    inp = input("你好请问您有什么问题? \n >>>")
    if inp == "q":
        obj.sendall(bytes(inp,encoding="utf-8"))
        break
    else:
        obj.sendall(bytes(inp, encoding="utf-8"))
        ret_bytes = obj.recv(1024)
        ret_str = str(ret_bytes,encoding="utf-8")
        print(ret_str)

I/O多路复用是指通过一种机制可以监视多个描述符。一旦某个描述符就绪(一般是读就绪或者写就绪),就通知程序执行相应的读/写操作。

在Python中,select模块专注于实现I/O多路复用功能,提供select()、poll()和epoll()这3个方法。其中后两个方法在Linux系统中可用,Windows系统仅支持select()方法。另外也提供kqueue()方法供freeBSD系统使用。

本章前面已经学习了套接字开发的知识,模块select在套接字编程中占据了比较重要的地位。对于大多数初学套接字的读者来说,不太喜欢用select模块写程序,只是习惯地编写诸如connect、accept、recv或recvfrom之类的阻塞程序(顾名思义,所谓阻塞就是进程或线程执行到这些函数时必须等待某个事件的发生。如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。

在Python程序中,完全可以使用select实现以非阻塞方式工作的程序,它能够监视文件描述符的变化情况——读/写或异常。所谓非阻塞(non-block)方式,就是进程或线程执行此函数时不必等待事件的发生,一旦执行,就肯定返回,并以返回值的不同来反映函数的执行情况。如果事件发生,则与阻塞方式相同;若事件没有发生,则返回一个编码来告知事件未发生,而进程或线程继续执行,所以效率较高。

在select模块中,核心功能方法是select(),其语法格式如下。

select.select(rlist, wlist, xlist[, timeout])

其中,前3个参数是“等待对象”的序列,使用名为fileno()的无参数方法表示文件描述符或对象的整数返回这样一个整数。

select方法用来监视文件描述符(当文件描述符的条件不满足时,select会阻塞),当某个文件的描述符状态改变时,会返回3个列表,这是前3个参数的子集。具体说明如下所示。

下面的实例代码演示了使用select同时监听多个端口的过程。

1)看文件duoser.py是否实现了服务器端的功能。具体实现代码如下。

源码路径:daima\2\2-3\duoser.py

import socket
import select

sk1 = socket.socket()
sk1.bind(("127.0.0.1",8000))
sk1.listen()

sk2 = socket.socket()
sk2.bind(("127.0.0.1",8002))
sk2.listen()

sk3 = socket.socket()
sk3.bind(("127.0.0.1",8003))
sk3.listen()

li = [sk1,sk2,sk3]

while True:
    r_list,w_list,e_list = select.select(li,[],[],1) # r_list可变化的
    for line in r_list:
        conn,address = line.accept()
        conn.sendall(bytes("Hello World !",encoding="utf-8"))

2)看文件duocli.py是否实现了客户端的功能。可通过如下代码与两个端口通信。

源码路径:daima\2\2-3\duocli.py

import socket

obj = socket.socket()
obj.connect(('127.0.0.1', 8001))

content = str(obj.recv(1024), encoding='utf-8')
print(content)

obj.close()

# 客户端c2.py
import socket

obj = socket.socket()
obj.connect(('127.0.0.1', 8002))

content = str(obj.recv(1024), encoding='utf-8')
print(content)

obj.close()

下面的实例代码演示了使用select模拟多线程并实现读/写分离的过程。

1)看文件fenliser.py是否实现了服务器端的功能。具体实现代码如下。

源码路径:daima\2\2-3\fenliser.py

#使用socket模拟多线程,让多用户可以同时连接
import socket
import select

sk1 = socket.socket()
sk1.bind(('0.0.0.0', 8000))
sk1.listen()

inputs = [sk1, ]
outputs = []
message_dict = {}

while True:
    r_list, w_list, e_list = select.select(inputs, outputs, inputs, 1)
    print('正在监听的socket对象%d' % len(inputs))
    print(r_list)
    for sk1_or_conn in r_list:
        #每一个连接对象
        if sk1_or_conn == sk1:
            #表示有新用户要连接
            conn, address = sk1_or_conn.accept()
            inputs.append(conn)
            message_dict[conn] = []
        else:
            #有老用户发消息了
            try:
                data_bytes = sk1_or_conn.recv(1024)
            except Exception as ex:
                #如果用户终止连接
                inputs.remove(sk1_or_conn)
            else:
                data_str = str(data_bytes, encoding='utf-8')
                message_dict[sk1_or_conn].append(data_str)
                outputs.append(sk1_or_conn)

    #w_list中仅保存了给我发送过消息的人
    for conn in w_list:
        recv_str = message_dict[conn][0]
        del message_dict[conn][0]
        conn.sendall(bytes(recv_str+'好', encoding='utf-8'))
        outputs.remove(conn)

    for sk in e_list:

        inputs.remove(sk)

2)看文件fenlicli.py是否实现了客户端的功能。具体实现代码如下。

源码路径:daima\2\2-3\fenlicli.py

import socket

obj = socket.socket()
obj.connect(('127.0.0.1', 8000))

while True:
    inp = input('>>>')
    obj.sendall(bytes(inp, encoding='utf-8'))
    ret = str(obj.recv(1024),encoding='utf-8')
    print(ret)

obj.close()

下面的实例代码演示了使用select实现一个可并发的服务器端的过程。

1)看文件bingser.py是否实现了服务器端的功能。具体实现代码如下。

源码路径:daima\2\2-3\bingser.py

import socket
import select

s = socket.socket()
s.bind(('127.0.0.1', 8888))
s.listen(5)
r_list = [s, ]
num = 0
while True:
    rl, wl, error = select.select(r_list, [], [], 10)
    num += 1
    print('counts is %s' % num)
    print("rl's length is %s" % len(rl))
    for fd in rl:
        if fd == s:
            conn, addr = fd.accept()
            r_list.append(conn)
            msg = conn.recv(200)
            conn.sendall(('first----%s' % conn.fileno()).encode())
        else:
            try:
                msg = fd.recv(200)
                fd.sendall('second'.encode())
            except ConnectionAbortedError:
                r_list.remove(fd)
s.close()

2)看文件bingcli.py是否实现了客户端的功能。具体实现代码如下。

源码路径:daima\2\2-3\bingcli.py

import socket

flag = 1
s = socket.socket()
s.connect(('127.0.0.1', 8888))
while flag:
    input_msg = input('input>>>')
    if input_msg == '0':
        break
    s.sendall(input_msg.encode())
    msg = s.recv(1024)
    print(msg.decode())

s.close()

从服务器端可以看到,我们需要不停地调用select,这充分说明了如下3点。

前面曾经讲解过,在Windows系统中只能使用select()方法,在Linux系统中只能使用方法poll()和方法epoll()。方法poll()的用法和前面讲解的select()方法十分相似,在此不再赘述。

1.方法epoll()很好地改进了方法select()

在Python程序中,每当在epoll句柄中注册新的事件时,会把所有的fd复制进内核中,而不是在执行epoll_wait的时候重复复制。epoll保证了每个fd在整个过程中只会复制一次。epoll()方法会在执行epoll_ctl时把指定的fd遍历一遍(这一遍必不可少),并为每个fd指定一个回调函数。当设备就绪并唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表中。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd,epoll对文件描述符没有额外限制。

2.水平触发和边缘触发

下面的实例代码演示了使用epoll()方法实现可并发的服务器端的过程。

1)看文件epollser.py是否实现了服务器端的功能。具体实现代码如下。

源码路径:daima\2\2-3\epollser.py

import socket
import select

s = socket.socket()
s.bind(('127.0.0.1', 8888))
s.listen(5)
epoll_obj = select.epoll()
epoll_obj.register(s, select.EPOLLIN)
connections = {}
while True:
    events = epoll_obj.poll()
    for fd, event in events:
        print(fd, event)
        if fd == s.fileno():
            conn, addr = s.accept()
            connections[conn.fileno()] = conn
            epoll_obj.register(conn, select.EPOLLIN)
            msg = conn.recv(200)
            conn.sendall('ok'.encode())
        else:
            try:
                fd_obj = connections[fd]
                msg = fd_obj.recv(200)
                fd_obj.sendall('ok'.encode())
            except BrokenPipeError:
                epoll_obj.unregister(fd)
                connections[fd].close()
                del connections[fd]

s.close()
epoll_obj.close()

2)看文件epollcli.py是否实现了客户端的功能。具体实现代码如下。

源码路径:daima\2\2-3\epollcli.py

import socket

flag = 1
s = socket.socket()
s.connect(('127.0.0.1', 8888))
while flag:
    input_msg = input('input>>>')
    if input_msg == '0':
        break
    s.sendall(input_msg.encode())
    msg = s.recv(1024)
    print(msg.decode())

s.close()

在计算机网络模型中,套接字编程属于底层网络协议开发的内容。虽然编写网络程序需要从底层开始构建,但是自行处理相关协议是一件比较麻烦的事情。对于大多数程序员来说,最常见的网络编程开发是针对应用协议进行的。在Python程序中,使用内置的包urllib和http可以完成HTTP协议层程序的开发工作。本节将详细讲解使用包urllib开发Python应用程序的知识。

在Python程序中,urllib包主要用于处理URL(Uniform Resource Locator,统一资源定位符)操作,使用urllib操作URL可以像使用和打开本地文件一样操作,非常简单而又易上手。在urllib包中主要包括如下模块。

在Python程序中,urllib.request模块定义了通过身份验证、重定向、cookie等方法打开URL的方法和类。

1.常用方法

模块urllib.request中的常用方法如下。

(1)方法urlopen()

在urllib.request模块中,方法urlopen()的功能是打开一个URL,语法格式如下。

urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, cadefault=False, context=None)

各参数的说明如下。

方法urlopen()将返回一个HTTPResponse实例(类文件对象),可以像操作文件一样使用read()、readline()和close()等方法对URL进行操作。

方法urlopen()能够打开url所指向的URL。如果没有给定协议或者下载方案(Scheme),或者传入了“file”方案,urlopen()会打开一个本地文件。

(2)方法urllib.request.install_opener(opener)

功能是安装opener作为urlopen()使用的全局URL opener对象,这意味着以后调用urlopen()时都会使用安装的opener对象。opener通常是build_opener()创建的opener对象。

(3)方法urllib.request.build_opener([handler, ...])

返回OpenerDirector实例,按给定的顺序连接处理程序。handler可以是BaseHandler的实例或BaseHandler的子类(这种情况下,可以调用没有任何参数的构造函数)。以下类的实例将在handler前面,除非handler包含它们的实例或它们的子类:ProxyHandler(如果检测到代理设置)、UnknownHandler、HTTPHandler、HTTPDefaultErrorHandler、HTTPRedirectHandler、FTPHandler、FileHandler、HTTPErrorProcessor。

(4)方法urllib.request.pathname2url(path)

功能是将路径名转换成路径,从本地语法形式的路径中使用一个URL的路径组成部分。这不会产生一个完整的URL。它将返回引用quote()函数的值。

(5)方法urllib.request.url2pathname(path)

功能是将路径组件转换为本地路径的语法,此方法不接受一个完整的URL。这个方法使用unquote()解码的通路。

(6)方法urllib.request.getproxies()

功能是返回一个日程表Dictionary去代理服务器的URL映射。针对所有的操作系统,以不区分大小写的方式扫描环境中名为<scheme>_proxy的变量,当找不到该变量时,从Mac OSX 的系统配置中查找代理信息和系统注册表。如果小写和大写环境变量都存在(且不统一),则首选小写。

2.公共属性

在请求对象request中,包含如下公共属性。

下面的实例文件url.py演示了使用urlopen()方法在百度中搜索关键字并得到第一页搜索结果的过程。

源码路径:daima\2\2-4\url.py

from urllib.request import urlopen       #导入Python的内置模块
from urllib.parse import urlencode       #导入Python的内置模块
import re                               #导入Python的内置模块
##wd = input('输入一个要搜索的关键字:')
wd= 'www.toppr.net'                     #初始化变量wd
wd = urlencode({'wd':wd})               #对URL进行编码
url = 'http://www.baidu.com/s?' + wd    #初始化url变量
page = urlopen(url).read()              #打开变量url的网页并读取内容
#定义变量content,对网页进行编码处理,并对特殊字符进行处理
content = (page.decode('utf-8')).replace("\n","").replace("\t","")
title = re.findall(r'<h3 class="t".*?h3>', content)
#处理正则表达式
title = [item[item.find('href =')+6:item.find('target=')] for item in title] #处理正则表达式
title = [item.replace(' ','').replace('"','') for item in title] #处理正则表达式
for item in title:                  #遍历title
    print(item)                     #显示遍历值

在上述实例代码中,使用方法urlencode()对搜索的关键字“www.toppr.net”进行URL编码,在拼接到百度的网址后,使用urlopen()方法发出访问请求并取得结果,最后通过将结果进行解码和输出。如果取消程序中第4行的注释,而把其后一句注释掉,就可以在运行时自主输入搜索的关键字。

注意:urllib.response模块是urllib使用的响应类,定义了与urllib.request模块类似的接口、方法和类,包括read()和readline()。为了节省篇幅,本书不再进行讲解。

在Python程序中,urllib.parse模块提供了一些用于处理URL字符串的功能。这些功能主要是通过如下方法实现的。

(1)方法urlparse.urlparse()

方法urlparse()的功能是将URL字符串拆分成前面描述的一些主要组件,其语法结构如下。

urlparse (urlstr, defProtSch=None, allowFrag=None)

方法urlparse()将urlstr 解析成一个6元组(prot_sch, net_loc, path, params, query, frag)。如果在urlstr中没有提供默认的网络协议或下载方案,则defProtSch会指定一个默认的网络协议。allowFrag用于标识一个URL是否允许使用片段。例如,下面是一个给定URL经urlparse()解析后的输出。

>>> urlparse.urlparse('http://www.example.com /doc/FAQ.html')
('http', 'www.example.com , '/doc/FAQ.html', '', '', '')

(2)方法urlparse.urlunparse()

方法urlunparse()的功能与方法urlpase()完全相反,它能够使用urlparse()的格式组合成一个URL(这个URL由6元组(prot_sch, net_loc, path, params, query, frag)组成),然后返回这个URL。可以用如下方式表示其等价性。

urlunparse(urlparse(urlstr)) ≡ urlstr

下面是使用urlunparse()的语法。

urlunparse(urltup)

(3)方法urlparse.urljoin()

当处理多个相关的URL时,需要使用urljoin()方法,例如,在一个Web 页面中可能会产生一系列页面的URL。方法urljoin()的语法格式如下。

urljoin (baseurl, newurl, allowFrag=None)

方法urljoin()能够取得根域名,并将其根路径(net_loc及其前面的完整路径,但是不包括末端的文件)与newurl 连接起来。例如下面的演示过程。

>>> urlparse.urljoin('http://www.example.com /doc/FAQ.html',
... 'current/lib/lib.htm')
'http://www.example.com/doc/current/lib/lib.html'

假设有一个要验证用户名和密码的Web站点,通过验证的最简单方法是在URL中使用登录信息进行访问,例如http://username:passwd@www.example.com。但是这种方法不具有可编程性。通过使用urllib可以很好地解决这个问题,假设合法的登录信息如下。

LOGIN = 'admin'
PASSWD = "admin"
URL = 'http://localhost'
REALM = 'Secure AAA'

此时便可以通过实例文件pa.py和urllib实现HTTP身份验证的过程。

源码路径:daima\2\2-4\pa.py

import urllib.request, urllib.error, urllib.parse

①LOGIN = 'admin'
PASSWD = "admin"
URL = 'http://localhost'
②REALM = 'Secure AAA'

③def handler_version(url):
    hdlr = urllib.request.HTTPBasicAuthHandler()
    hdlr.add_password(REALM,
        urllib.parse.urlparse(url)[1], LOGIN, PASSWD)
    opener = urllib.request.build_opener(hdlr)
    urllib.request.install_opener(opener)
④    return url

⑤def request_version(url):
    import base64
    req = urllib.request.Request(url)
    b64str = base64.b64encode(
        bytes('%s:%s' % (LOGIN, PASSWD), 'utf-8'))[:-1]
    req.add_header("Authorization", "Basic %s" % b64str)
⑥    return req

⑦for funcType in ('handler', 'request'):
    print('*** Using %s:' % funcType.upper())
    url = eval('%s_version' % funcType)(URL)
    f = urllib.request.urlopen(url)
    print(str(f.readline(), 'utf-8'))
⑧f.close()

①~② 实现普通的初始化功能,设置合法的登录验证信息。

③~④ 定义函数handler_version(),添加验证信息后建立一个URL开启器,安装该开启器以便所有已打开的URL 都能用到这些验证信息。

⑤~⑥ 定义函数request_version(),创建一个Request对象,并在HTTP 请求中添加简单的基64编码的验证头信息。在for循环里调用urlopen()时,该请求用来替换其中的URL字符串。

⑦~⑧ 分别打开给定的URL,通过验证后会显示服务器返回的HTML页面的第一行(转储了其他行)。如果验证信息无效,则会返回一个HTTP 错误(并且不会有HTML)。

在Python程序中,HTTP包实现了对HTTP协议的封装,在HTTP包中主要包含如下模块。

在http.client模块中,主要包括如下两个用于客户端的类。

在http.client模块中定义了实现http和https协议客户端的类。通常来说,不能直接使用http.client模块,需要在urllib.request模块中调用该模块来处理使用HTTP和HTTPS的URL。

1.HTTPConnection中的方法

1)HTTPConnection.request(method, url, body=None, headers={}):功能是使用指定的方法和链接向服务器发送请求。

2)HTTPConnection.getresponse():功能是在请求发送后才能调用服务器返回的内容,返回的是一个HTTPResponse实例。

3)HTTPConnection.set_debuglevel(level):设置调试级别,默认调试级别是0,表示没有调试输出。任何大于0的值都将导致所有当前定义的调试输出显示到stdout中,debuglevel会传递到创建的任何新的HTTPResponse对象中。

4)HTTPConnection.set_tunnel(host, port=None, headers=None):设置HTTPConnect隧道的主机和端口,允许通过代理服务器运行连接。

5)HTTPConnection.connect():连接指定的服务器。默认情况下,如果客户端没有连接,则会在请求时自动调用该方法。

6)HTTPConnection.close():关闭与服务器的连接。

7)HTTPConnection.putrequest(request, selector, skip_host=False, skip_accept_encoding= False):当和服务器的连接成功后,应当首先调用这个方法。发送到服务器的内容包括request字符串、selector字符串和HTTP版本。

8)HTTPConnection.putheader(header, argument[, ...]):发送HTTP头部到服务器,发送到服务器的内容包括头部、冒号、空格和参数列表中的第一个。

9)HTTPConnection.endheaders(message_body=None):发送一个空白行到服务器,表示头部的结束。其中可选参数message_body用于传递与请求相关联的消息体。如果消息头是字符串,那么消息体将与消息头在相同的分组中发送;否则,它将在单独的分组中发送。

2.HTTPResponse中的方法

在Python程序中,HTTPResponse实例包含了从服务器返回的HTTP响应,提供了访问请求头部和body部分的方法。HTTPResponse是一个可迭代的对象,而且可以使用with语句来声明。在类HTTPResponse中包含的方法如下。

1)HTTPResponse.read([amt]):读取和返回响应的body部分。

2)HTTPResponse.readinto(b):读取并返回响应中指定的字节。

3)HTTPResponse.getheader(name,default=None):返回指定名称的HTTP头部值。如果没有相应匹配的name值,则返回默认的None;如果有多个相匹配的name值,则返回所有的值,并用逗号进行分隔。

4)HTTPResponse.getheaders():以元组的形式返回所有的头部信息。

5)HTTPResponse.fileno():返回底层套接字的fileno。

6)HTTPResponse.msg:包含响应标头的http.client.HTTPMessage实例,http.client.HTTPMessage是email.message.Message的子类。

7)HTTPResponse.version:服务器使用的HTTP版本。其中10用于HTTP/1.0,11用于HTTP/1.1。

8)HTTPResponse.status:服务器返回的状态代码。

9)HTTPResponse.reason:服务器返回的原因短语。

10)HTTPResponse.debuglevel:调试等级,如果debuglevel大于零,则会在读取和解析响应时将消息输出到stdout中。

11)HTTPResponse.closed:如果数据流已经关闭,则值为True。

实例文件fang.py演示了使用http.client.HTTPConnection对象访问指定网站的过程。

源码路径:daima\2\2-5\fang.py

from http.client import HTTPConnection #导入内置模块
#基于HTTP访问的客户端
mc = HTTPConnection('www.baidu.com:80') 
mc.request('GET','/')                  #设置GET请求方法
res = mc.getresponse()                 #获取访问的网页
print(res.status,res.reason)           #输出响应的状态
print(res.read().decode('utf-8'))      #显示获取的内容

上述代码只实现了一个基本的访问实例。首先实例化http.client.HTTPConnection中请求的方法为GET,然后使用getresponse()方法获取访问的网页,并输出响应的状态。执行文件fang.py后的效果如图2-5所示。

图2-5 执行文件fang.py后的效果

实例文件httpmo.py演示了使用http.client对象中GET方法获取数据的过程。

源码路径:daima\2\2-5\httpmo.py

import http.client
conn = http.client.HTTPSConnection("www.python.org")
conn.request("GET", "/")
r1 = conn.getresponse()
print(r1.status, r1.reason)

data1 = r1.read()  #这将返回全部内容
#以块的方式读取数据
conn.request("GET", "/")
r1 = conn.getresponse()
while not r1.closed:
     print(r1.read(200))  # 200 bytes

#无效请求的示例
conn.request("GET", "/parrot.spam")
r2 = conn.getresponse()
print(r2.status, r2.reason)
data2 = r2.read()
conn.close()

下面的实例文件wangluo.py演示了综合使用http和urllib模块的过程。

源码路径:daima\2\2-5\wangluo.py

import http.client
import urllib,parser
#初始化一个HTTPS链接
conn = http.client.HTTPSConnection("www.example.com")
#指定请求的方法和请求的链接地址
conn.request("GET","/doc/")
#得到返回的 HTTP响应
r1 = conn.getresponse()
#HTTP状态码
print(r1.status,r1.reason)
#HTTP头部
print(r1.getheaders())
#body部分
print(r1.read())
#如果连接没有关闭,则输出前200字节
if not r1.closed:
 print(r1.read(200))
#关闭连接后才能重新请求
conn.close()
#请求一个不存在的文件或地址
conn.request("GET","/parrot.spam")
r2 = conn.getresponse()
print(r2.status,r2.reason)
conn.close()
#使用 HEAD 请求,但是不会返回任何数据
conn = http.client.HTTPSConnection("www.example.com")
conn.request("HEAD","/")
res = conn.getresponse()
print(res.status,res.reason)
data = res.read()
print(len(data))
conn.close()
#使用 POST请求,提交的数据放在 body 部分中
params = urllib.parse.urlencode({'@number':12524,'@type':'issue','@action':'show'})
#发送请求数据,要带上 Content-type 字段,以告知消息主体以何种方式编码
headers = {"Content-type":"application/x-www-form-urlencoded","Accept":"text/plain"}
conn = http.client.HTTPConnection("bugs.www.example.com")
conn.request("POST","/",params,headers)
response = conn.getresponse()
#访问被重定向
print(response.status,response.reason)
print(response.read().decode("utf-8"))
conn.close()

在现实应用中,有时需要以客户端的形式通过HTTP访问多种服务,例如,下载数据或与同一个基于REST的API进行交互。对于简单的任务来说,使用urllib.request模块就可以实现。例如,要发送一个简单的HTTP GET 请求到远程服务器上,通过实例文件fang1.py即可实现。

源码路径:daima\2\2-5\fang1.py

from urllib import request, parse

#访问的基本URL
url = 'http://    /get'

#查询参数的字典
parms = {
   'name1' : 'value1',
   'name2' : 'value2'
}

#对查询字符串进行编码
querystring = parse.urlencode(parms)

#创建一个GET请求并读取响应
u = request.urlopen(url+'?' + querystring)
resp = u.read()

import json
from pprint import pprint

json_resp = json.loads(resp.decode('utf-8'))
pprint(json_resp)

执行后会输出:

{'args': {'name1': 'value1', 'name2': 'value2'},
 'headers': {'Accept-Encoding': 'identity',
             'Connection': 'close',
             'Host': 'httpbin.org',
             'User-Agent': 'Python-urllib/3.6'},
 'origin': '       ',
 'url': 'http://    get?name1=value1&name2=value2'}

如果需要使用POST方法在请求主体中发送查询参数,则可以将编码后的参数作为可选参数提供给urlopen()函数。下面的实例文件fang2.py即可实现。

源码路径:daima\2\2-5\fang2.py

from urllib import request, parse

#访问的基本URL
url = 'http://      /post'

#查询参数的字典
parms = {
   'name1' : 'value1',
   'name2' : 'value2'
}

#对查询字符串进行编码
querystring = parse.urlencode(parms)

#创建一个GET请求并读取响应
u = request.urlopen(url, querystring.encode('ascii'))
resp = u.read()

import json
from pprint import pprint

json_resp = json.loads(resp.decode('utf-8'))
pprint(json_resp)

如果需要在发出的请求中提供自定义的HTTP头部,例如,修改user-agent字段,则可以创建一个包含字段值的字典来实现,并创建一个Request实例,然后将该实例传送给urlopen()。实例文件fang3.py即可实现。

源码路径:daima\2\2-5\fang3.py

import requests

#访问的基本URL
url = 'http://     /post'

#查询参数的字典
parms = {
   'name1' : 'value1',
   'name2' : 'value2'
}

#其他头部
headers = {
    'User-agent' : 'none/ofyourbusiness',
    'Spam' : 'Eggs'
}

resp = requests.post(url, data=parms, headers=headers)

#请求返回的解码后的文本
text = resp.text

from pprint import pprint
pprint(resp.json)

在上述代码中,因为需要交互的服务比较复杂,所以用到了requests库。执行后会输出:

<bound method Response.json of <Response [200]>>

当使用requests库时,读者要注意它能以多种方式从请求中返回响应结果的内容。从上面的代码来看,resp.text提供的是以Unicode 解码的响应文本。但是,如果访问resp.content,就会得到原始的二进制数据;而如果访问resp.json,就会得到JSON格式的响应内容。

自互联网诞生那一刻起,人们之间日常交互的方式就多了一种新的渠道。从此以后,交流变得更加迅速、快捷,更具有实时性。一时之间,很多网络通信产品出现了,例如QQ、MSN和邮件系统,其中电子邮件更是深受人们的追捧。使用Python可以开发出功能强大的邮件系统,本节将详细讲解使用Python开发邮件系统的过程。

在计算机应用中,使用POP3可以登录Email服务器收邮件。在Python程序中,内置模块poplib提供了对POP3的支持。现在市面中大多数邮箱软件都提供了通过POP3收取邮件的方式,例如Outlook等Email客户端就是如此。开发者可以使用Python中的poplib模块开发出一个支持POP3邮件协议的客户端脚本程序。

1.类

在poplib模块中,通过如下两个类实现POP3功能。

1)类poplib.POP3(host, port=POP3_PORT[, timeout]):用于实现实际的POP3,当实例初始化时创建连接。

2)类poplib.POP3_SSL(host, port=POP3_SSL_PORT, keyfile=None, certfile=None, timeout=None, context=None):POP3的子类,通过SSL加密的套接字连接到服务器。

在Python程序中,可以使用类POP3创建一个POP3对象实例。其语法原型如下。

POP3 (host, port)

2.方法

在poplib模块中,常用的内置方法如下。

(1)方法user()

当创建一个POP3对象实例后,可以使用其中的方法user()向POP3服务器发送用户名。其语法原型如下。

user (username)

参数username表示登录服务器的用户名。

(2)方法pass_()

可以使用POP3对象中的方法pass_()(注意,在pass后面有一条下画线)向POP3服务器发送密码。其语法原型如下。

pass- (password)

参数password是指登录服务器的密码。

(3)方法getwelcome()

当成功登录邮件服务器后,可以使用POP3对象中的方法getwelcome()获取服务器的欢迎信息。其语法原型如下。

getwelcome()

(4)方法set_debuglevel()

可以使用POP3对象中的方法set_debuglevel()设置调试级别。其语法原型如下。

set_debuglevel (level)

参数level表示调试级别,用于显示与邮件服务器交互的相关信息。

(5)方法stat()

使用POP3对象中的方法stat()可以获取邮箱的状态,例如邮件数、邮箱大小等。其语法原型如下。

stat()

(6)方法list()

使用POP3对象中的方法list()可以获得邮件内容列表。其语法原型如下。

list (which)

参数which是一个可选参数,如果指定,则仅列出指定的邮件内容。

(7)方法retr()

使用POP3对象中的方法retr()可以获取指定的邮件。其语法原型如下。

retr (which)

参数which用于指定要获取的邮件。

(8)方法dele()

使用POP3对象中的方法dele()可以删除指定的邮件。其语法原型如下。

dele (which)

参数which用于指定要删除的邮件。

(9)方法top()

使用POP3对象中的方法top()可以收取某个邮件的部分内容。其语法原型如下。

top (which,howmuch)

除上面介绍的常用内置方法之外,还可以使用POP3对象中的方法rset()清除收件箱中邮件的删除标记;使用POP3对象中的方法noop()保持同邮件服务器的连接;使用POP3对象中的方法quit()断开同邮件服务器的连接。

要使用Python获取某个Email邮箱中的邮件主题和发件人信息,首先应该知道自己所使用的Email的POP3服务器地址和端口。一般来说,邮箱服务器的地址格式如下。

pop.主机名.域名

而端口的默认值是110,例如126邮箱的POP3服务器地址为pop.126.com,端口为默认值110。下面的实例代码演示了使用poplib库获取指定邮件中最新两封邮件的主题和发件人的方法。实例文件pop.py的具体实现代码如下。

源码路径:daima\2\2-6\pop.py

from poplib import POP3               #导入内置邮件处理模块
import re,email,email.header          #导入内置文件处理模块
from p_email import mypass            #导入内置模块
def jie(msg_src,names):               #定义解码邮件内容的函数jie()
msg = email.message_from_bytes(msg_src)
    result = {}                       #变量初始化
    for name in names:                #遍历name
        content = msg.get(name)       #获取name
        info = email.header.decode_header(content)    #定义变量info
if info[0][1]:
            if info[0][1].find('unknown-') == -1:     #如果是已知编码
result[name] = info[0][0].decode(info[0][1])
            else:                     #如果是未知编码
                try:                  #异常处理
result[name] = info[0][0].decode('gbk')
except:
result[name] = info[0][0].decode('utf-8')
else:
            result[name] = info[0][0] #获取解码结果
    return result                     #返回解码结果
if __name__ == "__main__":
    pp = POP3("pop.sina.com")           #实例化邮件服务器类
    pp.user('              ')           #传入邮箱地址
    pp.pass_(mypass)                    #密码设置
    total,totalnum = pp.stat()          #获取邮箱的状态
    print(total,totalnum)               #显示统计信息
    for i in range(total-2,total):      #遍历最近的两封邮件
        hinfo,msgs,octet = pp.top(i+1,0)#返回bytes类型的内容
        b=b''
        for msg in msgs:                   #遍历msg
            b += msg+b'\n'
        items = jie(b,['subject','from'])  #调用函数jie()返回邮件主题
        print(items['subject'],'\nFrom:',items['from'])#调用函数jie()返回发件人的信息
        print()                            #显示空行
    pp.close()                             #关闭连接

在上述实例代码中,函数jie()的功能是使用email包来解码邮件头,使用POP3对象的方法连接POP3服务器并获取邮箱中的邮件总数。在程序中获取最近两封邮件的邮件头,然后传递给函数jie()进行分析,并返回邮件的主题和发件人的信息。执行文件pop.py后的效果如图2-6所示。

图2-6 执行文件pop.py后的效果

SMTP即简单邮件传输协议,是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。在Python中,通过smtplib模块对SMTP进行封装,通过这个模块可以登录SMTP服务器并发送邮件。使用SMTP发送邮件有两种方式。

在smtplib模块中,使用类SMTP可以创建一个SMTP对象实例。具体语法格式如下。

import smtplib
smtpObj = smtplib.SMTP(host , port , local_hostname)

各个参数的具体说明如下。

在smtplib模块中,比较常用的内置方法如下。

(1)方法connect()

在Python程序中,如果在创建SMTP对象时没有指定host和port,则可以使用SMTP对象中的方法connect()连接到服务器。connect()方法的语法原型如下。

connect (host, port)

(2)方法login()

在SMTP对象中,方法login()的功能是使用用户名和密码登录SMTP服务器。其语法原型如下。

login(user, password)

(3)方法set_debuglevel()

使用SMTP对象的方法set_debuglevel()可以设置调试级别。其语法原型如下。

set_debuglevel (level)

参数level是指设定的调试级别。

(4)方法docmd()

使用SMTP对象中的方法docmd()可以向SMTP服务器发送命令。其语法原型如下。

docmd (cmd, argstring)

(5)方法sendmail()

使用SMTP对象中的方法sendmail()可以发送邮件。其语法原型如下。

sendmail(from_addr, to_addrs, msg, mail_options, rcpt_options)

(6)方法quit()

使用SMTP对象中的方法quit()可以断开与服务器的连接。

当使用Python发送Email邮件时,需要找到所使用SMTP服务器的地址和端口。例如,对于新浪邮箱,其SMTP服务器的地址为smtp.sina.com,端口为默认值25。实例文件sm.py演示了向指定邮箱发送邮件的过程。

为了防止邮件被反垃圾邮件丢弃,这里采用前面提到的第二种方法,即登录认证后再发送。实例文件sm.py的具体实现代码如下。

源码路径:daima\2\2-6\sm.py

import smtplib,email                                       #导入内置模块
from p_email import mypass                                 #导入内置模块
#使用email模块构建一封邮件
chst = email.charset.Charset(input_charset='utf-8')
header = ("From: %s\nTo: %s\nSubject: %s\n\n"              #邮件主题
       % ("guan****@sina.com",                             #邮箱地址
          "好人" ,                                          #收件人
          chst.header_encode("Python smtplib 测试!")))     #邮件头
body = "你好!"                                             #邮件内容
email_con = header.encode('utf-8') + body.encode('utf-8')    #构建邮件完整内容,使用中文编码处理
smtp = smtplib.SMTP("smtp.sina.com")                       #邮件服务器
smtp.login("guan****@sina.com",mypass)                     #使用用户名和密码登录邮箱
#开始发送邮件
smtp.sendmail("guan****@sina.com","371****@qq.com",email_con)
smtp.quit()                                              #退出系统

在上述实例代码中,使用邮箱guan****@sina.com发送邮件,收件人的邮箱地址是371****@qq.com。首先使用email.charset.Charset()对象对邮件头部进行编码,然后创建SMTP对象,并通过验证的方式给371****@qq.com发送一封测试邮件。因为在邮件的主体内容中有中文字符,所以使用encode()函数进行编码。执行文件sm.py后的效果如图2-7所示。

图2-7 执行文件sm.py后的效果

在Python程序中,内置标准库email的功能是管理电子邮件。具体来说,不是实现向SMTP、NNTP或其他服务器发送任何电子邮件的功能,而是实现诸如smtplib和nntplib之类的库的功能。email库的主要功能是分割内部对象模型中电子邮件表示的电子邮件消息的解析和生成。通过使用email,可以向消息中添加子对象,从消息中删除子对象,完全重新排列内容。

1.核心模块email.message

类Message是email库的核心类,它是email对象模型中的基类,提供了设置和查询邮件头部、访问消息体的核心方法。从概念上讲,Message对象构成了邮件头部和消息体。头部格式在RFC 2822中进行了定义,每个头部由该项的名字和值组成,并由冒号分隔。消息体可以是简单消息对象的字符串或多个MIME容器的Message对象组成的多部分邮件。Message类在email.message模块中定义。

类Message中的主要内置方法如下。

1)as_string(unixfrom=False, maxheaderlen=0, policy=None):当可选unixfrom为真时,以字符串的形式返回全部消息,unixfrom默认为False。maxheaderlen默认为0,如果想要不同的值,则必须明确覆盖它(max_line_length的策略将忽略此方法)。参数policy用于覆盖从消息实例获取的默认策略,这可以用于控制由该方法生成的某些格式,因为指定的策略将传递到Generator。如果需要填写默认值来完成到字符串的转换(例如,可能生成或修改MIME边界),则可能会触发对Message的更改。

2)__str__():等效于前面的as_string(),允许str(msg)生成包含格式化消息的字符串。

3)as_bytes(unixfrom=False, policy=None):返回作为字节对象的整个消息。

4)__bytes__():等效于as_bytes(),允许bytes(msg)生成包含格式化消息的字节对象。

5)is_multipart():如果邮件的有效内容是子Message对象的列表,则返回True;否则,返回False。当is_multipart()返回False时,有效内容应该是字符串对象。

6)set_unixfrom(unixfrom):将邮件的信息头设置为unixfrom。

7)get_unixfrom():如果邮件标题从未设置,则返回邮件的信息头,默认为None。

8)attach(payload):将给定的内容添加到当前有效内容中。

2.模块email.parser

模块email.parser的功能是解析电子邮件的信息。消息对象结构可以通过如下两种方式创建。

模块email.feedparser中的常用成员如下。

1)类email.parser.FeedParser(_factory=email.message.Message, *, policy=policy.compat32):创建FeedParser实例。可选参数_factory是一个无参调用,每当需要新的消息对象时就会使用它,默认为email.message.Message类。如果指定了policy(它必须是policy类的实例),则需要使用它指定的规则来更新消息的表示形式。如果未设置具体的策略,请使用compat32策略。

类email.parser.FeedParser中的内置方法如下。

2)类email.parser.BytesFeedParser(_factory=email.message.Message):除feed()方法的输入必须是字节而不是字符串之外,其工作原理与FeedParser几乎相同。

3)类email.parser.Parser(_class=email.message.Message, *, policy=policy.compat32):是类Parser的构造函数,可选参数_class必须是一个可调用的工厂(例如一个函数或一个类),并且每当需要创建子消息对象时才使用它,默认值为Message(请参阅email.message)。当工厂被调用时没有参数。如果指定了参数policy(它必须是policy类的实例),请使用它指定的规则来更新消息的表示形式。如果未设置策略,请使用compat32策略。

类email.parser.Parser中的内置方法如下。

实例文件youjian.py演示了使用库email和smtplib发送带附件的邮件的过程。

源码路径:daima\2\2-6\youjian.py

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage

sender = '***'
receiver = '***'
subject = 'python email test'
smtpserver = '   '
username = '***'
password = '***'

msgRoot = MIMEMultipart('related')
msgRoot['Subject'] = 'test message'

#构造附件
att = MIMEText(open('h:\\python\\1.jpg', 'rb').read(), 'base64', 'utf-8')
att["Content-Type"] = 'application/octet-stream'
att["Content-Disposition"] = 'attachment; filename="1.jpg"'
msgRoot.attach(att)

smtp = smtplib.SMTP()
smtp.connect('smtp.163.com')
smtp.login(username, password)
smtp.sendmail(sender, receiver, msgRoot.as_string())
smtp.quit()

在计算机网络领域中,远程文件传输又是一个重要的分支。在网络协议中,TCP、FTP、Telnet、UDP可以实现远程文件处理。作为一门功能强大的开发语言,Python可以实现对远程文件的处理。本节将详细讲解使用Python开发远程文件传输系统的过程。

当使用Python编写FTP客户端程序时,需要首先将相应的库ftplib导入项目程序中,然后实例化一个ftplib.FTP类对象,所有的FTP操作(如登录、传输文件和注销等)都要使用这个对象完成。使用类FTP可以创建一个FTP连接对象。具体语法格式如下。

FTP(host, user, passwd, acct)

实例文件ftp.py演示了使用ftplib库创建一个简单的FTP客户端的过程。

源码路径:daima\2\2-7\ftp.py

from ftplib import FTP                          #导入FTP 
bufsize = 1024                                  #设置缓冲区的大小
def Get(filename):                              #定义函数Get()用于下载文件
    command = 'RETR ' + filename                #初始化变量command
    #下载FTP文件
ftp.retrbinary(command, open(filename,'wb').write, bufsize)
    print('下载成功')                            #下载成功的提示
def Put(filename):                              #定义函数Put()用于上传文件
    command = 'STOR ' + filename                #初始化变量command
    filehandler = open(filename,'rb')           #打开指定文件
    ftp.storbinary(command,filehandler,bufsize) #实现文件上传操作
    filehandler.close()                         #关闭连接
    print('上传成功')                            #显示提示
def PWD():                                      #定义获取当前目录的函数PWD()
    print(ftp.pwd())                            #返回当前所在位置
def Size(filename):                             #定义获取文件大小的函数Size()
    print(ftp.size(filename))                   #显示文件大小
def Help():                                     #定义系统帮助函数Help()
    print('''                                   #开始显示帮助提示
    ==================================
        Simple Python FTP 
    ==================================
    cd        进入文件夹
    delete    删除文件
    dir       获取当前文件列表
    get       下载文件
    help      帮助
    mkdir     创建文件夹
    put       上传文件
    pwd       获取当前目录
    rename    重命名文件
    rmdir     删除文件夹
    size      获取文件大小
    ''')
server = input('请输入FTP服务器地址:')              #输入信息
ftp = FTP(server)                                 #获取服务器地址
username = input('请输入用户名:')                  #输入用户名
password = input('请输入密码:')                     #输入密码
ftp.login(username,password)                      #使用用户名和密码登录FTP服务器
print(ftp.getwelcome())                           #显示欢迎信息
#定义一个字典actions,在里面保存操作命令
actions  = {'dir':ftp.dir, 'pwd': PWD, 'cd':ftp.cwd, 'get':Get,
        'put':Put, 'help':Help, 'rmdir': ftp.rmd, 
        'mkdir': ftp.mkd, 'delete':ftp.delete,
        'size':Size, 'rename':ftp.rename}
while True:                                      #执行循环操作
    print('pyftp>')                              #显示提示符
    cmds = input()                               #获取用户的输入
    cmd = str.split(cmds)                        #使用空格分隔用户输入的内容
    try:                                         #异常处理
        if len(cmd) == 1:                        #验证输入命令中是否有参数
            if str.lower(cmd[0]) == 'quit':      #如果输入命令是quit,则退出循环
break
else:
                actions[str.lower(cmd[0])]()     #调用与输出命令对应的操作函数
        elif len(cmd) == 2:                      #处理只有一个参数的命令
            actions[str.lower(cmd[0])](cmd[1])   #调用与输入命令对应的操作函数
        elif len(cmd) == 3:                      #处理有两个参数的命令
            actions[str.lower(cmd[0])](cmd[1],cmd[2]) #调用与输入命令对应的操作函数
        else:                                    #如果是其他情况
            print('输入错误')                     #显示错误提示
    except:
        print('命令出错')
ftp.quit()                                       #退出系统

运行上述实例代码后,会要求输入FTP服务器的地址、用户名和密码。如果正确输入上述信息,则完成FTP服务器的登录,并显示一个“pyftp>”提示符,等待用户输入命令。如果输入“dir”和“pwd”这两个命令,则会调用对应的函数。当运行本实例时,需要有一个FTP服务器及登录该服务器的用户名和密码。如果读者没有互联网中的FTP服务器,则可以尝试在本地计算机中通过IIS配置一个FTP服务器,然后进行测试。执行文件ftp.py后的效果如图2-8所示。

图2-8 执行文件ftp.py后的效果

XML是指可扩展标记语言(eXtensible Markup Language),是标准通用标记语言的子集,是一种用于标记电子文件并使它具有结构的标记语言。在Python应用程序中,常见的XML编程接口有两种,分别是SAX和DOM。所以,相应地,Python中有两种解析XML文件的方法,分别是SAX和DOM方法。Python通过xml库实现对XML的处理。xml库由如下核心模块构成。

本节将详细讲解使用Python解析XML文件的知识。

XML是一种固有的分层数据格式,其中ElementTree可以将整个XML文档表示为树,Element表示此树中的单个节点。与整个文档(从文件读取和写入文件)的交互通常在ElementTree级别上进行,与单个XML元素及其子元素的交互在Element级别上完成。

在Python程序中,Element类型是一种灵活的容器对象,用于在内存中存储结构化数据。xml.etree.ElementTree模块中常用的公共方法如下。

1)xml.etree.ElementTree.Comment(text=None):用于创建一个特殊的元素,被序列化为XML注释。注释字符串可以是字节表或Unicode字符串。参数text是一个包含注释字符串的字符串。返回一个表示注释的元素实例。

2)xml.etree.ElementTree.dump(elem):将元素树或元素结构写入sys.stdout,此方法仅用于调试。参数elem是元素树或单个元素。

3)xml.etree.ElementTree.fromstring(text):从字符串常量解析XM文档。text是一个包含XML数据的字符串。与XML()方法类似,该方法返回一个Element实例。

4)xml.etree.ElementTree.fromstringlist(sequence, parser=None):从字符串的序列对象中解析XML文档。默认parser为XMLParser,返回Element实例。

5)xml.etree.ElementTree.iselement(element):判断对象是否是一个element对象。参数element是元素实例,如果这是元素对象,则返回true值。

6)xml.etree.ElementTree.iterparse(source, events=None, parser=None):将文件或包含XML数据的文件对象递增地解析为ElementTree,并且报告进度。参数events是一个事件列表,如果忽略,则只报告end事件。

注意:iterparse()只有在遇到开始标签的">"符号时才会抛出start事件,由于此时属性已经定义,而text和tail属性还没有定义,同样子元素也没有定义,因此它们可能不能显示出来。如果要显示完整的元素,请使用end事件。

7)xml.etree.ElementTree.parse(source, parser=None):将一个文件或者字符串解析为ElementTree,并解析XML节点。其参数如下。

8)xml.etree.ElementTree.ProcessingInstruction(target, text=None):创建一个特别的element,该element被序列化为一个XML命令。其参数如下。

9)xml.etree.ElementTree.register_namespace(prefix, uri):注册命名空间的前缀,这个注册是全局有效的,任何已经给出的前缀或者命名空间URI的映射关系会被删除。参数uri是一个命名空间uri,此命名空间中的标签和属性将使用给定的前缀进行序列化。

10)xml.etree.ElementTree.SubElement(parent, tag, attrib={}, **extra):子元素的工厂方法,功能是创建一个Element实例并追加到已知的节点中。

11)xml.etree.ElementTree.tostring(element, encoding="us-ascii", method="xml"):生成一个字符串来表示XML的元素,包括所有子元素。参数element是一个Element实例,method为"xml" "html"或"text"。返回包含XML数据的字符串。

12)xml.etree.ElementTree.tostringlist(element, encoding="us-ascii", method="xml"):生成一个字符串来表示XML的元素,包括所有子元素。参数element是一个Element实例,method为"xml" "html"或"text"。返回包含XML数据的字符串列表。

13)xml.etree.ElementTree.XML(text, parser=None):功能是从一个字符串常量中解析出XML片段,并返回Element实例。

14)xml.etree.ElementTree.XMLID(text, parser=None):功能是从字符串常量解析出XML片段,同时返回一个字典,用于映射element的id到其自身。

模块xml.etree.ElementTree中常用的类和方法如下:

1)类xml.etree.ElementTree.Element(tag, attrib={}, **extra):定义Element接口,并提供此接口的参考实现。tag是元素名称;attrib是一个可选的字典,包含元素属性;extra包含作为关键字参数给出的其他属性。

在类Element中包含如下方法。

2)类xml.etree.ElementTree.ElementTree(element=None, file=None)

类ElementTree是一个包装类,此类表示整个元素的层次结构,并为标准XML中的序列化添加了一些额外的支持。参数element是根元素,如果不为空,则使用XML文件的内容初始化树。

在类ElementTree中包含如下方法。

3)类xml.etree.ElementTree.QName(text_or_uri, tag=None):QName是一个包装器,能够包装QName属性值,以便在输出上获得正确的命名空间。其参数如下。

4)类xml.etree.ElementTree.TreeBuilder(element_factory=None):类TreeBuilder是一个通用元素结构的构建器,能够将开始、数据和结束方法调用的顺序转换为格式良好的元素结构。可以使用类TreeBuilder的自定义XML解析器或其他类似XML格式的解析器来构建元素结构。当给定参数element_factory时,可调用函数必须接受两个位置参数——标记和属性的dict。

在类TreeBuilder中包含如下内置方法。

5)类xml.etree.ElementTree.XMLParser(target=None, encoding=None):类XMLParser是模块的低级构建块,使用xml.parsers.expat来进行基于事件的XML解析。类XMLParser可以使用feed()方法递增XML数据,并且通过调用目标对象上的回调对象,将解析事件转换为推送API。如果省略参数target,则使用标准TreeBuilder。如果给定了参数encoding,则该值将覆盖XML文件中指定的编码。

在类XMLParser中包含如下内置方法。

6)类xml.etree.ElementTree.XMLPullParser(events=None):类XMLPullParser是一个适用于非阻塞应用程序的拉式解析器。其输入端API类似于XMLParser,但不是将调用推送到回调目标。XMLPullParser能够收集解析事件的内部列表,并让用户从中读取它。参数events是指要报告的事件序列,支持的事件是字符串,有start、end、start-ns和end-ns。如果省略events参数,则仅报告end事件。

在类XMLPullParser中包含如下方法。

下面的实例代码演示了使用库xml.etree.ElementTree读取XML文件的过程。其中XML文件test.xml的具体实现代码如下。

源码路径:daima\2\2-7\test.xml

<students>
    <student name='刘备' sex='男' age='35'/>
    <student name='吕布' sex='男' age='38'/>
    <student name='貂蝉' sex='女' age='22'/>
</students>

文件ElementTreeuse.py的功能是获取上述XML文件节点中的元素。具体实现代码如下。

源码路径:daima\2\2-7\ElementTreeuse.py

#从文件中读取数据
import xml.etree.ElementTree as ET

#全局唯一标识符
unique_id = 1

#遍历所有的节点
def walkData(root_node, level, result_list):
    global unique_id
    temp_list = [unique_id, level, root_node.tag, root_node.attrib]
    result_list.append(temp_list)
    unique_id += 1

    #遍历每个子节点
    children_node = root_node.getchildren()
    if len(children_node) == 0:
        return
    for child in children_node:
        walkData(child, level + 1, result_list)
    return

#获得原始数据
# out:
# [
#    #ID, Level, Attr Map
#    [1, 1, {'ID':1, 'Name':'test1'}],
#    [2, 1, {'ID':1, 'Name':'test2'}],
# ]
def getXmlData(file_name):
    level = 1  # 节点的深度从1开始
    result_list = []
    root = ET.parse(file_name).getroot()
    walkData(root, level, result_list)

    return result_list

if __name__ == '__main__':
    file_name = 'test.xml'
    R = getXmlData(file_name)
    for x in R:
        print(x)
    pass

执行后会输出:

[1, 1, 'students', {}]
[2, 2, 'student', {'name': '刘备', 'sex': '男', 'age': '35'}]
[3, 2, 'student', {'name': '吕布', 'sex': '男', 'age': '38'}]
[4, 2, 'student', {'name': '貂蝉', 'sex': '女', 'age': '22'}]

在Python的标准库中包含了SAX解析器,SAX通过使用事件驱动模型,在解析XML的过程中触发一个个的事件并调用用户定义的回调函数来处理XML文件。

SAX是一种基于事件驱动的API,当利用SAX解析XML文档时,会涉及如下两个部分。

当在Python程序中使用SAX方式处理XML文件时,需要先引入文件xml.sax中的parse()函数,以及xml.sax.handler中的类ContentHandler。其中类ContentHandler中的常用函数如下。

1)characters(content):调用时机如下。

其中,标签可以是开始标签,也可以是结束标签。

2)startDocument():在文档启动时调用。

3)endDocument():当解析器到达文档结尾时调用。

4)startElement(name, attrs):在遇到XML开始标签时调用,name是标签的名字,attrs是标签的属性值字典。

5)endElement(name):在遇到XML结束标签时调用。

在xml.sax中的常用内置函数如下。

1)parse():通过如下语法格式创建一个SAX解析器,并解析XML文档。

xml.sax.parse( xmlfile, handler[, errorhandler])

各个参数的具体说明如下。

2)parseString():创建一个XML解析器并解析XML字符串。具体语法格式如下。

xml.sax.parseString(xmlstring, contenthandler[, errorhandler])

各个参数的具体说明如下。

下面的实例代码演示了使用SAX方法解析XML文件的过程。其中实例文件movies.xml是一个基本的XML文件,在里面保存了一些与电影有关的资料信息。文件movies.xml的具体实现代码如下。

源码路径:daima\2\2-8\movies.xml

<collection shelf="New Arrivals">
<movie title="Enemy Behind">
<type>War, Thriller</type>
<format>DVD</format>
<year>2003</year>
<rating>PG</rating>
<stars>10</stars>
<description>Talk about a US-Japan war</description>
</movie>
<movie title="Transformers">
<type>Anime, Science Fiction</type>
<format>DVD</format>
<year>1989</year>
<rating>R</rating>
<stars>8</stars>
<description>A scientific fiction</description>
</movie>
<movie title="Trigun">
<type>Anime, Action</type>
<format>DVD</format>
<episodes>4</episodes>
<rating>PG</rating>
<stars>10</stars>
<description>Vash the Stampede!</description>
</movie>
<movie title="Ishtar">
<type>Comedy</type>
<format>VHS</format>
<rating>PG</rating>
<stars>2</stars>
<description>Viewable boredom</description>
</movie>
</collection>

实例文件sax.py的功能是解析文件movies.xml的内容。具体实现代码如下。

源码路径:daima\2\2-8\sax.py

import xml.sax
class MovieHandler( xml.sax.ContentHandler ):
def __init__(self):
      self.CurrentData = ""
      self.type = ""
      self.format = ""
      self.year = ""
      self.rating = ""
      self.stars = ""
      self.description = ""
   #元素开始调用
def startElement(self, tag, attributes):
      self.CurrentData = tag
if tag == "movie":
print ("*****Movie*****")
title = attributes["title"]
print ("Title:", title)
   #元素结束调用
def endElement(self, tag):
      if self.CurrentData == "type":          #处理XML中的type元素
print ("Type:", self.type)
      elif self.CurrentData == "format":      #处理XML中的format元素
print ("Format:", self.format)
      elif self.CurrentData == "year":        #处理XML中的year元素
print ("Year:", self.year)
      elif self.CurrentData == "rating":      #处理XML中的rating元素
print ("Rating:", self.rating)
      elif self.CurrentData == "stars":       #处理XML中的stars元素
print ("Stars:", self.stars)
      elif self.CurrentData == "description": #处理XML中的description元素
print ("Description:", self.description)
      self.CurrentData = ""
   #在读取字符时调用
def characters(self, content):
if self.CurrentData == "type":
         self.type = content
elif self.CurrentData == "format":
         self.format = content
elif self.CurrentData == "year":
         self.year = content
elif self.CurrentData == "rating":
         self.rating = content
elif self.CurrentData == "stars":
         self.stars = content
elif self.CurrentData == "description":
         self.description = content
if ( __name__ == "__main__"):
   #创建一个XMLReader
parser = xml.sax.make_parser()
parser.setFeature(xml.sax.handler.feature_namespaces, 0)
   #重写ContentHandler
   Handler = MovieHandler()
parser.setContentHandler( Handler )
parser.parse("movies.xml")

执行文件movies.xml后的效果如图2-9所示。

图2-9 执行文件movies.xml后的效果

DOM是Document Object Model(文档对象模型)的简称,是W3C组织推荐的处理可扩展标记语言的标准编程接口。当DOM解析器在解析一个XML文档时,可以一次性读取整个文档。将文档中的所有元素保存在内存的树结构中后,可以利用DOM提供的不同的函数来读取或修改文档的内容和结构,也可以把修改过的内容写入XML文件中。

在Python中,与DOM解析方法相关的方法有3个,具体说明如下。

实例文件dom.py演示了使用DOM方法解析XML文件的过程。实例文件dom.py的功能是解析文件movies.xml的内容。具体实现代码如下。

源码路径:daima\2\2-8\dom.py

from xml.dom.minidom import parse
import xml.dom.minidom
#使用minidom解析器打开XML文档
DOMTree = xml.dom.minidom.parse("movies.xml")
collection = DOMTree.documentElement
if collection.hasAttribute("shelf"):
print ("Root element : %s" % collection.getAttribute("shelf"))
#在集合中获取所有电影
movies = collection.getElementsByTagName("movie")
#显示每部电影的详细信息
for movie in movies:
print ("*****Movie*****")
if movie.hasAttribute("title"):
print ("Title: %s" % movie.getAttribute("title"))
type = movie.getElementsByTagName('type')[0]
print ("Type: %s" % type.childNodes[0].data)
format = movie.getElementsByTagName('format')[0]
print ("Format: %s" % format.childNodes[0].data)
rating = movie.getElementsByTagName('rating')[0]
print ("Rating: %s" % rating.childNodes[0].data)
description = movie.getElementsByTagName('description')[0]
print ("Description: %s" % description.childNodes[0].data)

执行后会输出:

Root element : New Arrivals
*****Movie*****
Title: Enemy Behind
Type: War, Thriller
Format: DVD
Rating: PG
Description: Talk about a US-Japan war
*****Movie*****
Title: Transformers
Type: Anime, Science Fiction
Format: DVD
Rating: R
Description: A schientific fiction
*****Movie*****
Title: Trigun
Type: Anime, Action
Format: DVD
Rating: PG
Description: Vash the Stampede!
*****Movie*****
Title: Ishtar
Type: Comedy
Format: VHS
Rating: PG
Description: Viewable boredom

下面的实例代码演示了使用DOM获取XML文件中指定元素值的过程。其中XML文件user.xml的代码如下。

源码路径:daima\2\2-8\user.xml

<?xml version="1.0" encoding="UTF-8" ?>
<users>
    <user id="1000001">
        <username>Admin</username>
        <email>admin@example.com</email>
        <age>23</age>
        <sex>boy</sex>
    </user>
    <user id="1000002">
        <username>Admin2</username>
        <email>admin2@example.com</email>
        <age>22</age>
        <sex>boy</sex>
    </user>
    <user id="1000003">
        <username>Admin3</username>
        <email>admin3@example.com</email>
        <age>27</age>
        <sex>boy</sex>
    </user>
    <user id="1000004">
        <username>Admin4</username>
        <email>admin4@example.com</email>
        <age>25</age>
        <sex>girl</sex>
    </user>
    <user id="1000005">
        <username>Admin5</username>
        <email>admin5@example.com</email>
        <age>20</age>
        <sex>boy</sex>
    </user>
    <user id="1000006">
        <username>Admin6</username>
        <email>admin6@example.com</email>
        <age>23</age>
        <sex>girl</sex>
    </user>
</users>

实例文件domuse.py的功能是解析文件movies.xml的内容。具体实现代码如下。

源码路径:daima\2\2-8\domuse.py

from xml.dom import minidom

def get_attrvalue(node, attrname):
     return node.getAttribute(attrname) if node else ''

def get_nodevalue(node, index = 0):
    return node.childNodes[index].nodeValue if node else ''

def get_xmlnode(node, name):
    return node.getElementsByTagName(name) if node else []

def get_xml_data(filename = 'user.xml'):
    doc = minidom.parse(filename)
    root = doc.documentElement

    user_nodes = get_xmlnode(root, 'user')
    print ("user_nodes:", user_nodes)

    user_list=[]
    for node in user_nodes:
        user_id = get_attrvalue(node, 'id')
        node_name = get_xmlnode(node, 'username')
        node_email = get_xmlnode(node, 'email')
        node_age = get_xmlnode(node, 'age')
        node_sex = get_xmlnode(node, 'sex')

        user_name =get_nodevalue(node_name[0])
        user_email = get_nodevalue(node_email[0])
        user_age = int(get_nodevalue(node_age[0]))
        user_sex = get_nodevalue(node_sex[0])

        user = {}
        user['id'] , user['username'] , user['email'] , user['age'] , user['sex'] = (
            int(user_id), user_name , user_email , user_age , user_sex
        )
        user_list.append(user)
    return user_list

def test_load_xml():
    user_list = get_xml_data()
    for user in user_list :
        print ('-----------------------------------------------------')
        if user:
            user_str='No.:\t%d\nname:\t%s\nsex:\t%s\nage:\t%s\nEmail:\t%s' % (int(user  
                    ['id']) , user['username'], user['sex'] , user['age'] , user['email'])
            print (user_str)

if __name__ == "__main__":
    test_load_xml()

执行后会输出:

-----------------------------------------------------
No.:    1000001
name:    Admin
sex:    boy
age:    23
Email:    admin@example.com
-----------------------------------------------------
No.:    1000002
name:    Admin2
sex:    boy
age:    22
Email:    admin2@example.com
-----------------------------------------------------
No.:    1000003
name:    Admin3
sex:    boy
age:    27
Email:    admin3@example.com
-----------------------------------------------------
No.:    1000004
name:    Admin4
sex:    girl
age:    25
Email:    admin4@example.com
-----------------------------------------------------
No.:    1000005
name:    Admin5
sex:    boy
age:    20
Email:    admin5@example.com.
-----------------------------------------------------
Ran 1 test in 0.031s

OK

-----------------------------------------------------
No.:    1000006
name:    Admin6
sex:    girl
age:    23
Email:    admin6@example.com

在Python程序中,模块xml.parsers.expat的功能是动态地解析XML文件。其中提供了一个单一的扩展类型xmlparser,表示XML解析器的当前状态。在创建了xmlparser对象后,可以将对象的各种属性设置为处理函数。

模块xml.parsers.expat提供了如下异常。

除上面的异常之外,模块xml.parsers.expat还提供了一个类型对象xml.parsers.expat. XMLParserType,表示函数ParserCreate()的返回值类型。

在xml.parsers.expat模块中包含了如下两个方法。

在xmlparser对象中包含了如下方法。

在xmlparser对象中包含了如下属性。

实例文件xmlparser.py演示了使用xmlparser方法解析XML文件的过程。实例文件xmlparser.py的具体实现代码如下。

源码路径:daima\2\2-8\xmlparser.py

import xml.parsers.expat

#3个处理程序
def start_element(name, attrs):
    print('Start element:', name, attrs)
def end_element(name):
    print('End element:', name)
def char_data(data):
    print('Character data:', repr(data))

p = xml.parsers.expat.ParserCreate()

p.StartElementHandler = start_element
p.EndElementHandler = end_element
p.CharacterDataHandler = char_data

p.Parse("""<?xml version="1.0"?>
<parent id="top"><child1 name="paul">Text goes here</child1>
<child2 name="fred">More text</child2>
</parent>""", 1)

在上述代码中分别定义了3个函数start_element()、end_element()和char_data(),分别解析了指定XML代码中的开始标记、结束标记和数据元素。执行后会输出:

Start element: parent {'id': 'top'}
Start element: child1 {'name': 'paul'}
Character data: 'Text goes here'
End element: child1
Character data: '\n'
Start element: child2 {'name': 'fred'}
Character data: 'More text'
End element: child2
Character data: '\n'
End element: parent

JSON是 JavaScript Object Notation 的缩写,是一种轻量级的数据交换格式。JSON 基于ECMAScript的一个子集。本节将详细讲解使用Python解析JSON数据的知识。

在JSON的编码和解码过程中,Python的原始类型与JSON类型会相互转换。在编码时Python数据类型与JSON类型的转换关系如表2-4所示。

表2-4 在编码时Python数据类型与JSON类型的转换关系

Python数据类型

JSON类型

dict

object

list、tuple

array

str

string

int、float、派生自int与float的Enum

number

True

true

False

false

None

null

在解码时JSON类型与Python数据类型的转换关系如表2-5所示。

表2-5 在解码时JSON类型与Python数据类型的转换关系

JSON类型

Python数据类型

object

dict

array

list

string

str

number (int)

int

number (real)

float

true

True

false

False

null

None

在Python程序中,可以使用json模块对JSON数据进行编码和解码操作。json模块主要包含如下方法。

1)json.dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)

json.dump()方法的功能是对数据进行编码,将obj序列化为fp。因为json模块总是产生str对象,而不是bytes对象,所以fp.write()必须支持str输入。

2)json.dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None, separators=None, default=None, sort_keys=False, **kw)

json.dumps()方法的功能是使用转换关系将obj序列化为JSON格式的str。各个参数的含义与上面的方法json.dump()完全相同。

3)json.load(fp, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw)

方法json.load()的功能是对数据进行解码。

4)方法json.loads(s, encoding=None, cls=None, object_hook=None, parse_float=None, parse_int=None, parse_constant=None, object_pairs_hook=None, **kw)

方法json.loads()的功能是使用转换关系将包含JSON文档的s(一个str实例)解压缩为Python对象。其他参数的含义与方法json.load()完全相同,在此不再详细介绍。如果反序列化的数据不是有效的JSON文档,则会引发JSONDecodeError错误。

实例文件js.py演示了将Python字典类型转换为JSON对象的过程。

源码路径:daima\2\2-9\js.py

import json
#将字典类型转换为JSON对象
data = {
    'no' : 1,
    'name' : 'laoguan',
    'url' : 'http://***
}
json_str = json.dumps(data)
print ("Python 原始数据:", repr(data))
print ("JSON 对象:", json_str)

执行文件js.py后的效果如图2-10所示。通过输出结果可以看出,简单类型通过编码后和它原始的输出结果非常相似。

图2-10 执行文件js.py后的效果

在下面的实例文件fan.py中,可以将一个JSON编码的字符串转换为一个Python数据结构。

源码路径:daima\2\2-9\fan.py

import json
#将字典类型转换为JSON对象
data1 = {
    'no' : 1,
    'name' : 'laoguan',
    'url' : 'http://www.toppr.net'
}
json_str = json.dumps(data1)
print ("Python 原始数据:", repr(data1))
print ("JSON 对象:", json_str)
#将 JSON 对象转换为字典
data2 = json.loads(json_str)
print ("data2['name']: ", data2['name'])
print ("data2['url']: ", data2['url'])

执行文件fan.py后的效果如图2-11所示。

图2-11 执行文件fan.py后的效果

在Python程序中,如果要处理的是JSON文件而不是字符串,那么可以使用函数json.dump()和函数json.load()来编码与解码JSON数据,如下面的演示代码。

#写入JSON数据
with open('data.json', 'w') as f:
json.dump(data, f)
#读取数据
with open('data.json', 'r') as f:
data = json.load(f)

实例文件jsonparser.py演示了编写自定义类jsonparser解析JSON数据的过程。

源码路径:daima\2\2-9\jsonparser.py

import sys
from imp import reload
reload(sys)
import json

def txt2str(file='jsondata2.txt'):
    '''
    打开指定的JSON文件
    '''
    fp=open(file,encoding='UTF-8')
    allLines = fp.readlines()
    fp.close()
    str=""
    for eachLine in allLines:
        #eachLine=ConvertCN(eachLine)

        #转换成字符串
        for i in range(0,len(eachLine)):
            #if eachLine[i]!= ' ' and eachLine[i]!= '    ' and eachLine[i]!='\n': 
            #删除空格和换行符,但是json双引号中的空格不能删除
            str+=eachLine[i]
    return str

class jsonparser:

    def __init__(self, str=None):
        self._str = str
        self._index=0

    def _skipBlank(self):  
        '''
        跳过空白、换行符或制表符
        '''
        while self._index<len(self._str) and self._str[self._index] in ' \n\t\r':
            self._index=self._index+1
    def parse(self):
        '''
        进行解析的主要函数
        '''
        self._skipBlank()
        if self._str[self._index]=='{':
            self._index+=1
            return self._parse_object()
        elif self._str[self._index] == '[':
            self._index+=1
            return self._parse_array()
        else:
            print("Json format error!")
    def _parse_string(self):
        '''
        找出两个双引号中的字符串
        '''
        begin = end =self._index
        #找到字符串的范围
        while self._str[end]!='"':
            if self._str[end]=='\\': # 出现\,表明其后面是要转义的字符,如\"、\t、\r,主要
                                     # 针对\"的情况
                end+=1
                if self._str[end] not in '"\\/bfnrtu':
                    print
            end+=1
        self._index = end+1
        return self._str[begin:end]

    def _parse_number(self):
        '''
        数值没有双引号
        '''
        begin = end = self._index
        end_str=' \n\t\r,}]' #以数字结束的字符串
        while self._str[end] not in end_str:
            end += 1
        number = self._str[begin:end]

        #进行转换
        if '.' in number or 'e' in number or 'E' in number :
            res = float(number)
        else:
            res = int(number)
        self._index = end
        return res

    def _parse_value(self):
        '''
        解析值,包括字符串、数字
        '''
        c = self._str[self._index]

        #解析对象
        if c == '{':
            self._index+=1
            self._skipBlank()
            return self._parse_object()
        #解析数组
        elif c == '[':
            #数组
            self._index+=1
            self._skipBlank()
            return self._parse_array()
        #解析字符串
        elif c == '"':    
            #字符串
            self._index += 1
            self._skipBlank()
            return self._parse_string()
        #解析null
        elif c=='n' and self._str[self._index:self._index+4] == 'null':
            #null
            self._index+=4
            return None
        #解析布尔变量true
        elif c=='t' and self._str[self._index:self._index+4] == 'true':
            #true
            self._index+=4
            return True
        #解析布尔变量false
        elif c=='f' and self._str[self._index:self._index+5] == 'false':
            #false
            self._index+=5
            return False
        #剩下的情况为number
        else:
            return self._parse_number()

    def _parse_array(self):
        '''
        解析数组
        '''
        arr=[]
        self._skipBlank()
        #空数组
        if self._str[self._index]==']':
            self._index +=1
            return arr
        while True:
            val = self._parse_value()  #获取数组中的值,可能是字符串、obj等
            arr.append(val)            #添加到数组中
            self._skipBlank()          #跳过空白
            if self._str[self._index] == ',':
                self._index += 1
                self._skipBlank()
            elif self._str[self._index] ==']':
                self._index += 1
                return arr
            else:
                print("array parse error!")
                return None

    def _parse_object(self):
        '''
        解析对象
        '''
        obj={}
        self._skipBlank()
        #空object
        if self._str[self._index]=='}':
            self._index +=1
            return obj
        #elif self._str[self._index] !='"': 
            #报错

        self._index+=1 #跳过当前的双引号
        while True:
            key = self._parse_string() #获取键值
            self._skipBlank()

            self._index = self._index+1#跳过冒号
            self._skipBlank()

            #self._index = self._index+1#跳过双引号
            #self._skipBlank()
            #获取值,目前假设只有字符串的值和数字
            obj[key]= self._parse_value()
            self._skipBlank()
            #print key,":",obj[key]
            #对象结束了,使用break
            if self._str[self._index]=='}':
                self._index +=1
                break
            elif self._str[self._index]==',':
                self._index +=1
                self._skipBlank()
            self._index +=1#跳过下一个对象的第一个双引号
        return obj#返回对象

    def display(self):
        displayStr=""
        self._skipBlank()
        while self._index<len(self._str):
            displayStr=displayStr+self._str[self._index]
            self._index=self._index+1
            self._skipBlank()
        print(displayStr)

def _to_str(pv):
    '''把Python变量转换成字符串'''
    _str=''
    if type(pv) == type({}):
        #处理对象
        _str+='{'
        _noNull = False
        for key in pv.keys():
            if type(key) == type(''):
                _noNull = True #对象非空
                _str+='"'+key+'":'+_to_str(pv[key])+','
        if _noNull:
            _str = _str[:-1] #把最后的逗号去掉
        _str +='}'

    elif type(pv) == type([]):
        #处理数组
        _str+='['
        if len(pv) >0: #数组不为空,方便合并后续格式
            _str += _to_str(pv[0])
        for i in range(1,len(pv)):
            _str+=','+_to_str(pv[i])#因为已经合并了第一个,所以可以加逗号
        _str+=']'

    elif type(pv) == type(''): 
        #字符串
        _str = '"'+pv+'"'
    elif pv == True:
        _str+='true'
    elif pv == False:
        _str+='false'
    elif pv == None:
        _str+='null'
    else:
        _str = str(pv)
    return _str

#main函数
if __name__ == '__main__':
    print("test")
    '''
    jsonInstance=jsonparser(txt2str())
    jsonTmp = jsonInstance.parse()
    print jsonTmp
    print jsonTmp['obj1']['family']['father']
    print jsonTmp['obj1']['family']['sister']

    print ' '
    jsonInstance=jsonparser(txt2str('jsondataArray.txt'))
    jsonTmp = jsonInstance.parse()
    print jsonTmp
    print ' '
    '''
    jsonInstance=jsonparser(txt2str('jsonTestFile.txt'))
    jsonTmp = jsonInstance.parse()
    print(jsonTmp)
    print(_to_str(jsonTmp))

    print(' ')
    jsonInstance=jsonparser(txt2str('json.txt'))
    jsonTmp = jsonInstance.parse()
    print(jsonTmp)

    print(_to_str(jsonTmp))

在上述代码中,提及的JSON主体内容主要指对象(object)和数组(array)。因为一个JSON格式的字符串不是一个对象就是一个数组,所以在编写的类jsonparser中,有_parse_object()和_parse_array()两个函数。首先通过parse()函数直接判断开始的符号为大括号“{”还是中括号“[”,进而决定调用_parse_object()还是_parse_array()函数。在标准JSON格式中的键是字符串类型,使用双引号包括,其中_parse_string()函数专门用来解析键。而JSON格式中的值则相对复杂一些,类型可以是对象、数组、字符串、数字、true、false和null。

通过上述代码,设置如果遇到的字符为大括号“{”,则调用_parse_object()函数。如果遇到的字符为中括号“[”,则调用_parse_array()函数,并且把value的解析统一封装到_parse_value()函数中。

本实例执行后会输出:

test
['JSON Test Pattern pass1', {'object with 1 member': ['array with 1 element']}, {}, [], -42, True, False, None, {'integer': 1234567890, 'real': -9876.54321, 'e': 1.23456789e-13, 'E': 1.23456789e+34, '': -inf, 'zero': 0, 'one': 1, 'space': '', 'singlequote': '\'\\"', 'singlequote2': "'", 'quote': '\\"', 'backslash': '\\\\', 'controls': '\\b\\f\\n\\r\\t', 'slash': '/ & \\/', 'alpha': 'abcdefghijklmnopqrstuvwyz', 'ALPHA': 'ABCDEFGHIJKLMNOPQRSTUVWYZ', 'digit': '0123456789', 'special': "`1~!@#$%^&*()_+-={':[,]}|;.</>?", 'hex': '\\u0123\\u4567\\u89AB\\uCDEF\\uabcd\\uef4A', 'true': True, 'false': False, 'null': None, 'array': [], 'object': {}, 'address': '50 St. James Street', 'url': 'http://', 'comment': '// /* <!-- --', '# -- --> */': '', ' s p a c e d ': [1, 2, 3, 4, 5, 6, 7], 'compact': [1, 2, 3, 4, 5, 6, 7], 'jsontext': '{\\"object with 1 member\\":[\\"array with 1 element\\"]}', '\\/\\\\\\"\\uCAFE\\uBABE\\uAB98\\uFCDE\\ubcda\\uef4A\\b\\f\\n\\r\\t`1~!@#$%^&*()_+-=[]{}|;:\',./<>?': 'A key can be any string'}, 0.5, 98.6, 99.44, 1066, 'rosebud']
["JSON Test Pattern pass1",{"object with 1 member":["array with 1 element"]},{},[],-42,true,false,null,{"integer":1234567890,"real":-9876.54321,"e":1.23456789e-13,"E":1.23456789e+34,"":-inf,"zero":false,"one":true,"space":"","singlequote":"'\"","singlequote2":"'","quote":"\"","backslash":"\\","controls":"\b\f\n\r\t","slash":"/ & \/","alpha":"abcdefghijklmnopqrstuvwyz","ALPHA":"ABCDEFGHIJKLMNOPQRSTUVWYZ","digit":"0123456789","special":"`1~!@#$%^&*()_+-={':[,]}|;.</>?","hex":"\u0123\u4567\u89AB\uCDEF\uabcd\uef4A","true":true,"false":false,"null":null,"array":[],"object":{},"address":"50 St. James Street","url":"http://www.JSON.org/","comment":"// /* <!-- --","# -- --> */":""," s p a c e d ":[true,2,3,4,5,6,7],"compact":[true,2,3,4,5,6,7],"jsontext":"{\"object with 1 member\":[\"array with 1 element\"]}","\/\\\"\uCAFE\uBABE\uAB98\uFCDE\ubcda\uef4A\b\f\n\r\t`1~!@#$%^&*()_+-=[]{}|;:',./<>?":"A key can be any string"},0.5,98.6,99.44,1066,"rosebud"]

Json format error!
None
null


相关图书

深度学习的数学——使用Python语言
深度学习的数学——使用Python语言
动手学自然语言处理
动手学自然语言处理
Web应用安全
Web应用安全
Python高性能编程(第2版)
Python高性能编程(第2版)
图像处理与计算机视觉实践——基于OpenCV和Python
图像处理与计算机视觉实践——基于OpenCV和Python
Python数据科学实战
Python数据科学实战

相关文章

相关课程