Python语法学习笔记

字符与ASCII码之间的相互转化

Python自带函数:chr()ord()

  1. chr()
    功能:将数(十进制、二进制、八进制或十六进制)转化为其对应的字符

  2. ord()
    功能:将字符转化为对应的ASCII码,返回值为int类型

1
2
3
4
5
6
7
8
9
10
print(chr(123))
# 123 {
print(chr(89))
# 89 Y


print(ord('A'))
# A 65
print(ord('a'))
# a 97

四种引号的使用

众所周知,在Python中,单引号''、双引号""、三个单引号''''''和三个双引号""""""都可以用来表示字符串,那这有什么区别呢?

1、单引号''与双引号""

功能相同,都表示单行字符串

主要区别在于转义处理,当字符串中包含引号时,可以使用不同引号来避免进行转义处理

单引号字符串:

1
2
3
4
5
6
7
8
text1 = 'It\'s raining'     # 需要转义
text2 = '"Yes!", he said' # 不需要转义

print(text1)
print(text2)

>>> It's raining
>>> "Yes!", he said

双引号字符串:

1
2
3
4
5
6
7
8
text1 = "It's raining"        # 不需要转义
text2 = "\"Yes!\", he said" # 需要转义

print(text1)
print(text2)

>>> It's raining
>>> "Yes!", he said

2、三个单引号''''''与三个双引号""""""

功能相同,都表示多行字符串

主要区别在于转义处理,当字符串中包含引号时,可以使用不同引号来避免进行转义处理

三个单引号字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 不需要转义
text1 = '''
def hello():
"""这是一个文档字符串"""
print("Hello, World!")
'''
# 需要转义
text2 = '''
def world():
\'''这是一个文档字符串\'''
print("Hello, World!")
'''

print(text1)
print(text2)

>>> def hello():
"""这是一个文档字符串"""
print("Hello, World!")
>>> def world():
'''这是一个文档字符串'''
print("Hello, World!")

三个双引号字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 需要转义
text1 = """
def hello():
\"""这是一个文档字符串\"""
print("Hello, World!")
"""
# 不需要转义
text2 = """
def world():
'''这是一个文档字符串'''
print("Hello, World!")
"""

print(text1)
print(text2)

>>> def hello():
"""这是一个文档字符串"""
print("Hello, World!")
>>> def world():
'''这是一个文档字符串'''
print("Hello, World!")

其中,三个双引号是文档字符串(在函数中添加对函数功能、入参、出参等内容的介绍)的标准

综上,四种引号并无孰优孰劣的区别,但建议遵循一些原则以保持良好的编程习惯:

  • 保持一致性
  • 保证可读性
  • 根据内容以及使用场景选择合适的引号避免转义

常见情况:

①文档字符串:三个双引号

②SQL语句:单引号,如"SELECT * FROM users WHERE name = 'Alice'"


format格式化函数

Python2.6 开始,新增了一种格式化字符串的函数str.format(),增强了字符串格式化的功能

基本语法是通过{}:来代替以前的%

format函数可以接受不限个参数,位置可以不按顺序

1
2
3
4
5
6
7
8
print("{} {}".format("hello", "world"))        # 不设置指定位置,按默认顺序
>>> 'hello world'

print("{0} {1}".format("hello", "world")) # 设置指定位置
>>> 'hello world'

print("{1} {0} {1}".format("hello", "world")) # 设置指定位置
>>> 'world hello world'

or like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 指定字符串
print("网站名:{name}, 地址 {url}".format(name="菜鸟教程", url="www.runoob.com"))

# 通过字典设置参数
site = {"name": "菜鸟教程", "url": "www.runoob.com"}
print("网站名:{name}, 地址 {url}".format(**site))

# 通过列表索引设置参数
my_list = ['菜鸟教程', 'www.runoob.com']
print("网站名:{0[0]}, 地址 {0[1]}".format(my_list)) # "0" 是必须的

# 输出结果:
# 网站名:菜鸟教程, 地址 www.runoob.com
# 网站名:菜鸟教程, 地址 www.runoob.com
# 网站名:菜鸟教程, 地址 www.runoob.com

or like this:
也可以向str.format()传入对象:

1
2
3
4
5
6
7
8
9
class AssignValue(object):
def __init__(self, value):
self.value = value

my_value = AssignValue(6)
print('value 为: {0.value}'.format(my_value)) # "0" 是可选的

# 输出结果:
# value 为: 6

数字格式化

