书名:Python核心编程(第3版)
ISBN:978-7-115-41477-9
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
• 著 [美] Wesley Chun
译 孙波翔 李 斌 李 晗
责任编辑 傅道坤
• 人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
• 读者服务热线:(010)81055410
反盗版热线:(010)81055315
Authorized translation from the English language edition, entitled Core Python Applications Programming (3rd edition), 9780132678209 by Wesley J. Chun published by Pearson Education, Inc., publishing as Prentice Hall, Copyright © 2012 Pearson Education, Inc.
All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education Inc. CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD., and POSTS & TELECOMMUNICATIONS PRESS Copyright © 2016.
本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签。无标签者不得销售。
本书是经典畅销图书《Python核心编程(第二版)》的全新升级版本,总共分为3部分。第1部分讲解了Python的一些通用应用,包括正则表达式、网络编程、Internet客户端编程、多线程编程、GUI编程、数据库编程、Microsoft Office编程、扩展Python等内容。第2部分讲解了与Web开发相关的主题,包括Web客户端和服务器、CGI和WSGI相关的Web编程、Diango Web框架、云计算、高级Web服务。第3部分则为一个补充/实验章节,包括文本处理以及一些其他内容。
本书适合具有一定经验的Python开发人员阅读。
“本书简洁而不失其技术深度,内容丰富全面,历史资料翔实齐全,这让本书成为学习Python的完美教程。本书易于阅读,以极简的文字介绍了复杂的案例,同时涵盖了其他同类图书中很少涉及的历史参考资料。简而言之,本书棒极了!”
——Gloria. W
“期待已久的Core Python Programming(第2版)已经证明了本书确实值得期待——它深度与广度齐备,其中囊括的有用练习可以帮助读者掌握Python并付之于实践。”
——Alex Martelli,Python in a Nutshell作者兼Python Cookbook编辑
“Wesley Chun的Core Python Programming一书好评如潮,而且它也证明它配得上所有的好评。我想该书是当前学习Python的最佳图书。在市面上众多的Python图书中,我觉得Chun的这本书是最好的,因此强烈推荐本书。”
——David Mertz博士,IBM DeveloperWorks
“在过去多年,我一直在从事Python的研究,发现本书获得了大量的正面评价。这些评价证实了这样一个观点,即Core Python Programming被认为是标准的Python入门读物。”
——Richard Ozaki,Lockheed Martin公司
“终于,一本既可以作为Python教程又可以作为Python编程语言参考的图书问世了!”
——Michael Baxter,Linux Journal
“本书写作相当精良。这是我遇到的最清晰、最友好的Python图书,它在一个广阔的背景下介绍了Python。它仔细、深入地剖析了一些重要的Python主题,而且读者无需大量的相关经验也能看懂。与其他所有Python入门类图书不同的是,它不会用隐晦、难以理解的文字来折磨读者,而是始终立足于帮助读者牢固掌握Python的语法和结构。”
——http://python.org bookstore Web site
“如果我只能有一本Python图书,那它肯定是Wesley Chun著作的Core Python Programming。本书成功地涵盖了Python的多个主题,其详细程度远甚于Learning Python一书,而且涵盖的主题也远非Python核心语言这么简单。如果你只打算购买一本Python图书,我强烈推荐本书。你不仅会爱上本书,而且会爱上本书中包含的真知灼见。重要的是,你将学会Python。更更重要的是,你会发现本书会在你每日的Python编程生活中提供各种帮助。写得不错,Chun先生!”
——Ron Stephens,Python学习基金会
“我认为编程初学者的最佳语言是Python,这毋庸置疑!我最喜欢的图书是Core Python Programming。”
——s003apr,MP3Car.com论坛
“就我个人而言,我相当喜欢Python。它易于学习、非常直观、相当灵活,而且执行速度也相当快。在Windows领域中,Python虽然只是刚崭露头角,但是由于越来越多的人发现了它,因此选择从Python起步可以获得大量的支持。要学习Python,我选择从Wesley Chun的这本Core Python Programming起步。”
——Bill Boswell,MCSE,微软认证专家在线杂志
“如果你通过图书来学习编程,我推荐Core Python Programming,它是目前为止我发现的最佳Python图书。我也是一个Python新手,但是在3个月之后,我就可以在项目中实现Python了(自动处理MSOffice、SQL DB等)。”
——ptonman,Dev Shed论坛
“Python是一种美丽的语言。它易于学习、跨平台,而且能够良好运行。它已经实现了Java一直想要实现的很多技术目标。对Python的一句话描述是‘所有其他语言随着时间发生演进,但Python是设计出来的。’而且Python设计得相当不错。虽然现在市面上有大量的Python图书,但是目前为止我遇到的最好的一本是Core Python Programming。”
——Chris Timmons,C. R. Timmons Consulting公司
“如果你喜欢Prentice Hall出版社的Core系列图书,你需要考虑的另一本写作精良的图书是Core Python Programming__。它将其他Python图书中很少涵盖的许多实用主题进行了事无巨细的剖析。”
——Mitchell L. Model,MLM Consulting公司
Wesley Chun在高中阶段开始进入计算领域,当时他使用的是BASIC和6502汇编语言,系统是Commodore。随后开始在Apple IIe上使用Pascal语言,然后是在穿孔卡片上使用ForTran语言。正是在穿孔卡片上使用ForTran的经历使他成为一名谨慎小心的开发人员,因为将一组卡片发送到学校的主机并得到返回结果,往往需要一周的往返时间。他第一份有酬劳的工作是作为学生辅导员为四年级、五年级和六年级的学生及其父母家讲授BASIC编程课程。
高中毕业后,Wesley以加利福尼亚校友学者的身份进入加州大学伯克利分校。他主修应用数学(计算机科学),辅修音乐(古典钢琴),并以A级和B级的成绩毕业。在学校期间,他先后使用Pascal、Logo和C语言编写过程序。他还参加了一个以录像带培训和心理咨询为特色的辅导课程。他的暑期实习项目包括以第4代编程语言编写代码,并编写了一个“Getting Started”用户手册。几年过后,他开始在加州大学圣巴拉拉分校继续学习,并获得了计算机科学(分布式系统)的硕士学位。在此期间,他还讲授C编程课程。一篇以其硕士论文为基础的论文在第29届HICSS大会上被提名为最佳论文,其随后的一个论文版本刊登在新加坡大学Journal of __High Performance Computing上。
自从毕业之后,Wesley就投身于软件行业,编写和出版了多本图书,并且发表了数百篇会议报告和教程。此外还开发了针对公共企业和私有企业培训的Python课程。Wesley的Python使用经历始于Python 1.4版本(当时Python刚刚起步),他使用Python设计了Yahoo!Mail拼写检查程序以及地址簿。他随后成为Yahoo!People Search部门的首席工程师。在离开Yahoo!之后,他写作了本书第1版,然后开始周游世界。回来之后,他使用Python编写过许多程序,包括本地产品搜索程序、反垃圾邮件和防病毒邮件程序、Facebook游戏/应用,以及许多完全不同的其他东西,比如医生用来进行脊柱骨折分析的软件。
在闲暇时间,Wesley喜欢弹钢琴、打保龄球、打篮球、骑自行车、玩极限飞盘、打扑克、旅行,以及与家人共享人伦。他还是Tutor邮件列表和PyCon这两个Python用户组的志愿者。他还维护着艾伦帕森斯怪物项目目录(Alan Parsons Project Monster Discography)。在本书写作之时,Wesley是Google的开发大使,为其云产品背书。Wesley生活在硅谷,您可以通过@wescpy或plus.ly/wescpy找到他。
很高兴各位读者能够允许我们来帮助你们尽可能快、尽可能深入地学习Python。Core Python系列图书的目标不只是教会开发人员Python语言,我们还希望各位读者能够形成足够的知识库,从而能够开发任何应用领域的软件。
在其他的Core Python系列图书(Core Python Programming和Core Python Language Fundamentals)中,我们不仅向读者讲授Python语言的语法,还希望读者能够深入掌握Python的运行机制。我们相信,在具备了这些知识之后,无论你是Python语言的初学者还是资深程序员,都能够开发出更为高效的Python应用程序。
在学完任何其他入门类的Python图书之后,你可能觉得已经掌握了Python而且还觉得学得不错,并为此感到自豪。通过完成大量练习之后,你将会对自己新掌握的Python编程技能拥有更多信心。但是,你可能仍然会有这样的疑问,“现在该怎么办?我能用Python编写哪种类型的应用程序呢?”或许你是为了一个相当小众的工作项目而学习使用Python,你可能会考虑“我还能用Python写点其他的吗?”
在本书中,你将会用到从其他地方学习到的所有Python知识,并培养新的技能,从而构建自己的工具箱。借助于该工具箱,你能够使用Python开发各种类型的应用程序。关于高级主题的章节旨在快速概述各种不同的主题。如果你开始转向这些章节中涵盖的特定应用开发领域,你将会发现它们不仅给出了正确的方向,还包含了更多的信息。但是不要期待有一个深入的解决方案,因为这有悖于本书的初衷—提供更为广泛的解决方案。
与其他所有Core Python图书一样,本书同样包含了许多示例,你可以在计算机上进行尝试。为了牢固掌握概念,你也会在每章最后发现有趣、有挑战性的练习。这些初级和中级难度的练习旨在测试你的知识掌握情况,提升你的Python技能。毕竟,没有什么可以替代实践经验。我们相信,你不仅能够学到很多Python编程技能,同时还能在尽可能短的时间内迅速掌握它们。
对我们来讲,扩展Python技能的最佳方式就是动手练习,因此你会发现这些练习是本书的一个最大优势。它们可以测试你对每章主题和定义的掌握情况,并激励你尽可能多地动手编程。除了自己编写应用程序之外,没有其他方法可以更有效地提升你的编程技能。你需要解决初级、中级和高级难度的编程问题。而且你应该需要编写一个大型的应用程序(这也是很多读者想要在本书中看到的),而不是采用一些脚本来实现。坦白说,你可能做得没有那么好,但是通过亲自动手实践,你的收获会更大。附录A给出了每章中某些练习的答案。附录B包含了一些有用的参考表。
感谢所有读者的反馈和鼓励,你们是我写作这些图书的动力。希望你们能继续给我发送反馈信息,并促使本书第4版尽快问世,而且其质量优于之前所有版本。
如果你之前了解Python,并且希望进一步了解Python,同时希望扩展自己的应用程序开发技能,你就是本书的读者对象。
在众多领域中都可见Python的身影,包括工程领域、信息技术领域、科学领域、商业领域和娱乐领域等。这意味着Python用户(和本书的读者)列表包括但不限于下述人员:
软件工程师;
硬件设计/CAD工程师;
QA/测试和自动化框架开发人员;
IS/IT/系统和网络管理员;
科学家和数学家;
技术或项目管理人员;
多媒体或音频/视觉工程师;
SCM或发布工程师;
Web大师或内容管理人员;
客户/技术支持工程师;
数据库工程师和管理员;
研发工程师;
软件集成和专业服务人员;
大学及中学教育工作者;
Web服务工程师;
金融软件工程师;
其他人员。
使用Python的一些著名公司包括Google、Yahoo!、NASA、卢卡斯工业光魔公司、Red Hat、Zope、迪士尼、皮克斯和梦工厂。
大约10多年以前,我在一家名为Four11的公司接触到Python。当时,该公司有一个主要的产品——Four11.com White Page目录服务。它们使用Python来设计该产品的下一代:Rocketmail Web E-mail服务,该服务最终演变为今天的Yahoo!Mail。
学习Python并加入最初的Yahoo!Mail工程团队是一件相当有趣的事情。我帮助重新设计了地址簿和拼写检查程序。在当时,Python也成为其他Yahoo!站点的一部分,其中包括People Search、Yellow Pages、Maps和Driving Directions等。事实上,我当时是People Search部门的首席工程师。
尽管在当时Python对我而言是全新的,但是它也很容易学习—比我过去学习的其他语言都要简单。在当时,Python教程的缺乏迫使我使用Library Reference和Quick Reference Guide作为主要的学习工具,而这也是促使我写作本书的一个驱动力。
从我在Yahoo!的日子开始,我能够以各种有趣的方式在随后的工作中使用Python。在任何情况下,我都能使用Python的强大功能来及时地解决遇到的问题。我也开发了多门Python课程,并使用本书来讲授那些课程—完全使用自己的作品。
Core Python图书不仅是卓越的Python学习资料,它们还是用来讲解Python的最佳工具。作为一名工程师,我知道学习、理解和应用一种新技术所需要的东西。作为一名专业讲师,我也知道为客户提供最有效的会话(session)所需要的是什么。这些图书栩栩如生,同时包含你无法从“纯粹的培训师”或“纯粹的图书作者”那里获得的提示。
不同于严格的“入门”图书或者纯粹的“重口味”计算机科学参考图书,我过去的教学经验告诉我,一本易于阅读同时又面向技术的图书应该服务于这样的一个目的,即能够让人尽可能迅速地掌握Python,以便能将其应用到十万火急的任务上来。我们在介绍概念时会辅之以合适的案例,以加速学习过程。每章最后都会给出大量练习,旨在夯实你对书中概念和理念的理解。
能够与Bruce Eckel的写作风格相提并论,我很激动也很谦卑(见本书第1版的评论,网址为http://corepython.com)。本书并非一本枯燥的大学教材,我们的目标是营造一个与你交谈的环境,就像你是在参加我的一个广受好评的Python培训课程一样。作为一名终身学习的学生,我不断地因材施教,告诉你需要学习什么才能快速、彻底地掌握Python的概念。你也将发现,可以快速、轻松地阅读本书,而且不会错失任何技术细节。
作为一名工程师,我知道应该怎样做才能向你讲授Python中的概念。作为一名教师,我可以将技术细节全部打散,然后转换成一种易于理解和迅速掌握的语言。你将从我的写作风格和教学风格中获益,更重要的是,你会喜欢上用Python来编程。
因此,你也将注意到,尽管我是本书唯一的作者,但是我使用的是“第三人称”的写作风格,也就是说,我使用了诸如“我们”这样的一些废话,原因是在学习本书的过程中,我们是一起的,共同朝着扩展Python编程技能的目标而努力。
在本书第1版刚问世时,Python刚发布了2.0版本。从那时起,Python语言发生了重大的改进,Python语言被越来越多的人接受,其使用率也大幅提升。Python编程语言大获成功。Python语言的缺陷已被删除,而且有新的特性不断加入,这将全世界Python开发人员的能力和编程修养提升到了一个新的水平。本书第2版于2006年问世,当时也是Python的鼎盛时期,它的版本是迄今为止最为流行的2.5版本。
本书第2版问世之后好评如潮,其销量超过了第1版。在那期间,Python本身也赢得了无数荣誉,包括下面这些。
Tiobe(www.tiobe.com)
——年度编程语言(2007年、2010年)
LinuxJournal(linuxjournal.com)
——最喜欢的编程语言(2009~2011年)
——最喜欢的脚本语言(2006~2008年、2010年、2011年)
LinuxQuestions.org会员选择奖
——年度编程语言(2007~2010年)
这些奖项和荣誉推动着Python进一步发展。现在,Python已经进入了下一代:Python 3。同样,本书也在向着其“第三代”前进。我非常高兴Prentice Hall能够让我写作本书第3版。由于Python 3.x版本不能够后向兼容Python 1和Python 2,因此还需要一段时间,Python 3.x才能被业界全面采用和集成进来。我们很乐意引导你经历这个过渡。本书第3版的代码也适用于Python 2和Python 3(视情况而定——并非所有代码都移植了过来)。在移植代码时,我们还会讨论各种工具和做法。
Python 3.x版本带来的挑战延续着对Python编程语言进行迭代和改进的趋势,要移除Python语言最后的重大缺陷还有很长的路要走,而且在不断演变的Python语言中移除重大缺陷也是一个相当大的飞跃。与之相似,本书的结构也做出了相当重大的转变。限于篇幅和范围,已出版的第2版无法处理第3版中引入的所有新内容。
因此,Prentice Hall和我想到了一个好方法来向前推进本书,即从逻辑上将其拆分为两部分,其中一部分讲述Python核心语言主题,另一部分讲述高级应用主题,并由此将书拆分为两卷。而你手头上当前拿着的这本书是Core Python Programming(第3版)的第二部分。好消息是由于第二部分的内容已经相当完整齐备,因此第一部分的内容也就没有存在的必要了。要阅读本书,我们建议读者能够拥有Python中级编程经验。如果你最近已经学过Python,而且能够相当轻松地驾驭它,或者你已经具备Python技能,但是希望能进一步提升该技能,那么你算是找对图书了。
Core Python Programming的读者都知道,我的主要目标是以一种全面的方式来讲解Python语言的本质,而非仅仅是其语法(学习Python的语法貌似也不需要一本书)。在知道了Python的工作机制之后—包括数据对象和内存管理之间的关系—你将成为一名更高效的Python程序员。而这是第一部分(即Core Python Language Fundamentals)要做的工作。
与本书所有版本一样,我会继续更新图书的Web站点以及博客,以确保无论你移植到哪个新发布的Python版本,都可以让本书做到与时俱进。
对之前的读者来说,本书第3版新增了下述主题:
基于Web的E-mail示例(第3章);
使用Tile/Ttk(第5章);
使用MongoDB(第6章);
更重要的Outlook和PowerPoint示例(第7章);
Web服务器网关接口(WSGI)(第10章);
使用Twitter(第13章);
使用Google+(第15章)。
此外,我们还在当前版本中添加了全新的3章,分别是第11章、第12章和第14章。这几章代表着经常使用Python进行应用开发的一些新领域或正在进行的领域。所有的现有章节已经焕然一新,并更新到Python的最新版本,同时还包含了一些新内容。通过随后的“章节指南”部分,你可以了解到本书每部分要讲解的内容。
本书分为3部分。其中第1部分占据了本书2/3的篇幅,它讲解了应用开发工具箱中(当然,Python是关注重点)“核心”成员的解决方案。第2部分讲解了与Web编程相关的各种主题。第3部分是补充部分,它提供了一些仍然在开发过程中的实验章节,在本书后续版本中,这些章节有望成为独立的章节。
本书提供了一些高级主题,以展示Python可以用来开发什么应用程序。值得高兴的是,本书起码可以向你提供Python开发中许多关键领域的入门知识,其中包括之前版本中提到的一些主题。
下面是本书每章的内容简介。
正则表达式是一种功能强大的工具,它可以用来进行模式匹配、提取、查找和替换。
如今许多应用都是面向网络的。该章将介绍如何使用TCP/IP与UDP/IP来创建客户端和服务器,以及如何快速入门SocketServer和Twisted。
如今在用的大多数Internet协议都是使用套接字开发的。该章将探究一些用来构建Internet协议客户端的高级库。该章重点讨论的是FTP、Usenet消息协议(NNTP)以及各种E-mail协议(SMTP、POP3及IMAP4)。
多线程编程是一种通过引入并发来提升多种应用程序执行性能的方式。该章通过解释概念并展示正确创建Python多线程应用程序的方法、什么是最佳用例来讲解如何在Python中实现线程。
Tkinter(在Python 3中重名为tkinter)以Tk图形工具包为基础,是Python中的默认GUI开发库。该章通过演示如何创建简单的GUI应用来介绍Tkinter。一种最佳的学习方式是复制,并在某些应用的顶层进行创建,这样可以很快上手。该章最后简要讨论其他图形库,比如Tix、Pmw、wxPython、PyGTK和Ttk/Tile。
Python也有助于简化数据库编程。该章首先回顾一些基本概念,然后介绍Python数据库应用编程接口(DB-API)。随后介绍如何使用Python连接到关系数据库,并执行查询和操作。如果你更喜欢使用结构化查询语言(SQL)的放手管理方法(hands-off approach),而且只是想在无须考虑底层数据库层的情况下处理对象,则可以使用对象-关系映射。最后,该章以MongoDB作为NoSQL示例介绍了非关系数据库。
无论喜欢与否,我们都生活在一个不得不和Microsoft Windows PC打交道的世界。我们可能偶尔与它们打交道,也可能每天都要接触到它们,但是无论处于哪种情况下,都可以使用Python的强大功能来让生活更轻松一些。该章将探究使用Python来编写COM客户端,以控制Office应用程序(比如Word、Excel、PowerPoint和Outlook)并与它们进行通信。尽管该章在本书之前版本中是实验章节,但是我们很高兴能够为其添加足够的内容,使其单独成章。
前面提到,能够重用代码并对语言进行扩展将具有相当强大的功能。在纯Python中,这些扩展是模块和包,但是你也可以使用C/C++、C#或Java来开发底层的代码。这些扩展能够以无缝方式与Python相接。用低级编程语言来编写自己的扩展可以提升性能,并增强安全性(因为源代码没有必要泄露)。该章讲解使用C语言来开发扩展的整个过程。
该章将扩展第2章讨论的客户端/服务器架构,我们将这一概念应用到Web上。该章不仅探究客户端,还介绍用来解析Web内容的各种Web客户端工具。最后,该章介绍如何使用Python来定制自己的Web服务器。
Web服务器的主要工作是接受客户端的请求,然后返回结果。但是服务器如何获得客户端的请求数据呢?由于服务器只擅长返回结果,因此它们通常没有获取数据的能力或逻辑,于是这个工作需要在他处完成。CGI给了服务器生成另外一个程序的能力,让这个程序来进行数据处理(长久以来一直也是这么做的),但是该程序不具备扩展性,因此并不会在实践中使用。但是,无论使用的是什么框架,这一概念仍然适用,因此我们将用一章的篇幅来学习CGI。该章介绍WSGI如何通过通用编程接口来为应用开发人员提供帮助。此外,该章还将介绍当框架开发人员需要在一端连接Web服务器而应用程序的代码放在另外一端时,WSGI如何提供帮助,以便应用开发人员能够在无须担心执行平台的情况下编写代码。
Python有很多Web框架,Django是其中最为流行的一个。该章介绍这个框架,然后介绍如何编写简单的Web应用。在具备了这些知识后,你可以自行研究其他Web框架。
云计算在IT业界引发了轰动。尽管像Amazon的AWS这样的基础设施服务和Gmail、Yahoo!Mail这样的在线应用等在当今世界中更为常见,但是有很多平台凭借其强大的功能,成为这些服务的替代者。这些平台充分利用了基础设施,无须用户介入,而且要比云软件具有更多的灵活性,原因是你可以自行控制应用及其代码。该章全面介绍使用Python的第一个平台服务——Google App Egnine。在掌握了该章的内容后,你可以探讨该章介绍的其他类似服务。
该章介绍Web上的高级服务(使用HTTP)。该章先介绍一个较为古老的服务(Yahoo!Finance),然后再给出一个较新的服务(Twitter)。该章讨论如何使用Python以及前面学到的知识来与这些服务进行交互。
这是本书的第一个补充章节,它介绍使用Python来处理文本的方法。该章先介绍CSV,然后是JSON,最后是XML。在该章最后一节,我们将前面学到的客户端/服务器知识融合到XML中,以查看如何使用XML-RPC来创建在线的远程过程调用(RPC)。
该章包含一些附加材料,这些内容可能会在本书下一版中成为单独的章节。该章讨论的主题包含Java/Jython和Google+。
我们欢迎任何形式的读者反馈。如果你有任何意见、建议、投诉、抱怨、bug,甚至任何事情,请通过corepython@yahoo.com与我联系。
在本书的Web站点(http://corepython.com)上,你可以找到勘误表、源代码、更新、即将举行的会谈、Python培训、下载地址和其他信息。在本书的Google+页面(http://plus.ly /corepython),你可以参与和本书有关的社区讨论。
Gloria Willadsen(首席审稿人)
Martin Omander(审稿人兼第11章、15.2节的合著者)
Darlene Wong
Bryce Verdier
Eric Walstad
Paul Bissex(Python Web Development with Django一书的合著者)
Johan “proppy”Euphrosine
Anthony Vallone
感谢我的妻子Faye,无论我在路上开车时,还是在家埋头写作时,她总是将家庭照顾得井井有条,娴熟照顾孩子的起居和作息,并妥善处理家庭各种开支,因此她总是能够不断给我惊喜。
Mark Taub(总编辑)
Debra Williams Cauley(策划编辑)
John Fuller(执行编辑)
Elizabeth Ryan(项目编辑)
Bob Russell,Octal Publishing公司(文字编辑)
Dianne Russel,Octal Publishing公司(产品与管理服务)
Shannon –jj Behrens(首席审稿人)
Michael Santos(首席审稿人)
Rick Kwan
Lindell Aldermann(第6章Unicode小节的合著者)
Wai-Yip Tung(第20章Unicode案例的合著者)
Eric Foster-Johnson(Beginning Python一书的合著者)
Alex Martelli(Python Cookbook一书的编辑以及Python in a Nutshell一书的合著者)
Larry Rosenstein
Jim Orosz
Krishna Srivivasan
Chuck Kung
感谢我的孩子!
Guido van Rossum(Python语言的创始人)
Dowson Tong
James C. Ahlstrom(Internet Programming with Python一书的合著者)
S. Candelaria de Ram
Cay S. Horstmann(Core Java一书和Core JavaServer Faces一书的合著者)
Michael Santos
Greg Ward(distutils包及其文档的创始人)
Vincent C. Rubino
Martijn Faassen
Emile van Sebille
Raymond Tsai
Albert L. Anders(MT Programming章节的合著者)
Fredrik Lundh(Python Standard Library一书的合著者)
Cameron Laird
Fred L. Drake, Jr.(Python & XML一书的合著者以及Python官方文档的编辑)
Jeremy Hylton
Steve Yoshimoto
AahzMaruch(Python for Dummies一书的合著者)
Jeffrey E. F. Friedl(Mastering Regular Expressions一书的合著者)
Pieter Claerhout
Catriona(Kate)Johnston
David Ascher(Learning Python一书的合著者以及Python Cookbook一书的编辑)
Reg Charney
Christian Tismer(Stackless Python的创始人)
Jason Stillwell
我在加州大学圣克鲁兹分校的学生
非常感谢我的高中编程老师James P. Prior。
感谢Louise Moser和P. Michael Melliar-Smith(我在加州大学圣巴巴拉分校的研究生论文导师),向两位致以我最深切的感激之情。
感谢Alan Parsons、Eric Woolfson、Andrew Powell、Ian Bairnson、Stuart Elliott、David Paton以及其他项目参与人员,感谢我的Projectologists和Roadkillers同伴,谢谢你们的音乐、支持和陪伴。
还要感谢我的家人、朋友和上帝,在我过去的疯狂岁月和长途奔袭中,是他们让我保持安全和理智。还要感谢在过去20多年以来一直对我深信不疑的人—没有你们,我将无法坚持下来。
最后,还要感谢我的读者以及Python社区。能够教你Python编程,我非常激动,也希望你能在阅读本书的过程中能体验到学习的乐趣。
Wesley J. Chun
加州硅谷
有些人在碰到问题时,就想:“我知道,我可以使用正则表达式。”现在,他们就有了两个问题。
——Jamie“jwz”Zawinski,1997年8月
操作文本或者数据可是件大事。如果不相信,就仔细看看当今的计算机都在做些什么工作:文字处理、网页表单的填写、来自数据库转储的信息流、股票报价信息、新闻源,而且这个清单还会不断增长。因为我们可能还不知道需要用计算机编程来处理的文本或数据的具体内容,所以能将这些文本或者数据以某种可被计算机识别和处理的模式表达出来是非常有用的。
如果我在运营一个电子邮件存档公司,而作为我的一位客户,你希望查看你自己在去年2月份发送和接收的所有电子邮件。如果我能够设计一个计算机程序来收集这些信息,然后转发给你,而不是人工阅读你的邮件然后手动处理你的请求,无疑要好很多。因为如果有人看了你的邮件,哪怕只是用眼睛瞄了一下邮件的时间戳,你可能都会对此感到担心(甚至愤怒)。又比如,你可能会认为凡是带有“ILOVEYOU”这样主题的邮件都是已感染病毒的邮件,并要求从你的个人邮箱中删除它们。这就引出了一个问题,即我们如何通过编程使计算机具有在文本中检索某种模式的能力。
正则表达式为高级的文本模式匹配、抽取、与/或文本形式的搜索和替换功能提供了基础。简单地说,正则表达式(简称为regex)是一些由字符和特殊符号组成的字符串,它们描述了模式的重复或者表述多个字符,于是正则表达式能按照某种模式匹配一系列有相似特征的字符串(见图1-1)。换句话说,它们能够匹配多个字符串……一种只能匹配一个字符串的正则表达式模式是很乏味并且毫无作用的,不是吗?
Python通过标准库中的re模块来支持正则表达式。本节将做一个简短扼要的介绍。限于篇幅,内容将仅涉及Python编程中正则表达式方面的最常见内容。当然,读者对于正则表达式方面的经验(熟悉程度)肯定不同,我们强烈建议阅读一些官方帮助文档和与此主题相关的文档。你将再次会对字符串的理解方式有所改变!
核心提示:搜索和匹配的比较
本章通篇会使用搜索和匹配两个术语。当严格讨论与字符串中模式相关的正则表达式时,我们会用术语“匹配”(matching),指的是术语“模式匹配”(pattern-matching)。在Python术语中,主要有两种方法完成模式匹配:“搜索”(searching),即在字符串任意部分中搜索匹配的模式;而“匹配”(matching)是指判断一个字符串能否从起始处全部或者部分地匹配某个模式。搜索通过search()函数或方法来实现,而匹配通过调用match()函数或方法实现。总之,当涉及模式时,全部使用术语“匹配”;我们按照Python如何完成模式匹配的方式来区分“搜索”和“匹配”。
图1-1 可以使用正则表达式来识别有效的Python标识符,例如下面这些:[A-Za-z]\w+的含义是第一个字符是字母,也就是说要么A~Z,要么a~z,后面是至少一个(+)由字母数字组成的字符(\w)。如图所示,可以看到很多字符串被过滤,但是只有那些符合要求的正则表达式模式的字符串被筛选出来。比如“4xZ”被筛选出来,这是因为它是以数字开头的
前面讲到,正则表达式是包含文本和特殊字符的字符串,该字符串描述一个可以识别各种字符串的模式。我们还简单阐述了正则表达式字母表。对于通用文本,用于正则表达式的字母表是所有大小写字母及数字的集合。可能也存在一些特殊字母;例如,指仅包含字符“0”和“1”的字母表。该字母表可以表示所有二进制字符串的集合,即“0”、“1”、“00”、“01”、“10”、“11”、“100”等。
现在,让我们看看正则表达式的大部分基本内容,虽然正则表达式通常被视为“高级主题”,但是它们其实也非常简单。把标准字母表用于通用文本,我们展示了一些简单的正则表达式以及这些模式所表述的字符串。下面所介绍的正则表达式都是最基本、最普通的。它们仅仅用一个简单的字符串构造成一个匹配字符串的模式:该字符串由正则表达式定义。下面所示为几个正则表达式和它们所匹配的字符串。
正则表达式模式 |
匹配的字符串 |
---|---|
foo |
foo |
Python |
Python |
abc123 |
abc123 |
上面的第一个正则表达式模式是“foo”。该模式没有使用任何特殊符号去匹配其他符号,而只匹配所描述的内容,所以,能够匹配这个模式的只有包含“foo”的字符串。同理,对于字符串“Python”和“abc123”也一样。正则表达式的强大之处在于引入特殊字符来定义字符集、匹配子组和重复模式。正是由于这些特殊符号,使得正则表达式可以匹配字符串集合,而不仅仅只是某单个字符串。
本节将介绍最常见的特殊符号和字符,即所谓的元字符,正是它给予正则表达式强大的功能和灵活性。表1-1列出了这些最常见的符号和字符。
表1-1 常见正则表达式符号和特殊字符
表 示 法 |
描 述 |
正则表达式示例 |
---|---|---|
符号 |
|
|
literal |
匹配文本字符串的字面值literal |
foo |
re1 |re2 |
匹配正则表达式re1或者re2 |
foo|bar |
. |
匹配任何字符(除了\n之外) |
b.b |
^ |
匹配字符串起始部分 |
^Dear |
$ |
匹配字符串终止部分 |
/bin/*sh$ |
* |
匹配0次或者多次前面出现的正则表达式 |
[A-Za-z0-9]* |
+ |
匹配1次或者多次前面出现的正则表达式 |
[a-z]+\.com |
? |
匹配0次或者1次前面出现的正则表达式 |
goo? |
{N} |
匹配N次前面出现的正则表达式 |
[0-9]{3} |
{M,N} |
匹配M~N次前面出现的正则表达式 |
[0-9]{5,9} |
[…] |
匹配来自字符集的任意单一字符 |
[aeiou] |
[..x−y..] |
匹配x~y范围中的任意单一字符 |
[0-9], [A-Za-z] |
[^…] |
不匹配此字符集中出现的任何一个字符,包括某一范围的字符(如果在此字符集中出现) |
[^aeiou], [^A-Za-z0-9] |
(*|+|?|{})? |
用于匹配上面频繁出现/重复出现符号的非贪婪版本(*、+、?、{}) |
.*?[a-z] |
(…) |
匹配封闭的正则表达式,然后另存为子组 |
([0-9]{3})?,f(oo|u)bar |
特殊字符 |
|
|
\d |
匹配任何十进制数字,与[0-9]一致(\D与\d相反,不匹配任何非数值型的数字) |
data\d+.txt |
\w |
匹配任何字母数字字符,与[A-Za-z0-9_]相同(\W与之相反) |
[A-Za-z_]\w+ |
\s |
匹配任何空格字符,与[\n\t\r\v\f]相同(\S与之相反) |
of\sthe |
\b |
匹配任何单词边界(\B与之相反) |
\bThe\b |
\N |
匹配已保存的子组N(参见上面的(…)) |
price: \16 |
\c |
逐字匹配任何特殊字符c(即,仅按照字面意义匹配,不匹配特殊含义) |
\., \\, \* |
\A(\Z) |
匹配字符串的起始(结束)(另见上面介绍的^和$) |
\ADear |
扩展表示法 |
|
|
(?iLmsux) |
在正则表达式中嵌入一个或者多个特殊“标记”参数(或者通过函数/方法) |
(?x),(?im) |
(?:…) |
表示一个匹配不用保存的分组 |
(?:\w+\.)* |
(?P<name>…) |
像一个仅由name标识而不是数字ID标识的正则分组匹配 |
(?P<data>) |
(?P=name) |
在同一字符串中匹配由(?P<name)分组的之前文本 |
(?P=data) |
(?#…) |
表示注释,所有内容都被忽略 |
(?#comment) |
(?=…) |
匹配条件是如果…出现在之后的位置,而不使用输入字符串;称作正向前视断言 |
(?=.com) |
(?!…) |
匹配条件是如果…不出现在之后的位置,而不使用输入字符串;称作负向前视断言 |
(?!.net) |
(?<=…) |
匹配条件是如果…出现在之前的位置,而不使用输入字符串;称作正向后视断言 |
(?<=800-) |
(?<!…) |
匹配条件是如果…不出现在之前的位置,而不使用输入字符串;称作负向后视断言 |
(?<!192\.168\.) |
(?(id/name)Y|N ) |
如果分组所提供的id或者name(名称)存在,就返回正则表达式的条件匹配Y,如果不存在,就返回N;|N是可选项 |
(?(1)y|x) |
表示择一匹配的管道符号(|),也就是键盘上的竖线,表示一个“从多个模式中选择其一”的操作。它用于分割不同的正则表达式。例如,在下面的表格中,左边是一些运用择一匹配的模式,右边是左边相应的模式所能够匹配的字符。
正则表达式模式 |
匹配的字符串 |
---|---|
at| home |
at、home |
r2d2 | c3po |
r2d2、c3po |
bat | bet | bit |
bat、bet、bit |
有了这个符号,就能够增强正则表达式的灵活性,使得正则表达式能够匹配多个字符串而不仅仅只是一个字符串。择一匹配有时候也称作并(union)或者逻辑或(logical OR)。
点号或者句点(.)符号匹配除了换行符\n以外的任何字符(Python正则表达式有一个编译标记[S或者DOTALL],该标记能够推翻这个限制,使点号能够匹配换行符)。无论字母、数字、空格(并不包括“\n”换行符)、可打印字符、不可打印字符,还是一个符号,使用点号都能够匹配它们。
正则表达式模式 |
匹配的字符串 |
---|---|
f.o |
匹配在字母“f”和“o”之间的任意一个字符;例如fao、f9o、f#o等 |
.. |
任意两个字符 |
.end |
匹配在字符串end之前的任意一个字符 |
问:怎样才能匹配句点(dot)或者句号(period)字符?
答:要显式匹配一个句点符号本身,必须使用反斜线转义句点符号的功能,例如“.”。
还有些符号和相关的特殊字符用于在字符串的起始和结尾部分指定用于搜索的模式。如果要匹配字符串的开始位置,就必须使用脱字符(^)或者特殊字符\A(反斜线和大写字母A)。后者主要用于那些没有脱字符的键盘(例如,某些国际键盘)。同样,美元符号($)或者\Z将用于匹配字符串的末尾位置。
使用这些符号的模式与本章描述的其他大多数模式是不同的,因为这些模式指定了位置或方位。之前的“核心提示”记录了匹配(试图在字符串的开始位置进行匹配)和搜索(试图从字符串的任何位置开始匹配)之间的差别。正因如此,下面是一些表示“边界绑定”的正则表达式搜索模式的示例。
正则表达式模式 |
匹配的字符串 |
---|---|
^From |
任何以From作为起始的字符串 |
/bin/tcsh$ |
任何以/bin/tcsh作为结尾的字符串 |
^Subject: hi$ |
任何由单独的字符串Subject: hi构成的字符串 |
再次说明,如果想要逐字匹配这些字符中的任何一个(或者全部),就必须使用反斜线进行转义。例如,如果你想要匹配任何以美元符号结尾的字符串,一个可行的正则表达式方案就是使用模式.*\$$。
特殊字符\b和\B可以用来匹配字符边界。而两者的区别在于\b将用于匹配一个单词的边界,这意味着如果一个模式必须位于单词的起始部分,就不管该单词前面(单词位于字符串中间)是否有任何字符(单词位于行首)。同样,\B将匹配出现在一个单词中间的模式(即,不是单词边界)。下面为一些示例。
正则表达式模式 |
匹配的字符串 |
---|---|
the |
任何包含the的字符串 |
\bthe |
任何以the开始的字符串 |
\bthe\b |
仅仅匹配单词the |
\Bthe |
任何包含但并不以the作为起始的字符串 |
尽管句点可以用于匹配任意符号,但某些时候,可能想要匹配某些特定字符。正因如此,发明了方括号。该正则表达式能够匹配一对方括号中包含的任何字符。下面为一些示例。
正则表达式模式 |
匹配的字符串 |
---|---|
b[aeiu]t |
bat、bet、bit、but |
[cr][23][dp][o2] |
一个包含四个字符的字符串,第一个字符是“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问题是相同的。
除了单字符以外,字符集还支持匹配指定的字符范围。方括号中两个符号中间用连字符(-)连接,用于指定一个字符的范围;例如,A-Z、a-z或者0-9分别用于表示大写字母、小写字母和数值数字。这是一个按照字母顺序的范围,所以不能将它们仅仅限定用于字母和十进制数字上。另外,如果脱字符(^)紧跟在左方括号后面,这个符号就表示不匹配给定字符集中的任何一个字符。
正则表达式模式 |
匹配的字符串 |
---|---|
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之间 |
本节介绍最常用的正则表达式符号,即特殊符号*、+和?,所有这些都可以用于匹配一个、多个或者没有出现的字符串模式。星号或者星号操作符(*)将匹配其左边的正则表达式出现零次或者多次的情况(在计算机编程语言和编译原理中,该操作称为Kleene闭包)。加号(+)操作符将匹配一次或者多次出现的正则表达式(也叫做正闭包操作符),问号(?)操作符将匹配零次或者一次出现的正则表达式。
还有大括号操作符({}),里面或者是单个值或者是一对由逗号分隔的值。这将最终精确地匹配前面的正则表达式N次(如果是{N})或者一定范围的次数;例如,{M,N}将匹配M~N次出现。这些符号能够由反斜线符号转义;\*匹配星号,等等。
注意,在之前的表格中曾经多次使用问号(重载),这意味着要么匹配0次,要么匹配1次,或者其他含义:如果问号紧跟在任何使用闭合操作符的匹配后面,它将直接要求正则表达式引擎匹配尽可能少的次数。
“尽可能少的次数”是什么意思?当模式匹配使用分组操作符时,正则表达式引擎将试图“吸收”匹配该模式的尽可能多的字符。这通常被叫做贪婪匹配。问号要求正则表达式引擎去“偷懒”,如果可能,就在当前的正则表达式中尽可能少地匹配字符,留下尽可能多的字符给后面的模式(如果存在)。本章末尾将用一个典型的示例来说明非贪婪匹配是很有必要的。现在继续查看闭包操作符。
正则表达式模式 |
匹配的字符串 |
---|---|
[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”之间的棋盘坐标。前面的坐标表示从哪里开始走棋,后面的坐标代表走到哪个位置(棋格)上 |
我们还提到有一些特殊字符能够表示字符集。与使用“0-9”这个范围表示十进制数相比,可以简单地使用d表示匹配任何十进制数字。另一个特殊字符(\w)能够用于表示全部字母数字的字符集,相当于[A-Za-z0-9_]的缩写形式,\s可以用来表示空格字符。这些特殊字符的大写版本表示不匹配;例如,\D表示任何非十进制数(与[^0-9]相同),等等。
使用这些缩写,可以表示如下一些更复杂的示例。
正则表达式模式 |
匹配的字符串 |
---|---|
\w+-\d+ |
一个由字母数字组成的字符串和一串由一个连字符分隔的数字 |
[A-Za-z]\w* |
第一个字符是字母;其余字符(如果存在)可以是字母或者数字(几乎等价于Python中的有效标识符[参见练习]) |
\d{3}-\d{3}-\d{4} |
美国电话号码的格式,前面是区号前缀,例如800-555-1212 |
\w+@\w+.com |
以XXX@YYY.com格式表示的简单电子邮件地址 |
现在,我们已经可以实现匹配某个字符串以及丢弃不匹配的字符串,但有些时候,我们可能会对之前匹配成功的数据更感兴趣。我们不仅想要知道整个字符串是否匹配我们的标准,而且想要知道能否提取任何已经成功匹配的特定字符串或者子字符串。答案是可以,要实现这个目标,只要用一对圆括号包裹任何正则表达式。
当使用正则表达式时,一对圆括号可以实现以下任意一个(或者两个)功能:
关于为何想要对正则表达式进行分组的一个很好的示例是:当有两个不同的正则表达式而且想用它们来比较同一个字符串时。另一个原因是对正则表达式进行分组可以在整个正则表达式中使用重复操作符(而不是一个单独的字符或者字符集)。
使用圆括号进行分组的一个副作用就是,匹配模式的子字符串可以保存起来供后续使用。这些子组能够被同一次的匹配或者搜索重复调用,或者提取出来用于后续处理。1.3.9节的结尾将给出一些提取子组的示例。
为什么匹配子组这么重要呢?主要原因是在很多时候除了进行匹配操作以外,我们还想要提取所匹配的模式。例如,如果决定匹配模式\w+-\d+,但是想要分别保存第一部分的字母和第二部分的数字,该如何实现?我们可能想要这样做的原因是,对于任何成功的匹配,我们可能想要看到这些匹配正则表达式模式的字符串究竟是什么。
如果为两个子模式都加上圆括号,例如(\w+)-(\d+),然后就能够分别访问每一个匹配子组。我们更倾向于使用子组,这是因为择一匹配通过编写代码来判断是否匹配,然后执行另一个单独的程序(该程序也需要另行创建)来解析整个匹配仅仅用于提取两个部分。为什么不让Python自己实现呢?这是re模块支持的一个特性,所以为什么非要重蹈覆辙呢?
正则表达式模式 |
匹配的字符串 |
---|---|
\d+(\.\d*)? |
表示简单浮点数的字符串;也就是说,任何十进制数字,后面可以接一个小数点和零个或者多个十进制数字,例如“0.004”、“2”、“75.”等 |
(Mr?s?.)?[A-Z][a-z]*[A-Za-z-]+ |
名字和姓氏,以及对名字的限制(如果有,首字母必须大写,后续字母小写),全名前可以有可选的“Mr.”、“Mrs.”、“Ms.”或者“M.”作为称谓,以及灵活可选的姓氏,可以有多个单词、横线以及大写字母 |
我们还没介绍过的正则表达式的最后一个方面是扩展表示法,它们是以问号开始(?…)。我们不会为此花费太多时间,因为它们通常用于在判断匹配之前提供标记,实现一个前视(或者后视)匹配,或者条件检查。尽管圆括号使用这些符号,但是只有(?P<name>)表述一个分组匹配。所有其他的都没有创建一个分组。然而,你仍然需要知道它们是什么,因为它们可能最适合用于你所需要完成的任务。
正则表达式模式 |
匹配的字符串 |
---|---|
(?:\w+.)* |
以句点作为结尾的字符串,例如“google.”、“twitter.”、“facebook.”,但是这些匹配不会保存下来供后续的使用和数据检索 |
(?#comment) |
此处并不做匹配,只是作为注释 |
(?=.com) |
如果一个字符串后面跟着“.com”才做匹配操作,并不使用任何目标字符串 |
(?!.net) |
如果一个字符串后面不是跟着“.net”才做匹配操作 |
(?<=800-) |
如果字符串之前为“800-”才做匹配,假定为电话号码,同样,并不使用任何输入字符串 |
(?<!192\.168\.) |
如果一个字符串之前不是“192.168.”才做匹配操作,假定用于过滤掉一组C类IP地址 |
(?(1)y|x) |
如果一个匹配组1(\1)存在,就与y匹配;否则,就与x匹配 |
在了解了关于正则表达式的全部知识后,开始查看Python当前如何通过使用re模块来支持正则表达式,re模块在古老的Python 1.5版中引入,用于替换那些已过时的regex模块和regsub模块——这两个模块在Python 2.5版中移除,而且此后导入这两个模块中的任意一个都会触发ImportError异常。
re模块支持更强大而且更通用的Perl风格(Perl 5风格)的正则表达式,该模块允许多个线程共享同一个已编译的正则表达式对象,也支持命名子组。
表1-2列出了来自re模块的更多常见函数和方法。它们中的大多数函数也与已经编译的正则表达式对象(regex object)和正则匹配对象(regex match object)的方法同名并且具有相同的功能。本节将介绍两个主要的函数/方法——match()和search(),以及compile()函数。下一节将介绍更多的函数,但如果想进一步了解将要介绍或者没有介绍的更多相关信息,请查阅Python的相关文档。
表1-2 常见的正则表达式属性
函数/方法 |
描 述 |
---|---|
仅仅是re模块函数 |
|
compile(pattern,flags = 0) |
使用任何可选的标记来编译正则表达式的模式,然后返回一个正则表达式对象 |
re模块函数和正则表达式对象的方法 |
|
match(pattern,string,flags=0) |
尝试使用带有可选的标记的正则表达式的模式来匹配字符串。如果匹配成功,就返回匹配对象;如果失败,就返回None |
search(pattern,string,flags=0) |
使用可选标记搜索字符串中第一次出现的正则表达式模式。如果匹配成功,则返回匹配对象;如果失败,则返回None |
findall(pattern,string [, flags] )① |
查找字符串中所有(非重复)出现的正则表达式模式,并返回一个匹配列表 |
finditer(pattern,string [, flags] )② |
与findall()函数相同,但返回的不是一个列表,而是一个迭代器。对于每一次匹配,迭代器都返回一个匹配对象 |
split(pattern,string,max=0)③ |
根据正则表达式的模式分隔符,split函数将字符串分割为列表,然后返回成功匹配的列表,分隔最多操作max次(默认分割所有匹配成功的位置) |
re模块函数和正则表达式对象方法 |
|
sub(pattern,repl,string,count=0) ③ |
使用repl替换所有正则表达式的模式在字符串中出现的位置,除非定义count,否则就将替换所有出现的位置(另见subn()函数,该函数返回替换操作的数目) |
purge() |
清除隐式编译的正则表达式模式 |
常用的匹配对象方法(查看文档以获取更多信息) |
|
group(num=0) |
返回整个匹配对象,或者编号为num的特定子组 |
groups(default=None) |
返回一个包含所有匹配子组的元组(如果没有成功匹配,则返回一个空元组) |
groupdict(default=None) |
返回一个包含所有匹配的命名子组的字典,所有的子组名称作为字典的键(如果没有成功匹配,则返回一个空字典) |
常用的模块属性(用于大多数正则表达式函数的标记) |
|
re.I、re.IGNORECASE |
不区分大小写的匹配 |
re.L、re.LOCALE |
根据所使用的本地语言环境通过\w、\W、\b、\B、\s、\S实现匹配 |
re.M、re.MULTILINE |
^和$分别匹配目标字符串中行的起始和结尾,而不是严格匹配整个字符串本身的起始和结尾 |
re.S、rer.DOTALL |
“.”(点号)通常匹配除了\n(换行符)之外的所有单个字符;该标记表示“.”(点号)能够匹配全部字符 |
re.X、re.VERBOSE |
通过反斜线转义,否则所有空格加上#(以及在该行中所有后续文字)都被忽略,除非在一个字符类中或者允许注释并且提高可读性 |
① Python 1.5.2版中新增;2.4版中增加flags参数。
② Python 2.2版中新增;2.4版中增加flags参数。
③ Python 2.7和3.1版中增加flags参数。
核心提示:编译正则表达式(编译还是不编译?)
在Core Python Programming或者即将出版的Core Python Language Fundamentals的执行环境章节中,介绍了Python代码最终如何被编译成字节码,然后在解释器上执行。特别是,我们指定eval()或者exec(在2.x版本中或者在3.x版本的exec()中)调用一个代码对象而不是一个字符串,性能上会有明显提升。这是由于对于前者而言,编译过程不会重复执行。换句话说,使用预编译的代码对象比直接使用字符串要快,因为解释器在执行字符串形式的代码前都必须把字符串编译成代码对象。
同样的概念也适用于正则表达式——在模式匹配发生之前,正则表达式模式必须编译成正则表达式对象。由于正则表达式在执行过程中将进行多次比较操作,因此强烈建议使用预编译。而且,既然正则表达式的编译是必需的,那么使用预编译来提升执行性能无疑是明智之举。re.compile()能够提供此功能。
其实模块函数会对已编译的对象进行缓存,所以不是所有使用相同正则表达式模式的search()和match()都需要编译。即使这样,你也节省了缓存查询时间,并且不必对于相同的字符串反复进行函数调用。在不同的Python版本中,缓存中已编译过的 正则表达式对象的数目可能不同,而且没有文档记录。purge()函数能够用于清除这些缓存。
后续将扼要介绍的几乎所有的re模块函数都可以作为regex对象的方法。注意,尽管推荐预编译,但它并不是必需的。如果需要编译,就使用编译过的方法;如果不需要编译,就使用函数。幸运的是,不管使用函数还是方法,它们的名字都是相同的(也许你曾对此感到好奇,这就是模块函数和方法的名字相同的原因,例如,search()、match()等)。因为这在大多数示例中省去一个小步骤,所以我们将使用字符串替代。我们仍将会遇到几个预编译代码的对象,这样就可以知道它的过程是怎么回事。
对于一些特别的正则表达式编译,可选的标记可能以参数的形式给出,这些标记允许不区分大小写的匹配,使用系统的本地化设置来匹配字母数字,等等。请参考表1-2中的条目以及在正式的官方文档中查询关于这些标记(re.IGNORECASE、re.MULTILINE、re.DOTALL、re.VERBOSE等)的更多信息。它们可以通过按位或操作符(|)合并。
这些标记也可以作为参数适用于大多数re模块函数。如果想要在方法中使用这些标记,它们必须已经集成到已编译的正则表达式对象之中,或者需要使用直接嵌入到正则表达式本身的(?F)标记,其中F是一个或者多个i(用于re.I/IGNORECASE)、m(用于re.M/MULTILINE)、s(用于re.S/DOTALL)等。如果想要同时使用多个,就把它们放在一起而不是使用按位或操作,例如,(?im)可以用于同时表示re.IGNORECASE和re.MULTILINE。
当处理正则表达式时,除了正则表达式对象之外,还有另一个对象类型:匹配对象。这些是成功调用match()或者search()返回的对象。匹配对象有两个主要的方法:group()和groups()。
group()要么返回整个匹配对象,要么根据要求返回特定子组。groups()则仅返回一个包含唯一或者全部子组的元组。如果没有子组的要求,那么当group()仍然返回整个匹配时,groups()返回一个空元组。
Python正则表达式也允许命名匹配,这部分内容超出了本节的范围。建议读者查阅完整的re模块文档,里面有这里省略掉的关于这些高级主题的详细内容。
match()是将要介绍的第一个re模块函数和正则表达式对象(regex object)方法。match()函数试图从字符串的起始部分对模式进行匹配。如果匹配成功,就返回一个匹配对象;如果匹配失败,就返回None,匹配对象的group()方法能够用于显示那个成功的匹配。下面是如何运用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异常。
的对比) 其实,想要搜索的模式出现在一个字符串中间部分的概率,远大于出现在字符串起始部分的概率。这也就是search()派上用场的时候了。search()的工作方式与match()完全一致,不同之处在于search()会用它的字符串参数,在任意位置对给定正则表达式模式搜索第一次出现的匹配情况。如果搜索到成功的匹配,就会返回一个匹配对象;否则,返回None。
我们将再次举例说明match()和search()之间的差别。以匹配一个更长的字符串为例,这次使用字符串“foo”去匹配“seafood”:
>>> m = re.match('foo', 'seafood') # 匹配失败
>>> if m is not None: m.group()
...
>>>
可以看到,此处匹配失败。match()试图从字符串的起始部分开始匹配模式;也就是说,模式中的“f”将匹配到字符串的首字母“s”上,这样的匹配肯定是失败的。然而,字符串“foo”确实出现在“seafood”之中(某个位置),所以,我们该如何让Python得出肯定的结果呢?答案是使用search()函数,而不是尝试匹配。search()函数不但会搜索模式在字符串中第一次出现的位置,而且严格地对字符串从左到右搜索。
>>> m = re.search('foo', 'seafood') # 使用 search() 代替
>>> if m is not None: m.group()
...
'foo' # 搜索成功,但是匹配失败
>>>
此外,match()和search()都使用在1.3.2节中介绍的可选的标记参数。最后,需要注意的是,等价的正则表达式对象方法使用可选的pos和endpos参数来指定目标字符串的搜索范围。
本节后面将使用match()和search()正则表达式对象方法以及group()和groups()匹配对象方法,通过展示大量的实例来说明Python中正则表达式的使用方法。我们将使用正则表达式语法中几乎全部的特殊字符和符号。
在1.2节中,我们在正则表达式bat|bet|bit中使用了择一匹配(|)符号。如下为在Python中使用正则表达式的方法。
>>> bt = 'bat|bet|bit' # 正则表达式模式: bat、bet、bit
>>> m = re.match(bt, 'bat') # 'bat' 是一个匹配
>>> if m is not None: m.group()
...
'bat'
>>> m = re.match(bt, 'blt') # 对于 'blt' 没有匹配
>>> if m is not None: m.group()
...
>>> m = re.match(bt, 'He bit me!') # 不能匹配字符串
>>> if m is not None: m.group()
...
>>> m = re.search(bt, 'He bit me!') # 通过搜索查找 'bit'
>>> if m is not None: m.group()
...
'bit'
在后续的示例中,我们展示了点号(.)不能匹配一个换行符\n或者非字符,也就是说,一个空字符串。
>>> anyend = '.end'
>>> m = re.match(anyend, 'bend') # 点号匹配 'b'
>>> if m is not None: m.group()
...
'bend'
>>> m = re.match(anyend, 'end') # 不匹配任何字符
>>> if m is not None: m.group()
...
>>> m = re.match(anyend, '\nend') # 除了 \n之外的任何字符
>>> if m is not None: m.group()
...
>>> m = re.search('.end', 'The end.')# 在搜索中匹配 ' '
>>> if m is not None: m.group()
...
' end'
下面的示例在正则表达式中搜索一个真正的句点(小数点),而我们通过使用一个反斜线对句点的功能进行转义:
>>> patt314 = '3.14' # 表示正则表达式的点号
>>> pi_patt = '3\.14' # 表示字面量的点号 (dec. point)
>>> m = re.match(pi_patt, '3.14') # 精确匹配
>>> if m is not None: m.group()
...
'3.14'
>>> m = re.match(patt314, '3014') # 点号匹配'0'
>>> if m is not None: m.group()
...
'3014'
>>> m = re.match(patt314, '3.14') # 点号匹配 '.'
>>> if m is not None: m.group()
...
'3.14'
前面详细讨论了[cr][23][dp][o2],以及它们与r2d2|c3po之间的差别。下面的示例将说明对于r2d2|c3po的限制将比[cr][23][dp][o2]更为严格。
>>> m = re.match('[cr][23][dp][o2]', 'c3po')# 匹配 'c3po'
>>> if m is not None: m.group()
...
'c3po'
>>> m = re.match('[cr][23][dp][o2]', 'c2do')# 匹配 'c2do'
>>> if m is not None: m.group()
...
'c2do'
>>> m = re.match('r2d2|c3po', 'c2do')# 不匹配 'c2do'
>>> if m is not None: m.group()
...
>>> m = re.match('r2d2|c3po', 'r2d2')# 匹配 'r2d2'
>>> if m is not None: m.group()
...
'r2d2'
正则表达式中最常见的情况包括特殊字符的使用、正则表达式模式的重复出现,以及使用圆括号对匹配模式的各部分进行分组和提取操作。我们曾看到过一个关于简单电子邮件地址的正则表达式(\w+@\w+.com)。或许我们想要匹配比这个正则表达式所允许的更多邮件地址。为了在域名前添加主机名称支持,例如www.xxx.com,仅仅允许xxx.com作为整个域名,必须修改现有的正则表达式。为了表示主机名是可选的,需要创建一个模式来匹配主机名(后面跟着一个句点),使用“?”操作符来表示该模式出现零次或者一次,然后按照如下所示的方式,插入可选的正则表达式到之前的正则表达式中:\w+@(\w+.)?\w+.com。从下面的示例中可见,该表达式允许.com前面有一个或者两个名称:
>>> patt = '\w+@(\w+\.)?\w+\.com'
>>> re.match(patt, 'nobody@xxx.com').group()
'nobody@xxx.com'
>>> re.match(patt, 'nobody@www.xxx.com').group()
'nobody@www.xxx.com'
接下来,用以下模式来进一步扩展该示例,允许任意数量的中间子域名存在。请特别注意细节的变化,将“?”改为“. : \w+@(\w+.)\w+.com”。
>>> patt = '\w+@(\w+\.)*\w+\.com'
>>> re.match(patt, 'nobody@www.xxx.yyy.zzz.com').group()
'nobody@www.xxx.yyy.zzz.com'
但是,我们必须要添加一个“免责声明”,即仅仅使用字母数字字符并不能匹配组成电子邮件地址的全部可能字符。上述正则表达式不能匹配诸如xxx-yyy.com的域名或者使用非单词\W字符组成的域名。
之前讨论过使用圆括号来匹配和保存子组,以便于后续处理,而不是确定一个正则表达式匹配之后,在一个单独的子程序里面手动编码来解析字符串。此前还特别讨论过一个简单的正则表达式模式\w+-\d+,它由连字符号分隔的字母数字字符串和数字组成,还讨论了如何添加一个子组来构造一个新的正则表达式 (\w+)-(\d+)来完成这项工作。下面是初始版本的正则表达式的执行情况。
>>> m = re.match('\w\w\w-\d\d\d', 'abc-123')
>>> if m is not None: m.group()
...
'abc-123'
>>> m = re.match('\w\w\w-\d\d\d', 'abc-xyz')
>>> if m is not None: m.group()
...
>>>
在上面的代码中,创建了一个正则表达式来识别包含3个字母数字字符且后面跟着3个数字的字符串。使用abc-123测试该正则表达式,将得到正确的结果,但是使用abc-xyz则不能。现在,将修改之前讨论过的正则表达式,使该正则表达式能够提取字母数字字符串和数字。如下所示,请注意如何使用group()方法访问每个独立的子组以及groups()方法以获取一个包含所有匹配子组的元组。
>>> m = re.match('(\w\w\w)-(\d\d\d)', 'abc-123')
>>> m.group() # 完整匹配
'abc-123'
>>> m.group(1) # 子组 1
'abc'
>>> m.group(2) # 子组 2
'123'
>>> m.groups() # 全部子组
('abc', '123')
由以上脚本内容可见,group()通常用于以普通方式显示所有的匹配部分,但也能用于获取各个匹配的子组。可以使用groups()方法来获取一个包含所有匹配子字符串的元组。
如下为一个简单的示例,该示例展示了不同的分组排列,这将使整个事情变得更加清晰。
>>> m = re.match('ab', 'ab') # 没有子组
>>> m.group() # 完整匹配
'ab'
>>> m.groups() # 所有子组
()
>>>
>>> m = re.match('(ab)', 'ab') # 一个子组
>>> m.group() # 完整匹配
'ab'
>>> m.group(1) # 子组 1
'ab'
>>> m.groups() # 全部子组
('ab',)
>>>
>>> m = re.match('(a)(b)', 'ab') # 两个子组
>>> m.group() # 完整匹配
'ab'
>>> m.group(1) # 子组 1
'a'
>>> m.group(2) # 子组 2
'b'
>>> m.groups() # 所有子组
('a', 'b')
>>>
>>> m = re.match('(a(b))', 'ab') # 两个子组
>>> m.group() # 完整匹配
'ab'
>>> m.group(1) # 子组 1
'ab'
>>> m.group(2) # 子组 2
'b'
>>> m.groups() # 所有子组
('ab', 'b')
如下示例突出显示表示位置的正则表达式操作符。该操作符更多用于表示搜索而不是匹配,因为match()总是从字符串开始位置进行匹配。
>>> m = re.search('^The', 'The end.') # 匹配
>>> if m is not None: m.group()
...
'The'
>>> m = re.search('^The', 'end. The') # 不作为起始
>>> if m is not None: m.group()
...
>>> m = re.search(r'\bthe', 'bite the dog') # 在边界
>>> if m is not None: m.group()
...
'the'
>>> m = re.search(r'\bthe', 'bitethe dog') # 有边界
>>> if m is not None: m.group()
...
>>> m = re.search(r'\Bthe', 'bitethe dog') # 没有边界
>>> if m is not None: m.group()
...
'the'
读者将注意到此处出现的原始字符串。你可能想要查看本章末尾部分的核心提示“Python中原始字符串的用法”(Using Python raw strings),里面提到了在此处使用它们的原因。通常情况下,在正则表达式中使用原始字符串是个好主意。
读者还应当注意其他4个re模块函数和正则表达式对象方法:findall()、sub()、subn()和split()。
findall()查询字符串中某个正则表达式模式全部的非重复出现情况。这与search()在执行字符串搜索时类似,但与match()和search()的不同之处在于,findall()总是返回一个列表。如果findall()没有找到匹配的部分,就返回一个空列表,但如果匹配成功,列表将包含所有成功的匹配部分(从左向右按出现顺序排列)。
>>> re.findall('car', 'car')
['car']
>>> re.findall('car', 'scary')
['car']
>>> re.findall('car', 'carry the barcardi to the car')
['car', 'car', 'car']
子组在一个更复杂的返回列表中搜索结果,而且这样做是有意义的,因为子组是允许从单个正则表达式中抽取特定模式的一种机制,例如匹配一个完整电话号码中的一部分(例如区号),或者完整电子邮件地址的一部分(例如登录名称)。
对于一个成功的匹配,每个子组匹配是由findall()返回的结果列表中的单一元素;对于多个成功的匹配,每个子组匹配是返回的一个元组中的单一元素,而且每个元组(每个元组都对应一个成功的匹配)是结果列表中的元素。这部分内容可能第一次听起来令人迷惑,但是如果你尝试练习过一些不同的示例,就将澄清很多知识点。
finditer()函数是在Python 2.2版本中添加回来的,这是一个与findall()函数类似但是更节省内存的变体。两者之间以及和其他变体函数之间的差异(很明显不同于返回的是一个迭代器还是列表)在于,和返回的匹配字符串相比,finditer()在匹配对象中迭代。如下是在单个字符串中两个不同分组之间的差别。
>>> s = 'This and that.'
>>> re.findall(r'(th\w+) and (th\w+)', s, re.I)
[('This', 'that')]
>>> re.finditer(r'(th\w+) and (th\w+)', s,
... re.I).next().groups()
('This', 'that')
>>> re.finditer(r'(th\w+) and (th\w+)', s,
... re.I).next().group(1)
'This'
>>> re.finditer(r'(th\w+) and (th\w+)', s,
... re.I).next().group(2)
'that'
>>> [g.groups() for g in re.finditer(r'(th\w+) and (th\w+)',
... s, re.I)]
[('This', 'that')]
在下面的示例中,我们将在单个字符串中执行单个分组的多重匹配。
>>> re.findall(r'(th\w+)', s, re.I)
['This', 'that']
>>> it = re.finditer(r'(th\w+)', s, re.I)
>>> g = it.next()
>>> g.groups()
('This',)
>>> g.group(1)
'This'
>>> g = it.next()
>>> g.groups()
('that',)
>>> g.group(1)
'that'
>>> [g.group(1) for g in re.finditer(r'(th\w+)', s, re.I)]
['This', 'that']
注意,使用finditer()函数完成的所有额外工作都旨在获取它的输出来匹配findall()的输出。
最后,与match()和search()类似,findall()和finditer()方法的版本支持可选的pos和endpos参数,这两个参数用于控制目标字符串的搜索边界,这与本章之前的部分所描述的类似。
有两个函数/方法用于实现搜索和替换功能:sub()和subn()。两者几乎一样,都是将某字符串中所有匹配正则表达式的部分进行某种形式的替换。用来替换的部分通常是一个字符串,但它也可能是一个函数,该函数返回一个用来替换的字符串。subn()和sub()一样,但subn()还返回一个表示替换的总数,替换后的字符串和表示替换总数的数字一起作为一个拥有两个元素的元组返回。
>>> re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
'attn: Mr. Smith\012\012Dear Mr. Smith,\012'
>>>
>>> re.subn('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
('attn: Mr. Smith\012\012Dear Mr. Smith,\012', 2)
>>>
>>> print re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
attn: Mr. Smith
Dear Mr. Smith,
>>> re.sub('[ae]', 'X', 'abcdef')
'XbcdXf'
>>> re.subn('[ae]', 'X', 'abcdef')
('XbcdXf', 2)
前面讲到,使用匹配对象的group()方法除了能够取出匹配分组编号外,还可以使用\N,其中N是在替换字符串中使用的分组编号。下面的代码仅仅只是将美式的日期表示法MM/DD/YY{,YY}格式转换为其他国家常用的格式DD/MM/YY{,YY}。
>>> re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})',
... r'\2/\1/\3', '2/20/91') # Yes, Python is...
'20/2/91'
>>> re.sub(r'(\d{1,2})/(\d{1,2})/(\d{2}|\d{4})',
... r'\2/\1/\3', '2/20/1991') # ... 20+ years old!
'20/2/1991'
re模块和正则表达式的对象方法split()对于相对应字符串的工作方式是类似的,但是与分割一个固定字符串相比,它们基于正则表达式的模式分隔字符串,为字符串分隔功能添加一些额外的威力。如果你不想为每次模式的出现都分割字符串,就可以通过为max参数设定一个值(非零)来指定最大分割数。
如果给定分隔符不是使用特殊符号来匹配多重模式的正则表达式,那么re.split()与str.split()的工作方式相同,如下所示(基于单引号分割)。
>>> re.split(':', 'str1:str2:str3')
['str1', 'str2', 'str3']
这是一个简单的示例。如果有一个更复杂的示例,例如,一个用于Web站点(类似于Google或者Yahoo! Maps)的简单解析器,该如何实现?用户需要输入城市和州名,或者城市名加上ZIP编码,还是三者同时输入?这就需要比仅仅是普通字符串分割更强大的处理方式,具体如下。
>>> import re
>>> DATA = (
... 'Mountain View, CA 94040',
... 'Sunnyvale, CA',
... 'Los Altos, 94023',
... 'Cupertino 95014',
... 'Palo Alto CA',
... )
>>> for datum in DATA:
... print re.split(', |(?= (?:\d{5}|[A-Z]{2})) ', datum)
...
['Mountain View', 'CA', '94040']
['Sunnyvale', 'CA']
['Los Altos', '94023']
['Cupertino', '95014']
['Palo Alto', 'CA']
上述正则表达式拥有一个简单的组件:使用split语句基于逗号分割字符串。更难的部分是最后的正则表达式,可以通过该正则表达式预览一些将在下一小节中介绍的扩展符号。在普通的英文中,通常这样说:如果空格紧跟在五个数字(ZIP编码)或者两个大写字母(美国联邦州缩写)之后,就用split语句分割该空格。这就允许我们在城市名中放置空格。
通常情况下,这仅仅只是一个简单的正则表达式,可以在用来解析位置信息的应用中作为起点。该正则表达式并不能处理小写的州名或者州名的全拼、街道地址、州编码、ZIP+4(9位ZIP编码)、经纬度、多个空格等内容(或者在处理时会失败)。这仅仅意味着使用re.split()能够实现str.split()不能实现的一个简单的演示实例。
我们刚刚已经证实,读者将从正则表达式split语句的强大能力中获益;然而,记得一定在编码过程中选择更合适的工具。如果对字符串使用split方法已经足够好,就不需要引入额外复杂并且影响性能的正则表达式。
Python的正则表达式支持大量的扩展符号。让我们一起查看它们中的一些内容,然后展示一些有用的示例。
通过使用 (?iLmsux) 系列选项,用户可以直接在正则表达式里面指定一个或者多个标记,而不是通过compile()或者其他re模块函数。下面为一些使用re.I/IGNORECASE的示例,最后一个示例在re.M/MULTILINE实现多行混合:
>>> re.findall(r'(?i)yes', 'yes? Yes. YES!!')
['yes', 'Yes', 'YES']
>>> re.findall(r'(?i)th\w+', 'The quickest way is through this
tunnel.')
['The', 'through', 'this']
>>> re.findall(r'(?im)(^th[\w ]+)', """
... This line is the first,
... another line,
... that line, it's the best
... """)
['This line is the first', 'that line']
在前两个示例中,显然是不区分大小写的。在最后一个示例中,通过使用“多行”,能够在目标字符串中实现跨行搜索,而不必将整个字符串视为单个实体。注意,此时忽略了实例“the”,因为它们并不出现在各自的行首。
下一组演示使用re.S/DOTALL。该标记表明点号(.)能够用来表示\n符号(反之其通常用于表示除了\n之外的全部字符):
>>> re.findall(r'th.+', '''
... The first line
... the second line
... the third line
... ''')
['the second line', 'the third line']
>>> re.findall(r'(?s)th.+', '''
... The first line
... the second line
... the third line
... ''')
['the second line\nthe third line\n']
re.X/VERBOSE标记非常有趣;该标记允许用户通过抑制在正则表达式中使用空白符(除了在字符类中或者在反斜线转义中)来创建更易读的正则表达式。此外,散列、注释和井号也可以用于一个注释的起始,只要它们不在一个用反斜线转义的字符类中。
>>> re.search(r'''(?x)
... \((\d{3})\) # 区号
... [ ] # 空白符
... (\d{3}) # 前缀
... - # 横线
... (\d{4}) # 终点数字
... ''', '(800) 555-1212').groups()
('800', '555', '1212')
(?:…)符号将更流行;通过使用该符号,可以对部分正则表达式进行分组,但是并不会保存该分组用于后续的检索或者应用。当不想保存今后永远不会使用的多余匹配时,这个符号就非常有用。
>>> re.findall(r'http://(?:\w+\.)*(\w+\.com)',
... 'http://google.com http://www.google.com http://
code.google.com')
['google.com', 'google.com', 'google.com']
>>> re.search(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})',
... '(800) 555-1212').groupdict()
{'areacode': '800', 'prefix': '555'}
读者可以同时一起使用 (?P<name>) 和 (?P=name)符号。前者通过使用一个名称标识符而不是使用从1开始增加到N的增量数字来保存匹配,如果使用数字来保存匹配结果,我们就可以通过使用\1,\2 ...,\N \来检索。如下所示,可以使用一个类似风格的\g<name>来检索它们。
>>> re.sub(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-(?:\d{4})',
... '(\g<areacode>) \g<prefix>-xxxx', '(800) 555-1212')
'(800) 555-xxxx'
使用后者,可以在一个相同的正则表达式中重用模式,而不必稍后再次在(相同)正则表达式中指定相同的模式。例如,在本示例中,假定让读者验证一些电话号码的规范化。如下所示为一个丑陋并且压缩的版本,后面跟着一个正确使用的 (?x),使代码变得稍许易读。
>>> bool(re.match(r'\((?P<areacode>\d{3})\) (?P<prefix>\d{3})-
(?P<number>\d{4}) (?P=areacode)-(?P=prefix)-(?P=number)
1(?P=areacode)(?P=prefix)(?P=number)',
... '(800) 555-1212 800-555-1212 18005551212'))
True
>>> bool(re.match(r'''(?x)
...
... # match (800) 555-1212, save areacode, prefix, no.
... \((?P<areacode>\d{3})\)[ ](?P<prefix>\d{3})-(?P<number>\d{4})
...
... # space
... [ ]
...
... # match 800-555-1212
... (?P=areacode)-(?P=prefix)-(?P=number)
...
... # space
... [ ]
...
... # match 18005551212
... 1(?P=areacode)(?P=prefix)(?P=number)
...
... ''', '(800) 555-1212 800-555-1212 18005551212'))
True
读者可以使用 (?=...) 和 (?!…)符号在目标字符串中实现一个前视匹配,而不必实际上使用这些字符串。前者是正向前视断言,后者是负向前视断言。在后面的示例中,我们仅仅对姓氏为“van Rossum”的人的名字感兴趣,下一个示例中,让我们忽略以“noreply”或者“postmaster”开头的e-mail地址。
第三个代码片段用于演示findall()和finditer()的区别;我们使用后者来构建一个使用相同登录名但不同域名的e-mail地址列表(在一个更易于记忆的方法中,通过忽略创建用完即丢弃的中间列表)。
>>> re.findall(r'\w+(?= van Rossum)',
... '''
... Guido van Rossum
... Tim Peters
... Alex Martelli
... Just van Rossum
... Raymond Hettinger
... ''')
['Guido', 'Just']
>>> re.findall(r'(?m)^\s+(?!noreply|postmaster)(\w+)',
... '''
... sales@phptr.com
... postmaster@phptr.com
... eng@phptr.com
... noreply@phptr.com
... admin@phptr.com
... ''')
['sales', 'eng', 'admin']
>>> ['%s@aw.com' % e.group(1) for e in \
re.finditer(r'(?m)^\s+(?!noreply|postmaster)(\w+)',
... '''
... sales@phptr.com
... postmaster@phptr.com
... eng@phptr.com
... noreply@phptr.com
... admin@phptr.com
... ''')]
['sales@aw.com', 'eng@aw.com', 'admin@aw.com']
最后一个示例展示了使用条件正则表达式匹配。假定我们拥有另一个特殊字符,它仅仅包含字母“x”和“y”,我们此时仅仅想要这样限定字符串:两字母的字符串必须由一个字母跟着另一个字母。换句话说,你不能同时拥有两个相同的字母;要么由“x”跟着“y”,要么相反。
>>> bool(re.search(r'(?:(x)|y)(?(1)y|x)', 'xy'))
True
>>> bool(re.search(r'(?:(x)|y)(?(1)y|x)', 'xx'))
False
核心提示:使用Python原始字符串
读者可能在之前的一些示例中见过原始字符串的使用。正则表达式对于探索原始字符串有着强大的动力,原因就在于ASCII字符和正则表达式的特殊字符之间存在冲突。作为一个特殊符号,\b表示ASCII字符的退格符,但是\b同时也是一个正则表达式的特殊符号,表示匹配一个单词的边界。对于正则表达式编译器而言,若它把两个\b视为字符串内容而不是单个退格符,就需要在字符串中再使用一个反斜线转义反斜线,就像这样:\\b。
这样显得略微杂乱,特别是如果在字符串中拥有很多特殊字符,就会让人感到更加困惑。我们在Core Python Programming或者Core Python Language Fundamentals的Sequence章节中介绍了原始字符串,而且该原始字符串可以用于(且经常用于)帮助保持正则表达式查找某些可托管的东西。事实上,很多Python程序员总是抱怨这个方法,仅仅用原始字符串来定义正则表达式。
如下所示的一些示例用于说明退格符\b和正则表达式\b之间的差异,它们有的使用、有的不使用原始字符串。
读者可能回想起来我们在正则表达式中使用\d而没有使用原始字符串时并未遇到问题,这是因为ASCII中没有相应的特殊字符,所以正则表达式的编译器知道你想要表示十进制数字。
可能读者会对于正则表达式的特殊字符和特殊ASCII符号之间的差异感到迷惑。我们可以使用\n表示一个换行符,但是我们可以使用\d在正则表达式中表示匹配单个数字。
如果有符号同时用于ASCII和正则表达式,就会发生问题,因此在下面的核心提示中,建议使用Python的原始字符串来避免产生问题。另一个警告是:\w和\W字母数字字符集同时受re.L/LOCALE和Unicode(re.U/UNICODE)标记所影响。
>>> m = re.match('\bblow', 'blow') # backspace、no match
>> if m: m.group()
...
>> m = re.match('\\bblow', 'blow') # escaped\,now it works
>> if m: m.group()
...
'blow'
>> m = re.match(r'\bblow', 'blow') # use raw string instead
>> if m: m.group()
...
'blow'
下面看一些Python正则表达式的示例代码,这将使我们更接近实际应用中的程序。如下所示,以POSIX(UNIX风格操作系统,如Linux、Mac OS X等)的who命令的输出为例,该命令将列出所有登录当前系统中的用户信息。
$ who
wesley console Jun 20 20:33
wesley pts/9 Jun 22 01:38 (192.168.0.6)
wesley pts/1 Jun 20 20:33 (:0.0)
wesley pts/2 Jun 20 20:33 (:0.0)
wesley pts/4 Jun 20 20:33 (:0.0)
wesley pts/3 Jun 20 20:33 (:0.0)
wesley pts/5 Jun 20 20:33 (:0.0)
wesley pts/6 Jun 20 20:33 (:0.0)
wesley pts/7 Jun 20 20:33 (:0.0)
wesley pts/8 Jun 20 20:33 (:0.0)
可能我们想要保存一些用户登录信息,诸如登录名、用户登录的终端类型、用户登录的时间和地点。在前面的示例中使用str.split()方法并不高效,因为此处的空白符既不稳定也不一致。另一个问题是在登录时间戳中间的月、日和时间之间有空格,我们可能想要保存这些连续的字段。
读者需要一些方法描述诸如“分割两个或者多个空白符”之类的模式。这通过正则表达式很容易完成。很快,我们可以使用正则表达式模式\s\s+,该模式的意思是至少拥有两个以上的空白符。
下面创建一个名为rewho.py的程序,该程序读取who命令的输出,然后假定将得到的输出信息存入一个名为whoadat.txt的文件之中。rewho.py脚本最初如下所示:
import re
f = open('whodata.txt', 'r')
for eachLine in f:
print re.split(r'\s\s+', eachLine)
f.close()
上述代码同样使用原始字符串(将字母“r”或者“R”放置在左引号之前),主要目的是为了避免转义特殊字符串字符,如\n,该字符并不是特殊的正则表达式模式。对于确实拥有反斜线的正则表达式模式,读者可能希望逐字地处理它们;否则,读者必须在前面加上双斜线来保持它们的安全。
现在将执行who命令,保存输出到whodata.txt文件之中,然后调用rewho.py查看结果。
$ who > whodata.txt
$ rewho.py
['wesley', 'console', 'Jun 20 20:33\012']
['wesley', 'pts/9', 'Jun 22 01:38\011(192.168.0.6)\012']
['wesley', 'pts/1', 'Jun 20 20:33\011(:0.0)\012']
['wesley', 'pts/2', 'Jun 20 20:33\011(:0.0)\012']
['wesley', 'pts/4', 'Jun 20 20:33\011(:0.0)\012']
['wesley', 'pts/3', 'Jun 20 20:33\011(:0.0)\012']
['wesley', 'pts/5', 'Jun 20 20:33\011(:0.0)\012']
['wesley', 'pts/6', 'Jun 20 20:33\011(:0.0)\012']
['wesley', 'pts/7', 'Jun 20 20:33\011(:0.0)\012']
['wesley', 'pts/8', 'Jun 20 20:33\011(:0.0)\012']
这是非常好的一次尝试。首先,我们不期望单个制表符(ASCII \011)作为输出的一部分(可能看起来像是至少两个空白符),然后可能我们并不真的希望保存\n(ASCII \012)作为每一行的终止符。我们现在将修复这些问题,然后通过一些改进来提高应用的整体 质量。
首先,应当在脚本内部运行who命令而不是在外部,然后将输出存入whodata.txt文件,如果手动重复做这件事很快就会感到厌倦。要在该程序中调用其他程序,需要调用os.popen()命令。尽管os.popen()命令现在已经被subprocess模块所替换,但它更容易使用,而且此处的重点是展示re.split()的功能。
去除尾部的\n(使用str.rstrip()),然后添加单个制表符的检查,用于代替re.split()分隔符。示例1-1展示最终的rewho.py脚本在Python 2中的版本。
示例1-1 分割POSIX的who命令输出(rewho.py)
该脚本调用who命令,然后通过不同类型的空白字符分割输入的数据解析输入。
示例1-2表示rewho3.py,这是Python 3版本。和Python 2版本的主要差别在于print()函数(或者表达式)。这一整行表明了Python 2和3的关键区别。with语句在Python 2.5版中是试验性的,在Python 2.6版本中提供正式支持,该语句用于操作并支持所构建的对象实例。
示例1-2 rewho.py脚本的Python 3版本(rewho3.py)
该rewho.py的Python 3版本仅简单地运用print()函数替换了print语句。当使用with语句(从Python 2.5版本起可用)时,记住,file(Python 2)或者io(Python 3)对象的上下文管理器会自动调用f.close()。
通过使用with语句,拥有上下文管理器的对象变得更易于使用。关于with语句和上下文管理的更多信息,请参考Core Python Pragramming或者Core Pythom Language Fundamentals中的“Errors and Exceptions”章节。记住,两个版本(rewho.py或者rewho3.py)中的who命令仅能在POSIX系统中使用,除非可以在Windows系统的计算机中使用Cygwin。对于运行Microsoft Windows的个人电脑,可以尝试tasklist命令,但读者还需要做一个额外的调整。继续阅读本章后续的章节,查看一个执行that命令的示例。
示例1-3将rewho.py和rewho3.py合并为rewhoU.py,该名称的含义是“通用的rewho”。该程序能够在Python 2和3的解释器下运行。我们欺骗并避免使用print或者print(),方法是使用一个在2.x和3.x版本中都存在并且功能并不齐全的函数:distutils.log.warn()。这是一个单字符串输出函数,因此如果输出要复杂一些,就需要合并所有输出到一个字符串中,然后调用。要在该脚本中指明它的使用方式,就将它命名为printf()。
我们也在此使用with语句。这就意味着读者需要至少使用Python 2.6版本来运行该程序。这还不确切。之前提到过,在2.5版本中with语句是试验性的。这就意味着如果想要在Python 2.5中使用,就需要导入额外的语句:from __future__ import with_statement。如果读者仍在使用2.4或者更老的版本,就不能使用这个import语句,并且必须按照示例1-1那样运行这段代码。
示例1-3 rewho.py脚本的通用版本(rewhoU.py)
该脚本运行在Python 2 和 3 下,通过一个很简单的替换来代替print语句和print()函数。该脚本还包含从Python 2.5开始引入的with语句。
rewhoU.py的创建是一个介绍如何创建通用脚本的示例,这将帮助我们避免为Python 2和3同时维护两个版本的相同脚本。
使用合适的解释器执行这些脚本中的任何一个都会得到正确、简洁的输出。
$ rewho.py
['wesley', 'console', 'Feb 22 14:12']
['wesley', 'ttys000', 'Feb 22 14:18']
['wesley', 'ttys001', 'Feb 22 14:49']
['wesley', 'ttys002', 'Feb 25 00:13', '(192.168.0.20)']
['wesley', 'ttys003', 'Feb 24 23:49', '(192.168.0.20)']
同样不要忘记,之前的小节介绍过re.split()函数也可以使用可选的flage参数。
在Windows计算机上可以使用tasklist命令替代who来得到类似的结果。让我们查看该命令的输出结果。
C:\WINDOWS\system32>tasklist
Image Name PID Session Name Session# Mem Usage
========================= ====== ================ ======== ============
System Idle Process 0 Console 0 28 K
System 4 Console 0 240 K
smss.exe 708 Console 0 420 K
csrss.exe 764 Console 0 4,876 K
winlogon.exe 788 Console 0 3,268 K
services.exe 836 Console 0 3,932 K
. . .
可以看到,输出包含不同于who命令的输出信息,但格式是类似的,所以可以考虑之前的方案:在一个或多个空白符上执行re.split()(此处没有制表符的问题)。
问题是命令名称可能有一个空白符,而且我们(应当)更倾向于将整个命令名称连接在一起。对于内存的使用也有这个问题,我们通常得到的是“NNN K”,其中NNN是内存数量大小,K表示千字节。我们也希望将这些数据连接在一起,因此,最好分隔至少一个空白符,对吧?
不,不能这样做。注意,进程 ID(PID)和会话名称列仅仅由一个空白符分隔。这就意味着如果去掉至少一个空白符,PID和会话名称将被合并在一起作为单个结果。如果复制之前的一个脚本,重命名它为retasklist.py,然后将who命令修改为tasklist /nh(/nh选项将会去除每一列的标题),并使用一个\s\s+正则表达式,就将得到如下所示的输出。
Z:\corepython\ch1>python retasklist.py
['']
['System Idle Process', '0 Console', '0', '28 K']
['System', '4 Console', '0', '240 K']
['smss.exe', '708 Console', '0', '420 K']
['csrss.exe', '764 Console', '0', '5,028 K']
['winlogon.exe', '788 Console', '0', '3,284 K']
['services.exe', '836 Console', '0', '3,924 K']
. . .
已经确认,尽管我们将命令名称和内存使用字符串保存在一起,但也不经意地将PID和会话名称放在一起。因此我们不得不放弃使用split函数,而且通过正则表达式匹配实现。我们可以这样实现,然后滤除会话名称和编号,因为两者都会为输出添加数值。示例1-4显示Python 2版本下retasklist.py的最终版本。
示例1-4 处理DOS环境下tasklist命令的输出(retasklist.py)
这里的脚本使用一个正则表达式和findall()来解析DOS环境下tasklist命令的输出,但是仅仅显示感兴趣的数据。将该脚本移植到Python 3时,仅仅需要修改print()函数。
如果运行这个脚本,就能得到期望(已截断)的输出。
Z:\corepython\ch1>python retasklist.py
[]
[('System Idle Process', '0', '28 K')]
[('System', '4', '240 K')]
[('smss.exe', '708', '420 K')]
[('csrss.exe', '764', '5,016 K')]
[('winlogon.exe', '788', '3,284 K')]
[('services.exe', '836', '3,932 K')]
. . .
细致的正则表达式将会扫描全部的5列输出字符串,仅对重要的数据进行分组:命令名称、命令相应的PID,以及该命令使用的内存大小。该脚本使用已经在本章中介绍过的正则表达式的很多特性。
显然,在本小节中实现的全部脚本只向用户显示输出。实际上,我们有可能在处理数据,并将数据保存入数据库,使用得到的输出来为管理层生成报表等。
我们现在将浏览一个深入的示列,它以不同的方式使用正则表达式来操作字符串。首先是一些实际上生成用于操作随机数(但不是太随机)的代码。示例1-5展示了gendata.py,这是一个生成数据集的脚本。尽管该程序只是将简单地将生成的字符串集显示到标准输出,但是该输出可以很容易重定向到测试文件。
示例1-5 用于正则表达式练习的数据生成器(gendata.py)
该脚本为正则表达式练习创建随机数据,然后将生成的数据输出到屏幕。要将该程序移植到Python 3,仅需要将print语句修改为函数,将xrange()函数修改为range(),以及将sys.maxint修改为sys.maxsize。
该脚本生成拥有三个字段的字符串,由一对冒号或者一对双冒号分隔。第一个字段是随机(32位)整数,该整数将被转换为一个日期。下一个字段是一个随机生成的电子邮件地址。最后一个字段是一个由单横线(-)分隔的整数集。
运行这段代码,我们将获得以下输出(读者将会从此获益颇多),并将该输出在本地另存为redata.txt文件。
Thu Jul 22 19:21:19 2004::izsp@dicqdhytvhv.edu::1090549279-4-11
Sun Jul 13 22:42:11 2008::zqeu@dxaibjgkniy.com::1216014131-4-11
Sat May 5 16:36:23 1990::fclihw@alwdbzpsdg.edu::641950583-6-10
Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::1171590364-6-8
Thu Jun 26 19:08:59 2036::ugxfugt@jkhuqhs.net::2098145339-7-7
Tue Apr 10 01:04:45 2012::zkwaq@rpxwmtikse.com::1334045085-5-10
读者或者可能会辨别出来,但是来自该程序的输出是为正则表达式处理做准备的。后续将逐行解释,我们将实现一些正则表达式来操作这些数据,以及为本章末尾的练习留下很多内容。
在示例脚本中,需要使用多个模块。由于多种原因,尽管我们小心翼翼地避免使用from-import语句(例如,很容易判断一个函数来自哪个模块,以及可能导致本地模块冲突等),我们还是选择从这些模块中仅导入特定的属性,来帮助读者仅专注于那些属性,以及缩短每行代码的长度。
tlds是一组高级域名集合,当需要随机生成电子邮件地址时,就可以从中随机选出一个。
每次执行gendata.py,就会生成第5行和第10行之间的输出(该脚本对于所有需要随机整数的场景都使用random.randrange()函数)。对于每一行,我们选取所有可能范围(0~231–1 [sys.maxint])中的随机整数,然后使用time.ctime()函数将该整数转换为日期。Python中的系统时间和大多数基于POSIX的计算机一样,两者都使用从“epoch”至今的秒数,epoch是指1970年1月1日格林威治时间的午夜。如果我们选择一个32位整数,那么该整数将表示从epoch到最大可能时间(即epoch后的232秒)之间的某个时刻。
伪造邮件地址的登录名长度为4~7个字符(因此使用randrange(4,8))。为了将它们放在一起,需要随机选择4~7个小写字母,将所有字母逐个连接成一个字符串。random.choice()函数的功能就是接受一个序列,然后返回该序列中的一个随机元素。在该示例中,string.ascii_lowercase是字母表中拥有26个小写字母的序列集合。
我们决定伪造电子邮件地址的主域名长度不能多于12个字符,但是至少和登录名一样长。再一次使用随机的小写字母,逐个字母来组合这个名字。
该脚本的关键部分就是将所有随机数据放入输出行。先是数据字符串,然后是分隔符。然后将所有电子邮件地址通过登录名、“@”符号、域名和一个随机选择的高级域名组合在一起。在最终的双冒号之后,我们将使用用于表示初始时间的随机数字符串(日期字符串),后面跟着登录名和域名的长度,所有这些都由一个连字符分隔。
对于后续的练习,为正则表达式创建宽松和约束性的版本。建议读者在一个简短的应用中测试这些正则表达式,该应用利用之前所展示的示例文件redata.txt(或者使用通过运行gendata.py生成的数据)。当做练习时,读者将需要再次使用该数据。
在将正则表达式放入应用中之前,为了测试正则表达式,我们将导入re模块,然后将redata.txt中的一个示例行赋给字符串变量data。如下所示,这些语句在所有展示的示例中都是常量。
>>> import re
>>> data = 'Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::1171590364-6-8'
在第一个示例中,我们将创建一个正则表达式来提取(仅仅)数据文件redata.txt中每一行时间戳中一周的几天。我们将使用下面的正则表达式。
"^Mon|^Tue|^Wed|^Thu|^Fri|^Sat|^Sun"
该示例需要字符串以列出的7个字符串中的任意一个开头(“^”正则表达式中的脱字符)。如果我们将该正则表达式“翻译”成自然语言,读起来就会像这样:“字符串应当以“Mon”,“Tue”,. . . ,“Sat”或者“Sun”开头。
换句话说,如果按照如下所示的方式对日期字符串分组,我们就可以使用一个脱字符来替换所有脱字符。
"^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)"
括住字符串集的圆括号意思是:这些字符串中的一个将会有一次成功匹配。这是我们一开始就使用的“友好的”正则表达式版本,该版本并没有使用圆括号。如下所示,在这个修改过的正则表达式版本中,可以以子组的方式来访问匹配字符串。
>>> patt = '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)'
>>> m = re.match(patt, data)
>>> m.group() # entire match
'Thu'
>>> m.group(1) # subgroup 1
'Thu'
>>> m.groups() # all subgroups
('Thu',)
我们在该示例所实现的这个特性可能看起来并不是革命性的,但是在下一个示例或者作为正则表达式的一部分提供额外数据来实现字符串匹配操作的任何地方,它确定有它的独到之处,即使这些字符并不是你所感兴趣字符的一部分。
以上两个正则表达式都是非常严格的,尤其是要求一个字符串集。这可能在一个国际化的环境中并不能良好地工作,因为所在的环境中会使用当地的日期和缩写。一个宽松的正则表达式将为:^\w{3}。该正则表达式仅仅需要一个以三个连续字母数字字符开头的字符串。再一次,将正则表达式转换为正常的自然语言:脱字符^表示“作为起始”,\w表示任意单个字母数字字符,{3}表示将会有3个连续的正则表达式副本,这里使用{3}来修饰正则表达式。再一次,如果想要分组,就必须使用圆括号,例如^(\w{3})。
>>> patt = '^(\w{3})'
>>> m = re.match(patt, data)
>>> if m is not None: m.group()
...
'Thu'
>>> m.group(1)
'Thu'
注意,正则表达式^(\w){3}是错误的。当{3}在圆括号中时,先匹配三个连续的字母数字字符,然后表示为一个分组。但是如果将{3}移到外部,它就等效于三个连续的单个字母数字字符。
>>> patt = '^(\w){3}'
>>> m = re.match(patt, data)
>>> if m is not None: m.group()
...
'Thu'
>>> m.group(1)
'u'
当我们访问子组1时,出现字母“u”的原因是子组1持续被下一个字符替换。换句话说,m.group(1)以字母“T”作为开始,然后变为“h”,最终被替换为“u”。这些是单个字母数字字符的三个独立(并且重叠)分组,与一个包含三个连续字母数字字符的单独分组相反。
在下一个(而且是最后)的示例中,我们将创建一个正则表达式来提取redata.txt每一行的末尾所发现的数字字段。
然而,在创建任何正则表达式之前,我们就意识到这些整数数据项位于数据字符串的末尾。这就意味着我们需要选择使用搜索还是匹配。发起一个搜索将更易于理解,因为我们确切知道想要查找的内容(包含三个整数的数据集),所要查找的内容不是在字符串的起始部分,也不是整个字符串。如果我们想要实现匹配,就必须创建一个正则表达式来匹配整个行,然后使用子组来保存想要的数据。要展示它们之间的差别,就需要先执行搜索,然后实现匹配,以展示使用搜索更适合当前的需要。
因为我们想要寻找三个由连字符分隔的整数,所以可以创建自己的正则表达式来说明这一需求:\d+-\d+-\d+。该正则表达式的含义是,“任何数值的数字(至少一个)后面跟着一个连字符,然后是多个数字、另一个连字符,最后是一个数字集。”我们现在将使用search()来测试该正则表达式:
>>> patt = '\d+-\d+-\d+'
>>> re.search(patt, data).group() # entire match
'1171590364-6-8'
一个匹配尝试失败了,为什么呢?因为匹配从字符串的起始部分开始,需要被匹配的数值位于字符串的末尾。我们将不得不创建另一个正则表达式来匹配整个字符串。但是可以使用惰性匹配,即使用“.+”来表明一个任意字符集跟在我们真正感兴趣的部分之后。
patt = '.+\d+-\d+-\d+'
>>> re.match(patt, data).group() # entire match
'Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::1171590364-6-8'
该正则表达式效果非常好,但是我们只想要末尾的数字字段,而并不是整个字符串,因此不得不使用圆括号对想要的内容进行分组。
>>> patt = '.+(\d+-\d+-\d+)'
>>> re.match(patt, data).group(1) # subgroup 1
'4-6-8'
发生了什么?我们将提取1171590364-6-8,而不仅仅是4-6-8。第一个整数的其余部分在哪儿?问题在于正则表达式本质上实现贪婪匹配。这就意味着对于该通配符模式,将对正则表达式从左至右按顺序求值,而且试图获取匹配该模式的尽可能多的字符。在之前的示例中,使用“.+”获取从字符串起始位置开始的全部单个字符,包括所期望的第一个整数字段。\d+仅仅需要一个数字,因此将得到“4”,其中.+匹配了从字符串起始部分到所期望的第一个数字的全部内容:“Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::117159036”,如图1-2所示。
图1-2 为什么匹配出错了:+是一个贪婪操作符
其中的一个方案是使用“非贪婪”操作符“?”。读者可以在“*”、“+”或者“?”之后使用该操作符。该操作符将要求正则表达式引擎匹配尽可能少的字符。因此,如果在“.+”之后放置一个“?”,我们将获得所期望的结果,如图1-3所示。
>>> patt = '.+?(\d+-\d+-\d+)'
>>> re.match(patt, data).group(1) # subgroup 1
'1171590364-6-8'
图1-3 解决贪婪匹配的问题:“?”表示非贪婪匹配
另一个实际情况下更简单的方案,就是把“::”作为字段分隔符。读者可以仅仅使用正则字符串strip(':: ')方法获取所有的部分,然后使用strip('-')作为另一个横线分隔符,就能够获取最初想要查询的三个整数。现在,我们不想先选择该方案,因为这就是我们如何将字符串放在一起,以使用gendata.py作为开始!
最后一个示例:假定我们仅想取出三个整数字段中间的那个整数。如下所示,这就是实现的方法(使用一个搜索,这样就不必匹配整个字符串):-(\d+)-。尝试该模式,将得到以下内容。
>>> patt = '-(\d+)-'
>>> m = re.search(patt, data)
>>> m.group() # entire match
'-6-'
>>> m.group(1) # subgroup 1
'6'
本章几乎没有涉及正则表达式的强大功能,在有限的篇幅里面我们不可能做到。然而,我们希望已经向读者提供了足够有用的介绍性信息,使读者能够掌握这个强有力的工具,并融入到自己的编程技巧里面。建议读者阅读参考文档以获取在Python中如何使用正则表达式的更多细节。对于想要更深入研究正则表达式的读者,建议阅读由 Jeffrey E. F. Friedl.编写的Mastering Regular Expressions。
正则表达式。按照练习1-1~1-12的要求创建正则表达式。
1-1 识别后续的字符串:“bat”、“bit”、“but”、“hat”、“hit”或者“hut”。
1-2 匹配由单个空格分隔的任意单词对,也就是姓和名。
1-3 匹配由单个逗号和单个空白符分隔的任何单词和单个字母,如姓氏的首字母。
1-4 匹配所有有效Python标识符的集合。
1-5 根据读者当地的格式,匹配街道地址(使你的正则表达式足够通用,来匹配任意数量的街道单词,包括类型名称)。例如,美国街道地址使用如下格式:1180 Bordeaux Drive。使你的正则表达式足够灵活,以支持多单词的街道名称,如3120 De la Cruz Boulevard。
1-6 匹配以“www”起始且以“.com”结尾的简单Web域名;例如,www://www. yahoo.com/。选做题:你的正则表达式也可以支持其他高级域名,如.edu、.net等(例如,http://www.foothill.edu)。
1-7 匹配所有能够表示Python整数的字符串集。
1-8 匹配所有能够表示Python长整数的字符串集。
1-9 匹配所有能够表示Python浮点数的字符串集。
1-10 匹配所有能够表示Python复数的字符串集。
1-11 匹配所有能够表示有效电子邮件地址的集合(从一个宽松的正则表达式开始,然后尝试使它尽可能严谨,不过要保持正确的功能)。
1-12 匹配所有能够表示有效的网站地址的集合(URL)(从一个宽松的正则表达式开始,然后尝试使它尽可能严谨,不过要保持正确的功能)。
1-13 type()。内置函数type()返回一个类型对象,如下所示,该对象将表示为一个Pythonic类型的字符串。
>>> type(0)
<type 'int'>
>>> type(.34)
<type 'float'>
>>> type(dir)
<type 'builtin_function_or_method'>
创建一个能够从字符串中提取实际类型名称的正则表达式。函数将对类似于<type 'int' >的字符串返回int(其他类型也是如此,如 'float' 、'builtin_function_or_method' 等)。注意:你所实现的值将存入类和一些内置类型的__name__属性中。
1-14 处理日期。1.2节提供了来匹配单个或者两个数字字符串的正则表达式模式,来表示1~9的月份(0?[1-9])。创建一个正则表达式来表示标准日历中剩余三个月的数字。
1-15 处理信用卡号码。1.2节还提供了一个能够匹配信用卡(CC)号码([0-9]{15,16})的正则表达式模式。然而,该模式不允许使用连字符来分割数字块。创建一个允许使用连字符的正则表达式,但是仅能用于正确的位置。例如,15位的信用卡号码使用4-6-5的模式,表明4个数字-连字符-6个数字-连字符-5个数字;16位的信用卡号码使用4-4-4-4的模式。记住,要对整个字符串进行合适的分组。选做题:有一个判断信用卡号码是否有效的标准算法。编写一些代码,这些代码不但能够识别具有正确格式的号码,而且能够识别有效的信用卡号码。
使用gendata.py。下面一组练习(1-16~1-27)专门处理由gendata.py生成的数据。在尝试练习1-17和1-18之前,读者需要先完成练习1-16以及所有正则表达式。
1-16 为gendata.py更新代码,使数据直接输出到redata.txt而不是屏幕。
1-17 判断在redata.tex中一周的每一天出现的次数(换句话说,读者也可以计算所选择的年份中每个月中出现的次数)。
1-18 通过确认整数字段中的第一个整数匹配在每个输出行起始部分的时间戳,确保在redata.txt中没有数据损坏。
创建以下正则表达式。
1-19 提取每行中完整的时间戳。
1-20 提取每行中完整的电子邮件地址。
1-21 仅仅提取时间戳中的月份。
1-22 仅仅提取时间戳中的年份。
1-23 仅仅提取时间戳中的时间(HH:MM:SS)。
1-24 仅仅从电子邮件地址中提取登录名和域名(包括主域名和高级域名一起提取)。
1-25 仅仅从电子邮件地址中提取登录名和域名(包括主域名和高级域名)。
1-26 使用你的电子邮件地址替换每一行数据中的电子邮件地址。
1-27 从时间戳中提取月、日和年,然后以“月,日,年”的格式,每一行仅仅迭代一次。
处理电话号码。对于练习1-28和1-29,回顾1.2节介绍的正则表达式\d{3}-\d{3}-\d{4},它匹配电话号码,但是允许可选的区号作为前缀。更新正则表达式,使它满足以下条件。
1-28 区号(三个整数集合中的第一部分和后面的连字符)是可选的,也就是说,正则表达式应当匹配800-555-1212,也能匹配555-1212。
1-29 支持使用圆括号或者连字符连接的区号(更不用说是可选的内容);使正则表达式匹配800-555-1212、555-1212以及(800)555-1212。
正则表达式应用程序。下面练习在处理在线数据时生成了有用的应用程序脚本。
1-30 生成HTML。提供一个链接列表(以及可选的简短描述),无论用户通过命令行方式提供、通过来自于其他脚本的输入,还是来自于数据库,都生成一个Web页面(.html),该页面包含作为超文本锚点的所有链接,它可以在Web浏览器中查看,允许用户单击这些链接,然后访问相应的站点。如果提供了简短的描述,就使用该描述作为超文本而不是URL。
1-31 tweet精简。有时候你想要查看由Twitter用户发送到Twitter服务的tweet纯文本。创建一个函数以获取tweet和一个可选的“元”标记,该标记默认为False,然后返回一个已精简过的tweet字符串,即移除所有无关信息,例如,表示转推的RT符号、前导的“.”符号,以及所有#号标签。如果元标记为True,就返回一个包含元数据的字典。这可以包含一个键“RT”,其相应的值是转推该消息的用户的字符串元组和/或一个键“#号标签”(包含一个#号标签元组)。如果值不存在(空元组),就不要为此创建一个键值条目。
1-32 亚马逊爬虫脚本。创建一个脚本,帮助你追踪你最喜欢的书,以及这些书在亚马逊上的表现(或者能够追踪图书排名的任何其他的在线书店)。例如,亚马逊对于任何一本图书提供以下链接:http://amazon.com/dp/ISBN (例如,http://amazon.com/ dp/0132678209)。读者可以改变域名,检查亚马逊在其他国家的站点上相同的图书排名,例如德国(.de)、法国(.fr)、日本(.jp)、中国(.cn)和英国(.co.uk)。使用正则表达式或者标记解析器,例如BeautifulSoup、lxml或者html5lib来解析排名,然后让用户传入命令行参数,指明输出是否应当在一个纯文本中,也许包含在一个电子邮件正文中,还是用于Web的格式化HTML中。
所以,出路就是IPv6。你们都知道,我们几乎用尽了IPv4地址空间。对此我感到有点尴尬,因为我就是决定32位IP地址足够因特网实验使用的那个人。我唯一能够辩驳的是,当时是在1977年做出的那个选择,并且当时我认为它仅仅是一个实验。然而,问题是这个实验并没有结束,所以我们才陷入了这个困境。
——Vint Cerf,2011年1月[1]
(在linux.conf.au会议上口述)
2.1 简介 本节将简要介绍使用套接字进行网络编程的知识。然而,在深入研究之前,将介绍一些有关网络编程的背景信息,以及套接字如何应用于Python之中,然后展示如何使用Python的一些模块来创建网络应用程序。
什么是客户端/服务器架构?对于不同的人来说,它意味着不同的东西,这取决于你问谁以及描述的是软件还是硬件系统。在这两种情况中的任何一种下,前提都很简单:服务器就是一系列硬件或软件,为一个或多个客户端(服务的用户)提供所需的“服务”。它存在唯一目的就是等待客户端的请求,并响应它们(提供服务),然后等待更多请求。
另一方面,客户端因特定的请求而联系服务器,并发送必要的数据,然后等待服务器的回应,最后完成请求或给出故障的原因。服务器无限地运行下去,并不断地处理请求;而客户端会对服务进行一次性请求,然后接收该服务,最后结束它们之间的事务。客户端在一段时间后可能会再次发出其他请求,但这些都被当作不同的事务。
目前最常见的客户端/服务器架构如图2-1所示,其中描绘了一个用户或客户端计算机通过因特网从一台服务器上检索信息。尽管这样的系统确实是一个客户端/服务器架构的例子,但它不是唯一的情况。此外,客户端/服务器架构既可以应用于计算机硬件,也可以应用于软件。
图2-1 因特网上客户端/服务器系统的典型概念图
打印(打印机)服务器是硬件服务器的一个例子。它们处理传入的打印作业并将其发送给系统中的打印机(或其他的打印设备)。这样的计算机通常可以通过网络进行访问,并且客户端计算机将向它发送打印请求。
硬件服务器的另一个例子就是文件服务器。这些通常都是拥有庞大通用存储容量的计算机,可以被客户端远程访问。客户端计算机会挂载服务器计算机上的磁盘,看起来好像这个磁盘就在本地计算机上一样。支持文件服务器的一个最流行的网络操作系统就是Sun公司的网络文件系统(NFS)。如果你正在访问一个网络磁盘驱动器,并且无法分辨它是在本地还是网络上,那么此时客户端/服务器系统就已经完成了它的任务。它的目标就是让用户得到与访问本地磁盘完全相同的体验,抽象起来就是正常的磁盘访问,而这些都是通过编程实现来确保以这种方式进行。
软件服务器也运行在一块硬件之上,但是没有像硬件服务器那样的专用外围设备(如打印机、磁盘驱动器等)。软件服务器提供的主要服务包括程序执行、数据传输检索、聚合、更新,或其他类型的编程或数据操作。
现在一个更常见的软件服务器就是Web服务器。如果个人或公司想要运行自己的Web服务器,那么必须拥有一台或多台计算机,在上面安装希望提供给用户的Web页面和Web应用程序,然后启动Web服务器。一个这样的服务器的工作就是接受客户端请求,并向(Web)客户端(即用户计算机上的浏览器)回送Web页面,然后等待下一个客户端的请求。这些服务器一旦开启,都将可能永远运行。虽然它们并不能实现这一目标,但是它们会尽可能长时间地运行,除非受到一些外力驱使才会停止,如显式地关闭,或灾难性地关闭(由于硬件故障)。
数据库服务器是另一种类型的软件服务器。它们接受客户端的存储或检索请求,响应请求,然后等待更多的事务。与Web服务器类似,它们也是永远运行的。
我们将讨论的最后一类软件服务器就是窗体(window)服务器,几乎可以认为这些服务器是硬件服务器。它们运行在一台附带(外接)显示设备(如显示器)的计算机上。窗体客户端其实就是一些程序,这些程序需要一个窗口化的环境来运行。这些通常被当作图形用户界面(GUI)应用程序。如果在没有窗体服务器的情况下执行它们,也即意味着在一个基于文本的环境中,如DOS窗口或一个UNIX shell中,那么将无法启动它们。一旦能够访问窗体服务器,那么一切都会正常。
在网络领域,这种环境会变得更加有趣。窗体客户端通常的显示设备就是本地计算机上的服务器,但是在一些网络化的窗体环境(如X Window系统)中,也可以选择另一台计算机的窗体服务器作为一个显示设备。在这种情况下,你就可以在一台计算机上运行一个GUI程序,而将它显示在另一台计算机上!
想象客户端/服务器架构如何工作的一个方法就是,在你的脑海中创建一个画面,那就是一个银行出纳员,他既不吃不睡,也不休息,服务一个又一个的排队客户,似乎永远不会结束(见图2-2)。这个队列可能很长,也可能空无一人,但在任何给定的某个时刻,都可能会出现一个客户。当然,在几年前这样的出纳员完全是一种幻想,但是现在的自动取款机(ATM)似乎比较接近这种模型。
图2-2 图中的银行出纳员“永远”处于工作状态,为客户的请求提供服务。出纳员运行在一个无限循环中,不断地接收请求并服务客户,然后返回服务或等待另一位客户。可能会有一个很长的客户队列,也可能队列中空无一人。但在任何一种情况下,服务器的工作都永远不会结束
当然,出纳员就是一个运行在无限循环中的服务器,而每个客户就是一个客户端,每个客户端都有一个需要解决的需求。这些客户到达银行,并由出纳以“先来先服务”的方式处理。一旦一个事务完成,客户就会离开,而出纳员要么为下一位客户服务,要么坐下来等待,直到下一位客户到来。
为什么所有这些都很重要呢?因为在一般意义上,这种执行风格正是客户端/服务器架构的工作方式。既然现在你已经有了基本的概念,接下来就让我们将它应用到网络编程上,而网络编程正是遵循客户端/服务器架构的软件模型。
在服务器响应客户端请求之前,必须进行一些初步的设置流程来为之后的工作做准备。首先会创建一个通信端点,它能够使服务器监听请求。可以把服务器比作公司前台,或者应答公司主线呼叫的总机接线员。一旦电话号码和设备安装成功且接线员到达时,服务就可以开始了。
这个过程与网络世界一样,一旦一个通信端点已经建立,监听服务器就可以进入无限循环中,等待客户端的连接并响应它们的请求。当然,为了使公司电话接待员一直处于忙碌状态,我们绝不能忘记将电话号码放在公司信笺、广告或一些新闻稿上;否则,将没有人会打电话过来!
相似地,必须让潜在的客户知道存在这样的服务器来处理他们的需求;否则,服务器将永远不会得到任何请求。想象着创建一个全新的网站,这可能是最了不起的、劲爆的、令人惊异的、有用的并且最酷的网站,但如果该网站的Web地址或URL从来没有以任何方式广播或进行广告宣传,那么永远也不会有人知道它,并且也将永远不会看到任何访问者。
现在你已经非常了解了服务器是如何工作的,这就已经解决了较困难的部分。客户端比服务器端更简单,客户端所需要做的只是创建它的单一通信端点,然后建立一个到服务器的连接。然后,客户端就可以发出请求,该请求包括任何必要的数据交换。一旦请求被服务器处理,且客户端收到结果或某种确认信息,此次通信就会被终止。
本节将介绍套接字(socket),给出有关其起源的一些背景知识,并讨论各种类型的套接字。最后,将讲述如何利用它们使运行在不同(或相同)计算机上的进程相互通信。
套接字是计算机网络数据结构,它体现了上节中所描述的“通信端点”的概念。在任何类型的通信开始之前,网络应用程序必须创建套接字。可以将它们比作电话插孔,没有它将无法进行通信。
套接字的起源可以追溯到20世纪70年代,它是加利福尼亚大学的伯克利版本UNIX(称为BSD UNIX)的一部分。因此,有时你可能会听过将套接字称为伯克利套接字或BSD套接字。套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。这就是所谓的进程间通信(Inter Process Communication,IPC)。有两种类型的套接字:基于文件的和面向网络的。
UNIX套接字是我们所讲的套接字的第一个家族,并且拥有一个“家族名字”AF_UNIX(又名AF_LOCAL,在POSIX1.g标准中指定),它代表地址家族(address family):UNIX。包括Python在内的大多数受欢迎的平台都使用术语地址家族及其缩写AF;其他比较旧的系统可能会将地址家族表示成域(domain)或协议家族(protocol family),并使用其缩写PF而非AF。类似地,AF_LOCAL(在2000~2001年标准化)将代替AF_UNIX。然而,考虑到后向兼容性,很多系统都同时使用二者,只是对同一个常数使用不同的别名。Python本身仍然在使用AF_UNIX。
因为两个进程运行在同一台计算机上,所以这些套接字都是基于文件的,这意味着文件系统支持它们的底层基础结构。这是能够说得通的,因为文件系统是一个运行在同一主机上的多个进程之间的共享常量。
第二种类型的套接字是基于网络的,它也有自己的家族名字AF_INET,或者地址家族:因特网。另一个地址家族AF_INET6用于第6版因特网协议(IPv6)寻址。此外,还有其他的地址家族,这些要么是专业的、过时的、很少使用的,要么是仍未实现的。在所有的地址家族之中,目前AF_INET是使用得最广泛的。
Python 2.5中引入了对特殊类型的Linux套接字的支持。套接字的AF_NETLINK家族(无连接[见2.3.3节])允许使用标准的BSD套接字接口进行用户级别和内核级别代码之间的IPC。之前那种解决方案比较麻烦,而这个解决方案可以看作一种比前一种更加优雅且风险更低的解决方案,例如,添加新系统调用、/proc支持,或者对一个操作系统的“IOCTL”。
针对Linux的另一种特性(Python 2.6中新增)就是支持透明的进程间通信(TIPC)协议。TIPC允许计算机集群之中的机器相互通信,而无须使用基于IP的寻址方式。Python对TIPC的支持以AF_TIPC家族的方式呈现。
总的来说,Python只支持AF_UNIX、AF_NETLINK、AF_TIPC和AF_INET家族。因为本章重点讨论网络编程,所以在本章剩余的大部分内容中,我们将使用AF_INET。
如果一个套接字像一个电话插孔——允许通信的一些基础设施,那么主机名和端口号就像区号和电话号码的组合。然而,拥有硬件和通信的能力本身并没有任何好处,除非你知道电话打给谁以及如何拨打电话。一个网络地址由主机名和端口号对组成,而这是网络通信所需要的。此外,并未事先说明必须有其他人在另一端接听;否则,你将听到这个熟悉的声音“对不起,您所拨打的电话是空号,请核对后再拨”。你可能已经在浏览网页的过程中见过一个网络类比,例如“无法连接服务器,服务器没有响应或者服务器不可达。”
有效的端口号范围为0~65535(尽管小于1024的端口号预留给了系统)。如果你正在使用POSIX兼容系统(如Linux、Mac OS X等),那么可以在/etc/services文件中找到预留端口号的列表(以及服务器/协议和套接字类型)。众所周知的端口号列表可以在这个网站中查看:http://www.iana.org/assignments/port-numbers。
不管你采用的是哪种地址家族,都有两种不同风格的套接字连接。第一种是面向连接的,这意味着在进行通信之前必须先建立一个连接,例如,使用电话系统给一个朋友打电话。这种类型的通信也称为虚拟电路或流套接字。
面向连接的通信提供序列化的、可靠的和不重复的数据交付,而没有记录边界。这基本上意味着每条消息可以拆分成多个片段,并且每一条消息片段都确保能够到达目的地,然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序。
实现这种连接类型的主要协议是传输控制协议(更为人熟知的是它的缩写TCP)。为了创建TCP套接字,必须使用SOCK_STREAM作为套接字类型。TCP套接字的名字SOCK_STREAM基于流套接字的其中一种表示。因为这些套接字(AF_INET)的网络版本使用因特网协议(IP)来搜寻网络中的主机,所以整个系统通常结合这两种协议(TCP和IP)来进行(当然,也可以使用TCP和本地[非网络的AF_LOCAL/AF_UNIX]套接字,但是很明显此时并没有使用IP)。
与虚拟电路形成鲜明对比的是数据报类型的套接字,它是一种无连接的套接字。这意味着,在通信开始之前并不需要建立连接。此时,在数据传输过程中并无法保证它的顺序性、可靠性或重复性。然而,数据报确实保存了记录边界,这就意味着消息是以整体发送的,而并非首先分成多个片段,例如,使用面向连接的协议。
使用数据报的消息传输可以比作邮政服务。信件和包裹或许并不能以发送顺序到达。事实上,它们可能不会到达。为了将其添加到并发通信中,在网络中甚至有可能存在重复的消息。
既然有这么多副作用,为什么还使用数据报呢(使用流套接字肯定有一些优势)?由于面向连接的套接字所提供的保证,因此它们的设置以及对虚拟电路连接的维护需要大量的开销。然而,数据报不需要这些开销,即它的成本更加“低廉”。因此,它们通常能提供更好的性能,并且可能适合一些类型的应用程序。
实现这种连接类型的主要协议是用户数据报协议(更为人熟知的是其缩写UDP)。为了创建UDP套接字,必须使用SOCK_DGRAM作为套接字类型。你可能知道,UDP套接字的SOCK_DGRAM名字来自于单词“datagram”(数据报)。因为这些套接字也使用因特网协议来寻找网络中的主机,所以这个系统也有一个更加普通的名字,即这两种协议(UDP和IP)的组合名字,或UDP/IP。
既然你知道了所有关于客户端/服务器架构、套接字和网络方面的基础知识,接下来就让我们试着将这些概念应用到Python中。本节中将使用的主要模块就是socket模块,在这个模块中可以找到socket()函数,该函数用于创建套接字对象。套接字也有自己的方法集,这些方法可以实现基于套接字的网络通信。
要创建套接字,必须使用socket.socket()函数,它一般的语法如下。
socket(socket_family, socket_type,protocol=0)
其中,socket_family是AF_UNIX或AF_INET(如前所述),socket_type是SOCK_STREAM或SOCK_DGRAM(也如前所述)。protocol通常省略,默认为0。
所以,为了创建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)
一旦有了一个套接字对象,那么使用套接字对象的方法将可以进行进一步的交互。
表2-1列出了最常见的套接字方法。在下一节中,我们将使用其中的一些方法创建TCP和UDP客户端与服务器。虽然我们专注于网络套接字,但这些方法与使用本地/不联网的套接字时有类似的含义。
表2-1 常见的套接字对象方法和属性
名 称 |
描 述 |
---|---|
服务器套接字方法 |
|
s.bind() |
将地址(主机名、端口号对)绑定到套接字上 |
s.listen() |
设置并启动TCP监听器 |
s.accept() |
被动接受TCP客户端连接,一直等待直到连接到达(阻塞) |
客户端套接字方法 |
|
s.connect() |
主动发起TCP服务器连接 |
s.connect_ex() |
connect()的扩展版本,此时会以错误码的形式返回问题,而不是抛出一个异常 |
普通的套接字方法 |
|
s.recv() |
接收TCP消息 |
s.recv_into()① |
接收TCP消息到指定的缓冲区 |
s.send() |
发送TCP消息 |
s.sendall() |
完整地发送TCP消息 |
s.recvfrom() |
接收UDP消息 |
s.recvfrom_into()① |
接收UDP消息到指定的缓冲区 |
s.sendto() |
发送UDP消息 |
s.getpeername() |
连接到套接字(TCP)的远程地址 |
s.getsockname() |
当前套接字的地址 |
s.getsockopt() |
返回给定套接字选项的值 |
s.setsockopt() |
设置给定套接字选项的值 |
s.shutdown() |
关闭连接 |
s.close() |
关闭套接字 |
s.detach()② |
在未关闭文件描述符的情况下关闭套接字,返回文件描述符 |
s.ioctl()③ |
控制套接字的模式(仅支持Windows) |
面向阻塞的套接字方法 |
|
s.setblocking() |
设置套接字的阻塞或非阻塞模式 |
s.settimeout()④ |
设置阻塞套接字操作的超时时间 |
s.gettimeout()④ |
获取阻塞套接字操作的超时时间 |
面向文件的套接字方法 |
|
s.fileno() |
套接字的文件描述符 |
s.makefile() |
创建与套接字关联的文件对象 |
数据属性 |
|
s.family① |
套接字家族 |
s.type① |
套接字类型 |
s.proto① |
套接字协议 |
① Python 2.5中新增。
② Python 3.2中新增。
③ Python 2.6中新增,仅仅支持Windows平台;POSIX系统可以使用functl模块函数。
④ Python 2.3中新增。
核心提示:在不同的计算机上分别安装客户端和服务器来运行网络应用程序
在本章众多的例子中,你会经常看到指示主机“localhost”的代码和输出,或者看到127.0.0.1的IP地址。在这里的示例中,客户端和服务器运行在同一台计算机上。不过,鼓励读者修改主机名,并将代码复制到不同的计算机上,因为这样开发的代码运行起来更加有趣,让计算机通过网络相互通信,然后可以看到网络程序确实能够工作!
首先,我们将展现创建通用TCP服务器的一般伪代码,然后对这些代码的含义进行一般性的描述。需要记住的是,这仅仅是设计服务器的一种方式。一旦熟悉了服务器设计,那么你将能够按照自己的要求修改下面的伪代码来操作服务器。
ss = socket() # 创建服务器套接字
ss.bind() # 套接字与地址绑定
ss.listen() # 监听连接
inf_loop: # 服务器无限循环
cs = ss.accept() # 接受客户端连接
comm_loop: # 通信循环
cs.recv()/cs.send() # 对话(接收/发送)
cs.close() # 关闭客户端套接字
ss.close() # 关闭服务器套接字#(可选)
所有套接字都是通过使用socket.socket()函数来创建的。因为服务器需要占用一个端口并等待客户端的请求,所以它们必须绑定到一个本地地址。因为TCP是一种面向连接的通信系统,所以在TCP服务器开始操作之前,必须安装一些基础设施。特别地,TCP服务器必须监听(传入)的连接。一旦这个安装过程完成后,服务器就可以开始它的无限循环。
调用accept()函数之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接。默认情况下,accept()是阻塞的,这意味着执行将被暂停,直到一个连接到达。另外,套接字确实也支持非阻塞模式,可以参考文档或操作系统教材,以了解有关为什么以及如何使用非阻塞套接字的更多细节。
一旦服务器接受了一个连接,就会返回(利用accept())一个独立的客户端套接字,用来与即将到来的消息进行交换。使用新的客户端套接字类似于将客户的电话切换给客服代表。当一个客户电话最后接进来时,主要的总机接线员会接到这个电话,并使用另一条线路将这个电话转接给合适的人来处理客户的需求。
这将能够空出主线(原始服务器套接字),以便接线员可以继续等待新的电话(客户请求),而此时客户及其连接的客服代表能够进行他们自己的谈话。同样地,当一个传入的请求到达时,服务器会创建一个新的通信端口来直接与客户端进行通信,再次空出主要的端口,以使其能够接受新的客户端连接。
一旦创建了临时套接字,通信就可以开始,通过使用这个新的套接字,客户端与服务器就可以开始参与发送和接收的对话中,直到连接终止。当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接。
在代码中,一个客户端连接关闭之后,服务器就会等待另一个客户端连接。最后一行代码是可选的,在这里关闭了服务器套接字。其实,这种情况永远也不会碰到,因为服务器应该在一个无限循环中运行。在示例中这行代码用来提醒读者,当为服务器实现一个智能的退出方案时,建议调用close()方法。例如,当一个处理程序检测到一些外部条件时,服务器就应该关闭。在这些情况下,应该调用一个close()方法。
核心提示:多线程处理客户端请求
我们没在该例子中实现这一点,但将一个客户端请求切换到一个新线程或进程来完成客户端处理也是相当普遍的。SocketServer模块是一个以socket为基础而创建的高级套接字通信模块,它支持客户端请求的线程和多进程处理。可以参考文档或在第4章的练习部分获取SocketServer模块的更多信息。
示例2-1给出了tsTserv.py文件,它是一个TCP服务器程序,它接受客户端发送的数据字符串,并将其打上时间戳(格式:[时间戳]数据)并返回给客户端(“tsTserv”代表时间戳TCP服务器,其他文件以类似的方式命名)。
示例2-1 TCP时间戳服务器(tsTserv.py)
这个脚本创建一个TCP服务器,它接受来自客户端的消息,然后将消息加上时间戳前缀并发送回客户端。
在UNIX启动行后面,导入了time.ctime()和socket模块的所有属性。
HOST变量是空白的,这是对bind()方法的标识,表示它可以使用任何可用的地址。我们也选择了一个随机的端口号,并且该端口号似乎没有被使用或被系统保留。另外,对于该应用程序,将缓冲区大小设置为1KB。可以根据网络性能和程序需要改变这个容量。listen()方法的参数是在连接被转接或拒绝之前,传入连接请求的最大数。
在第11行,分配了TCP服务器套接字(tcpSerSock),紧随其后的是将套接字绑定到服务器地址以及开启TCP监听器的调用。
一旦进入服务器的无限循环之中,我们就(被动地)等待客户端的连接。当一个连接请求出现时,我们进入对话循环中,在该循环中我们等待客户端发送的消息。如果消息是空白的,这意味着客户端已经退出,所以此时我们将跳出对话循环,关闭当前客户端连接,然后等待另一个客户端连接。如果确实得到了客户端发送的消息,就将其格式化并返回相同的数据,但是会在这些数据中加上当前时间戳的前缀。最后一行永远不会执行,它只是用来提醒读者,如果写了一个处理程序来考虑一个更加优雅的退出方式,正如前面讨论的,那么应该调用close()方法。
现在让我们看一下Python 3版本(tsTserv3.py),如示例2-2所示。
示例2-2 Python 3 TCP时间戳服务器(tsTserv3.py)
这个脚本创建一个TCP服务器,它接受来自客户端的消息,并返回加了时间戳前缀的相同消息。
已经在第16、18和25行中以斜体标出了相关的变化,其中print变成了一个函数,并且也将字符串作为一个ASCII字节“字符串”发送,而并非Unicode编码。本书后面部分我们将讨论Python 2到Python 3的迁移,以及如何编写出无须修改即可运行于2.x版本或3.x版本解释器上的代码。
支持IPv6的另外两个变化并未在这里展示出来,但是当创建套接字时,你仅仅需要将地址家族中的AF_INET(IPv4)修改成AF_INET6(IPv6)(如果你不熟悉这些术语,那么IPv4描述了当前的因特网协议,而下一代是版本6,即“IPv6”)。
创建客户端比服务器要简单得多。与对TCP服务器的描述类似,本节将先给出附带解释的伪代码,然后揭示真相。
cs = socket() # 创建客户端套接字
cs.connect() # 尝试连接服务器
comm_loop: # 通信循环
cs.send()/cs.recv() # 对话(发送/接收)
cs.close() # 关闭客户端套接字
正如前面提到的,所有套接字都是利用socket.socket()创建的。然而,一旦客户端拥有了一个套接字,它就可以利用套接字的connect()方法直接创建一个到服务器的连接。当连接建立之后,它就可以参与到与服务器的一个对话中。最后,一旦客户端完成了它的事务,它就可以关闭套接字,终止此次连接。
示例2-3给出了tsTclnt.py的代码。这个脚本连接到服务器,并以逐行数据的形式提示用户。服务器则返回加了时间戳的相同数据,这些数据最终会通过客户端代码呈现给 用户。
示例2-3 TCP时间戳客户端(tsTclnt.py)
这个脚本创建一个TCP客户端,它提示用户输入发送到服务器端的消息,并接收从服务器端返回的添加了时间戳前缀的相同消息,然后将结果展示给用户。
在UNIX启动行后,从socket模块导入所有属性。
HOST和PORT变量指服务器的主机名与端口号。因为在同一台计算机上运行测试(在本例中),所以HOST包含本地主机名(如果你的服务器运行在另一台主机上,那么需要进行相应修改)。端口号PORT应该与你为服务器设置的完全相同(否则,将无法进行通信)。此外,也将缓冲区大小设置为1KB。
在第10行分配了TCP客户端套接字(tcpCliSock),接着主动调用并连接到服务器。
客户端也有一个无限循环,但这并不意味着它会像服务器的循环一样永远运行下去。客户端循环在以下两种条件下将会跳出:用户没有输入(第14~16行),或者服务器终止且对recv()方法的调用失败(第18~20行)。否则,在正常情况下,用户输入一些字符串数据,把这些数据发送到服务器进行处理。然后,客户端接收到加了时间戳的字符串,并显示在屏幕上。
类似于对服务器所做的,下面Python 3和IPv6版本的客户端(tsTclnt3.py),示例2-4展示了Python 3版本。
示例2-4 Python 3 TCP时间戳客户端(tsTclnt3.py)
这是与tsTclnt.py等同的Python 3版本。
除了将print变成了一个函数,我们还必须解码来自服务器端的字符串(借助于distutils.log.warn(),很容易将原始脚本转换,使其同时能运行在Python 2和Python3上,就像第1章中的rewhoU.py一样)。最后,我们看一下(Python 2)IPv6版本(tsTclntV6.py),如示例2-5所示。
示例2-5 IPv6 TCP时间戳客户端(tsTclntV6.py)
这是前面两个示例中TCP客户端的IPv6版本。
在这个代码片段中,需要将本地主机修改成它的IPv6地址“::1”,同时请求套接字的AF_INET6家族。如果结合tsTclnt3.py和tsTclntV6.py中的变化,那么将得到一个Python 3版本的IPv6 TCP客户端。
现在,运行服务器和客户端程序,看看它们是如何工作的。然而,应该先运行服务器还是客户端呢?当然,如果先运行客户端,那么将无法进行任何连接,因为没有服务器等待接受请求。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动地等待连接。另一方面,客户端是一个主动的合作伙伴,因为它主动发起一个连接。换句话说:
首先启动服务器(在任何客户端试图连接之前)。
在该示例中,使用相同的计算机,但是完全可以使用另一台主机运行服务器。如果是这种情况,仅仅需要修改主机名就可以了(当你在不同计算机上分别运行服务器和客户端以此获得你的第一个网络应用程序时,这将是相当令人兴奋的!)。
现在,我们给出客户端对应的输入和输出,它以一个未带输入数据的简单Return(或Enter)键结束。
$ tsTclnt.py
> hi
[Sat Jun 17 17:27:21 2006] hi
> spanish inquisition
[Sat Jun 17 17:27:37 2006] spanish inquisition
>
$
服务器的输出主要是诊断性的。
$ tsTserv.py
waiting for connection...
...connected from: ('127.0.0.1', 1040)
waiting for connection...
当客户端发起连接时,将会收到“…connected from…”的消息。当继续接收“服务”时,服务器会等待新客户端的连接。当从服务器退出时,必须跳出它,这就会导致一个异常。为了避免这种错误,最好的方式就是创建一种更优雅的退出方式,正如我们一直讨论的那样。
核心提示:优雅地退出和调用服务器close()方法
在开发中,创建这种“友好的”退出方式的一种方法就是,将服务器的while循环放在一个try-except语句中的except子句中,并监控EOFError或KeyboardInterrupt异常,这样你就可以在except或finally字句中关闭服务器的套接字。在生产环境中,你将想要能够以一种更加自动化的方式启动和关闭服务器。在这些情况下,需要通过使用一个线程或创建一个特殊文件或数据库条目来设置一个标记以关闭服务。
关于这个简单的网络应用程序,有趣的一点是我们不仅展示了数据如何从客户端到达服务器,并最后返回客户端;而且使用服务器作为一种“时间服务器”,因为我们接收到的时间戳完全来自服务器。
UDP服务器不需要TCP服务器那么多的设置,因为它们不是面向连接的。除了等待传入的连接之外,几乎不需要做其他工作。
ss = socket() # 创建服务器套接字
ss.bind() # 绑定服务器套接字
infloop: # 服务器无限循环
cs = ss.recvfrom()/ss.sendto() # 关闭(接收/发送)
ss.close() # 关闭服务器套接字
从以上伪代码中可以看到,除了普通的创建套接字并将其绑定到本地地址(主机名/端口号对)外,并没有额外的工作。无限循环包含接收客户端消息、打上时间戳并返回消息,然后回到等待另一条消息的状态。再一次,close()调用是可选的,并且由于无限循环的缘故,它并不会被调用,但它提醒我们,它应该是我们已经提及的优雅或智能退出方案的一部分。
UDP和TCP服务器之间的另一个显著差异是,因为数据报套接字是无连接的,所以就没有为了成功通信而使一个客户端连接到一个独立的套接字“转换”的操作。这些服务器仅仅接受消息并有可能回复数据。
你将会在示例2-6的tsUserv.py中找到代码,这是前面给出的TCP服务器的UDP版本,它接受一条客户端消息,并将该消息加上时间戳然后返回客户端。
示例2-6 UDP时间戳服务器(tsUserv.py)
这个脚本创建一个UDP服务器,它接受客户端发来的消息,并将加了时间戳前缀的该消息返回给客户端。
在UNIX启动行后面,导入time.ctime()和socket模块的所有属性,就像TCP服务器设置中的一样。
HOST和PORT变量与之前相同,原因与前面完全相同。对socket()的调用的不同之处仅仅在于,我们现在需要一个数据报/UDP套接字类型,但是bind()的调用方式与TCP服务器版本的相同。再一次,因为UDP是无连接的,所以这里没有调用“监听传入的连接”。
一旦进入服务器的无限循环之中,我们就会被动地等待消息(数据报)。当一条消息到达时,我们就处理它(通过添加一个时间戳),并将其发送回客户端,然后等待另一条消息。如前所述,套接字的close()方法在这里仅用于显示。
在本节中所强调的4个客户端中, UDP客户端的代码是最短的。它的伪代码如下所示。
cs = socket() # 创建客户端套接字
comm_loop: # 通信循环
cs.sendto()/cs.recvfrom() # 对话(发送/接收)
cs.close() # 关闭客户端套接字
一旦创建了套接字对象,就进入了对话循环之中,在这里我们与服务器交换消息。最后,当通信结束时,就会关闭套接字。
示例2-7中的tsUclnt.py给出了真正的客户端代码。
示例2-7 UDP时间戳客户端(tsUclnt.py)
这个脚本创建一个UDP客户端,它提示用户输入发送给服务器的消息,并接收服务器加了时间戳前缀的消息,然后将它们显示给用户。
在UNIX启动行之后,从socket模块中导入所有的属性,就像在TCP版本的客户端中一样。
因为这次还是在本地计算机上运行服务器,所以使用“localhost”及与客户端相同的端口号,并且缓冲区大小仍旧是1KB。另外,以与UDP服务器中相同的方式分配套接字对象。
UDP客户端循环工作方式几乎和TCP客户端完全一样。唯一的区别是,事先不需要建立与UDP服务器的连接,只是简单地发送一条消息并等待服务器的回复。在时间戳字符串返回后,将其显示到屏幕上,然后等待更多的消息。最后,当输入结束时,跳出循环并关闭套接字。
在TCP客户端/服务器例子的基础上,创建Python 3和IPv6版本的UDP应该相当直观。
UDP客户端的行为与TCP客户端相同。
$ tsUclnt.py
> hi
[Sat Jun 17 19:55:36 2006] hi
> spam! spam! spam!
[Sat Jun 17 19:55:40 2006] spam! spam! spam!
>
$
服务器也类似。
$ tsUserv.py
waiting for message...
...received from and returned to: ('127.0.0.1', 1025)
waiting for message...
事实上,之所以输出客户端的信息,是因为可以同时接收多个客户端的消息并发送回复消息,这样的输出有助于指示消息是从哪个客户端发送的。利用TCP服务器,可以知道消息来自哪个客户端,因为每个客户端都建立了一个连接。注意,此时消息并不是“waiting for connection”,而是“waiting for message”。
除了现在熟悉的socket.socket()函数之外,socket模块还提供了更多用于网络应用开发的属性。其中,表2-2列出了一些最受欢迎的属性。
表2-2 socket模块属性
属 性 名 称 |
描 述 |
---|---|
数据属性 |
|
AF_UNIX、AF_INET、AF_INET6①、AF_NETLINK②、AF_TIPC③ |
Python中支持的套接字地址家族 |
SO_STREAM、SO_DGRAM |
套接字类型(TCP=流,UDP=数据报) |
has_ipv6④ |
指示是否支持IPv6的布尔标记 |
异常 |
|
error |
套接字相关错误 |
herror① |
主机和地址相关错误 |
gaierror① |
地址相关错误 |
timeout |
超时时间 |
函数 |
|
socket() |
以给定的地址家族、套接字类型和协议类型(可选)创建一个套接字对象 |
socketpair()⑤ |
以给定的地址家族、套接字类型和协议类型(可选)创建一对套接字对象 |
create_connection() |
常规函数,它接收一个地址(主机名,端口号)对,返回套接字对象 |
fromfd() |
以一个打开的文件描述符创建一个套接字对象 |
ssl() |
通过套接字启动一个安全套接字层连接;不执行证书验证 |
getaddrinfo()① |
获取一个五元组序列形式的地址信息 |
getnameinfo() |
给定一个套接字地址,返回(主机名,端口号)二元组 |
getfqdn()⑥ |
返回完整的域名 |
gethostname() |
返回当前主机名 |
gethostbyname() |
将一个主机名映射到它的IP地址 |
gethostbyname_ex() |
gethostbyname()的扩展版本,它返回主机名、别名主机集合和IP地址列表 |
gethostbyaddr() |
将一个IP地址映射到DNS信息;返回与gethostbyname_ex()相同的3元组 |
getprotobyname() |
将一个协议名(如‘tcp’)映射到一个数字 |
getservbyname()/getservbyport() |
将一个服务名映射到一个端口号,或者反过来;对于任何一个函数来说,协议名都是可选的 |
ntohl()/ntohs() |
将来自网络的整数转换为主机字节顺序 |
htonl()/htons() |
将来自主机的整数转换为网络字节顺序 |
inet_aton()/inet_ntoa() |
将IP地址八进制字符串转换成32位的包格式,或者反过来(仅用于IPv4地址) |
inet_pton()/inet_ntop() |
将IP地址字符串转换成打包的二进制格式,或者反过来(同时适用于IPv4和IPv6地址) |
getdefaulttimeout()/setdefaulttimeout() |
以秒(浮点数)为单位返回默认套接字超时时间;以秒(浮点数)为单位设置默认套接字超时时间 |
① Python 2.2中新增。
② Python 2.5中新增。
③ Python 2.6中新增。
④ Python 2.3中新增。
⑤ Python 2.4中新增。
⑥ Python 2.0中新增。
要获取更多信息,请参阅Python参考库中的socket模块文档。
SocketServer是标准库中的一个高级模块(Python 3.x中重命名为socketserver),它的目标是简化很多样板代码,它们是创建网络客户端和服务器所必需的代码。这个模块中有为你创建的各种各样的类,如表2-3所示。
通过复制前面展示的基本TCP示例,我们将创建一个TCP客户端和服务器。你会发现它们之间存在明显的相似性,但是也应该看到我们如何处理一些繁琐的工作,于是你不必担心样板代码。这些代表了你能够编写的最简单的同步服务器(为了将你的服务器配置为异步运行,可以查看本章末尾的练习)。
除了为你隐藏了实现细节之外,另一个不同之处是,我们现在使用类来编写应用程序。因为以面向对象的方式处理事务有助于组织数据,以及逻辑性地将功能放在正确的地方。你还会注意到,应用程序现在是事件驱动的,这意味着只有在系统中的事件发生时,它们才会工作。
表2-3 SocketServer模块类
类 |
描 述 |
---|---|
BaseServer |
包含核心服务器功能和mix-in类的钩子;仅用于推导,这样不会创建这个类的实例;可以用TCPServer或UDPServer创建类的实例 |
TCPServer/UDPServer |
基础的网络同步TCP/UDP服务器 |
UnixStreamServer/UnixDatagramServer |
基于文件的基础同步TCP/UDP服务器 |
ForkingMixIn/ThreadingMixIn |
核心派出或线程功能;只用作mix-in类与一个服务器类配合实现一些异步性;不能直接实例化这个类 |
ForkingTCPServer/ForkingUDPServer |
ForkingMixIn和TCPServer/UDPServer的组合 |
ThreadingTCPServer/ThreadingUDPServer |
ThreadingMixIn和TCPServer/UDPServer的组合 |
BaseRequestHandler |
包含处理服务请求的核心功能;仅仅用于推导,这样无法创建这个类的实例;可以使用StreamRequestHandler或DatagramRequestHandler创建类的实例 |
StreamRequestHandler/DatagramRequestHandler |
实现TCP/UDP服务器的服务处理器 |
事件包括消息的发送和接收。事实上,你会看到类定义只包括一个用来接收客户端消息的事件处理程序。所有其他的功能都来自使用的SocketServer类。此外,GUI编程(见第5章)也是事件驱动的。你会立即注意到它们的相似性,因为最后一行代码通常是一个服务器的无限循环,它等待并响应客户端的服务请求。它工作起来几乎与本章前面的基础TCP服务器中的无限while循环一样。
在原始服务器循环中,我们阻塞等待请求,当接收到请求时就对其提供服务,然后继续等待。在此处的服务器循环中,并非在服务器中创建代码,而是定义一个处理程序,这样当服务器接收到一个传入的请求时,服务器就可以调用你的函数。
在示例2-8中,首先导入服务器类,然后定义与之前相同的主机常量。其次是请求处理程序类,最后启动它。更多细节请查看下面的代码片段。
示例2-8 SocketServer时间戳TCP服务器(tsTservSS.py)
通过使用SocketServer类、TCPServer和StreamRequestHandler,该脚本创建了一个时间戳TCP服务器。
最初的部分包括从SocketServer导入正确的类。注意,这里使用了Python 2.4中引入的多行导入功能。如果使用的是较早版本的Python,那么将不得不使用完全限定的module.attribute名称,或者在同一行中导入两个属性。
from SocketServer import TCPServer as TCP, StreamRequestHandler as SRH
这里进行了大量的工作。我们得到了请求处理程序MyRequestHandler,作为SocketServer中StreamRequestHandler的一个子类,并重写了它的handle()方法,该方法在基类Request中默认情况下没有任何行为。
def handle(self):
pass
当接收到一个来自客户端的消息时,它就会调用handle()方法。而StreamRequestHandler类将输入和输出套接字看作类似文件的对象,因此我们将使用readline()来获取客户端消息,并利用write()将字符串发送回客户端。
因此,在客户端和服务器代码中,需要额外的回车和换行符。实际上,在代码中你不会看到它,因为我们只是重用那些来自客户端的符号。除了这些细微的差别之外,它看起来就像以前的服务器。
最后的代码利用给定的主机信息和请求处理类创建了TCP服务器。然后,无限循环地等待并服务于客户端请求。
如示例2-9所示,这里的客户端很自然地非常像最初的客户端,比服务器像得多,但必须稍微调整它以使其与新服务器很好地工作。
示例2-9 SocketServer时间戳TCP客户端(tsTclntSS.py)
这是一个时间戳TCP客户端,它知道如何与类似文件的SocketServer类StreamRequest Handler对象通信。
这里没有什么特别之处,这是复制原来客户端的代码。
SocketServer请求处理程序的默认行为是接受连接、获取请求,然后关闭连接。由于这个原因,我们不能在应用程序整个执行过程中都保持连接,因此每次向服务器发送消息时,都需要创建一个新的套接字。
这种行为使得TCP服务器更像是一个UDP服务器。然而,通过重写请求处理类中适当的方法就可以改变它。不过,我们将其留作本章末尾的一个练习。
除了客户端现在有点“由内而外”(因为我们必须每次都创建一个连接)这个事实之外,其他一些小的区别已经在服务器代码的逐行解释中给出:因为这里使用的处理程序类对待套接字通信就像文件一样,所以必须发送行终止符(回车和换行符)。而服务器只是保留并重用这里发送的终止符。当得到从服务器返回的消息时,用strip()函数对其进行处理并使用由print声明自动提供的换行符。
这里是SocketServer TCP客户端的输出。
$ tsTclntSS.py
> 'Tis but a scratch.
[Tue Apr 18 20:55:49 2006] 'Tis but a scratch.
> Just a flesh wound.
[Tue Apr 18 20:55:56 2006] Just a flesh wound.
>
$
这是服务器的输出。
$ tsTservSS.py
waiting for connection...
...connected from: ('127.0.0.1', 53476)
...connected from: ('127.0.0.1', 53477)
此时的输出与最初的TCP客户端和服务器的输出类似。然而,你应该会发现,我们连接了服务器两次。
Twisted是一个完整的事件驱动的网络框架,利用它既能使用也能开发完整的异步网络应用程序和协议。在编写本书时,因为它还不是Python标准库的一部分,所以必须单独下载并安装它(可以使用本章末尾的链接)。它提供了大量的支持来建立完整的系统,包括网络协议、线程、安全性和身份验证、聊天/ IM、DBM及RDBMS数据库集成、Web/因特网、电子邮件、命令行参数、GUI集成工具包等。
使用Twisted来实现简单的例子,有点小题大做,但是你必须开始使用它,并且该应用程序就相当于网络应用程序的“hello world”。
与SocketServer类似,Twisted的大部分功能都存在于它的类中。特别是对于该示例,我们将使用Twisted因特网组件中的reactor和protocol子包中的类。
你会发现示例2-10中的代码类似于SocketServer例子中的代码。然而,相比于处理程序类,我们创建了一个协议类,并以与安装回调相同的方式重写了一些方法。另外,这个例子是异步的。现在就让我们看一下服务器代码。
示例2-10 Twisted Reactor时间戳TCP服务器(tsTservTW.py)
这是一个时间戳TCP服务器,它使用了Twisted Internet类。
设置行代码包括常用模块导入,尤其是twisted.internet的protocol和reactor子包以及常数端口号的设置。
我们获得protocol类并为时间戳服务器调用TSServProtocol。然后重写了connectionMade()和dataReceived()方法,当一个客户端连接到服务器时就会执行connectionMade()方法,而当服务器接收到客户端通过网络发送的一些数据时就会调用dataReceived()方法。reactor会作为该方法的一个参数在数据中传输,这样就能在无须自己提取它的情况下访问它。
此外,传输实例对象解决了如何与客户端通信的问题。你可以看到我们如何在connectionMade()中使用它来获取主机信息,这些是关于与我们进行连接的客户端的信息,以及如何在dataReceived()中将数据返回给客户端。
在服务器代码的最后部分中,创建了一个协议工厂。它之所以被称为工厂,是因为每次得到一个接入连接时,都能“制造”协议的一个实例。然后在reactor中安装一个TCP监听器,以此检查服务请求。当它接收到一个请求时,就会创建一个TSServProtocol实例来处理那个客户端的事务。
与SocketServer TCP客户端不同,示例2-11看起来与其他客户端都不同,这个是明显的Twisted。
示例2-11 Twisted Reactor时间戳TCP客户端(tsTclntTW.py)
同样是我们熟悉的时间戳TCP客户端,只是从一个Twisted的角度来写的。
再一次,除了导入Twisted组件之外,并没有什么新内容。它与其他的客户端非常类似。
类似于服务器,我们通过重写connectionMade()和dataReceived()方法来扩展Protocol,并且这两者都会以与服务器相同的原因来执行。另外,还添加了自己的方法sendData(),当需要发送数据时就会调用它。
因为这次我们是客户端,所以我们是开启与服务器对话的一端。一旦建立了连接,就进行第一步,即发送一条消息。服务器回复之后,我们就将接收到的消息显示在屏幕上,并向服务器发送另一个消息。
以上行为会在一个循环中继续,直到当提示输入时我们不输入任何内容来关闭连接。此时,并非调用传输对象的write()方法发送另一个消息到服务器,而是执行loseConnection()来关闭套接字。当发生这种情况时,将调用工厂的clientConnectionLost()方法以及停止reactor,结束脚本执行。此外,如果因为某些其他的原因而导致系统调用了clientConnectionFailed(),那么也会停止reactor。
在脚本的最后部分创建了一个客户端工厂,创建了一个到服务器的连接并运行reactor。注意,这里实例化了客户端工厂,而不是将其传递给reactor,正如我们在服务器上所做的那样。这是因为我们不是服务器,需要等待客户端与我们通信,并且它的工厂为每一次连接都创建一个新的协议对象。因为我们是一个客户端,所以创建单个连接到服务器的协议对象,而服务器的工厂则创建一个来与我们通信。
与其他客户端类似,Twisted客户端也展示了输出。
$ tsTclntTW.py
> Where is hope
...sending Where is hope...
[Tue Apr 18 23:53:09 2006] Where is hope
> When words fail
...sending When words fail...
[Tue Apr 18 23:53:14 2006] When words fail
>
$
服务器恢复到单个连接。Twisted会保持连接,在每条消息发送后不会关闭传输。
$ tsTservTW.py
waiting for connection...
...connected from: 127.0.0.1
“connection from”的输出并不包含其他信息,因为我们只从服务器传输对象的getPeer()方法请求了主机/地址。
需要记住的是,大多数基于Twisted的应用程序都比本节给出的例子更加复杂。因为这是一个功能丰富的库,但是它确实有一定的复杂度,所以你需要做好准备。
表2-4列出了其他一些与网络和套接字编程有关的Python模块。当开发低级套接字程序时,经常配合使用select模块和socket模块。select模块提供了select()函数,该函数管理套接字对象集合。它所做的最有用的一个事情就是接收一套套接字,并监听它们活动的连接。select()函数将会阻塞,直到至少有一个套接字已经为通信做好准备,而当其发生时,它将提供一组准备好读信息的集合(它还可以确定哪些套接字准备好写入,虽然它不像前一种操作那么常见)。
表2-4 网络/套接字编程相关模块
模 块 |
描 述 |
---|---|
socket |
正如本章讨论的,它是低级网络编程接口 |
asyncore/asynchat |
提供创建网络应用程序的基础设施,并异步地处理客户端 |
select |
在一个单线程的网络服务器应用中管理多个套接字连接 |
SocketServer |
高级模块,提供网络应用程序的服务器类,包括forking或threading簇 |
在创建服务器方面,async*和SocketServer模块都提供更高级的功能。它们以socket和/或select模块为基础编写,能够使客户端/服务器系统开发更加迅速,因为它们已经自动处理了所有的底层代码。你需要做的所有工作就是以自己的方式创建或继承适当的基类。正如前面所提到的,SocketServer甚至提供了将线程或新进程集成到服务器的功能,它提供了一个更像并行处理的客户端请求的流程。
虽然在标准库中async*提供了唯一的异步开发支持,但是在前一节中,我们引入了一个比旧版本更加强大的第三方包Twisted。虽然本章中我们已经看到的示例代码稍长于粗糙的脚本,但是Twisted提供了一个更加强大和灵活的框架,并且已经实现了很多协议。可以在http://twistedmatrix.com 网站上找到更多关于Twisted的消息。
Concurrence是一个更现代化的网络框架,它是荷兰社交网络Hyves的后台引擎。Concurrence是一个搭配了libevent的高性能I/O系统,libevent是一个低级事件回调调度系统。Concurrence是一个异步模型,它使用轻量级线程(执行回调)以事件驱动的方式进行线程间通信和消息传递工作。可以在http://opensource.hyves.org/concurrence 网址找到更多关于Concurrence的信息。
现代网络框架遵循众多异步模型(greenlet、generator等)之一来提供高性能异步服务器。这些框架的其中一个目标就是推动异步编程的复杂性,以允许用户以一种更熟悉的同步方式进行编码。
本章介绍的主题主要是在Python中利用套接字进行网络编程,以及如何使用低层协议套件(如TCP/IP和UDP/IP)创建自定义应用程序。如果你想开发高级Web和网络应用程序,我们强烈鼓励你阅读第3章,或者跳到本书第2部分。
2-1 套接字。面向连接的套接字和无连接套接字之间的区别是什么?
2-2 客户端/服务器架构。用自己的话描述这个术语的意思,并给出几个例子。
2-3 套接字。TCP和UDP之中,哪种类型的服务器接受连接,并将它们转换到独立的套接字进行客户端通信?
2-4 客户端。更新TCP(tsTclnt.py)和UDP(tsUclnt.py)客户端,以使得服务器名称无须硬编码到应用程序中。此外,应该允许用户指定主机名和端口号,且如果二者中任何一个或者全部参数丢失,那么应该使用默认值。
2-5 网络互连和套接字。实现Python库参考文档中关于socket模块中的TCP客户端/服务器程序示例,并使其能够正常工作。首先运行服务器,然后启动客户端。也可以在http://docs.python.org/library/socket#example 网址中找到在线源码。
如果你觉得示例中服务器的功能太单调,那么可以更新服务器代码,以使它具有更多功能,令其能够识别以下命令。
date 服务器将返回其当前日期/时间戳,即time.ctime()。
os 获取操作系统信息(os.name)。
ls 列出当前目录文件清单(提示:os.listdir()列出一个目录,os.curdir是当前目录)。选做题:接受ls dir命令,返回dir目录中的文件清单。
你不需要一个网络来完成这个任务,因为你的计算机可以与自己通信。请注意,在服务器退出之后,在再次运行它之前必须清除它的绑定。否则,可能会遇到“端口已绑定”的错误提示。此外,操作系统通常会在5分钟内清除绑定,所以请耐心等待。
2-6 Daytime服务。使用socket.getservbyname()来确定使用UDP协议的“daytime”服务的端口号。检查getservbyname()的文档以获得其准确的使用语法(即socket. getservbyname. _ doc_)。那么,现在编写一个应用程序,使该应用程序能够通过网络发送一条虚拟消息,然后等待服务器回复。一旦你收到服务器的回复,就将其显示到屏幕上。
2-7 半双工聊天。创建一个简单的半双工聊天程序。指定半双工,我们的意思就是,当建立一个连接且服务开始后,只有一个人能打字,而另一个参与者在得到输入消息提示之前必须等待消息。并且,一旦发送者发送了一条消息,在他能够再次发送消息之前,必须等待对方回复。其中,一位参与者将在服务器一侧,而另一位在客户端一侧。
2-8 全双工聊天。更新上一个练习的解决方案,修改它以使你的聊天服务现在成为全双工模式,意味着通信两端都可以发送并接收消息,并且二者相互独立。
2-9 多用户全双工聊天。进一步修改你的解决方案,以使你的聊天服务支持多用户。
2-10 多用户、多房间、全双工聊天。现在让你的聊天服务支持多用户和多房间功能。
2-11 Web客户端。编写一个TCP客户端,使其连接到你最喜欢的网站(删除“http://”和任何后续信息;只使用主机名)的80端口。一旦建立一个连接,就发送HTTP命令字符串GET / \n,并将服务器返回的所有数据写入一个文件中(GET命令会检索一个Web页面,/file表明要获取的文件,\n将命令发送到服务器)。检查检索到的文件的内容。内容是什么?你如何检查能确保所接收到的数据是正确的?(注意:你可能必须在命令字符串后面插入一个或两个换行符,通常一个就能正常工作)
2-12 睡眠服务器。创建一个睡眠服务器。客户端将请求一段时间之后进入睡眠状态。服务器将代表客户端发送命令,然后向客户端返回一条表明成功的消息。客户端应该睡眠或空闲所请求的时间长度。这是一个远程过程调用的简单实现,此过程中一个客户端的请求会通过网络调用另一台计算机上的命令。
2-13 名称服务器。设计并实现一个名称服务器。该服务器负责维护一个包含主机名-端口号对的数据库,也许还有对应服务器所提供的服务的字符串描述。针对一个或多个现有的服务器,注册它们的服务到你的名称服务器中(注意,在这种情况下,这些服务器是名称服务器的客户端)。
每个启动的客户端都不知道它们所寻找的服务器地址。同样地,对于名称服务器的客户端来说,这些客户端应该发送一个请求到名称服务器,以指示它们正在寻找什么类型的服务。作为回复,名称服务器会向该客户端返回一个主机名-端口号对,然后该客户端就可以连接到适当的服务器来处理它的请求。
选做题:
1)为名称服务器添加缓存流行请求的功能。
2)为你的名称服务器添加日志记录功能,跟踪哪些服务器注册了名称服务器,以及客户端正在请求哪些服务。
3)你的名称服务器应该定期通过相应的端口号ping已经注册的主机,以确保它们的服务确实处于开启状态。反复的失败将会导致名称服务器将其从服务列表中划去。
你可以为那些注册了名称服务器的服务器实现真正的服务,或者仅仅使用虚拟服务器(仅仅应答一个请求)。
2-14 错误检查和优雅的关闭。本章所有的客户端/服务器示例代码都缺乏错误检查功能。我们并没有处理以下几种场景,例如,用户按Ctrl+C快捷键退出服务器或Ctrl+D快捷键终止客户端输入,也没有检查其他对raw_input()的不适当输入或处理网络错误。因为这个缺陷,经常我们终止一个应用程序时并没有关闭套接字,很可能会导致丢失数据。本练习中,在示例中选择一对客户端/服务器程序,并添加足够的错误检查,这样每个应用程序就能正确地关闭,即关闭网络连接。
2-15 异步性和SocketServer/socketserver。使用TCP服务器的示例,并使用其中一个mix-in类来支持一个异步服务器。为了测试你的服务器,同时创建并运行多个客户端,并交叉显示你的服务器满足二者中请求的输出。
2-16 *扩展SocketServer类。在SocketServer TCP服务器代码中,我们不得不从原始的基础TCP客户端中修改客户端,因为SocketServer类没有维护多个请求之间的连接。
a)继承TCPServer和StreamRequestHandler类并重新设计服务器,使其能够为每个客户端维持并使用单个连接(而不是每个请求一个连接)。
b)将前面练习的解决方案集成到(a)部分中的方案中,这样就可以并行为多个客户端提供服务。
2-17 *异步系统。研究至少5个基于Python的不同异步系统,可以从Twisted、Greenlets、Tornado、Diesel、Concurrence、Eventlet、Gevent等中选择。描述它们是什么,对它们进行分类,并找到它们之间的相似点和差异性,然后创建一些演示代码示例。
[1] 通过网址[http://www.educause.edu/EDUCAUSE+Review/ EDUCAUSEReviewMagazineVolume39/Musing sontheInternetPart2/157899](http://www.educause.edu/EDUCAUSE+Review/EDUCAUSEReviewMagazineVolume39/MusingsontheInternetPart2/157899)回到2004年。