嘀~正则表达式快速上手指南(下篇)

以循环方式获取每个名称和地址

接下来我们在电子邮件的 contents 列表中工作。

上面的代码中用 for 循环去遍历 contents 这样我们就可以一个一个处理每封邮件。我们创建一个字典, emails_dict,这将保存每个电子邮件的所有细节,如发件人的地址和姓名。事实上,这些是我们要寻找的第一项信息。

这个过程总共有 3 步,首先是找到 From: 字段

第一步,我们通过 re.search() 函数找到完整的 From: 字段。 句点 . 表示除了\n之外的任何字符 ,* 延伸到该行的结尾处。然后将它赋给变量 sender.

但是,数据并不总是直截了当的。常常会有意想不到的情况出现。例如,如果没有 From: 字段怎么办?脚本将报错并中断。在步骤2中可以避免这种情况。

为了避免由 From: 域导致的错误,我们要用一个 if 来检查 sender 是不是 None。如果是一个空字段的话,用 s_email 和 s_name 的值来取代 None ,这样脚本就可以继续运行而不是意外中断。

虽然这个教程让使用正则表达式看起来很简单(Pandas在下面)但是也要求你有一定实际经验。例如,我们知道使用if-else语句来检查数据是否存在。事实上,之所以我们知道如何处理,是因为我们在写这个脚本时反复地尝试过。编写代码是一个迭代过程。值得注意的是,即使教程看起来是线性的,即使教程看起来是直截了当的,但实践中需要更多的尝试。

第二步中使用了一个之前熟悉的正则表达式 \w\S*@.*\w, 用来 匹配实际的邮件地址格式。

我们用不同的规则来命名,每一个名字的左边都用 "From:" 字段中的:来分割,电子邮件的右边用开括号 <。因此可以用 :.*< 形式来找邮件名称。 我们从每个结果中快速的去掉 : 和 <

现在,让我们打印出代码的结果来看看。

注意我们没有使用 sender 变量在 re.search()函数中作为搜索字符串。我们已经打印了 sender 和 sender.group() 的类型,这样就能看到区别。看起来 sender 是一个 re 的匹配对象,并且不能用re.search()来搜索。然而sender.group() 是一个字符串,而 re.search 接受的参数即是字符串形式。

我们来看看 s_email 和 s_name 长什么样子。

同样,我们得到了匹配的对象。每次对字符串进行re.search() 操作, 都会生成匹配对象, 我们必须将其转换为字符串对象。

在转换之前,回想一下如果没有From: 字段,,sender 的值将会是None,那么 s_email和s_name 的值也将为None。因此,我们必须再次进行检查,以便脚本不会意外中断。先看看如何针对s_email 构造代码。

在步骤3A中,我们使用了if 语句来检查s_email的值是否为 None, 否则将抛出错误并中断脚本。

然后,我们只需将s_email 匹配的对象转换为字符串并将其分配给变量sender_email 即可。将转换完的字符串添加到 emails_dict 字典中,以便后续能极其方便地转换为pandas数据结构。

在步骤3B中,我们对 s_name 进行几乎一致的操作.

就像之前做的一样,我们在步骤3B中首先检查s_name 的值是否为None 。

然后,在将字符串分配给变量前,我们调用两次了 re 模块中的re.sub() 函数。首先,通过用空字符“”代替:\s* ,删除冒号及冒号与姓名之间的任何空格字符。然后删除姓名另一侧的空格字符和角括号,再次使用空字符进行替换。最终,将字符串分配给 sender_name并添加到字典中。

让我们检查下结果。

非常棒!我们已经分离了邮箱地址和发件人姓名, 还将它们都添加到了字典中,接下来很快就能用上。

既然我们已经得到了发件人的邮箱地址和姓名,通过同样的步骤就能获得收件人的邮箱地址和姓名并保存到字典中去。

首先,我们找到To: 字段。

