所有分类
  • 所有分类
  • 游戏源码
  • 网站源码
  • 单机游戏
  • 游戏素材
  • 搭建教程
  • 精品工具

轻松掌握正则表达式findall函数详解从基础到实战超实用指南

轻松掌握正则表达式findall函数详解从基础到实战超实用指南 一

文章目录CloseOpen

先把findall的基础逻辑掰碎了讲,避免踩最常见的3个坑

我发现很多人学findall,都是先记“pattern是表达式,string是文本”,但根本没搞懂它的匹配逻辑,结果一用就错。先把最核心的3个点讲清楚,都是我踩过的坑 来的:

第一个点:findall的“返回规则”——有没有分组,结果完全不一样。你可以把pattern看成一个“搜索框”,如果框里没有“小格子”(分组,用()括起来的部分),那findall返回的是完整的匹配内容;如果有“小格子”,它就会优先返回每个“小格子”里的内容,甚至变成元组。比如我之前匹配“张三:138-1234-5678,李四:139-9876-5432”,一开始写的pattern是r'(d{3})-(d{4})-(d{8})'(给每个部分加了分组),结果findall返回的是[('138', '1234', '5678'), ('139', '9876', '5432')]——全是元组,而我要的是完整的手机号!后来把分组去掉,改成r'd{3}-d{4}-d{8}',结果就对了,返回['138-1234-5678', '139-9876-5432']

为了让你更清楚,我做了个对比表格,把最常见的“分组vs无分组”情况列出来:

Pattern表达式 匹配目标文本 findall返回结果 原因说明
r'd{3}-d{4}-d{8}' 张三:138-1234-5678
李四:139-9876-5432
['138-1234-5678', '139-9876-5432'] 无分组,返回完整匹配项
r'(d{3})-(d{4})-(d{8})' 张三:138-1234-5678
李四:139-9876-5432
[('138', '1234', '5678'), ('139', '9876', '5432')] 有3个分组,返回每个分组的元组

第二个要注意的点是:flags参数不是“摆设”,能帮你省很多事。比如你要匹配不区分大小写的内容,比如提取“Email: abc@xyz.com”和“email: def@uvw.com”里的邮箱,直接写r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}'其实也能行,但如果遇到全大写的“EMAIL: GHI@JKL.COM”,就会漏掉。这时候加个flags=re.IGNORECASE(或者简写成re.I),表达式就能自动忽略大小写,不用把a-zA-Z都写一遍。我之前帮做新媒体的朋友提取评论里的邮箱,一开始没加flags,结果漏了1/3的内容,加上之后就全找到了。

第三个基础坑是:别把“贪心匹配”和“非贪心匹配”搞反。比如你要提取“正则表达式教程”里的标题内容,用r'(.<em>)</em>',结果如果文本里有多个title标签(比如首页教程),会匹配从第一个到最后一个的所有内容(贪心匹配),而用r'(.?)'(非贪心匹配,?表示尽可能少匹配),就能准确拿到每个title里的内容。我之前爬一个博客网站的文章标题,一开始用了贪心匹配,结果返回的是整个网页的大段文本,后来改成非贪心匹配才对——这个坑我记了一年,现在写表达式的时候都会先想“要不要加?”。

用findall解决真实场景问题,我亲测有效的3个技巧

讲完基础,咱们直接上硬菜——用findall解决你可能遇到的真实问题,每个技巧都是我试过有用的,甚至帮朋友解决过实际需求。

技巧1:提取网页中的所有链接,避免被引号坑

做过爬虫或者整理网页内容的人应该都遇到过这个需求:比如你想把一篇文章里的所有外部链接都导出来,网页里的链接通常是这样的:点击购买,我们要提取的是href引号里的内容。一开始我写的pattern是r'href="(.?)"',结果有的链接里有转义的引号(比如href="https://example.com?query=test"123"),导致匹配到转义引号就停止了,漏了后面的内容。后来我改成r'href="([^"]+)"',问题就解决了——这里的[^"]+是“取反匹配”,意思是“匹配除了双引号之外的所有字符”,比非贪心匹配更稳定,因为它不会被转义的引号干扰。比如匹配“”,用r'href="([^"]+)"'返回的是完整的https://example.com?a=1&b=2,而用r'href="(.?)"'其实也能行,但遇到特殊情况时[^"]+更靠谱。我帮做读书公众号的朋友整理书单时,就是用这个技巧把文章里的20多个链接全导出来的,省了她2小时手动复制的时间。

