学习数据分析:掌握正则表达式基础与R语言、Python中的应用
学习数据分析,掌握一些智能分析工具,可以让数据清理事半功倍。例如,在处理非结构化文本数据时,如果能看懂简单的正则表达式,就可以避免大量冗余代码,提高效率。这就是所谓的高。

正则表达式是一组非常强大的微型袖珍语言。它们依靠一些特定的字母和符号作为匹配模式,并且可以灵活组合以匹配我们需要的任何文本信息。
而且,它不依赖于任何软件平台,也没有自己的GUI。就像流水一样,它可以支持大多数主流编程语言。
今天的文章只是给大家简单介绍一下正则表达式的基础知识,包括一些常用的字符及其对应的含义,以及R语言和Python中支持的常用函数。
R语言中有两套支持正则表达式的函数,基本函数和stringr包中的字符串处理函数系统。
由于两个系统所实现的需求差别不大,所以我个人使用基本功能系统。同时,对于一些基本功能无法满足的需求,给出了stringr中相应功能的解决方案。最后给出了基本函数和stringr系统函数。功能对比表供您参考。
R语言的基本函数中,支持正则表达式的函数主要包括以下几个:
以上是R语言中支持正则表达式的高频应用函数。其中,R语言的基本函数缺少精确返回匹配模式结果的函数,但stringr弥补了这个缺陷。这里仅详细说明stringr的这个功能。如果您对其他功能感兴趣,可以查看源码文档。
字符串分割()
strsplit()函数用于字符串分割,可以根据给定的分隔符进行分割操作。
我的话
结果
strsplit函数分割后,输出一个与输入对象长度相同的列表。如果需要在分割后提取两列,则需要自己构造一个循环。
我的数据
for(i in 1:length(myword)){
mydata$first[i]
mydata$第二[i]
};我的数据
这样就完成了批量字符串的切分和提取。
grep/grepl()
这是一组功能类似的字符串过滤函数(前者可以输出对应的记录序号或符合条件的实数值,后者直接输出布尔值)。什么是过滤,就是它只能过滤包含目标匹配模式的字符串对象。过滤掉,但是如果需要继续提取目标字符串模式,则需要进一步使用其他提取函数来提取,所以实际上它只是过滤掉那些不包含目标模式的字符串。
我的话
grep("[az]{3}",myword,value=FALSE)
grep("[az]{3}",myword,value=TRUE)
grepl("[az]{3}",myword)
上述要求匹配包含三个小写英文字母的记录,并分别返回序列号、记录和布尔值。这三个方法可以作为合法的输入条件,进行进一步的过滤和行索引。
子/gsub()
这是一组成对的字符串替换函数,用于清除输入字符串中的多个对象或将其替换为目标对象。
我的话
例如,如果我想去掉上面字符串向量中的横杠,我可以写成下面的格式:
子(“-”,“”,myword)
[1]“fff888”“hh333”“ff666”“ccc666”
子(“-”,“_”,myword)
[1]“fff_888”“hh_333”“ff_666”“ccc_666”
子(“-”,“*”,myword)
[1]“fff*888”“hh*333”“ff*666”“ccc*666”
当单个记录有多个匹配模式时,gsub 可以执行所有替换。
我的话
子(“-”,“”,myword)
[1]“fff888-ccc”“hh333”“ff666”“ccc666”
gsub("-","",myword)
[1]“fff888ccc”“hh333”“ff666”“ccc666”
正则表达式()/gregexpr()
这是一对返回目标字符串起始位置的匹配函数。
我的话
如果你想知道上面字符串中每个对象中数字部分的起始位置,就需要这个函数来进行位置锁定。
regexpr("\\d{3,}",myword)
[1] 5 4 4 5
attr(,"匹配长度")
[1] 7 3 3 3
attr(,"useBytes")
[1] 正确
返回值是带有属性信息的原子向量。可以看到,4条记录中目标编号的起始位置分别为5、4、4、5,长度分别为7、3、3、3。
gregexpr() 和 regexpr 之间的关系类似于 sub 和 gsub 之间的关系。当记录中出现多个匹配模式时,gregexpr 值输出第一个匹配模式的起始位置和长度,而 regexpr 输出所有匹配模式和长度。
我的话
regexpr("\\d{3,}",myword)
gregexpr("\\d{3,}",myword)
从输出来看,regexpr 忽略了第一条记录中的最后几个数字,但是 gregexpr 成功捕获并返回了它的起始位置和长度,但这也使得它的输出变得冗长和繁琐,一般不常用。
子字符串()/子字符串()
现在已经获得了目标字符串在原始记录中的位置和长度,提取它只需几分钟的事情。
接下来是 substr()/substring() 大显其魔力的时候了。
虽然这两个函数满足相同的要求,但它们的工作原理却有很大不同。 substr() 一次只能匹配一个字符串,因此对于向量来说,需要构造一个循环,而 substring() 可以直接指定其起始向量和结束向量。向量,所以我们只需要提前构造起起始位置向量和结束位置向量,直接将参数传递给它即可,避免手动循环。
我的话
添加
[1] 5 4 4 5
attr(,"匹配长度")
[1] 7 3 3 3
attr(,"useBytes")
[1] 正确
使用 substr 方法提取:
结果
for(i in 1:length(myword)){
结果[i]
};结果
[1]“8880000”“333”“666”“666”
使用子字符串方法提取:
子字符串(myword,as.numeric(add),as.numeric(add)+attr(add,"match.length")-1)
[1]“8880000”“333”“666”“666”
结果完全一样,但效率却截然不同。很明显,子串具有矢量化的优点,可以避免显式循环。
但是,你不觉得上面两步简直就是一个火坑吗?正则表达式无敌了,为什么匹配目标字符串这么难?
下面就来使用一把大杀器吧! ! !
str_extract(myword,"\\d{3,}")
[1]“8880000”“333”“666”“666”
请睁大眼睛仔细看。 str_extract 函数等于 regexpr+substr/substring。这样的神器你敢信吗?主要原因是它的作者也很邪恶。大名鼎鼎的Hadley Wycombe,没错,他就是肖编经常提到的ggplot2神一般的作者。关于stringr,今天只讲了一个功能,主要是很多类型的需求基本上都可以在基础功能中找到控制功能。
如果你想了解更多关于 stringr 包的信息,请务必仔细阅读其官方文档。
这里总结一下R语言的基本字符串处理函数和stringr包函数的对比。
下图是R语言中的基本字符串处理函数(支持正则表达式)与stringr内部函数的对比图。你可以选择一个适合记忆的集合并依次熟练使用(很多都是同名+str_前缀),但是无论你使用哪一个集合,str_extract()函数都必须给我记住清楚,除非你告诉我你喜欢使用基本函数编写更详细的代码,这取决于你。
-------------
Python:
----------
每次写R VS Pyhton系列的时候,我都感到很不安,因为我知道很多Pyhton大佬都在看我的公众号,我怕我会不好意思。毕竟我学python才不到四个月。但因为怕尴尬所以不敢进步,所以还是坚持写哈哈~_~。
Python中的正则表达式函数比较集中,没有那么分散。我认为主要原因是很多不应该用正则表达式解决或者不必要的字符串处理需求被内置到了很多对象的方法中。没了,os库只保留了最强大的几组Core字符串处理函数,而Pyhton作为高级面向对象编程语言,对正则表达式的支持度很高。保留了很多正则的原生方法,比如字符串打包、匹配分组等(在R中你做不到,R对正则化的支持确实有限)。
为了解决转义字符“\”的问题,Python使用r作为字符前缀,直接绕过了转义问题。我们可以大胆地使用原生的正则表达式方法。 (R中无解,遇到多次逃脱不迷茫的都是英雄)。
re模块提供了几个常用的字符串处理函数,支持正则匹配。
#以下是用于匹配的函数
re.split(模式, 字符串[, maxsplit])
split函数与R语言中的strsplit函数功能相同。它按照一定的规则分割字符串。
进口再
myword=["fff-888","hh-333","ff-666","ccc-666"]
名称=[];值=[]
对于我来说:
name.append(re.split("-",i)[0])
value.append(re.split("-",i)[1])
myresult={"名称":名称,"值":值}
{'名称':['fff','hh','ff','ccc'],'值':['888','333','666','666']}
上述过程成功将myword中的所有字符串按照“-”划分为两栏字典。
re.match()/re.search()
这是一对与目标字符串匹配的函数。前者只能匹配从字符串开头开始的模式,而后者不限制位置,只要匹配模式即可。
名称=[]
对于我来说:
name.append(re.match("[az]{2,3}",i).group())
['fff'、'hh'、'ff'、'ccc']
上述过程成功提取了 myword 中所有以小写字母开头的连续小写字母。
myword=["333-fff-888","hh-333","ff-666","ccc-666"]
名称=[]
对于我来说:
name.append(re.match("[az]{2,3}",i).group())
AttributeError:“NoneType”对象没有属性“group”
名称=[]
对于我来说:
name.append(re.search("[az]{2,3}",i).group())
['fff'、'hh'、'ff'、'ccc']
我在 myword 中第一个字符串的字母部分的开头添加了一个数字。在这种情况下,re.match无能为力。这个时候,re.search就需要发挥它的魔力了。
re.findall() 是一个强大的字符串搜索函数,默认以列表形式返回所有搜索结果。
myword=["333-fff-888","hh-333-hhh","ff-666-nnn","ccc-666"]
名称=[]
对于我来说:
name.append(re.findall("[az]{2,3}",i))
[['fff'], ['hh', 'hhh'], ['ff', 'nnn'], ['ccc']]
re.findall 成功捕获了 myword 中所有观察值的不同位置处满足条件的模式字符串。如果单个记录有两个与目标模式匹配的字符串,则会同时输出一个列表。
这时候大家肯定会想知道re.search和re.findall如何不同的使用,以及它们各自的使用场景是什么。
我认为re.search更适合目标字符串中嵌套有非常标准化的匹配对象的情况。例如,一段文本包含一组日期或职业信息。您可以最大限度地利用正则表达式的组捕获功能来分别提取它们。位置信息。 re.findall不会想,无论你要返回哪一个具体信息,它都会一口气转储给你。之后,仍然需要在嵌套列表中继续进行过滤,但如果是非标准文本,且嵌套的信息不是很有规律,re.findall就可以利用其全面性的优势,将符合的全部过滤掉适合你的条件。这对于网页文本等非结构化文本非常有用。 (个人意见,比较肤浅)。
举个小例子吧!
字=“222-555ggg999dddd000dfff”
re.search(r"(\d{3})-(\d{3}).*?(\d{3}).*?(\d{3})[az].+$",word )。团体()
re.search(r"(\d{3})-(\d{3}).*?(\d{3}).*?(\d{3})[az].+$",word ).组(1)
re.search(r"(\d{3})-(\d{3}).*?(\d{3}).*?(\d{3})[az].+$",word ).组(2)
re.search(r"(\d{3})-(\d{3}).*?(\d{3}).*?(\d{3})[az].+$",word ).组(3)
re.search(r"(\d{3})-(\d{3}).*?(\d{3}).*?(\d{3})[az].+$",word ).组(4)
'222-555ggg999dddd000dfff'
‘222’
‘555’
‘999’
‘000’
re.search结合正则表达式的分组功能,可以轻松地按顺序匹配特定位置的所有目标模式字符串。
re.findall(r"(\d{3})-(\d{3}).*?(\d{3}).*?(\d{3})[az].+$",word )
[('222', '555', '999', '000')]
re.findall就更狠了。不管你喜欢与否,它都会直接将所有捕获的内容输出到一个列表中。
re.sub()
最后的re.sub很容易理解。它的作用与R语言中的sub函数相同,就是替换。但我们通常用它来清理数据中的无效内容。
myword=["333-fff-888","hh-333","ff-666","ccc-666"]
比如我们要去掉myword中所有的“-”,只需要下面的步骤即可。
名称=[]
对于我来说:
name.append(re.sub("-","",i))
打印(姓名)
['333fff888','hh333hhh','ff666nnn','ccc666']
那么输出基本上不会包含无效字符串。
好了,R语言和Parsons中的字符串处理和常规支持函数基本上就这些了(没有全部包含,主要是我用的很有限,这些都是很频繁的需求,可以解决数据清理的大部分问题)。
妈的,介绍了这么多,但是上面用到的正则表达式却没有介绍太多。然而正则表达式博大精深,不是一两篇文章就能讲清楚的。我这里只列出一些常见的匹配模式。我强烈建议你去专业的参考书和网站,说实话,正则表达式一旦写出来,就像艺术家一样,只需几行火星文就可以节省你十行代码。
首先,必须掌握几个元字符:
\ 转义字符,转义没有任何特殊含义的字母,使其具有一些特殊含义(包括转义本身)
^ 匹配以目标模式开头的字符串。
$ 匹配以目标模式结尾的字符串。
* 这是一个数量限定符,与前面的子表达式匹配零次或多次,不能单独使用。
+ 与上面相同,匹配前一个子表达式一次或多次。
?与上面相同,匹配前面的子表达式零次或一次。
{n} 同上,n是非负整数。比赛确定了n次。
{n,} 同上,n为非负整数。至少匹配n次。
{n,m} 同上,匹配目标字符串出现的次数在n~m之间。
。匹配除“\n”之外的任何单个字符。
[] 匹配一组可能的组合,并且其中的任何单个模式之间存在 OR 关系。
[^] 匹配一组不可能的组合,并且其中的任何单个模式之间存在 OR 关系。
() 对可能的模式进行分组,分组内容可以从返回的匹配结果中捕获。
x|y 匹配 x 或 y
常用的数字和字母匹配:
[0-9] #匹配任意数字(0~9之间)
[az] #匹配任何小写字母
[AZ] #匹配任意大写字母
[a-zA-Z] #匹配任何字母
[0-9a-zA-Z] #匹配任意字母或数字
当出现连续的数字或字母时,使用上面的模式看起来很难看,正则表达式提供了转义缩写。
\d #匹配任意数字
\w # 匹配包括下划线在内的任何单词字符。相当于“[A-Za-z0-9_]”。
[\u4e00-\u9fa5] #匹配汉字
.*? #这里是将.*的贪婪匹配模式转换为惰性匹配模式,防止匹配过多的内容
当然,这些只是正则表达式的冰山一角。真正能达到简化代码效率的正则表达式有时是很可怕的。至少超过一行的正则表达式对我来说基本上很难理解。再次,正则表达式的表达方式博大精深,大家一定要勇于探索。
这是一个很棒的正则表达式备忘单,对其进行了总结: