正则表达式基础

正则表达式基础

正则表达式工作中会经常用到,只是很少去系统的总结其中的一些基础的东西,导致有时候容易疏忽,上次修复一个url跳转的漏洞就考虑的简单,写错了正则,真是糗大了,所以还是写篇文章来系统总结一下。此外,上周发现了系统中隐藏已久的一个BUG,导致系统卡顿了大半天,最终发现居然是redis使用不当,用了"keys AAA|BBB|*|CCC“这种模糊查询语句,导致性能急剧下降,修复了这个问题,整个系统顺畅多了。正则表达式所有的编程语言几乎都是支持的,用于处理字符串匹配。大概流程就是根据正则表达式模式字符串,然后根据模式去匹配文本。记得我很久之前还写过一篇写正则匹配算法的文章正则表达式简易实现,有兴趣的可以看看。由于工作后用python较多,这篇文章就用python来写demo了。

1 元字符

首先看看正则表达式中的元字符,主要有.,^,$等等,也可以细分为基本元字符,数量元字符,位置元字符,特殊字符元字符以及分组等,详细解释如下,贪婪模式以及分组等在后面列出。

代码说明
 基本元字符
.匹配除了换行符“\n”以外的任意字符,在DOTALL模式中也能匹配换行符
\转义字符,当我们的模式中有元字符如.,\时,需要进行转义
|逻辑或操作符
-字符集合定义一个区间,如后面的A-Z
[...]字符集合,中括号中可以是字符集中的任意字符,可以逐个列出如[abc],也可以给出范围,如[a-c],如果里面的字符以^开头,表示取反。如[^abc]表示匹配不为a,b或者c的字符。如果字符集合中要包含特殊字符如],-,^等,则需要加转义字符\,或者也可以将],-放在开头,^不放在开头。要注意的是,也只有在字符集合中可以这样,其他地方还是要价转义字符。
 位置元字符
^匹配字符串的开头(多行模式匹配每一行开头)
$匹配字符串的结尾(多行模式匹配每一行结尾)
\A匹配字符串的开头
\Z匹配字符串的结尾
\b匹配单词边界,即单词开始或结束
\B不匹配单词边界,即[^b]
 数量元字符
*匹配前一个字符(或者表达式)零次或多次
+匹配前一个字符(或者表达式)一次或多次
?匹配前一个字符(或者表达式)零次或一次
{n}匹配前一个字符(或者表达式)n次
{m,n}匹配前一个字符(或者表达式)最少m次,最多n次
{m,}匹配前一个字符(或者表达式)最少m次
 特殊元字符
\d匹配数字,等同于[0-9]
\D匹配非数字,等同于[^\d]
\w匹配单词字符,即字母,数字,下划线,等同于[A-Za-z0-9_]
\W匹配非单词字符,等同于[^\w]
\s匹配空白字符,空白字符包括空格,\t, \r, \n等
\S匹配非空白字符,等同于[^\s]
\n匹配回车符
\t匹配制表符
 分组元字符
(...)小括号中的内容表示一个分组,每匹配一个分组,编号加1.分组作为一个整体,后面可以加数量元字符。比如(abc){2}
(?P<name>...)分组除了原有的编号外加个别名,如(?P<id>abc)匹配abc并将组的别名设为id
\number引用编号为number的分组匹配到字符串里,如(\d)abc\1,其中\1引用(\d)分组,该表达式可以匹配1abc1等
(?P=name)引用别名为name的分组匹配到字符串中,如(?P\<id>\d)abc(?P=id),该表达式可以匹配1abc1等

2 匹配模式和转义字符(反斜杠)

匹配模式可以配置为忽略大小写或者多行匹配,在python中常用的设置模式主要有如下几种:re.I(IGNORECASE): 忽略大小写。re.M(MULTILINE): 多行模式,该模式下^/$可以匹配每一行的开头/末尾。re.S(DOTALL): 点任意匹配模式,该模式.可以匹配任意字符。re.X(VERBOSE): 详细模式。这个模式下正则表达式可以是多行,忽略空白字符,并可以加入注释。以下两个正则表达式是等价的:

a = re.compile(r"""\d +  # the integral part
                   \.    # the decimal point
                   \d *  # some fractional digits""", re.X)
b = re.compile(r"\d+\.\d*")
此外,根据正则表达式不同还分贪婪模式和非贪婪模式。如果在表达式后面加?表示非贪婪模式,而如果不加在python中默认是贪婪模式。例如匹配的字符串为“abbbbc”,正则为“ab*”的时候,匹配结果是"abbbb";如果正则为"ab*?",则匹配结果为"a"。
大多数的编程语言里面也是用的反斜杠来转义,所以如果代码中要匹配"\"的话,就需要用四个反斜杠才能匹配。不过python里面用原生字符串解决了这个问题,只要在字符串前面加r就行,比如r"\d",不用原生字符串的话就要写成"\\d"了。

3 简单例子

先不管分组,看看比较基本的用法好了。光看文字说明也不一定清晰,还是栗子最实在。注意re.compile返回的Pattern对象,这样提前编译好在大批量匹配的时候会对性能有所提升。当然,也可以直接用re模块提供的各种方法来进行匹配。