数字 格式 输出 描述
3.1415926 {:.2f} 3.14 保留小数点后两位
3.1415926 {:+.2f} +3.14 带符号保留小数点后两位
-1 {:-.2f} -1.00 带符号保留小数点后两位
2.71828 {:.0f} 3 不带小数
5 {:0>2d} 05 数字补零 (填充左边, 宽度为2)
5 {:x<4d} 5xxx 数字补x (填充右边, 宽度为4)
10 {:x<4d} 10xx 数字补x (填充右边, 宽度为4)
1000000 {:,} 1,000,000 以逗号分隔的数字格式
0.25 {:.2%} 25.00% 百分比格式
1000000000 {:.2e} 1.00e+09 指数记法
13 {:>10d} 13 右对齐 (默认, 宽度为10)
13 {:<10d} 13 左对齐 (宽度为10)
13 {:^10d} 13 中间对齐 (宽度为10)
11 '{:b}'.format(11) '{:d}'.format(11) '{:o}'.format(11) '{:x}'.format(11) '{:#x}'.format(11) '{:#X}'.format(11) 1011 11 13 b 0xb 0XB 进制

^<> 分别是居中、左对齐、右对齐,后面带宽度

: 号后面带填充的字符,只能是一个字符,不指定则默认是用空格填充

+ 表示在正数前显示+,负数前显示-

(空格)表示在正数前加空格,比如:print("{: }".format(10)),输出结果10的前面会有个空格;而print("{: }".format(-10)),输出结果为-10,无空格

bdox 分别是二进制、十进制、八进制、十六进制

此外还可以使用大括号 {} 来转义大括号,如下:

1
2
3
4
print ("{} 对应的位置是 {{0}}".format("runoob"))

# 输出结果:
# runoob 对应的位置是 {0}

切片操作

支持切片操作的数据类型:

list(列表)、tuple(元组)、string(字符串)等可迭代对象

  • 切片是操作,不改变原值
  • 切片返回结果的类型和切片对象类型一致,返回的是切片对象的子序列
  • 切片生成的子序列元素是源版的拷贝,因此切片是一种浅拷贝(👈后文述)

Python切片的索引方式包括:正索引、负索引

且语法格式包含三种:[i][:][::]

1、[i] - 索引操作

返回单个数,i为正数时表示从左往右取,且最左边的数索引为0;为负数时表示从右往左取,且最右边的数索引为-1

1
2
3
arr = [10, 20, 30, 40, 50]
print(arr[2]) # 30 - 获取索引2的元素(第3个)
print(arr[-1]) # 50 - 获取最后一个元素

2、[::] - 完整切片

语法格式:

1
object[start_index : end_index : step]
  1. start_index:表示起始索引(包含该索引本身);省略该参数时,表示从“起点”开始取值step为正则起点在最左端,为负则起点在最右端)
  2. end_index:表示终止索引(不包含索引本身);省略该参数时,表示取到“终点”step为正则终点在最右端,为负则终点在最左端)
  3. step: 正负均可,其绝对值大小决定了切取数据时的“步长”,其正负号决定了“切取方向”,正表示“从左往右”取值,负表示“从右往左”取值

默认值:

  • start_index
    • 步长step>0时,默认为0
    • 步长step<0时,默认为-1
  • end_index
    • 步长step>0时,默认为列表的长度+1
    • 步长step<0时,默认为列表的长度取负并-1
  • step:默认为1
    • 大于0时表示从左往右走
    • 小于0时表示从右往左走

3、[:] - 简单切片

语法格式:

1
object[start_index : end_index]

同2,但注意:

  • start_index省略时为默认值0end_index省略时为默认值序列长度
  • 步长step省略,默认为1
1
2
3
4
5
6
arr = [10, 20, 30, 40, 50]

print(arr[1:4]) # [20, 30, 40] - 从索引1到4(不包含4)
print(arr[:3]) # [10, 20, 30] - 从头到索引3
print(arr[2:]) # [30, 40, 50] - 从索引2到最后
print(arr[:]) # [10, 20, 30, 40, 50] - 复制整个列表

4、操作示例

均以列表a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]为例

1)取偶数位置

1
2
b = a[::2]
>>> [0, 2, 4, 6, 8]

2)取奇数位置

1
2
b = a[1::2]
>>> [1, 3, 5, 7, 9]

3)切取完整对象

1
2
3
4
5
6
7
8
a[:]      # 从左往右
>>> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

a[::]     # 从左往右
>>> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

a[::-1]    # 从右往左
>>> [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

4)切取单个值

1
2
3
4
5
a[0]
>>> 0

a[-4]
>>> 6

5)修改单个元素

1
2
a[3] = ['A','B']
>>> [0, 1, 2, ['A', 'B'], 4, 5, 6, 7, 8, 9]

6)替换一部分元素

1
2
a[3:6] = ['A','B']            # [3:6]==[3,4,5],所以将3-5的索引的值替换成A、B
>>> [0, 1, 2, 'A', 'B', 6, 7, 8, 9]

7)连续切片操作

1
2
a[:8][2:5][-1:]
>>> 4

相当于:

1
2
3
a[:8]=[0, 1, 2, 3, 4, 5, 6, 7]
a[:8][2:5]= [2, 3, 4]
a[:8][2:5][-1:] = 4  # [-1:] = [-1] 即:取最后一个值

