欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

python 历险记(六)— python 对正则表达式的使用(上篇)

程序员文章站 2022-06-08 20:39:14
[TOC] 引言 刚接触正则表达式,我也曾被它们天书似的符号组合给吓住,但经过一段时间的深入学习,发现它并没有想象中那么可怕,只要多实践,多理解,也是可以轻松搞定的。 而且我发现带着问题去学习,求知欲会驱使着你往前走,不知不觉就懂了。 下面就是我在学习中提出的几个问题,在后面会依次进行讨论。由于正则 ......

目录

引言

刚接触正则表达式,我也曾被它们天书似的符号组合给吓住,但经过一段时间的深入学习,发现它并没有想象中那么可怕,只要多实践,多理解,也是可以轻松搞定的。
而且我发现带着问题去学习,求知欲会驱使着你往前走,不知不觉就懂了。
下面就是我在学习中提出的几个问题,在后面会依次进行讨论。由于正则表达式涉及到的内容确实非常多,分成两篇来阐述。

  1. 什么是正则表达式?
  2. 正则表达式可以干什么?
  3. 正则表达式的语法以及在 python 中这些语法是如何使用的?
  4. 正则表达式如何处理中文字符?
  5. python 的正则表达式库中有哪些重要的函数?

什么是正则表达式?

正则表达式使用单个字符串来描述,匹配一系列符合某个句法规则的字符串。

— *

先来划重点:

  1. 正则表达式的表现形式是 单个字符串
  2. 它用来执行匹配的动作
  3. 匹配的对象也是字符串

语言总是有些苍白的,必须要结合实例才能理解的更清楚,先来看一个例子:

>>> import re
>>>re.search(r'wo\w+d', 'hello world!')
<re.match object; span=(6, 11), match='world'>
>>> 

这里先概略说明 re.search 方法:引入的 re 模块就是 python 的正则表达式模块,re.search 函数目的就是接受一个正则表达式和一个字符串,并以 match 对象的形式返回匹配的第一个元素。如果没有匹配到,则会返回 none。(关于 search 函数先了解这些就可以,后面会有详细讲解。)

