2.5正则表达式的使用方法

titledatecommentscategoriestagspermalink
正则表达式的使用
2020/2/4
true
必备技能
必备
技能
正则
2.5

概念

备注(此篇内容引自《自学师门手艺》) 则表达式本质上是个独立的语言,短小却格外强悍 —— 乃至于,如果你竟然没学会它的话,你的之前学的编程技能干脆与残疾无异。

维基百科对正则表达式的说明如下:

正则表达式(英语:Regular Expression,在代码中常简写为 regex、regexp 或 RE),又称正规表示式、正规表示法、正规运算式、规则运算式、常规表示法,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列符合某个句法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。许多程序设计语言都支持利用正则表达式进行字符串操作。例如,在 Perl 中就内建了一个功能强大的正则表达式引擎。正则表达式这个概念最初是由 Unix 中的工具软件(例如 sed 和 grep)普及开的。

以下是绝大多数翻译成中文的教程中对正则表达式进行讲解时所使用的描述:

一个正则表达式(Regular Expression)通常被称为一个模式(Pattern)。

我常常觉得当初要是它被翻译成 “规则表达式”,那么很可能初学者不会感到那么大的压力 —— 谁都一样,看着由 “每个都认识的字构成的词组” 却不能直观地想到它究竟是什么东西,都会感到莫名的压力。

Regular,其实在它的众多语义中,取以下释义最符合 Regular Expression 的原意[1]:

Linguistics 规则的 ▸ regular verbs 规则动词

而 Pattern 这个词,在词典里有好几个对应的中文词汇:

① 图案;② 式样;③ 图样;④ 榜样;⑤ 模式;⑥ 样品;⑦ 模子

在当前语境之下,把 Pattern 翻译成 “模式”,显然不如 “模子” 更好(甚至连 “样品” 感觉都比 “模式” 更恰当)—— “模子” 这个词很直观啊,拿着一个模子去找与它一致的字符串…… “与规则一致”,英文用的是 Match,一般被翻译作 “匹配”。

在自学编程的过程中,处处都是这种语言翻译带来的迷惑、障碍,或者耽误。既然应该把 Regular Expression 理解为 “规则表达式” 更好,那其实吧,把 Pattern 直接理解为中文的 “规则”,可能更直观更准确,理解上更是毫无障碍:

一个规则表达式(Regular Expression)通常被称为一个规则(Pattern)。

那么,规则表达式里写的是什么呢?只能是规则了…… 到最后好像也就 “捕获”(Capture)这个词没什么歧义。

现在,我们已经把术语全部 “解密” 了,然后再看看下面的表述:

我们可以用书写特定的规则,用来在文本中捕获与规则一致的字符串,而后对其进行操作……

理解起来相当顺畅。

以下的 Python 代码中,\wo\w 就是一个规则表达式(或称为规则);

而 re.findall(pttn, str) 的作用就是,在 str 里找到所有与这个规则(Pattern,模式)一致(Match,匹配)的字符串:

import re
str = 'The quick brown fox jumps over the lazy dog'
pttn = re.compile(r'\wo\w')
re.findall(pttn, str)

['row', 'fox', 'dog']

总结一下:

规则表达式(Regular Expressions,通常缩写为 Regex)是最强大且不可或缺的文本处理工具 —— 它的用处就是在文本中扫描/搜索(Scan/Search)与某一规则(Pattern)匹配(Match,即,与规则一致)的所有实例,并且还可以按照规则捕获(Capture)其中的部分或者全部,对它们进行替换(Replace)。

接下来为了避免歧义,我们干脆用 Regex 这个缩写,以及与它相关的英文单词:pattern, match, capture, replace(ment)……

有时,使用 Regex 并不是为了 Replace,而是为了检查格式,比如,可以用 Regex 检查用户输入的密码是否过于简单(比如,全部都由数字构成),比如可以用来验证用户输入的电话号码、证件号码是否符合特定格式等等。

另外,在自学的过程中,想尽一切办法把一切术语用简单直白的 “人话” 重新表述,是特别有效的促进进步的行为模式。

视觉体验

所谓百闻不如一见。

眼见为实 —— 想办法让一个陌生的概念视觉上直观,是突破大多学习障碍的最简单粗暴直接有效的方式。

