本文为雷锋字幕组编译的技术博客,原标题Regular Expressions for Data Scientists,来源dataquest。
翻译 | 汪其香 Noddleleslee 陈亚彬 赵朋飞 杨婉迪 校对 | 余杭 整理 | 凡江
作为数据科学家,快速处理海量数据是他们的必备技能。有时候,这包括大量的文本语料库。例如,假设要找出在 Panama Papers(https://en.wikipedia.org/wiki/Panama_Papers) 泄密事件中邮件的发送方和接收方,我们需要详细筛查1150万封文档!我们可以手工完成上述任务,人工阅读每一封邮件,读取每一份最后发给我们的邮件,或者我们可以借助Python的力量。毕竟,代码存在的一个至关重要的理由就是自动处理任务。
尽管如此,从头开始编写脚本、写脚本、抓取数据需要大量的时间和精力。这正是正则表达式的用武之地。RE,regex 和regular patterns 表达的意思皆是正则表达式,它形成一门简洁的语言帮助我们快速地整理和分析文本。
正则出现在1956年,Stephen Cole Kleene 创建它用于描述人类神经系统的MP模型(McCulloch and Pitts model)(http://aishack.in/tutorials/artificial-neurons-mccullochpitts-model/)的概念。1960年代,Ken Thompson 将这个概念添加到类似Windows记事本的文本编辑器中,自此正则开始壮大。
正则一个关键特性是节省脚本。我们可以视其为代码的捷径。没有它,我们不得不为同样目的敲大量的垃圾代码。
本教程需要Python基础知识。如果你理解if-else 表达式,while 语句和for 循环,列表和字典,本教程的大部分都可以搞定啦。此外你需要代码编辑器,如Visual Studio Code,PyCharm 或Atom都可以。这样当我们遍历每一行代码时就不会茫然,此外基础的pandas库也是必要的。如果你需要复习,可以跳转到pandas 的教程(https://www.dataquest.io/blog/pandas-python-tutorial/)。
学完本教程,你会对正则的使用熟悉很多,可以使用re模块的基础模式和函数完成字符串分析。我们也学会如何高效地使用正则和pandas库化大量紊乱的数据集为有序。
现在,让我们看看正则可以做些什么。
数据集介绍
我们使用Kaggle的欺诈邮件文本语料库。它包括1998到2007发出的上千封钓鱼邮件。点击此处(https://www.kaggle.com/rtatman/fraudulent-email-corpus)可以下载数据集。在对整个语料库操作之前,让我们先学习在一封邮件应用正则表达。
Python 正则表达式模块的介绍
首先打开文本文件读取数据,设置为只读模式,并读取数据集,最后将上述操作结果赋给变量 fh(“file handle” 即文件句柄)。
请注意我们在设置目录路径之前添加 r。它将转换字符串为原始字符串,避免机器读取字符时候引起冲突,例如 Windows 的目录路径中的反斜杠。
你也许注意到我们现在并没有使用整个语料库。相反地,我们先人工挑选语料库的相对靠前的一些邮件作为测试文件。本教程不打算每次都展示上千行的结果,每次都打印其中的一部分作为测试。这可能会让人感到恼怒。你可以使用整个语料库,也可以使用我们的测试文件。无论哪种方式,都能很好得获得学习经验。
现在,假设我们现在想知道邮件的来源。我们可以在自己的Python尝试如下代码:
或者,我们可以使用正则表达式:
我们来遍历这段代码。首先导入 re 模块。然后敲出图示余下代代码。这个例子中,这比原来的Python 代码仅少 1 行 。然而随着脚本行数的快速增长,正则表达式可以节省脚本的代码量。
re.findall() 以列表形式返回字符串中符合模式的所有实例。它是Python内置 re 模块中最经常使用的函数。让我们来剖析 re.findall。re.findall(pattern, string)接受两个参数。pattern表示我们想要搜索的子字符串,string 表示我们想要搜索的主字符串。主字符串可以由多行组成。
.* 是字符串模式的简写。我们很快就会解释它的细节。现在它们与From: 域中的名称和电子邮件地址相匹配。
在让我们更深一步探索之前,先浏览一下常用的正则表达式。
常用的正则表达式
我们之前用到的 re.findall() 包含"From:"的字符串。这个函数当我们明确知道搜索目标时候十分有用,甚至包括明确字母拼写和是否大小写。如果我们不明确知道搜索目标时,该函数就会失效。幸运的是正则表达有解决这个问题的基本模式。让我们看一些这篇文章将用到的:
\w 匹配字母数字字符,即a-z,A-Z,0-9。它也匹配下划线和波折号。
\d 即0-9。
\s matches 匹配空白格,包括制表符、换行字符、回车符和空格字符。
\S 匹配非空白格字符。
. 匹配除换行字符\n外的任意字符串。
有这些正则表达式的说明在手,你就可以在我们解释上述代码时能够快速地理解。
使用正则表达式
现在我们来解释re.findall("From:.*", text) 中.* 的作用。首先看. :
From:后面添加. ,表示寻找它旁边的字符,因为.查找 \n外的任何字符,它也会捕捉肉眼不可见的空格。我们可以添加更多的点来验证。
看起来添加很多点可以获得行中我们想要的剩余部分。但这是冗余的而且我们不知道要敲多少个点。这就是很有用的*的由来。
* 匹配其左侧表达式的0个或多个模式的实例。这意味它寻找重复模式。当我们寻找重复模式时,称为贪婪搜索。否则,我们称之为非贪婪搜索或懒惰搜索。
让我们用* 构建一个对 . 的贪婪搜索。
因为 * 匹配其左侧 0 个或多个模式类的实例,而 . 在其左侧,因此我们可以获得From: 到行末的所有字符。这种漂亮高效的方式可以输出完整的行。
我们甚至可以更进一步,只分离出名字:
我们使用re.findall() 返回包含"From:.*" 模式的列表,就像我们以前做的那样。为了简洁起见 我们给match 变量赋以上述操作的结果。接下来,我们迭代列表。每一次循环,我们都再次执行re.findall 。这一次,这个函数从第一个引号开始匹配。
请注意我们在第一个引号旁使用反斜杠。反斜杠是用于转义其他特殊字符的特殊字符。例如,当我们想使用引号作为字符串而不是特殊字符时,我们用反斜杠来表示转义:\"。如果不使用反斜杠表示转义,就是"".*"",Python解释器视作两个空字符串之间读取一个句点和一个星号。这就会出现错误,脚本不能运行。因此,关键是使用反斜杠表示转义。
在第一个引号匹配之后,.* 获取行中直到下一个转义的引号的所有字符。获取引号内的名字。每个名字都在方括号内打印出,因为re.findall 以列表形式返回匹配内容。如果我们需要获取电子邮件地址呢?
看起来很简单不是嘛?只是匹配模式有些许不同,让我们逐一攻破。
以下是如何匹配电子邮件地址的前面部分:
电子邮件总是包含@符号,让我们从它开始。电子邮件@符号之前的部分可能包含字母数字字符,\w 就派上用场。然而,因为一些邮件包含句点或破折号,这是不够的。我们用\S 来查找非空白字符。但\w\S 仅仅找到两个字符。添加 * 重复寻找过程。因此模式前半部分是:\w\S*@。
现在来看看@符号后半部分的模式:
域名通常包含字母数字字符、句点和破折号。这很简单,一个 . 就能搞定。为了使用贪婪模式,我们用*来扩展搜索。这使我们可以匹配直到行结束的任何字符。
如果我们仔细观察这行,我们会发现每个电子邮件都封装在尖括号内,。 我们的模式.*包括闭合的尖括号。让我们纠正一下:
电子邮件地址以字母数字字符结束,所以我们用\w模式覆盖。因此@ 符号后面是.*\w,这意味着我们想要的模式是一组以字母数字字符结尾的字符。这不包括>。
完整电子邮件地址模式是:\w\S*@.*\w。
这是相当多的工作。熟练使用正则表达式需要一段时间,但是一旦您掌握它的模式,您就能够更快地为字符串分析编写代码。接下来,我们将运行一些re 模块常见函数,当我们开始重新整理语料库时它们将非常有用。
常见的正则表达式函数
re.findall() 无疑是有用的,re 模块提供了更多同样便捷的函数。
包括:
re.search()
re.split()
re.sub()
在使用它们把杂乱无序的语料库变为有序之前,我们对它们逐一分析。
re.search()
re.findall() 以列表形式返回匹配字符串中满足模式的所有实例,re.search() 匹配字符串中模式的第一个实例,并将其作为一个re 模块的匹配对象。
和 re.findall() 类似, re.search() 也接受两个参数。第一个参数是匹配的模式,第二个参数是要搜索的字符串范围。这里为了简洁起见,我们已经将结果赋值给match 变量。
因为 re.search() 返回一个re 模块的匹配对象,我们不能直接打印出对应的名字和电子邮件地址。 相反,我们必须先采用 group()这个函数. 我们已经在上面的代码中打印了它们类型,可以看出group() 将匹配对象转化成一个字符串。
我们也可以看到打印match 时显示的是对应的属性而不是字符串本身, 而打印 match.group() 只显示字符串。
re.split()
假设我们需要一种快速的方法来获取电子邮件地址的域名。我们可以用三次正则操作,像这样:
第一行用法前面已经提到了。我们返回一个字符串列表,每个字符串包含From: 字段的内容,并将其赋给变量。接下来的通过遍历这个列表来查找邮件的地址。同时通过迭代电子邮件地址和使用 re 模块的split() 函数来把每一个地址剪成两半,用 @作为分隔符。最后再打印出来。
re.sub()
另一个方便的 re 函数是 re.sub()。正如函数名所示,它用来替换字符串的各个部分。举个例子:
前两行已经在前面出现过了。
在第三行我们将 address 作为 re.sub() 函数的第三个参数,即邮件标题中完整的From: 字段。
re.sub() 需要三个参数。第一个是被代替的子字符串,第二是想要放在目标位置的字符串,而第三是主字符串。
pandas 中的正则表达式
现在我们有了正则表达式的一些基础知识,我们可以尝试一些更复杂的。然而,我们需要正则表达式跟pandas Python数据分析库结合。Pandas 库中有一个很有用的把数据组织成整齐表格的对象,即 DataFrame 对象,也可以从不同的角度理解它。结合正则表达式的代码,它就像用一个特别锋利的刀雕刻软黄油。
不用担心从来没用过 Pandas。我们会通过代码一步一步进行,这样你就不会感到困惑。正如我们在引言中提到的,如果你想详细学习,请访问Pandas tutotial(https://www.dataquest.io/blog/pandas-python-tutorial/)。
我们可以通过 Anaconda(https://docs.continuum.io/anaconda/) 或者 pip 来下载 pandas 库。 详情请查看安装指南(http://pandas.pydata.org/pandas-docs/stable/install.html)。
用正则表达式和Pandas分拣邮件
Corpus 是一个包含数千封电子邮件的文本文件。我们将使用正则表达式和Pandas 来将每封电子邮件适当分类 使Corpus 语料库更便于阅读和分析。我们会将每封邮件分为以下几个类别之一:
sender_name
sender_address
recipient_address
recipient_name
date_sent
subject
email_body
每个类别将成为我们Pandas数据帧或表格中的一列。这非常有用,因为我们可以自行处理每一列。例如,我们可以直接编写来找出电子邮件来自哪个域名,而不需要首先编码来将电子邮件地址与其他部分隔离开来。基本上,对数据集先分类可以让我们编写更简洁的代码。反过来,简洁的代码减少了机器所需的操作数量,这加快了我们的处理速度,特别是在处理大量数据集时。
准备Script
我们从上面一个简单的脚本开始。从头开始以便弄清楚它们内部运行的原理。
在代码的一开始首先导入 re 和pandas 模块,我们导入的Python email 包对于邮件正文很重要,如果仅仅使用正则表达式来处理电子邮件的正文会相当复杂,可能需要足够的清理不必要信息方面的工作才能保证它能正常运行。
email 包。然后我们创建一个空的列表emails 用来存放包含每个电子邮件详细信息的字典。
我们经常将代码的结果打印到屏幕上来判断代码是对还是错。然而,由于数据集中有成千上万的电子邮件,打印出上千行到屏幕上会占据本教程页面。我们当然不想让你一遍又一遍地滚动成千上万行的结果。因此,正如我们在本教程开始时所做的,我们打开并阅读了Corpus的较短版本。为了本次教程我们手工编写一点。你可以使用实际的数据集。
每次运行 print() 函数,你只需几秒钟就可以把几千行打印到屏幕上。
现在我们开始使用正则化表达式。
我们用 re 模块的 split 函数将 fh 中整个文本块拆分为一个单独的电子邮件列表,分配给 contents。这很重要,因为我们希望通过循环遍历列表来一个个地处理电子邮件。但是我们怎么知道用 "From r"来分割呢?我们之所以知道这一点,是因为在编写脚本之前查看了文件。我们没有必要仔细阅读数千电子邮件。只需要通过前几行来大致看看数据的结构是什么样子的。正因为如此,每个电子邮件前面都是字符串 "From r"。我们已经截图了文本文件的样子:
邮件用 “From r”开头
绿色部分是第一个电子邮件。蓝色部分是第二个电子邮件。我们可以看到,这两个电子邮件都是以 "From r"开头,用红色的框来显示。
我们在这个教程中之所以使用 Fraudulent Email Corpus是为了表明当数据是无序的和不熟悉的时候,我们不能只依靠代码来处理,它需要一双眼睛。就像刚刚展示的那样,我们需要查看 Corpus 来研究它的结构。另外 这样的数据可能还需要再处理 ,这个 Corpus 语料库也是同理。举个例子,即使我们用本教程的完整脚本算出本数据集包含3977 封邮件,实际上更多。有些邮件的开头没有 "From r"字段所以没有被拆分成单独的邮件。但是我们保留了这个结果以免它无穷无尽。
注意我们也用了 contents.pop(0)去掉列表中的第一个元素。那是在第一封电子邮件的前面有"From r" 字符串。当这个字段被分割的时候,在索引0的位置生成了一个空字符串。我们即将编写的脚本是为电子邮件而设计的。如果出现空字符串它可能会报错。去掉空字符串可以让我们避免这些错误打断脚本的运行。
阅读剩余内容,敬请期待周日的「嘀~正则表达式快速上手指南(下篇)」。
雷锋字幕组正在招募中
领取专属 10元无门槛券
私享最新 技术干货