接下来,我们将先发制人,避免recipient 为None的情况发生。

如果 recipient 不为 None, 使用 re.search() 来查找包含发件人邮箱地址和姓名的匹配对象,否则,我们将传递None值给 r_email 和 r_name 。

然后我们将匹配对象转换为字符串并添加至字典中去。

因为From: 和 To: 字段具有相同的结构,因此我们可以对两者使用相同的代码,但对其他字段来说,我们需要定制稍微不同的代码。

获取邮件的日期

现在让我们来获取邮件的发送日期。

我们获取的Date:字段的代码与From:及To:字段的代码相同。就像保证这两个字段的值不是None一样,我们同样要检查被赋值到变量date_field的值是否为 None。

我们已经输出 date_field.group(),因此可以更清楚地看到这一字符串的结构,它包含了邮件发送当天的具体日期并以“日-月-年” 的格式呈现,同时还包含了时间,但我们只想知道日期。 得到日期的代码与得到姓名和邮件地址的代码非常相似,但更简单一些,可能这儿唯一的疑惑点是正则表达式:\d+\s\w+\s\d+。

日期是以数字开始的,因此我们可以用 \d 来解析它,就像日期格式中具体天数部分一样,它可能是由一位或者两位数字组成,所以在此+ 就变得非常重要了。在正则表达式里, 在+ 的左侧来匹配一个或多个模式实例。用\d+ 来匹配可以不用考虑日期的具体天数是一位还是两位数字。

之后的一个空格可以通过寻找空白字符的 \s 来解析。月份是由三个字母组成的,因此使用\w+ 来解析,再接另一个空格,所以继续用 \s 解析。因为年份是由多个数字组成,所以我们需要再用一次\d+ 。

表达式 \d+\s\w+\s\d+之所以能起作用,是因为精确的模式匹配约束着空格之间的内容。

接下来,我们做和之前相同的 None 值检查。

如果 date 不为 None ,我们就把它从这个匹配对象转换成一个字符串,然后赋值给变量 date_sent,再将其键值添加到字典中。

进行下一步前,我们应特别注意的是+ 和 * 看起来很相似,但是它们差异很大。用日期字符串来举例:

如果使用 * 我们将匹配到大于等于零个的结果,而 + 匹配大于等于一个的结果。参照以上示例,我们输出了两种不同的结果,它们之间存在非常大的差异。正如所见, + 可以解析出整个日期而*只解析出一个空格和数字1。

接下来讲解邮件的标题。

获得邮件的标题

我们可以像之前一样,用相同的代码架构来获取我们需要的信息。

现在我们对正则表达式的格式已经很熟悉了对吧?这个代码与之前的类似,为获得标题,我们可以用一个空的字符串来代替"Subject: " 。

获取邮件的内容

最后要添加到字典里的一项就是邮件的内容了。

将标题从邮件内容中分离出来是非常复杂的任务,尤其当文中有很多不同形式的标题。在原始混乱的数据中是很难找到一致性的规律,但是幸运的是这个工作有人帮我们解决了——Python的email 模块包非常适用这项任务。

我们之前已经导入了email模块. 现在,我们将 message_from_string()方法应用于item, 将整个email转换成 email消息对象. 一个消息对象由消息头和消息体组成, 分别对应于email的头部和主体.

接下来, 我们对email消息对象使用 get_payload()方法. 提取email内容. 并将内容传递给变量 body, 稍后我们会将其存储在字典 emails_dict 的键 "email_body"下.

在处理邮件正文时为什么选择email包而非正则表达式

你可能会疑惑, 为什么使用 email 包而不是正则表达式呢? 因为在不需要大量的清理工作时,正则表达式并不是最好的方法。我们需要为这段代码做详细解释。

我们值得探讨为何会作出这个选择。但在开始之前,我们需要先理解方括号[ ] 在正则表达式中的含义, .