我们最好先直接看看 Regex 的工作过程。以下,是用微软发行的代码编辑工具 Visual Studio Code 针 对一小段文本使用若干条 Regex 进行匹配的过程:

Python 的项目代码仓库里有一个很简短的 Demo 程序,叫 redemo.py,它使用 Tcl/Tk 作为图形界面,也可以用来测试正则表达式。

https://raw.githubusercontent.com/python/cpython/master/Tools/demo/redemo.py

它运行起来长这个样子

目前(2019)网上最方便的 Regex 测试器,是 regex101.com

以下,就是在一段文本中,找出所有首写字母大写的词汇的过程,并将其先全部替换成小写,再将其全部替换为大写的过程;使用的正则表达式是 ([A-Z]\w+),替换表达式分别是 \L$1 和 \U$1: image.png

这个网站太好了,所以,平日里我是用 Nativefier 工具将这个网站打包为一个 Mac Desktop App 使用。不过,它也有局限,就是被搜索文件略微大点就报错,说 timeout……

优先级

毕竟,你已经不是 “啥都不懂” 的人了。你已经知道一个事实:编程语言无非是用来运算的。

所谓的运算,就有操作符(Operators)和操作元(Operands)—— 而操作符肯定是有优先级的,不然的话,那么多操作元和操作符放在一起,究竟先操作哪个呢?

Regex 也一样,它本身就是个迷你语言(Mini Language)。在 Regex 中,操作符肯定也有优先级。它的操作元有个专门的名称,原子(Atom)。

先大致看看它的操作符优先级,你就会对它有相当不错的了解:

排列原子与操作符优先级(从高到低)
1转义符号 (Escaping Symbol)\
2分组、捕获 (Grouping or Capturing)(...) (?:...) (?=...) (?!...) (?<=...) (?<!...)
3数量 (Quantifiers)a* a+ a? a{n, m}
4序列与定位(Sequence and Anchor)abc ^ $ \b \B
5或(Alternation)a|b|c
6原子 (Atoms)a [^abc] \t \r \n \d \D \s \S \w \W .

当然,你若是在之前,没有自学过、理解过 Python(或者任何其它编程语言)表达式中的操作符优先级,那么一上来就看上面的表格不仅对你没有帮助,只能让你更迷惑。

—— 这就是理解能力逐步积累逐步加强的过程。

原子

在 Regex 的 Pattern 中,操作元,即,被运算的 “值”,被称为原子(Atom)。

本义字符

最基本的原子,就是本义字符,它们都是单个字符。

本义字符包括从 a 到 z,A 到 Z,0 到 9,还有 _ —— 它们所代表的就是它们的字面值。

即,相当于,string.ascii_letters 和 string.digits 以及 _。

以下字符在 Regex 中都有特殊含义:

\ + * . ? - ^ $ | ( ) [ ] { } < >

当你在写 Regex 的时候,如果你需要搜索的字符不是本义字符,而是以上这些特殊字符时,建议都直接加上转义符号 \ 来表示,比如,你想搜索 ',那你就写 ',或者你想搜索 # 那你就写 #(事实上,# 并不是 Regex 的特殊符号,所以,它之前的转义符号可有可无)—— 这对初学者来说可能是最安全的策略。

跟过往一样,所有的细节都很重要,它们就是需要花时间逐步熟悉到牢记。

集合原子

集合原子还是原子。

标示集合原子,使用方括号 []。[abc] 的意思是说,“a or b or c”,即,abc 中的任意一个字符。

比如,beg[iau]n 能够代表 begin、began,以及 begun。

类别原子

类别原子,是指那些能够代表 “一类字符” 的原子,它们都得使用转义符号再加上另外一个符号表达,包括:

\d 任意数字;等价于 [0-9]

\D 任意非数字;等价于 [^0-9]

\w 任意本义字符;等价于 [a-zA-Z0-9_]

\W 任意非本义字符;等价于 [^a-zA-Z0-9_]

\s 任意空白;相当于 [ \f\n\r\t\v](注意,方括号内第一个字符是空格符号)

\S 任意非空白;相当于 [^ \f\n\r\t\v](注意,紧随 ^ 之后的是一个空格符号)

