背景

字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在。

而生物信息学中,基本上所有的序列数据,本质上都是字符串,无论是核酸DNA、RNA序列,还是蛋白质序列,进行序列分析本质上就是对字符串数据结构进行处理,而正则表达式模糊匹配基本上是字符串处理必须掌握的基本功,应用也是非常基础。

正则表达式是一种用来匹配字符串的强有力的武器。它的设计思想是用一种描述性的语言来给字符串定义一个规则,凡是符合规则的字符串,我们就认为它“匹配”了,否则,该字符串就是不合法的。

⚠️ PS:依旧是去年的囤货,想想还是放出来

基本概念

上面的特殊字符,也就是中间的短连接符需要进行转义,因为只有转义了以后,正则表达式才能够理解我这里是要匹配一个短横线的中间连接符。

1,谈一谈转义

转义的本质是:当一个字符同时存在 “普通含义” 和 “特殊含义” 时,用 \ 告诉解析者(Python 或正则引擎)“请按我指定的含义解读”。

“特殊含义” 不是固定的,而是由「解析者」决定 ——Python 眼里的 “特殊” 和正则引擎眼里的 “特殊”,是两码事。

2,字符串转义 vs 正则转义

(1)字符串转义(Python 解释器解析)

Python 读取字符串时,会默认把某些 \ + 字符 解读为 “特殊功能”(而非原始字符),这些就是 Python 眼里的 “特殊含义”。

  • 需要转义的场景:比如说换行符,当我们想让 Python 把 \n 当作 “字符 \ + 字符 n”(而非 “换行符”)时,就需要转义 \ 本身(用 \\n),或用 r 前缀(raw string,告诉 Python“别解析特殊功能,全按原始字符处理”)。