#coding:utf8                                                                                                                                                  
import re

def basic():
  string_pat = []

  string = "adbbb"
  pat = r"a.b*" #贪婪模式,匹配adbbb
  string_pat.append((string,pat))

  string = "adb^?]bb"
  pat = r"a.b[][bc?^]*" ##不需要加转义字符,因为^没有放在最前面,而]放在了最前面
  string_pat.append((string,pat))

  string = "adbbb"
  pat = r"a.b*?" #非贪婪模式,匹配ad
  string_pat.append((string,pat))

  string = "a\nbbb"
  pat = r"a.b*" #匹配不了\n, 匹配模式改为re.S才能匹配,即pattern=re.compile(pat, re.S)
  string_pat.append((string,pat))

  string = "adbbb"
  pat = r"^a.b$" #匹配不了,不加$则可以成功匹配
  string_pat.append((string,pat))

  string = "kkk\nadb\nddd"
  pat = r"\Aadb"
  string_pat.append((string,pat))

  string = "kkk\nadbcc\nddd"
  pat = r"^adb" #匹配失败
  string_pat.append((string,pat))
  for string,pat in string_pat:
    pattern = re.compile(pat)
    ret =  pattern.match(string)
    if ret:
      print "string:%s, pat:%s, result:%s" % (repr(string), pat, ret.group())
    else:
      print "string:%s, pat:%s, result:NO" % (repr(string), pat)

###运行结果
string:'adbbb', pat:a.b*, result:adbbb
string:'adb^?]bb', pat:a.b[][bc?^]*, result:adb^?]bb
string:'adbbb', pat:a.b*?, result:ad
string:'a\nbbb', pat:a.b*, result:NO
string:'adbbb', pat:^a.b$, result:NO
string:'kkk\nadb\nddd', pat:\Aadb, result:NO
string:'kkk\nadbcc\nddd', pat:^adb, result:NO</code>

4 Match对象和Pattern对象

Match对象是匹配的结果,包含的属性和方法如下:

属性

  • string:匹配的文本
  • re:匹配使用的Pattern对象
  • pos:匹配开始位置
  • endpos:匹配结束位置
  • lastindex:最后一个分组在文本中的索引
  • lastgroup:最后一个分组的别名
import re
m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+)", "Malcolm Reynolds")
print "string:", m.string
print "re:", m.re
print "pos:", m.pos
print "endpos:", m.endpos
print "lastindex:", m.lastindex
print "lastgroup:", m.lastgroup

##输出结果如下:
string: Malcolm Reynolds
re: <_sre.SRE_Pattern object at 0x10baa5200>
pos: 0
endpos: 16
lastindex: 2
lastgroup: last_name

方法
  • group([group1,group2]):获得一个或多个分组匹配到的字符串。指定多个参数时以元组格式返回,默认参数为0,即返回整个匹配的子串,参数可以是组的编号如1,2等,也可以是组的别名。如果一个组匹配了多次,那么group()函数只能返回最后一次的匹配值。见下面的栗子。
  • groups([default]):以元组形式返回全部匹配的分组,没有参与匹配的分组则返回None,如果指定了default,则没有参与匹配的分组返回default值,见栗子。
  • groupdict([default]):返回有别名的组的别名和匹配字符串的字典,default含义同上。
  • start([group]):分组匹配到的子串的起始位置。默认参数group为0,即整个匹配的子串的起始位置。
  • end([group]):分组匹配到的子串的结束位置。group含义同上。
  • span([group]):返回(start(group), end(group))。
  • expand(template):将匹配的分组代入template中返回。见例子。
##group函数示例
>>> m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+)", "Malcolm Reynolds")
>>> m.group() ##等价于m.group(0)
'Malcolm Reynolds'
>>> m.group(1)
'Malcolm'
>>> m.group(2)
'Reynolds'
>>> m.group(1,2)
('Malcolm', 'Reynolds')
>>> m.group('first_name')
'Malcolm'
>>> m.group('last_name')
'Reynolds'
>>> m.expand(r'\2 \1')
'Reynolds Malcolm'

>>> m = re.match(r"(..)+", "a1b2c3")  # Matches 3 times.
>>> m.group(1)                        # Returns only the last match.
'c3'

##groups函数示例
>>> m = re.match(r"(\d+)\.?(\d+)?", "24")
>>> m.groups()      # Second group defaults to None.
('24', None)
>>> m.groups('0')   # Now, the second group defaults to '0'.
('24', '0')

##groupdict函数示例
>>> m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+)", "Malcolm Reynolds")
>>> m.groupdict()
{'first_name': 'Malcolm', 'last_name': 'Reynolds'}

##start,end,span函数示例
>>>m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+)", "Malcolm Reynolds")
>>>m.start()
0
>>>m.start(2)
8
>>>m.end()
16
>>>m.end(1)
7
>>>m.span(1)
(0, 7)

Pattern对象是编译好的正则表达式,通过re.compile构造,如re.compile(r"a.b"),其属性有pattern,flags,groups和groupindex等,具体可以参见python docs。