. 除 \r \n 之外的任意字符;相当于 [^\r\n]

类别原子挺好记忆的,如果你知道各个字母是哪个词的首字母的话:

d 是 digits w 是 word characters s 是 spaces

另外,在空白的集合 [ \f\n\r\t\v] 中:\f 是分页符;\n \r 是换行符;\t 是制表符;\v 是纵向制表符(很少用到)。各种关于空白的转义符也同样挺好记忆的,如果你知道各个字母是那个词的首字母的话:

f 是 flip n 是 new line r 是 return t 是 tab v 是 vertical tab

import re
str = '<dl>(843) 542-4256</dl> <dl>(431) 270-9664</dl>'
pttn = r'\d\d\d\-'
re.findall(pttn, str)
['542-', '270-']

边界原子

我们可以用边界原子指定边界。也可以称作 “定位操作符”。

^ 匹配被搜索字符串的开始位置;

$ 匹配被搜索字符串的结束位置;

\b 匹配单词的边界;er\b,能匹配 coder 中的 er,却不能匹配 error 中的 er;

\B 匹配非单词边界;er\B,能匹配 error 中的 er,却不能匹配 coder 中的 er。

import re

str = 'never ever verb however everest'
pttn = r'er\b'
re.findall(pttn, str)
pttn = r'er\B'
re.findall(pttn, str)
['er', 'er', 'er']
['er', 'er']

注意:^ 和 $ 在 Python 语言中被 \A 和 \Z 替代。

事实上,每种语言或多或少都对 Regex 有自己的定制。不过,本章讨论的绝大多数细节,都是通用的。

数量操作符

数量操作符有:+ ? * {n, m}。

它们是用来限定位于它们之前的原子允许出现的个数;不加数量限定则代表出现一次且仅出现一次:

+ 代表前面的原子必须至少出现一次,即:出现次数 ≧ 1

例如,go+gle可以匹配 google gooogle goooogle 等;

? 代表前面的原子最多只可以出现一次,即:0 ≦ 出现次数 ≦ 1

例如,colou?red可以匹配 colored 或者 coloured;

* 代表前面的原子可以不出现,也可以出现一次或者多次,即:出现次数 ≧ 0

例如,520*可以匹配 52 520 52000 5200000 520000000000 等。

{n} 之前的原子出现确定的 n 次;

{n,} 之前的原子出现至少 n 次;

{n, m} 之前的原子出现至少 n 次,至多 m 次

例如,go{2,5}gle,能匹配 google gooogle goooogle 或 gooooogle,但不能匹配 gogle 和 gooooooogle

数量操作符是对它之前的原子进行操作的,换言之,数量操作符的操作元是操作符之前的原子。

上一节提到,要注意区别:er、[er] 和 (er) 各不相同。

er 是两个原子,'e' 之后 'r' [er] 是一个原子,或者 'e' 或者 'r'; (er) 是一个原子,'er'

或操作符 |

或操作符 | 是所有操作符中优先级最低的,数量操作符的优先级比它高,所以,在 | 前后的原子被数量操作符(如果有的话)操作之后才交给 | 操作。

于是,begin|began|begun 能够匹配 begin 或 began 或 begun。

import re

str = 'begin began begun begins beginn'
pttn = r'begin|began|begun'
re.findall(pttn, str)

输出:
['begin', 'began', 'begun', 'begin', 'begin']

在集合原子中(即,[] 内的原子)各个原子之间的关系,只有 “或” —— 相当于方括号中的每个原子之间都有一个被省略的 |。

注意:方括号的 | 不被当作特殊符号,而是被当作 | 这个符号本身。在方括号中的圆括号,也被当作圆括号 () 本身,而无分组含义。

匹配并捕获

捕获(Capture),使用的是圆括号 ()。使用圆括号得到的匹配的值被暂存成一个带有索引的列表,第一个是 $1,第二个是 $2…… 以此类推。随后,我们可以在替换的过程中使用 $1 $2 中所保存的值。

注意:在 Python 语言中调用 re 模块之后,在 re.sub() 中调用被匹配的值,用的索引方法是 \1、\2…… 以此类推。