拷贝

在Python中,包含三种复制方式:赋值、浅拷贝和深拷贝,在内存引用和数据独立性上有显著差异

赋值b = a

对复制对象的引用,两个变量指向同一内存,修改任意一方都会影响另一方

1
2
3
4
5
6
7
8
9
10
list1 = [1, 2, [3, 4]]
list2 = list1 # 赋值操作,list2只是list1的引用

list1.append(5)
print(list1) # [1, 2, [3, 4], 5]
print(list2) # [1, 2, [3, 4], 5] - list1和list2都改变了!

list1[2][0] = 99
print(list1) # [1, 2, [99, 4], 5]
print(list2) # [1, 2, [99, 4], 5] - 嵌套对象也被影响
浅拷贝

copy.copy()list.copy()、切片[:]等)会创建一个新的顶层对象,但内部的可变子对象仍然共享引用。因此,修改顶层数据不会影响原对象,但修改嵌套对象会同步变化

1
2
3
4
5
6
7
8
9
10
11
12
import copy

list1 = [1, 2, [3, 4]]
list2 = list1.copy() # 或者 list2 = list1[:] 或 list2 = list(list1)

list1.append(5)
print(list1) # [1, 2, [3, 4], 5]
print(list2) # [1, 2, [3, 4]] - 第一层不受影响

list1[2][0] = 99
print(list1) # [1, 2, [99, 4], 5]
print(list2) # [1, 2, [99, 4]] - 嵌套对象会被影响!
深拷贝

copy.deepcopy())会递归复制对象及其所有子对象,生成完全独立的副本,任何修改都不会影响原对象,但内存和性能开销更大

1
2
3
4
5
6
7
8
9
10
import copy

list1 = [1, 2, [3, 4]]
list2 = copy.deepcopy(list1)

list1.append(5)
list1[2][0] = 99

print(list1) # [1, 2, [99, 4], 5]
print(list2) # [1, 2, [3, 4]] - 互相独立,不会被影响!

Tips:对于不可变对象(如strtuple),浅拷贝和深拷贝效果相同

特性 赋值 浅拷贝 深拷贝
创建新对象 ❌ 否 ✅ 是 ✅ 是
第一层独立 ❌ 否 ✅ 是 ✅ 是
嵌套对象独立 ❌ 否 ✅ 否 ✅ 是
性能 ⚡ 最快 ⚡ 快 🐌 较慢

字符串前缀ubrf

1、u前缀 - Unicode字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Python 2中区分Unicode字符串和普通字符串
# Python 3中所有字符串默认都是Unicode,前缀可选,一般加上的目的是为了保持兼容

# Python 3中两者相同
str1 = "你好"
str2 = u"你好" # 在Python 3中与不加u相同

print(str1 == str2) # True

# 但可用于明确表示Unicode编码
unicode_str = u'\u4f60\u597d' # "你好"的Unicode编码
print(unicode_str) # 你好

# 在Python 2中很重要
# Python 2: u"中文" 是Unicode字符串,而"中文"是字节串