字符串写法 Python 解析结果(最终存储的内容) 说明(“特殊含义” 的消除)
'abc\n' abc
+ 换行符(功能)
Python 把 \n
解析为 “换行”(特殊含义)
'abc\\n' abc\n
(字符 \
+ 字符 n
\\
转义 \
,消除 “换行” 的特殊含义
r'abc\n' abc\n
(字符 \
+ 字符 n
r
前缀直接禁用 Python 的特殊解析,无需写 \\

这里的 “特殊含义” 是 Python 赋予的功能(如 \n 换行、\t 制表符),转义的目的是 “阻止 Python 触发这些功能,保留字符的原始样子”。

(2)正则转义(正则引擎解析)

当 Python 把字符串传递给正则引擎后,正则引擎会自己解读字符串中的 \ + 字符—— 它也有自己的 “特殊含义” 规则(和 Python 无关)。正则里的 “特殊字符”(如 -^$\d),默认表示 “正则功能”,若想让它们表示 “普通字符”,就需要用 \ 转义。

正则表达式(Python 传递给引擎的内容) 正则引擎解读结果 说明(“特殊含义” 的消除)
\d{3}-\d{3,8} 可能出错! 正则引擎会先判断 -
的位置:若不在 []
内,大概率仍当作普通 -
;但存在歧义风险(若后续调整正则结构,-
可能被当作 “范围符号”)
\d{3}\-\d{3,8} 匹配 3个数字 + 普通- + 3-8个数字 \-
明确告诉正则引擎:-
是普通字符,消除 “范围符号” 的特殊含义
[\d\-] 匹配 “数字” 或 “普通 -” 若写成 [\d-]
-
会被当作 “范围符号”(解析为 “数字到某个字符的范围”,完全错误),必须用 \-
转义

这里的 “特殊含义” 是 正则引擎赋予的功能(如 - 表示范围、\d 表示数字),转义的目的是 “阻止正则引擎触发这些功能,让字符按原始样子匹配”。

(3)最容易混淆的场景:\ 的 “双重转义”

当我们不用 r 前缀定义正则字符串时,Python 会先解析一次 \,再把结果传给正则引擎 —— 这就导致 \ 需要 “转两次”,很容易出错。

以匹配 010-12345 为例:

  • 目标:让正则引擎看到 \d{3}\-\d{3,8}(即 \ 要传递给正则引擎,用于转义 -)。也就是得让正则引擎看到我们就是原始的要转义-,要把我们的转义这个意思也传过去,而不让这个转义先被字符串本身转义掉。
  • 若不用 r 前缀:Python 会把 \ 当作 “需要转义的特殊字符”,所以要写 \\ 才能让 Python 最终传递 \ 给正则引擎。因此正则字符串必须写成 '\\d{3}\\-\\d{3,8}'(Python 解析后,传给正则引擎的是 \d{3}\-\d{3,8})。
  • 若用 r 前缀:Python 不解析 \,直接把 r'\d{3}\-\d{3,8}' 传给正则引擎 —— 这就是很多相关的教程文档推荐用 r 前缀的原因,能避免 \ 的双重转义混淆。

字符串转义:不是“故意把正常变不正常”,而是“Python默认会把某些**+字符解读成特殊功能,所以需要额外处理(要么用r前缀、要么用**)才能保留我们想要的‘正常字符’”

有句话怎么说来着“看山是山,看水是水”,还是“看山不是山,看水不是水”,看原来的山、水是怎么处理的效果,再决定我们是否需要进行转义理解(所以还是只有理解原本解释器和正则引擎对于字符串的处理,我们才能知道做什么操作是有什么意义,才能做出转义不转义的判断)。

  • 字符串转义:Python默认会把某些\组合“想复杂”(解读成换行、制表符等特殊功能),我们需要用r\\让它“想简单点”,回归字符本身的正常样子(比如把Python眼里的“换行符”打回原形,变成“\和n两个普通字符”)。
  • 正则转义:正则引擎默认会把某些字符(比如-^)“想复杂”(解读成范围、开头锚点等特殊功能),我们需要用\让它“想简单点”,回归字符本身的正常样子(比如把正则眼里的“范围符号-”打回原形,变成“普通横线-”)。

本质上,两种转义都是“阻止解析者(Python/正则引擎)搞特殊,让字符按我们想要的‘正常样子’工作”——只是Python和正则引擎的“特殊解读规则”不一样,所以需要针对它们分别用对应的方式(r前缀/\ 或 \)来“纠正”。总之就是不是原本的通用意思。

进阶概念

1,re模块

细节参考:https://www.runoob.com/python/python-reg-expressions.html

有了准备知识,我们就可以在Python中使用正则表达式了。Python提供re模块,包含所有正则表达式的功能。由于Python的字符串本身也用\转义,所以要特别注意:

s = 'ABC\\-001' # Python的字符串
# 对应的正则表达式字符串变成 'ABC\-001'

如果用re.escape的话是将其进行转义包裹,就是什么字符串转义解析之后会变成我们的输入(输出转义变为输入)

import re

# 定义一个字符串
s = 'ABC\\-001'

# 打印原始字符串
print("原始字符串:", s)

# 使用 re.escape() 来转义字符串中的特殊字符
escaped_s = re.escape(s)

# 打印转义后的字符串
print("转义后的字符串:", escaped_s)

# 定义一个正则表达式字符串
regex = 'ABC\\-001'

# 使用 re.escape() 来转义正则表达式字符串中的特殊字符
escaped_regex = re.escape(regex)

# 打印转义后的正则表达式字符串
print("转义后的正则表达式字符串:", escaped_regex)

# 使用正则表达式匹配
match = re.match(regex, s)

# 打印匹配结果
if match:
    print("匹配成功:", match.group())
else:
    print("匹配失败")

因此我们强烈建议使用Python的r前缀,就不用考虑转义的问题了:

s = r'ABC\-001' # Python的字符串
# 对应的正则表达式字符串不变:'ABC\-001'

2,Regex模块:

前面我们说的re库,基本上实现的都是非重叠匹配,比如说我想匹配一个符号sign(±)转变,如果是re的非重叠匹配,基本上就会丢掉很多符号转变:

我们举一个更简单的例子:

我们拿简单的101作为pattern,这个正则对象去匹配string “1010101”

import re
pattern = re.compile(r'(101)')
pattern.findall("1010101")

可以发现,不重叠的匹配基本上只识别到了首尾的两个101

我们可以稍微修改一下操作,也可以直接用现成的支持重叠匹配的工具,比如说regex

总之效果如下,如果还是非重叠匹配:

import regex
pattern = regex.compile(r'(101)')
pattern.findall("1010101",overlapped=False)

如果是重叠匹配,效果就是我们想要的

同理,再次回到我们前面的例子,如果不重叠效果和re是一样的,匹配了7个(按照顺序不重叠的数sign切换)

如果是重叠匹配了:正好14个,符合预期

import regex
pattern = regex.compile(r'(\+0*-)|(-0*\+)')
pattern.findall("+0-0+-+0-+-0+-+0-+0-+", overlapped=True)

再来点复杂一点的模式

import regex
pattern=regex.compile(r'(\+0*-0*\+)|(-0*\+0*-)')
pattern.findall("+0-0+-+0-+-0+-+0-+0-+", overlapped=True)

安装地址参考:https://pypi.org/project/regex/

测试网址:https://regex101.com/

捕获组与非捕获组

这里再提一个概念:捕获组与非捕获组,?:在括号内表示这匹配的是一个非捕获组

我们举一个例子:

下面的正则表达式是匹配两端同号,中间有异号片段,异号片段可以由0字符隔开(至少匹配0次),中间异号片段有多个(至少匹配1次),多个该片段之间必须由0字符隔开。

import regex
pat_nc = regex.compile(r'(\+0*-(0+-)*0*\+)|(-0*\+(0+\+)*0*-)')
print(pat_nc.findall("+0-0-0+"))

这个例子是有捕获组的,

匹配 “+0-0-0+” 时只走左侧,所以 findall 给出一个元组类似:

(‘+0-0-0+’, ‘0-’, ‘’, ‘’)。

在 (0±)* 中,星号 * 让这个捕获组可以多次出现,但普通捕获组只记录最终那一轮的匹配。前面的轮次不会累积。想获取所有重复内容,需要用 finditer 手动遍历或用 (?:(0±))* 并在迭代中查看每个 match 的 captures(regex 库有 .captures() 扩展)。

如果我们使用非捕获组,也就是内部加上去?:的符号

import regex
pat_nc = regex.compile(r'(?:\+0*-(?:0+-)*0*\+)|(?:-0*\+(?:0+\+)*0*-)')
print(pat_nc.findall("+0-0-0+"))

所有括号都是 (?: … ),不捕获 → findall 仅返回整体匹配:

[‘+0-0-0+’]。

如果我们只是关注这个模式整体出现了几次,那么我们就使用非捕获组,这样结果比较干净。

当我们需要单独取组内容、或者进行引用时,我们才使用捕获组。

所以一般的结论如下:

不需要用到分组里面的内容的时候,用非捕获组,主要是为了提升效率,因为捕获组多了一步保存数据的步骤,所以一般会多耗费一些时间,虽然时间很短。

如果我们只是进行模式计数,而不关注捕获到的分组里到底是什么内容,我们可以使用非捕获组。

同样的,前面的交替模式,也可以改成

r'(?:\+0*-)|(?:-0*\+)'
import regex
pat_nc = regex.compile(r'(\+0*-)|(-0*\+)')
print(pat_nc.findall("+0-0-0+"))

可以看到,捕获组的(A|B)其实是两次匹配内容中分别捕获了A、B模式的内容,返回的是(A,空),(空,B)的元组。

import regex
pat_nc = regex.compile(r'(?:\+0*-)|(?:-0*\+)')
print(pat_nc.findall("+0-0-0+"))

参考:https://segmentfault.com/a/1190000021043947

3,切分字符串

用正则表达式切分字符串比用固定的字符更灵活,

正常的切分字符串,

参考:

https://www.runoob.com/python/python-reg-expressions.html

https://docs.python.org/zh-cn/3/library/re.html

https://liaoxuefeng.com/books/python/reg-exp/index.html

更多推荐