下面就拿这个示例中 re.search 中的参数来匹配下上面的概念,加深一下理解

  • 'wo\w+d' 就是正则表达式,它还有一个名称叫做_模式(pattern)_ ,表示wo 字母后有多个字母并一直到d 字母出现为止(现在不明白没关系,只要知道它就是正则表达式就可以了,后面会详细讲
  • 'wo\w+d' 前面还有一个 r 表示什么呢?这个 r 表示 raw的意思,就是原始字符串。原始字符串不会将 \ 解释成一个转义字符,而是这样做对正则表达式好处是大大的,只有这样 \w 才能起作用。
  • 'hello world!' 就是要匹配的字符串。
  • 整个函数就表示从 'hello world!' 字符串中搜索出符合_'wo\w+d'_ 模式的字符串,并展示出来,于是 world 字符串就被筛选了出来。

正则表达式有什么用?

我们学习正则表达式的目的是什么?当然是为了有朝一日能使用它解决我们面临的问题,要不然,学它干嘛。那我们就来聊聊正则表达式的用途:

  • 字符串验证

    你肯定在网页上注册过账号吧,假如你在注册 github 网站,它要求你输入 email,而你却胡乱填写了几个数字就想注册,这时就会弹出提示 "email is invalid",意思就是你的邮箱是无效的,这就是正则表达式的功劳。

  • 替换文本

    假如你正在写一篇关于 java 的文章,写着写着,你觉得换成 python 更好些,你想一下把出现 java , java 的地方全都替换成 python , 正则表达式可以帮你做到。

  • 从字符串中提取出要获取的字符串

    假如你正在爬取一个汽车排行榜页面,想要获取每个车型的编号,而车型编号则隐藏在链接中,怎么获取呢?用正则表达式可以。

正则表达式的语法及使用实例

对刚接触的同学来说,正则表达式的语法很晦涩。不用担心,我们先大致浏览一下完整的语法组合,后面再使用前面讲过的 re.search 方法一个个详细介绍,讲着讲着,我相信你就明白了。

正则表达式语法有哪些?

字符 功能描述
\ 特殊字符转义
^ 匹配字符串的开始位置
$ 匹配字符串的结束位置
* 匹配前面的子表达式零次或多次
+ 匹配前面的子表达式一次或多次
? 匹配前面的子表达式零次或一次
{n} n是非负整数,匹配n次
{n,} n 是非负整数,匹配 >=n 次
{n,m} m是非负整数,n<=m, 匹配>= n 并且 <=m 次
? 非贪心量化
. 匹配除“\r”“\n”之外的任何单个字符
(pattern) 匹配pattern并获取这一匹配的子字符串
(?:pattern) 非获取匹配
(?=pattern) 正向肯定预查
(?!pattern) 正向否定预查
(?<=pattern) 反向(look behind)肯定预查
(?<!pattern) 反向否定预查
x|y 没有包围在()里,其范围是整个正则表达式
[xyz] 字符集合(character class),匹配所包含的任意一个字符
[^xyz] 排除型字符集合(negated character classes),匹配未列出的任意字符
[a-z] 字符范围,匹配指定范围内的任意字符
[^a-z] 排除型的字符范围,匹配任何不在指定范围内的任意字符
\b 匹配一个单词边界,也就是指单词和空格间的位置
\b 匹配非单词边界
\cx 匹配由x指明的控制字符
\d 匹配一个数字字符。等价于[0-9]
\d 匹配一个非数字字符。等价于[^0-9]
\f 匹配一个换页符。等价于\x0c和\cl。
\n 匹配一个换行符。等价于\x0a和\cj。
\r 匹配一个回车符。等价于\x0d和\cm。
\s 匹配任何空白字符。等价于[ \f\n\r\t\v]
\s 匹配任何非空白字符。等价于[^ \f\n\r\t\v]
\t 匹配一个制表符。等价于\x09和\ci。
\v 匹配一个垂直制表符。等价于\x0b和\ck。
\w 匹配包括下划线的任何单词字符。等价于“[a-za-z0-9_]
\w 匹配任何非单词字符。等价于“[^a-za-z0-9_]”。
\ck 匹配控制转义字符。k代表一个字符。等价于“ctrl-k
\xnn 十六进制转义字符序列
\n 标识一个八进制转义值或一个向后引用
\un unicode转义字符序列

这些正则到底该怎么用?

浏览一遍,感觉怎么样,是不是摩拳擦掌,想要立刻实践一番,嘿嘿。好的我们现在就开干。

  • ^ 匹配字符串的开始位置

    >>> import re
    >>> re.search(r'^h', 'he is a hero!')          
    <re.match object; span=(0, 1), match='h'>

    这个例子中虽然有两个 h,因为前面有 ^ 所以只会匹配第一个

  • $ 匹配字符串的结束位置

    >>> import re
    
    >>> re.search(r't$','this is an object')
    
    <re.match object; span=(16, 17), match='t'>

    虽然这个句子前后都有 t,却是最后的被匹配到了

  • * 匹配前面的子表达式 0 次或多次,例如,"zo*" 能匹配"z","zo","zoo",我们来验证下

    >>> import re
    
    >>> re.search(r'zo*', 'z')
    
    <re.match object; span=(0, 1), match='z'>
    >>> re.search(r'zo*', 'zo')
    
    <re.match object; span=(0, 2), match='zo'>
    >>> re.search(r'zo*', 'zoo')
    
    <re.match object; span=(0, 3), match='zoo'>

    ​ 这里 * 还有一种写法 {0,},两者等价。其中 {} 叫做重复。来看例子。

    import re
    re.search(r'zo{0,}','z')
    <_sre.sre_match object; span=(0, 1), match='z'>
    re.search(r'zo{0,}','zo')
    <_sre.sre_match object; span=(0, 2), match='zo'>
    re.search(r'zo{0,}','zoo')
    <_sre.sre_match object; span=(0, 3), match='zoo'>
  • + 匹配前面的子表达式一次或多次,以 "zo+" 为例,它能匹配 "zo","zoo",来验证下

    >>> import re
    >>> re.search(r'zo+', 'zo')
    
    <re.match object; span=(0, 2), match='zo'>
    >>> re.search(r'zo+', 'zoo')
    
    <re.match object; span=(0, 3), match='zoo'>

    这里 + 还有一种写法 {1,} 两者是等价的,来看例子。

    >>> import re
    
    >>> re.search(r'zo{1,}','zo')
    
    <re.match object; span=(0, 2), match='zo'>
    
    >>> re.search(r'zo{1,}','zoo')
    
    <re.match object; span=(0, 3), match='zoo'>
  • ? 匹配前面的子表达式0次或 1次,以 "ab(cd)?" 为例,可以匹配 "ab","abcd",看下面例子

    import re
    re.search(r'ab(cd)?','ab')
    <_sre.sre_match object; span=(0, 2), match='ab'>
    re.search(r'ab(cd)?','abcd')
    <_sre.sre_match object; span=(0, 4), match='abcd'>

    这里 ? 还有一种写法 {0,1} 两者等价,看下面

    import re
    re.search(r'ab(cd){0,1}', 'ab')
    <_sre.sre_match object; span=(0, 2), match='ab'>
    re.search(r'ab(cd){0,1}', 'abcd')
    <_sre.sre_match object; span=(0, 4), match='abcd'>
  • {n} n 必须是非负整数,能匹配确定的 n 次,以 "o{2}" 为例,它能匹配 "good", 却不能匹配 "god"

    import re
    re.search(r'o{2}', 'god')
    re.search(r'o{2}', 'good')
    <_sre.sre_match object; span=(1, 3), match='oo'>
  • {n,} n是一个非负整数。至少能匹配 n次。例如,“o{2,}”不能匹配 “god”中的 “o”,但能匹配“foooood”中的所有o。“o{1,}”等价于“o+”。“o{0,}”则等价于“o*”,这个可看前面示例。

  • {n,m} m和n均为非负整数,其中n<=m。例如,“o{1,2}”将匹配“google”中的两个o。“o{0,1}”等价于“o?”。注意在逗号和两个数之间不能有空格

    re.search(r'o{1,2}', 'google')
    <_sre.sre_match object; span=(1, 3), match='oo'>
  • ? 这个叫做非贪心量化(non-greedy quantifiers),这个字符和前面的 ? 有什么区别?应用场合是什么呢?

    当该字符紧跟在任何一个其他重复修饰符(*,+,?,{n},{n,},{n,m})后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。举个例子,"o+" 默认会匹配 o 一次或多次,如果在后面加上 "?",则匹配一次。来看代码。

    re.search(r'o+?', 'google')
    <_sre.sre_match object; span=(1, 2), match='o'>
    re.search(r'o+', 'google')
    <_sre.sre_match object; span=(1, 3), match='oo'>
  • . 匹配除了 \r ,\n 之外的任何单个字符,要匹配包括“\r”“\n”在内的任何字符,请使用像“(.|\r|\n)”的模式

    import re
    re.search(r'a.', 'ab')
    <_sre.sre_match object; span=(0, 2), match='ab'>
  • (pattern) 匹配 pattern 并获取这一匹配的子字符串,并用于向后引用。使用圆括号可以指定分组。当使用分组时,除了获取到整个匹配的完整字符串,也可以从匹配中选择每个单独的分组。

    下面给出一个本地电话号码的示例,其中每个括号内匹配的数字都是一个分组。

    >>> import re
    >>> match = re.search(r'([\d]{3,4})-([\d]{7,8})', '010-12345678')
    >>> match
    <re.match object; span=(0, 12), match='010-12345678'>
    >>> match.group(1)
    '010'
    >>> match.group(2)
    '12345678'
    >>> match.group()
    '010-12345678'
    >>> match.groups()
    ('010', '12345678')

    前面我们只是简单介绍了 match 对象,为了深入的理解分组,这里还要简单介绍下该对象的几个方法以及如何对应分组信息的:

    • groups() 用于返回一个对应每一个单个分组的元组。

      >>> match.groups()
      ('010', '12345678')
    • group() 方法(不含参数)则返回完整的匹配字符串

      >>> match.group()
      '010-12345678'
    • group(num) num 是分组编号,按照分组顺序,从 1 开始取值,能获取具体的分组数据。

      >>> match.group(1)
      '010'
      >>> match.group(2)
      '12345678'
  • (?:pattern) 匹配 pattern 但不获取匹配的子字符串(shy groups),也就是说这是一个非获取匹配,不存储匹配的子字符串用于向后引用。这种格式的圆括号不会作为分组信息,只用于匹配,即在python 调用search 方法而得到的 match 对象不会将圆括号作为分组存储起来。

    来看下面例子,只获取电话号,而不获取地区区号。

    >>> match = re.search(r'(?:[\d]{3,4})-([\d]{7,8})', '010-12345678')
    >>> match.groups()
    ('12345678',)
    >>> match.group()
    '010-12345678'

    这种形式对使用 字符`(|)”来组合一个模式的各个部分是很有用的,来看一个例子,想要同时匹配 city 和 cities (复数形式),就可以这样干

    >>> match = re.search(r'cit(?:y|ies)','cities')
    >>> match
    <re.match object; span=(0, 6), match='cities'>
    >>> match = re.search(r'cit(?:y|ies)','city')
    >>> match
    <re.match object; span=(0, 4), match='city'>
  • (?=pattern)正向肯定预查(look ahead positive assert),在任何匹配 pattern 的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。

    举个例子,假设我要获取从不同 python 版本中只获取 "python" 字符串,就可以这样写:

    >>> match = re.search(r'python(?=2.7|3.5|3.6|3.7)', 'python3.7')
    >>> match
    <re.match object; span=(0, 6), match='python'>

    预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。那么在 python 版本后再加上其他信息,整体就无法匹配了。

    看下面例子,得到的结果只能是 null。

    >>> match = re.search(r'python(?=2.7|3.5|3.6|3.7) is hacking!', 'python3.7 is hacking!')
    >>> match
  • (?!pattern) 正向否定预查(negative assert),看名字也知道是 正向肯定预查的反面。在任何不匹配 pattern 的字符串开始处匹配查找字符串。是一个非获取匹配,而且预查不消耗字符。

    看下面例子,和正向肯定预查一对比就明白了。

    >>> match = re.search(r'python(?!2.7|3.5|3.6|3.7)', 'python3.7')
    >>> match
    >>> match = re.search(r'python(?!2.7|3.5|3.6|3.7)', 'python3.1')
    >>> match
    <re.match object; span=(0, 6), match='python'>
    >>> match = re.search(r'python(?!2.7|3.5|3.6|3.7) is hacking!', 'python3.1 is hacking!')
    >>> match
  • (?<=pattern) 反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。

  • (?<!pattern) 反向否定预查,与正向否定预查类似,只是方向相反。

  • x|y 或,分两种情况:没有没括号包围,范围则是整个表达式;被括号包围,返回是括号内。

    >>> match = re.search(r'today is sunday|tommory is monday','tommory is monday')
    >>> match
    <re.match object; span=(0, 17), match='tommory is monday'>
  • [xyz] 字符集合,匹配所包含的任意一个字符。分为下面情况

    • 普通字符

      >>> match = re.search(r'[pp]ython','python')
      >>> match
      <_sre.sre_match object; span=(0, 6), match='python'>
    • 特殊字符仅有反斜线 \保持特殊含义,用于转义字符

    • 其它特殊字符如星号、加号、各种括号等均作为普通字符

      >>> match = re.search(r'[*?+()]python','*python')
      >>> match
      <_sre.sre_match object; span=(0, 7), match='*python'>
      >>> match = re.search(r'[*?+()]python','+python')
      >>> match
      <_sre.sre_match object; span=(0, 7), match='+python'>
      >>> match = re.search(r'[*?+()]python','(python')
      >>> match
      <_sre.sre_match object; span=(0, 7), match='(python'>
    • ^ 出现在字符串中间和末尾仅作为普通字符,出现在最前面后面会讲。

      >>> match = re.search(r'[*^{]python','^python')
      >>> match
      <_sre.sre_match object; span=(0, 7), match='^python'>
      >>> match = re.search(r'[*^]python','^python')
      >>> match
      <_sre.sre_match object; span=(0, 7), match='^python'>
    • - 出现在字符集合首位和末尾,仅作为普通字符,出现在中间是字符范围描述,后面会讲。

      >>> match = re.search(r'[-^]python','-python')
      >>> match
      <_sre.sre_match object; span=(0, 7), match='-python'>
  • [^xyz] 排除型字符集合(negated character classes)。匹配未列出的任意字符

    >>> re.search(r'[^abc]def','edef')
    
    <re.match object; span=(0, 4), match='edef'>
  • [a-z] 字符范围。匹配指定范围内的任意字符。

    >>> re.search(r'[a-g]bcd','ebcd')
    
    <re.match object; span=(0, 4), match='ebcd'>
    >>> re.search(r'[a-g]bcd','hbcd')
  • [^a-z] 排除型的字符范围。匹配任何不在指定范围内的任意字符。

    >>> re.search(r'[^a-g]bcd','hbcd')
    
    <re.match object; span=(0, 4), match='hbcd'>
  • \b 匹配一个单词边界,也就是指单词和空格间的位置。

    >>> re.search(r'an\b apple','an apple')
    
    <re.match object; span=(0, 8), match='an apple'>
  • \b 匹配非单词边界

    >>> re.search(r'er\b','verb')
    
    <re.match object; span=(1, 3), match='er'>
  • \d 匹配一个数字字符。等价于[0-9]

    >>> re.search(r'\d apples', '3 apples')
    
    <re.match object; span=(0, 8), match='3 apples'>
  • \d 匹配一个非数字字符。等价于[^0-9]

    >>> re.search(r'\d dog', 'a dog')
    
    <re.match object; span=(0, 5), match='a dog'>
  • \s 匹配任何空白字符,包括空格、制表符、换页符等等。等价于[ \f\n\r\t\v]。

    >>> re.search(r'a\sdog', 'a dog')
    
    <re.match object; span=(0, 5), match='a dog'>
  • \s 匹配任何非空白字符。等价于[^ \f\n\r\t\v]

    >>> re.search(r'\s dog', 'a dog')
    
    <re.match object; span=(0, 5), match='a dog'>
  • \w 匹配包括下划线的任何单词字符。等价于“[a-za-z0-9_]

    >>> re.search(r'\w', 'h')
    
    <re.match object; span=(0, 1), match='h'>
  • \w 匹配任何非单词字符。等价于“[^a-za-z0-9_]

    >>> re.search(r'\w', '@')
    
    <re.match object; span=(0, 1), match='@'>
  • \un unicode转义字符序列。其中n是一个用四个十六进制数字表示的unicode字符

    >>> re.search(r'\u00a9','©')
    
    <re.match object; span=(0, 1), match='©'>

小结

如果你真的读完了这些实例,我敢说你对正则表达式会有一定的理解了吧。下篇会重点讲解python 中的正则表达式库函数,对中文的处理等,敬请期待~

参考文档

  1. *—正则表达式

系列文章列表