正则表达式在数据科学中的使用模板
Posted 机器学习算法与Python精研
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了正则表达式在数据科学中的使用模板相关的知识,希望对你有一定的参考价值。
本文为雷锋字幕组编译的技术博客,原标题Regular Expressions for Data Scientists,来源dataquest。
翻译 | 汪其香 Noddleleslee 陈亚彬 赵朋飞 杨婉迪 校对 | 余杭 整理 | 凡江 鱼大大
本文略长,时间为10分钟,请收藏阅读。
作为数据科学家,快速处理海量数据是他们的必备技能。有时候,这包括大量的文本语料库。例如,假设要找出在 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库化大量紊乱的数据集为有序。
现在,让我们看看正则可以做些什么。
本文略长,时间为10分钟,请收藏阅读。
神数据集介绍
我们使用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 表示我们想要搜索的主字符串。主字符串可以由多行组成。
在让我们更深一步探索之前,先浏览一下常用的正则表达式。
常用的正则表达式
我们之前用到的 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解释器视作两个空字符串之间读取一个句点和一个星号。这就会出现错误,脚本不能运行。因此,关键是使用反斜杠表示转义。
看起来很简单不是嘛?只是匹配模式有些许不同,让我们逐一攻破。
电子邮件总是包含@符号,让我们从它开始。电子邮件@符号之前的部分可能包含字母数字字符,\w 就派上用场。然而,因为一些邮件包含句点或破折号,这是不够的。我们用\S 来查找非空白字符。但\w\S 仅仅找到两个字符。添加 * 重复寻找过程。因此模式前半部分是:\w\S*@。
现在来看看@符号后半部分的模式:
域名通常包含字母数字字符、句点和破折号。这很简单,一个 . 就能搞定。为了使用贪婪模式,我们用*来扩展搜索。这使我们可以匹配直到行结束的任何字符。
如果我们仔细观察这行,我们会发现每个电子邮件都封装在尖括号内,<和>。 我们的模式.*包括闭合的尖括号。让我们纠正一下:
这是相当多的工作。熟练使用正则表达式需要一段时间,但是一旦您掌握它的模式,您就能够更快地为字符串分析编写代码。接下来,我们将运行一些re 模块常见函数,当我们开始重新整理语料库时它们将非常有用。
常见的正则表达式函数
re.findall() 无疑是有用的,re 模块提供了更多同样便捷的函数。
包括:
re.search()
re.split()
re.sub()
在使用它们把杂乱无序的语料库变为有序之前,我们对它们逐一分析。
re.search()
re.findall() 以列表形式返回匹配字符串中满足模式的所有实例,re.search() 匹配字符串中模式的第一个实例,并将其作为一个re 模块的匹配对象。
和 re.findall() 类似, re.search() 也接受两个参数。第一个参数是匹配的模式,第二个参数是要搜索的字符串范围。这里为了简洁起见,我们已经将结果赋值给match 变量。
我们也可以看到打印match 时显示的是对应的属性而不是字符串本身, 而打印 match.group() 只显示字符串。
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
准备Script
我们从上面一个简单的脚本开始。从头开始以便弄清楚它们内部运行的原理。
在代码的一开始首先导入 re 和pandas 模块,我们导入的Python email 包对于邮件正文很重要,如果仅仅使用正则表达式来处理电子邮件的正文会相当复杂,可能需要足够的清理不必要信息方面的工作才能保证它能正常运行。
email 包。然后我们创建一个空的列表emails 用来存放包含每个电子邮件详细信息的字典。
我们经常将代码的结果打印到屏幕上来判断代码是对还是错。然而,由于数据集中有成千上万的电子邮件,打印出上千行到屏幕上会占据本教程页面。我们当然不想让你一遍又一遍地滚动成千上万行的结果。因此,正如我们在本教程开始时所做的,我们打开并阅读了Corpus的较短版本。为了本次教程我们手工编写一点。你可以使用实际的数据集。
每次运行 print() 函数,你只需几秒钟就可以把几千行打印到屏幕上。
现在我们开始使用正则化表达式。
我们用 re 模块的 split 函数将 fh 中整个文本块拆分为一个单独的电子邮件列表,分配给 contents。这很重要,因为我们希望通过循环遍历列表来一个个地处理电子邮件。但是我们怎么知道用 "From r"来分割呢?我们之所以知道这一点,是因为在编写脚本之前查看了文件。我们没有必要仔细阅读数千电子邮件。只需要通过前几行来大致看看数据的结构是什么样子的。正因为如此,每个电子邮件前面都是字符串 "From r"。我们已经截图了文本文件的样子:
邮件用 “From r”开头,
绿色部分是第一个电子邮件。蓝色部分是第二个电子邮件。我们可以看到,这两个电子邮件都是以 "From r"开头,用红色的框来显示。
注意我们也用了 contents.pop(0)去掉列表中的第一个元素。那是在第一封电子邮件的前面有"From r" 字符串。当这个字段被分割的时候,在索引0的位置生成了一个空字符串。我们即将编写的脚本是为电子邮件而设计的。如果出现空字符串它可能会报错。去掉空字符串可以让我们避免这些错误打断脚本的运行。
以循环方式获取每个名称和地址
接下来我们在电子邮件的 contents 列表中工作。
这个过程总共有 3 步,首先是找到 From: 字段
第一步,我们通过 re.search() 函数找到完整的 From: 字段。 句点 . 表示除了\n之外的任何字符 ,* 延伸到该行的结尾处。然后将它赋给变量 sender.
但是,数据并不总是直截了当的。常常会有意想不到的情况出现。例如,如果没有 From: 字段怎么办?脚本将报错并中断。在步骤2中可以避免这种情况。
为了避免由 From: 域导致的错误,我们要用一个 if 来检查 sender 是不是 None。如果是一个空字段的话,用 s_email 和 s_name 的值来取代 None ,这样脚本就可以继续运行而不是意外中断。
虽然这个教程让使用正则表达式看起来很简单(Pandas在下面)但是也要求你有一定实际经验。例如,我们知道使用if-else语句来检查数据是否存在。事实上,之所以我们知道如何处理,是因为我们在写这个脚本时反复地尝试过。编写代码是一个迭代过程。值得注意的是,即使教程看起来是线性的,即使教程看起来是直截了当的,但实践中需要更多的尝试。
我们用不同的规则来命名,每一个名字的左边都用 "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的情况发生。
然后我们将匹配对象转换为字符串并添加至字典中去。
因为From: 和 To: 字段具有相同的结构,因此我们可以对两者使用相同的代码,但对其他字段来说,我们需要定制稍微不同的代码。
获取邮件的日期
现在让我们来获取邮件的发送日期。
我们获取的Date:字段的代码与From:及To:字段的代码相同。就像保证这两个字段的值不是None一样,我们同样要检查被赋值到变量date_field的值是否为 None。
日期是以数字开始的,因此我们可以用 \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" 对应的行索引。请留意我们是如何使用正则表达式来完成这项任务的。
第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/
-end-
以上是关于正则表达式在数据科学中的使用模板的主要内容,如果未能解决你的问题,请参考以下文章