[ ] 用于匹配所有被它括起来的内容. 比如, 如果需要在字符串中查找 "a", "b", 或 "c" , 可以使用 [abc] 作为模式. 上文提到过的模式也适用。[\w\s] 用于查找字母、数字或空格。不同之处在于,它匹配的是方括号中的文字部分。

现在,可以更好的理解我们为何会决定选择email模块了。

仔细留意下数据就会发现email头部采用字符串 "Status: 0" 或 "Status: R0"作为结束,并在下一封邮件的 From r 字符串前结束,我们可以使用 Status:\s*\w*\n*[\s\S]*From\sr* 来获取email内容. [\s\S]* 用来查找空格或非空格字符,所以用于大段的文本、数字,以及标点符号。

不幸的是一封 email 不止一个“Status: ” 字符串,也并不一定都包含 "From r",即邮件拆分之后的数目可能会比邮件列表的字典数目多 也可能会比它少 ,但它们不会和已有的其他类别相匹配。如果使用 pandas 包来解决这个问题的话 会遇到问题 ,因此,我们选择使用 email 包。

创建字典列表

最后,添加字典emails_dict到 emails 列表:

此时可以打印emails列表。执行 print(len(emails_dict)) 函数,查看列表中有多少字典和email 。如前述,全部语料库包含 3977个email。我们的小型测试文件中只有7个。全部代码如下:

我们已经打印出了emails 列表的第一项, 它是由键和键值对组成的字典. 由于使用了 for 循环,因此每个字典拥有相同的键,但键值不同。

我们为每个 item 赋值 "email content here" ,所以不需要打印所有的email来占据电脑屏幕. 如果你在家应用时打印email,你将会看到实际的email内容。

使用 pandas 处理数据

如果使用 pandas 库处理列表中的字典 那将非常简单。每个键会变成列名, 而键值变成行的内容。

我们需要做的就是使用如下代码:

通过上面这行代码,使用pandas的DataFrame() 函数,我们将字典组成的 emails 转换成数据帧,并赋给变量emails_df.

就这么简单。我们已经拥有了一个精致的Pandas数据帧,实际上它是一个简洁的表格,包含了从email中提取的所有信息。

请看下数据帧的前几行:

The dataframe.head() 函数显示了数据序列的前几行。该函数接受1个参数。一个可选的参数用于定义需要显示的行数, n=3 表示前3行。

也可以精确地查找。例如,查找从特定域名发来的邮件。但是,我们需要先学习一种新的正则表达式来完成精确查询工作。

管道符号, |, 用于查找位于它两边的任意字符。 如, a|b查找 a 或 b。

| 有点类似 [ ], 但二者有区别。假设我们需要查找"crab", "lobster", 或 "isopod"。 使用 crab|lobster|isopod 会比 [crablobsterisopod] 更精确,前者会匹配完整单词,而后者只匹配单个字符。

现在我们可以使用 | 符号查找从特定域名发送来的email。

这里我们使用了一行超长的代码。由内及外剖析它。

emails_df['sender_email'] 选择了标记为 sender_email的列,接下来,如果在该列中匹配到 子字符串 "maktoob" 或 "spinfinder" ,则str.contains(maktoob|spinfinder) 返回 True . 最后, 最外面的emails_df[] 返回 sender_email 列视图,该列包含需要匹配的目标字符串。干的漂亮!

我们也可以单个检视邮件。 只需要以下4步。 第1步,查找包含字符串"@maktoob"的列 "sender_email" 对应的行索引。请留意我们是如何使用正则表达式来完成这项任务的。

第2步,使用索引查找email地址, loc[] 方法返回一系列不同属性的对象. 并将其打印出来,以便查看。

第3步,从这一系列对象中提取email地址,并罗列出来,现在你会发现他的类型是now类。

第4步将展示提取到的email正文

在第四步中 emails_df['sender_email'] == "james_ngola2002@maktoob.com" 是用来查找包含 "james_ngola2002@maktoob.com" 的邮件发送者列,接下来 ['email_body'].values 用来查找邮件正文的相同行的列值,最后输出该列值。