import re
str = 'The white dog wears a black hat.'
pttn = r'The (white|black) dog wears a (white|black) hat.'
re.findall(pttn, str)

repl = r'The \2 dog wears a \1 hat.'
re.sub(pttn, repl, str)

repl = r'The \1 dog wears a \1 hat.'
re.sub(pttn, repl, str)
[('white', 'black')]
'The black dog wears a white hat.'
'The white dog wears a white hat.'

非捕获匹配

有时,你并不想捕获圆括号中的内容,在那个地方你使用括号的目的只是分组,而非捕获,那么,你就在圆括号内最开头加上 ?: —— (?:...):

import re
str = 'The white dog wears a black hat.'
pttn = r'The (?:white|black) dog wears a (white|black) hat.'
re.findall(pttn, str)                   # 只捕获了一处,也就是说只有一个值将来可以被引用

repl = r'The \1 dog wears a \1 hat.'    # 之前的一处捕获,在替换时可被多次引用
書評
re.sub(pttn, repl, str)
['black']

'The black dog wears a black hat.'

在 Python 代码中使用正则表达式,匹配和捕获以及随后的替换,有更灵活的方式,因为可以对那些值直接编程。re.sub() 中,repl 参数甚至可以接收另外一个函数作为参数 —— 以后你肯定会自行认真阅读以下页面中的所有内容:

https://docs.python.org/3/library/re.html

非捕获匹配,还有若干个操作符:

(?=pattern)

正向肯定预查(look ahead positive assert),在任何匹配规则的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,Windows(?=95|98|NT|2000) 能匹配 Windows2000 中的 Windows,但不能匹配 Windows3.1 中的 Windows。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。

((?!pattern))

正向否定预查(negative assert),在任何不匹配规则的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如Windows(?!95|98|NT|2000) 能匹配 Windows3.1 中的 Windows,但不能匹配 Windows2000 中的 Windows。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。

((?<=pattern))

反向(look behind)肯定预查,与正向肯定预查类似,只是方向相反。例如,(?<=95|98|NT|2000)Windows 能匹配 2000Windows 中的 Windows,但不能匹配 3.1Windows 中的 Windows。

((?<!pattern))

反向否定预查,与正向否定预查类似,只是方向相反。例如 (?<!95|98|NT|2000)Windows 能匹配 3.1Windows 中的 Windows,但不能匹配 2000Windows 中的 Windows。

控制标记

有几个全局控制标记(Flag)需要了解,其中最常默认指定的有 G 和 M:

A / ASCII,默认为 False

\d, \D, \w, \W, \s, \S, \b, 和 \B 等只限于 ASCII 字符 行内写法:(?a) Python re 模块中的常量:re.A re.ASCII

I/IGNORECASE,默认为 False

忽略字母大小写
行内写法:(?i)
Python re 模块中的常量:re.I re.IGNORECASE

G/GLOBAL,默认为 True

找到第一个 match 之后不返回
行内写法:(?g)
Python re 模块中这个标记不能更改,默认为 TRUE

L/LOCALE,默认为 False

由本地语言设置决定 \d, \D, \w, \W, \s, \S, \b, 和 \B 等等的内容
行内写法:(?L)
Python re 模块中的常量:re.L re.LOCALE

M/MULTILINE,默认为 True

使用本标志后,^ 和 $ 匹配行首和行尾时,会增加换行符之前和之后的位置。
行内写法:(?m)
Python re 模块中的常量:re.M re.MULTILINE

S/DOTALL,默认为 False

使 . 完全匹配任何字符,包括换行;没有这个标志,. 匹配除了 n r 之外的任何字符。 行内写法:(?s) Python re 模块中的常量:re.S re.DOTALL

X/VERBOSE,默认为 False

当该标志被指定时,Pattern 中的的空白符会被忽略,除非该空白符在圆括号或方括号中,或在反斜杠 \ 之后。这样做的结果是允许将注释写入 Pattern,这些注释会被 Regex 解析引擎忽略。注释用 # 号来标识,不过该符号不能在字符串或反斜杠之后。
行内写法:(?x)
Python re 模块中的常量:re.X re.VERBOSE

能学到这里,正则表达式的基本知识基本都知道了,剩下的就是你去“刻意练习”了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值