5 re模块重要函数

5.1 match() vs search()

  • re.match(pattern, string, flags=0)match函数是从字符串头部开始匹配,如果匹配成功则返回Match对象,否则返回None。
  • re.search(pattern, string, flags=0)search函数与match不同的地方在于search会从所有位置试探匹配,如果从0开始没有匹配到,则从1开始...如此循环。也可以加^来限制search必须从头开始匹配。

此外,在多行模式re.M中,match只能匹配字符串开头,而search以及findall可以匹配每一行开头。见下面例子。

>>> re.match("c", "abcdef")  # No match
>>> re.search("c", "abcdef") # Match
<_sre.SRE_Match object at ...>

>>> re.match("c", "abcdef")  # No match
>>> re.search("^c", "abcdef") # No match
>>> re.search("^a", "abcdef")  # Match
<_sre.SRE_Match object at ...>

>>> re.match('X', 'A\nB\nX', re.MULTILINE)  # No match
>>> re.search('^X', 'A\nB\nX', re.MULTILINE)  # Match
<_sre.SRE_Match object at ...>

5.2 split(pattern,string,maxsplit=0)

split用于将字符串按照指定的模式分割为列表,其中maxsplit指定最多分割次数。

>>> text = """Ross McFluff: 834.345.1254 155 Elm Street
...
... Ronald Heathmore: 892.345.3428 436 Finley Avenue
... Frank Burger: 925.541.7625 662 South Dogwood Way
...
...
... Heather Albrecht: 548.326.4584 919 Park Place"""

>>> entries = re.split("\n+", text) ##按换行符分割
>>> entries
['Ross McFluff: 834.345.1254 155 Elm Street',
'Ronald Heathmore: 892.345.3428 436 Finley Avenue',
'Frank Burger: 925.541.7625 662 South Dogwood Way',
'Heather Albrecht: 548.326.4584 919 Park Place']

>>> [re.split(":? ", entry, 3) for entry in entries]
[['Ross', 'McFluff', '834.345.1254', '155 Elm Street'],
['Ronald', 'Heathmore', '892.345.3428', '436 Finley Avenue'],
['Frank', 'Burger', '925.541.7625', '662 South Dogwood Way'],
['Heather', 'Albrecht', '548.326.4584', '919 Park Place']]

5.3 sub(pattern,repl,text,count=0)

sub使用字符串repl或者一个函数返回值替换每个匹配的字符串。例子中对字符串中每个匹配的值的中间部分单词进行了shuffle操作,前后字母不变。count为最多替换次数,默认是全部替换。

>>> def repl(m):
...   inner_word = list(m.group(2))
...   random.shuffle(inner_word)
...   return m.group(1) + "".join(inner_word) + m.group(3)
>>> text = "Professor Abdolmalek, please report your absences promptly."
>>> re.sub(r"(\w)(\w+)(\w)", repl, text)
'Poefsrosr Aealmlobdk, pslaee reorpt your abnseces plmrptoy.'
>>> re.sub(r"(\w)(\w+)(\w)", repl, text)
'Pofsroser Aodlambelk, plasee reoprt yuor asnebces potlmrpy.'

5.4 findall(pattern,string,flag=0)

返回全部匹配的子串,结果为列表形式。

>>> text = "He was carefully disguised but captured quickly by police."
>>> re.findall(r"\w+ly", text)
['carefully', 'quickly']

5.5 finditer(pattern, string, flag=0)

如果需要匹配的子串的位置信息,那用finditer更好,因为它返回的是Match对象。

>>> text = "He was carefully disguised but captured quickly by police."
>>> for m in re.finditer(r"\w+ly", text):
...     print '%02d-%02d: %s' % (m.start(), m.end(), m.group(0))
07-16: carefully
40-47: quickly


6 常用正则表达式

工作中常用的就是电话号码,url匹配等一些正则表达式,下面也一并总结下,如有错误请指出。

6.1 电话号码

  • 下面的电话号码正则匹配13,14,15,17,18段的手机号码,其中14段的只有145,147这两种,可以根据需求加上对应的数字。电话号码前面也可以加0,86,17951等。
  • ^(0|86|17951)?(13[0-9]|14[57]|15[012356789]|17[678]|18[0-9])[0-9]{8}$
  • 座机号码则可以通过下面这个正则来匹配
  • '^0\d{2,3}-?\d{7,8}$

6.2 URL

  • 要匹配一个URL会复杂一点,有时候会容易漏掉一些特殊情况,所以要比较小心。如果要匹配通用的URL,可以写成这样,后缀可以补充成自己需要限制的内容:
  • r"^http(s)?://([\w-]+\.)*(com|cn|org|net)(:\d*)?(/.*)?$"
  • 如果要限制URL只能是某些公司的网址,可以这样限制:
  • r"^http(s)?://([\w-]*\.)*((163\.com)|(baidu\.com)|(qq\.com))(:\d*)?(/.*)?$"


7 参考资料

由于正则表达式也是第一次完整学习,参考了很多资料,主要有《python正则表达式指南》和python docs中关于re模块的章节。完整参考列表如下:

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值