如你所见,我们可以多种方式应用正则表达式,正则表达式也能与pandas完美配合。

其他资源

自从应用范围从生物学扩展到工程领域,过去这些年正则表达式发展速度惊人 。今天,正则表达式已可在多种变成语言中应用,除基本模式外,有适当变化。在这份教程中,我们使用Python练习使用正则表达式,但如果你喜欢,也可以使用 Stack Overflow 发掘它的其他特点。维基百科用一张表格比较了不同正则表达式引擎的特点。

正则表达式还有很多特性本教程不能一一列举,完整的文档可以参考Python文档中的 re 模块. 谷歌也有一份快速参考手册(https://developers.google.com/edu/python/regular-expressions)。

如果需要一系列数据进行实验的话, Kaggle 和 StatsModels 将对你有所帮助。

这里是正则表达式的速查表,但对大多数来说也是有帮助的。

如果这篇教程对你有用的话,你也会喜欢 Dataquest 的正则表达式课程。

原文链接:https://www.dataquest.io/blog/regular-expressions-data-scientists/

原文发布于微信公众号 - AI研习社(okweiwu)

原文发表时间:2018-04-15

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏北京马哥教育

Python老司机也会翻车!10个最容易犯的Python开发错误

? Python是一门简单易学的编程语言,语法简洁而清晰,并且拥有丰富和强大的类库。与其它大多数程序设计语言使用大括号不一样 ,它使用缩进来定义语句块。 在平...

41180
来自专栏跟着阿笨一起玩NET

设计模式之UML类图的常见关系(一)

本篇会讲解在UML类图中,常见几种关系: 泛化(Generalization),依赖(Dependency),关联(Association),聚合(Aggreg...

19810
来自专栏思考的代码世界

Python编程从入门到实践之用户输入|第7天

函数input()让程序暂停运行,等待用户输入一些文本。获取用户输入后,Python将其存储在 一个变量中,以方便你使用。

33680
来自专栏牛肉圆粉不加葱

[8] - Actor 与并发

Actor 是 Scala 基于消息传递的并发模型,虽然自 Scala-2.10 其默认并发模型的地位已被 Akka 取代,但这种与传统 Java、C++完全不...

12010
来自专栏玩转JavaEE

MongoDB管道操作符(一)

熟悉Linux操作系统的小伙伴们应该知道Linux中有管道的说法,可以用来方便的处理数据。MongoDB2.2版本也引入了新的数据聚合框架,一个文档可以经过多个...

35350
来自专栏算法channel

面试被问到动态内存分配时需要注意哪些坑,该怎么回答?

面试时,面试官问我们Java,Python这种语言那是必须要准确回答的,很多系统如果对性能要求高的话,底层一般会用到C/C++语言,因此被问到底层语言的相关知识...

16430
来自专栏开源优测

[快学Python3]数据结构-队列

概述 什么是队列,简单而言:先进先出。 队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,...

26490
来自专栏程序员互动联盟

【专业技术】C++ RTTI及“反射”技术

RTTI   RTTI(Run-Time Type Information)运行时类型检查的英文缩写,它提供了运行时确定对象类型的方法。面向对象的编程语言,象C...

35850
来自专栏程序员互动联盟

【答疑解惑】C/C++参数传递

有群友问如下一个问题,他说在下图中sun函数内部的打印是对的,但是为什么调用结束之后主调的结果确是错误的。也就是说,函数sun为什么不能把相加的结果带回主调函数...

37560
来自专栏用户2442861的专栏

JSON 入门指南(IBM)

尽管有许多宣传关于 XML 如何拥有跨平台,跨语言的优势,然而,除非应用于 Web Services,否则,在普通的 Web 应用中,开发者经常为 XML 的...

12510

扫码关注云+社区

领取腾讯云代金券