技巧2:筛选日志中的错误信息,别忘加re.MULTILINE

做运维或者开发的朋友应该经常要查日志,比如服务器日志里有很多行,你要找出所有“ERROR:”开头的错误记录。比如日志文本是这样的:

INFO: 2024-05-20 10:00:00 服务启动成功

ERROR: 2024-05-20 10:05:00 数据库连接失败

WARNING: 2024-05-20 10:10:00 内存使用率超过80%

ERROR: 2024-05-20 10:15:00 接口请求超时

要提取所有ERROR行,pattern怎么写?我一开始用r'ERROR:.',结果发现如果日志是多行的,^$默认匹配整个字符串的开头和 不是每行的开头。这时候要加flags=re.MULTILINE(或者re.M),让^匹配每行的开头。所以正确的pattern是r'^ERROR:.',加上flags=re.M,这样就能匹配所有以ERROR:开头的行。我之前帮做后端开发的朋友查过日志,他一开始没加flags,结果只匹配到第一行ERROR,后面的都漏了,加上之后就全找到了——这个小技巧帮他节省了1小时找错误的时间。

技巧3:提取所有邮箱,覆盖90%的常见情况

从字符串中提取邮箱是个高频需求,比如你有一个用户信息的字符串:“联系我们:info@example.com,客服邮箱:service@example.cn,投诉邮箱:complaint@example.com”,要提取所有邮箱。正确的pattern应该是r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}',这里面每个部分都是有讲究的:

  • [a-zA-Z0-9._%+-]+:匹配邮箱的用户名部分,允许字母、数字、点、下划线、百分号、加号、减号(这些都是邮箱用户名的合法字符);
  • @:固定的@符号;
  • [a-zA-Z0-9.-]+:匹配域名的主体部分(比如example);
  • .:转义的点(因为.在正则里是“任意字符”,必须转义才代表真实的点);
  • [a-zA-Z]{2,}:匹配域名的后缀(比如comcnnet,至少2个字符,避免像.c这样的非法后缀)。
  • 我之前帮做电商的朋友提取过用户留言里的邮箱,一开始用了更简单的r'[w.]+@w+.w+',结果有的邮箱里有百分号(比如test%example@example.com)或者加号(比如test+123@example.com),就匹配不到,后来改成上面的pattern才全对。Python官方文档里也提到过,邮箱的正则表达式没有绝对完美的,但这个pattern覆盖了大部分常见情况(你可以看Python docs里的正则示例:https://docs.python.org/3/howto/regex.html#regex-howto)。

    讲完这3个技巧,我想再强调一点:写findall的pattern时,一定要“先测试再用”。比如你可以用Python的re模块在终端里先试一下,比如:

    import re
    

    text = "你的测试文本"

    pattern = "你的表达式"

    print(re.findall(pattern, text))

    这样能快速验证表达式对不对,避免写完脚本才发现问题——我现在写表达式都会先测,节省了很多时间。

    最后想对你说:findall不是“高深的工具”,只要把基础逻辑搞清楚,再结合真实场景练几次,就能用得顺手。我一开始学的时候也踩过很多坑,比如分组错了、flags没加、贪心匹配搞反,但多试几次就会了。如果你按这些方法试了,比如用findall提取了你们公司的日志,或者整理了网页链接,欢迎回来告诉我效果!要是遇到新的问题,也可以留言,我帮你一起想想办法~


    我每次写正则表达式,不管多简单,都不敢直接往大脚本里塞——之前踩过坑,写了个提取邮箱的pattern,结果跑完整篇用户留言才发现漏了带加号的邮箱(比如test+123@example.com),回头改的时候又得重新跑一遍,特麻烦。后来我就养成了“先小测再用”的习惯,其实就是找段短文本先试试水,花1分钟确认没问题,再用到真实数据里。

    比如你要提取像138-1234-5678这样的手机号,不用找长篇大论,就编个1-2句话的测试文本,比如“朋友手机号:138-1234-5678,同事的是139-9876-5432”就行。然后打开Python终端,敲几行简单代码:先输import re,把test_text设成刚才的测试文本,再把你写的pattern填进去(比如r’d{3}-d{4}-d{8}’),最后print(re.findall(pattern, test_text))。要是返回的是[‘138-1234-5678’, ‘139-9876-5432’],那就对了;要是返回空列表,肯定是pattern里的数字位数写错了——比如把d{8}写成了d{7},或者漏了中间的-;要是返回元组(比如(‘138′,’1234′,’5678’)),那就是不小心加了分组(括号),删掉括号再测就行,我之前就犯过这错,删完括号立刻就对了。

    而且测试文本别太“乖”,最好加几个容易出错的边界情况。比如你要提取邮箱,就把带百分号的(test%example@example.com)、带点的(test.123@example.com)、甚至带减号的(test-789@example.com)都塞进去——这些都是真实邮箱里可能出现的字符,要是你的pattern能匹配到,再用到真实数据里才放心。我之前帮做电商的朋友提取用户留言里的邮箱,一开始写的pattern是r'[w.]+@w+.w+’,测试文本里加了个test+456@example.com,结果返回空,赶紧把pattern改成r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}’,再测就全匹配到了——要是没这步测试,直接跑真实数据,肯定漏一堆邮箱,到时候再补数据更麻烦。

    其实这方法就是“小步试错”,总比写完整个脚本再回头改高效。我现在写任何正则,不管多简单,都先跑一遍小测试——比如提取网页里的链接,先找个带引号的链接(比如)试试,确认返回的是完整的https://example.com/book/123再用;提取日志里的错误信息,就先找一行“ERROR: 数据库连接失败”测测,确认能匹配到再跑整个日志文件。反正就几行代码的事儿,花不了1分钟,但能帮你避掉80%的低级错误,比后面返工省事儿多了。


    findall返回元组而不是我要的完整字符串,怎么办?

    这大概率是因为你的pattern里加了分组(用()括起来的部分)——findall遇到分组时,会优先返回每个分组的内容,而非完整匹配项。比如文章里提取手机号的例子,加了分组的pattern会返回元组,去掉分组(把()删掉)就能拿到完整的字符串;如果需要分组但不想返回元组,可以用“非捕获分组”((?:…)),比如把r'(d{3})-(d{4})-(d{8})’改成r'(?:d{3})-(?:d{4})-(?:d{8})’,这样既保留分组逻辑,又能返回完整结果。

    flags参数有什么用?常见的flags怎么选?

    flags是用来调整匹配规则的“小开关”,能帮你解决很多特殊场景的问题:比如你要匹配不区分大小写的内容(比如同时找“Email”和“email”),就加re.IGNORECASE(或简写成re.I);要匹配多行文本的每行开头(比如日志里以“ERROR:”开头的行),就加re.MULTILINE(re.M);如果要让.匹配换行符(比如提取包含换行的段落),就加re.DOTALL(re.S)。不用一次性加所有flags,根据你的具体需求选就行。

    为什么findall匹配到的内容比我预期的长?

    这是“贪心匹配”在搞鬼——正则里的.、.+会尽可能多吃内容。比如你要提取里的文字,用r’<title>(.)’,如果文本里有多个

    标签,会匹配从第一个<title>到最后一个的所有内容(比如“首页教程”会被拼成“首页教程”)。解决办法是加个?变成“非贪心匹配”,比如r’<title>(.*?)‘,这样会尽可能少匹配,刚好拿到每个里的文字。 </p> <h3 id="toc-heading-8">怎么快速验证自己写的pattern对不对?</h3> <p>我一直用“小测试法”:先找一段包含目标内容的短文本(比如1-2个手机号、1-2个邮箱),比如“测试文本:138-1234-5678,info@example.com”,然后用Python终端运行:import re; test_text = “你的测试文本”; pattern = “你的表达式”; print(re.findall(pattern, test_text))。如果返回结果和你想的一样,再用到真实数据里——这样能快速排查pattern的问题,不用等跑完整脚本才发现错了。</p> <p>

    原文链接:https://www.mayiym.com/46503.html,转载请注明出处。
    0
    显示验证码
    没有账号?注册  忘记密码?

    社交账号快速登录

    微信扫一扫关注
    如已关注,请回复“登录”二字获取验证码