(没咋遇到过编码/解码Error,没太搞懂,啥时候遇到bug了再补充……

2、b前缀 - 字节字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 字节字符串(bytes) - 不是文本,是原始字节
byte_str = b'Hello World'
print(type(byte_str)) # <class 'bytes'>
print(byte_str) # b'Hello World'

# 只支持ASCII字符
# b'你好' # 错误!字节字符串不能包含非ASCII字符

# 字节字符串操作
data = b'ABCD'
print(data[0]) # 65 (ASCII码)
print(data.decode('utf-8')) # 'ABCD' (转换为字符串)

# 字符串编码为字节
text = "你好"
utf8_bytes = text.encode('utf-8')
print(utf8_bytes) # b'\xe4\xbd\xa0\xe5\xa5\xbd'
print(utf8_bytes.decode('utf-8')) # '你好'
  • 特点:表示原始字节数据,不是文本

  • 用途

    • 网络编程中,服务器和浏览器只认bytes类型数据。如:socket.send()的参数和 socket.recv()函数的返回值都是 bytes 类型
      • socket.send(bytes):要求参数是bytes类型。如果有一个字符串str,需使用str.encode('utf-8')将其转换为bytes
      • socket.recv(bufsize):返回值是 bytes 类型。如果想得到一个字符串,需使用 bytes.decode('utf-8') 将其转换回来

    Tips:

    在 Python3 中,bytesstr的互相转换方式是

    1
    2
    str.encode('utf-8')
    bytes.decode('utf-8')
  • 注意:只能包含ASCII字符和转义序列

3、r前缀 - 原始字符串

1
2
3
4
5
6
7
8
9
10
11
# 原始字符串 - 反斜杠不进行转义
path = r'C:\Users\name\Documents' # 不转义反斜杠
regex = r'\d+\.\d+' # 正则表达式常用

print(r'\n\t\\') # 输出: \n\t\\
print('\\n\\t\\\\') # 输出: \n\t\\
print('\\n\\t\\\\\\') # 输出: \n\t\\\
print('\\n\\t\\\\\\\\') # 输出: \n\t\\\\

# 也常用在Windows路径中
win_path = r'C:\Program Files\Python'
  • 特点:去掉反斜杠\的转义机制

    特殊字符:即反斜杠加上对应字母,可表示特殊含义。比如最常见的\n表示换行,\t表示Tab等

  • 用途:正则表达式、文件路径、转义字符较多的场景

  • 注意:即便可表示原始字符串,但最后的引号前仍不能有奇数个反斜杠

    • 比如这样就会报错:

      1
      print(r'\n\t\')
      1
      print(r'\n\t\\\')
      1
      print(r'\n\t\\\\\')

4、f前缀 - 格式化字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
name = "Alice"
age = 25

# f-string(Python 3.6+)
message = f"My name is {name} and I'm {age} years old"
print(message) # My name is Alice and I'm 25 years old

# 表达式计算
print(f"10 + 20 = {10 + 20}") # 10 + 20 = 30

# 调用函数
print(f"Name uppercase: {name.upper()}") # Name uppercase: ALICE

# 格式化数字
pi = 3.1415926535
print(f"π ≈ {pi:.3f}") # π ≈ 3.142

# 字典访问
person = {"name": "Bob", "age": 30}
print(f"{person['name']} is {person['age']}") # Bob is 30

or like this:

1
2
3
4
5
6
7
8
9
import time
t0 = time.time()
time.sleep(1)
name = 'processing'

# 以f开头表示在字符串内支持大括号内的python表达式
print(f'{name} done in {time.time() - t0:.2f} s')

>>> processing done in 1.00 s
  • 特点:支持在字符串内直接嵌入表达式
  • 用途:字符串格式化,比%str.format()更简洁,观感更佳
  • 注意:Python 3.6及以上版本支持

正则表达式

正则表达式,又称规则表达式(Regular Expression),是一种用来匹配字符串中字符组合的文本模式。

用来干甚?常用于文本处理

  1. 查找:在文本中找到特定模式的内容
  2. 替换:将符合某种模式的内容替换为其他内容
  3. 验证:检查输入的数据是否符合预期格式
  4. 提取:从复杂文本中提取需要的信息

搭配使用:RegExr – 正则表达式在线测试工具。

常见特殊字符(元字符):

1
. * + ? \ [ ] ^ $ { } | ( )

1、元字符

具有特殊含义的字符,不表示字面意义,用于控制匹配模式

(1)基本元字符⭐

字符 名称 功能 示例
. 点号 匹配除换行符(\n)外的任意单个字符 a.c匹配”abc”、”a@c”等
^ 脱字符 匹配字符串的开始位置 ^abc 匹配以 “abc” 开头的字符串
$ 美元符 匹配字符串的结束位置 xyz$ 匹配以 “xyz” 结尾的字符串
\ 反斜杠 转义字符,使后面的字符失去特殊含义 \. 匹配实际的点号而不是任意字符

(2)字符类元字符

字符 名称 功能 示例
[] 方括号 匹配方括号内任意字符 [aeiou] 匹配任意一个元音字母
[^] 否定字符类 匹配不在方括号中的任意字符 [^0-9] 匹配任意非数字字符
- 连字符 在字符类中表示范围 [a-z] 匹配任意小写字母

(3)量词元字符⭐

字符 名称 功能 示例
* 星号 匹配前面的子表达式0次多次 ab*c 匹配 “ac”, “abc”, “abbc” 等
+ 加号 匹配前面的子表达式1次多次 ab+c 匹配 “abc”, “abbc” 但不匹配 “ac”
? 问号 匹配前面的子表达式0次1次 colou?r 匹配 “color” 和 “colour”
{n} 花括号(n次) 匹配恰好n次 a{3} 匹配 “aaa”
{n,} 花括号(至少n次) 至少匹配n次 a{2,} 匹配 “aa”, “aaa” 等
{n,m} 花括号(n到m次) 匹配n到m a{2,4} 匹配 “aa”, “aaa”, “aaaa”

(4)分组和选择元字符

字符 名称 功能 示例
() 圆括号 定义子表达式或捕获组 (ab)+ 匹配 “ab”, “abab” 等
| 竖线 表示”或”关系 cat|dog 匹配 “cat” 或 “dog”

(5)预定义转义字符⭐

字符 功能 等价字符类
\d 匹配任意数字 [0-9]
\D 匹配任意非数字 [^0-9]
\w 匹配任意单词字符(字母、数字、下划线 [a-zA-Z0-9_]
\W 匹配任意非单词字符 [^a-zA-Z0-9_]
\s 匹配任意空白字符(空格、制表符、换行符等 [ \t\n\r\f\v]
\S 匹配任意非空白字符 [^ \t\n\r\f\v]

(6)边界匹配转义字符

字符 功能 示例
\b 匹配单词边界 \bcat\b 匹配 “cat”,但不匹配 “category”
\B 匹配非单词边界 \Bcat\B 匹配 “scattered” 中的 “cat”,但不匹配单独的 “cat”

(7)特殊转义字符

字符 功能
\n 匹配换行符
\t 匹配制表符
\r 匹配回车符
\f 匹配换页符
\v 匹配垂直制表符

(8)贪婪与非贪婪量词⭐

默认情况下,量词(*, +, ?, {})是贪婪的,会尽可能多地匹配字符。在量词后加?可使其变为非贪婪(懒惰)模式

量词 贪婪版本 非贪婪版本 区别
* * *? 0次或多次——尽可能多 vs 尽可能少
+ + +? 1次或多次——尽可能多 vs 尽可能少
? ? ?? 0次或1次——尽可能多 vs 尽可能少
{n,m} {n,m} {n,m}? n到m次——尽可能多 vs 尽可能少

(9)正向和负向预查

字符 名称 功能 示例
(?=...) 正向肯定预查 匹配后面跟着特定模式的位置 Windows(?=95|98) 匹配后面跟着95或98的”Windows”
(?!...) 正向否定预查 匹配后面不跟着特定模式的位置 Windows(?!95|98) 匹配后面不跟着95或98的”Windows”
(?<=...) 反向肯定预查 匹配前面是特定模式的位置 (?<=95|98)Windows 匹配前面是95或98的”Windows”
(?<!...) 反向否定预查 匹配前面不是特定模式的位置 (?<!95|98)Windows 匹配前面不是95或98的”Windows”

2、修饰符(拓展了解)

又称标记,用于改变正则表达式匹配行为

(1)i(ignore case) - 忽略大小写

  • 使匹配不区分大小写

  • 示例:/abc/i 可以匹配 “abc”, “Abc”, “ABC” 等

  • 支持语言:大部分都支持(JavaScript、PHP、Python等)

(2)g(global) - 全局匹配

  • 查找所有匹配项,而不是在匹配到第一个后就停止
  • 示例:在字符串 “ababab” 中,/ab/g 会匹配所有三个 “ab”
  • 支持语言:JavaScript、PHP等

(3)m(multiline) - 多行模式

  • 改变 ^$ 的行为,使其匹配每行的开头和结尾,而不仅是整个字符串的开头和结尾
  • 示例:在多行字符串中,/^abc/m 会匹配每行开头的 “abc”
  • 支持语言:JavaScript、PHP、Python、Perl等

(4)s(single line/dotall) - 单行模式

  • 使点号 . 匹配包括换行符在内的所有字符

  • 在JavaScript中称为”dotall”模式,使用 /s 修饰符

  • 示例:/a.b/s 可以匹配 “a\nb”

  • 支持语言:PHP、Perl、Python(作为re.DOTALL)、JavaScript(ES2018+)

    • Python - re.DOTALL

      前面说过, .(点) 是不匹配换行符的,可是有时候字符串就是跨行的,比如要找出下面文字中所有的职位名称

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      <div class="el">
      <p class="t1">
      <span>
      <a>Python开发工程师</a>
      </span>
      </p>
      <span class="t2">南京</span>
      <span class="t3">1.5-2万/月</span>
      </div>
      <div class="el">
      <p class="t1">
      <span>
      <a>java开发工程师</a>
      </span>
      </p>
      <span class="t2">苏州</span>
      <span class="t3">1.5-2/月</span>
      </div>

      如果直接使用表达式 class=\"t1\">.*?<a>(.*?)</a> 会发现匹配不上,因为 t1<a> 之间有换行

      这时可以使用 re.DOTALL 使得点.也能匹配到换行符

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      content = '''
      <div class="el">
      <p class="t1">
      <span>
      <a>Python开发工程师</a>
      </span>
      </p>
      <span class="t2">南京</span>
      <span class="t3">1.5-2万/月</span>
      </div>
      <div class="el">
      <p class="t1">
      <span>
      <a>java开发工程师</a>
      </span>
      </p>
      <span class="t2">苏州</span>
      <span class="t3">1.5-2/月</span>
      </div>
      '''

      import re
      p = re.compile(r'class=\"t1\">.*?<a>(.*?)</a>', re.DOTALL) # <---i'm here
      for one in p.findall(content):
      print(one)

      >>> Python开发工程师
      >>> java开发工程师

(5)u(unicode) - Unicode模式

  • 启用完整的Unicode支持
  • 正确处理UTF-16代理对和Unicode字符属性
  • 示例:/\p{Script=Greek}/u 可以匹配希腊字母
  • 支持语言:JavaScript、PHP等

(6)y(sticky) - 粘性匹配

  • 从目标字符串的当前位置开始匹配(使用lastIndex属性)
  • 类似于^锚点,但针对的是匹配的起始位置
  • 示例:在JavaScript中,/a/y 会从lastIndex开始匹配 “a”
  • 支持语言:JavaScript

(7)x(extended) - 扩展模式

  • 忽略模式中的空白和注释,使正则表达式更易读

  • 示例:在PHP中,/a b c/x 等同于 /abc/

  • 支持语言:PHP、Perl、Python(作为re.VERBOSE)

    • Python - re.VERBOSE

      re.VERBOSE 是 Python re 模块的一个标志,使用后可在正则表达式中添加空白和注释,使复杂的正则表达式更易读和维护

      比如匹配IPv4地址,如果不添加注释的话维护难度+++(写完过两个小时就看不懂自己写的什么鬼了,得从头再梳理一遍正则表达式的逻辑)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      import re

      # 普通正则表达式 - 难以阅读
      pattern_compact = r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'

      # 使用 VERBOSE 模式 - 清晰可读
      pattern_verbose = re.compile(r'''
      ^ # 字符串开始
      ( # 第一个分组(匹配前三段IP地址)
      (25[0-5] | # 250-255
      2[0-4][0-9] | # 200-249
      [01]?[0-9][0-9]? # 0-199
      )\. # 匹配点号
      ){3} # 重复3次
      (25[0-5] | # 最后一段IP地址
      2[0-4][0-9] |
      [01]?[0-9][0-9]?
      ) # 最后一段(没有点号)
      $ # 字符串结束
      ''', re.VERBOSE)

      # 测试
      ip_addresses = ["192.168.1.1", "256.0.0.1", "10.0.0.255", "999.999.999.999"]

      for ip in ip_addresses:
      print(f"{ip:15} Compact: {bool(re.match(pattern_compact, ip))}, "
      f"Verbose: {bool(pattern_verbose.match(ip))}")

      >>> 192.168.1.1 Compact: True, Verbose: True
      >>> 256.0.0.1 Compact: False, Verbose: False
      >>> 10.0.0.255 Compact: True, Verbose: True
      >>> 999.999.999.999 Compact: False, Verbose: False

      核心功能

      ①忽略空白字符 - 即所有空白字符都会被忽略,但如果需要匹配空格的话就需要额外添加空白字符\s
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      import re

      # 1. 普通模式 - 空白是模式的一部分
      pattern_normal = r'\d{3} \d{2} \d{4}'
      text = "123 45 6789"
      print(f"正常模式匹配: {bool(re.match(pattern_normal, text))}") # True

      # 2. VERBOSE模式 - 空白被忽略(除非转义)
      pattern_with_spaces = re.compile(r'''
      \d{3} # 三位数字
      \s # 空白字符(必须显式匹配) ---如果模式串中要匹配空格的话就必须添加空白字符\s
      \d{2} # 两位数字
      \s # 空白字符
      \d{4} # 四位数字
      ''', re.VERBOSE)

      print(f"VERBOSE模式匹配: {bool(pattern_with_spaces.match(text))}") # True

      # 3. 错误示例:忘记用\s匹配空白
      pattern_wrong = re.compile(r'''
      \d{3} # 三位数字
      \d{2} # 两位数字(这里没有空白匹配!)
      \d{4} # 四位数字
      ''', re.VERBOSE)

      print(f"无空白模式串匹配: {bool(pattern_wrong.match(text))}") # False
      # 如果想要输出True
      # 需改为:
      text_no_spaces = "123456789"
      print(f"无空白字符串匹配: {bool(pattern_wrong.match(text_no_spaces))}") # True

      >>> 正常模式匹配: True
      >>> VERBOSE模式匹配: True
      >>> 无空白模式串匹配: False
      >>> 无空白字符串匹配: True
      ②忽略注释
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      import re

      # 复杂的密码验证正则表达式
      password_pattern = re.compile(r'''
      ^ # 字符串开始
      (?=.*[A-Z]) # 至少一个大写字母(正向预查)
      (?=.*[a-z]) # 至少一个小写字母
      (?=.*\d) # 至少一个数字
      (?=.*[@$!%*?&]) # 至少一个特殊字符
      [A-Za-z\d@$!%*?&]{8,20} # 长度8-20,允许以上字符
      $ # 字符串结束
      ''', re.VERBOSE)

      test_passwords = [
      "Password123!", # 符合要求
      "password123!", # 缺少大写
      "PASSWORD123!", # 缺少小写
      "Password!", # 缺少数字
      "Password123", # 缺少特殊字符
      "Pwd1!", # 长度不足
      "VeryLongPassword1234567890!" # 太长
      ]

      for pwd in test_passwords:
      match = password_pattern.match(pwd)
      print(f"{pwd:30} {'✓' if match else '✗'}")

      >>> Password123! ✓
      >>> password123! ✗
      >>> PASSWORD123! ✗
      >>> Password! ✗
      >>> Password123 ✗
      >>> Pwd1! ✗
      >>> VeryLongPassword1234567890! ✗

3、运算符优先级

正则表达式从左往右进行计算,并遵循优先级顺序(与算数表达式类似)

  • 相同优先级的从左往右进行计算
  • 不同优先级的按先高后低进行计算

运算符的优先级顺序具体看下表(优先级按行从上至下从最高到最低):

运算符 描述
\ 转义符
(),(?:),(?=),[] 圆括号和方括号
*,+,?,{n},{n,},{n,m} 限定符
^,$,\任何元字符、任何字符 定位点和序列(即:位置和顺序)
| 替换,”或”操作 字符具有高于替换运算符的优先级,使得”m|food”匹配”m”或”food”。若要匹配”mood”或”food”,请使用括号创建子表达式,从而产生”(m|f)ood”。

示例解析:

1
r'\d{2,3}|[a-z]+(abc)*'

①—\d{2,3}:匹配两到三个数字

②—|:表示或

③—[a-z]+:匹配一个或多个小写字母

④—(abc)*:匹配零个或多个 “abc”

顺序:④→③→①→②

4、常见匹配规则

1
2
3
4
5
6
[a-z]         # 匹配所有的小写字母 
[A-Z] # 匹配所有的大写字母
[a-zA-Z] # 匹配所有的字母
[0-9] # 匹配所有的数字
[0-9\.\-] # 匹配所有的数字,句号和减号
[ \f\r\t\n] # 匹配所有的白字符
1
2
3
[^a-z]      # 除了小写字母以外的所有字符 
[^\\\/\^] # 除了(\)(/)(^)之外的所有字符
[^\"\'] # 除了双引号(")和单引号(')之外的所有字符
1
2
3
4
^[a-zA-Z0-9_]+$           # 所有包含一个以上的字母、数字或下划线的字符串 
^[1-9][0-9]*$ # 所有的正整数
^\-?[0-9]+$ # 所有的整数
^[-]?[0-9]+(\.[0-9]+)?$ # 所有的浮点数

5、断言 - 高级扩展语法

断言(Assertion):正则表达式元字符,不匹配任何实际字符,用于指定匹配位置

断言适用于某些场景,比如价格,通常是xx元,通过断言即可只匹配在“元”这个字之前的数字

特点:

  • 零宽度:不占用匹配字符的位置
  • 条件检查:只检查是否满足特定条件
  • 不影响匹配结果:仅作为匹配的约束条件

听着很唬人,但依旧paper tiger···

通过这个例子可以很好理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re

# 断言不消耗字符
text = "苹果售价10元"

# 不使用断言:匹配价格
pattern_without_assertion = r'售价\d+'
match2 = re.search(pattern_without_assertion, text)
print(f"不使用断言匹配: '{match2.group()}'") # '售价10' <---- 消耗了字符串"售价"

# 使用断言:匹配价格
pattern_with_assertion = r'(?<=售价)\d+'
match1 = re.search(pattern_with_assertion, text)
print(f"使用断言匹配: '{match1.group()}'") # '10' <---- 只匹配数字

通俗来讲就是不使用断言来匹配字符时,会有额外的我们不需要的字符也被匹配到了,就像上面这个例子,我们只需要价格,但是“售价”这个字符串也在输出结果里面,如果想使用价格进行下一步处理的话得先把字符串再隔断一下取出数据;而使用断言后直接就可获得我们需要的结果。在处理超长文本时这将会极大方便的我们

即:pattern(正则表达式) 中的内容不会成为最终匹配结果的一部分

断言类型:

断言类型 正则语法 别称 检查方向 期望条件 讲人话
正向先行断言 (?=pattern) 正前瞻 向右(向前) 存在 pattern 我要找的位置,它的右边必须是…
负向先行断言 (?!pattern) 负前瞻 向右(向前) 不存在 pattern 我要找的位置,它的右边一定不能是…
正向后行断言 (?<=pattern) 正后顾 向左(向后) 存在 pattern 我要找的位置,它的左边必须是…
负向后行断言 (?<!pattern) 负后顾 向左(向后) 不存在 pattern 我要找的位置,它的左边一定不能是…

表格看着复杂,但只需要看懂最后一列的通俗解释以及第二列的语法即可(至于其余列,写者表示看不见😎😎😎)

各举一例:

(1)正向先行断言

我的右边必须是….

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import re

# 匹配后面跟着特定模式的字符串
text = "I like Python, I love Python, I use Java"

# 匹配"Python",但要求后面跟着逗号或结尾
pattern = r'Python(?=,|$)'
matches = re.findall(pattern, text)
print(f"正向先行断言: {matches}") # ['Python', 'Python'] - 第二个Python后面是结尾

# 实际应用:提取后面有单位的数字
text2 = "身高175cm, 体重70kg, 年龄25岁"
pattern2 = r'\d+(?=cm|kg|岁)'
matches2 = re.findall(pattern2, text2)
print(f"带单位的数字: {matches2}") # ['175', '70', '25']

>>> 正向先行断言: ['Python', 'Python']
>>> 带单位的数字: ['175', '70', '25']

(2)负向先行断言

我的右边一定不能是…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import re

# 匹配后面不跟着特定模式的字符串
text = "apple pie, apple juice, apple core, apple"

# 匹配"apple",但后面不能是" pie"或" juice"
pattern = r'apple(?! (?:pie|juice))'
matches = re.findall(pattern, text)
print(f"负向先行断言: {matches}") # ['apple', 'apple'] - 匹配"core"和结尾的apple

# 实际应用:查找没有特定后缀的单词
text2 = "helpful, helpless, helpfulness, help"
pattern2 = r'help(?!ful|less)'
matches2 = re.findall(pattern2, text2)
print(f"非派生词: {matches2}") # ['help'] - 只匹配基础形式

>>> 负向先行断言: ['apple', 'apple']
>>> 非派生词: ['help']

(3)正向后行断言

我的左边必须是…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import re

# 匹配前面有特定模式的字符串
text = "价格: $10, 成本: €20, 价值: $30"

# 匹配美元金额($后面的数字)
pattern = r'(?<=\$)\d+'
matches = re.findall(pattern, text)
print(f"正向后行断言: {matches}") # ['10', '30']

# 实际应用:提取特定标签后的内容
html = '<div>Content1</div><span>Content2</span><div>Content3</div>'
pattern = r'(?<=<div>)[^<]+(?=</div>)'
matches = re.findall(pattern, html)
print(f"div内容: {matches}") # ['Content1', 'Content3']

>>> 正向后行断言: ['10', '30']
>>> div内容: ['Content1', 'Content3']

注意:pattern 必须有固定长度(不能是 *+ 等可变长度量词),根据写者实操,在python中,断言中通过|选取的内容也需要保持相同的长度

这一块比较奇怪,因为查了python官方re文档

没有找到这方面的具体内容,故写此处作为提醒!⚠️⚠️⚠️

1
2
3
4
5
6
7
8
'''这段代码不会报错'''

# Python后行断言的限制:模式必须固定长度
text2 = "Mr. Smith, Mrs. Jones, Dr. Brown, Prof. Wilson"
# 正确:匹配称呼后的名字(固定长度模式)
pattern2 = r'(?<=Mr\. |Dr\. )\w+' # <---here,注意(?<=...)中的内容
matches2 = re.findall(pattern2, text2)
print(f"称呼后的名字: {matches2}") # 称呼后的名字: ['Smith', 'Brown']
1
2
3
4
5
6
7
8
'''这段代码会报错'''
# 报错原因为Mr.|Dr.|Prof.这三个的长度不一致

text2 = "Mr. Smith, Mrs. Jones, Dr. Brown, Prof. Wilson"
pattern2 = r'(?<=Mr\. |Dr\. |Prof\. )\w+' # <---here,注意(?<=...)中的内容

matches2 = re.findall(pattern2, text2)
print(f"称呼后的名字: {matches2}")

验证代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import re

# 测试不同长度的分支
test_cases = [
(r'(?<=ab|cd)', "长度相同(都是2)"),
(r'(?<=abc|def)', "长度相同(都是3)"),
(r'(?<=a|bc)', "长度不同(1和2)"),
(r'(?<=ab|cde)', "长度不同(2和3)"),
(r'(?<=Mr|Mrs)', "长度不同(2和3)"),
(r'(?<=Mr\.|Mrs\.)', "长度不同(Mr\.=3, Mrs\.=4)"),
]

print("=== Python re模块的实际行为 ===")
for pattern, description in test_cases:
try:
re.compile(pattern)
print(f"✅ {pattern:20} - {description} - 编译成功")
except re.error as e:
print(f"❌ {pattern:20} - {description} - 编译失败: {e}")
re-positive-lookbehind-assertion-checking

(4)负向后行断言

我的左边一定不能是…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import re

# 匹配前面没有特定模式的字符串
text = "$100, ¥200, $300, €400"

# 匹配不是美元符号开头的金额
pattern = r'(?<!\$)\b\d+\b'
matches = re.findall(pattern, text)
print(f"负向后行断言: {matches}") # ['200', '400'] - 排除了$100和$300

# 实际应用:查找没有被引号括起来的单词
text2 = 'He said "hello" to the world and said goodbye'
# 匹配不在双引号内的"said"
pattern2 = r'(?<!")said(?!")'
matches2 = re.findall(pattern2, text2)
print(f"非引号内的said: {matches2}") # ['said'] - 只匹配第二个said

>>> 负向后行断言: ['200', '400']
>>> 非引号内的said: ['said', 'said']

负向后行断言估计和前文正向后行断言有一样的限制条件,pattern 必须有固定长度且通过|选取的内容也需要保持相同的长度,这里写者懒得验证了😴😴😴