我把正则分组比喻成拆快递,一拆一个准

8 阅读4分钟

上回给你讲了 re 模块的摸金手套——matchsearchfindall 那些。但你有没有发现一个问题:

你摸出来的东西,是一整坨。

比如摸到 "蛋炒粉15元",但你要的是 "蛋炒粉""15" 分开。就像从仓库里摸出一个包裹,外面缠着胶带,你得拆开,把里面的货分门别类

这个"拆包裹、分篮子"的手艺,就叫正则分组。


一、分组是啥?就是给货贴篮子

你在正则表达式里加一对圆括号 (),就像在手套上挂了个篮子。手套摸到整串东西,但篮子里只装你想要的那部分。

import re

html = '<li class="food">蛋炒粉 - <span class="price">15元</span></li>'

# 不加篮子:摸到一整串
re.findall(r'<span class="price">\d+元</span>', html)
# ['<span class="price">15元</span>']   ← 带着标签,脏得很

# 加个篮子 ( ):只掏里面的数字
re.findall(r'<span class="price">(\d+)元</span>', html)
# ['15']   ← 干净,直接能用

人话:不加 (),你摸到的是快递外包装;加了 (),你摸到的是快递里的货


二、group(0):整串包裹

当你用 searchmatch 摸到东西后,返回的对象是个"包裹"。

text = "蛋炒粉15元"

result = re.search(r'(\D+)(\d+)', text)

print(result.group(0))  # 蛋炒粉15元   ← 整串,原封不动
print(result.group(1))  # 蛋炒粉       ← 第一个篮子里的货
print(result.group(2))  # 15           ← 第二个篮子里的货

group(0) 永远等于你摸到的完整字符串,相当于快递单上的"总重量"。

group(1)、group(2)... 才是你真正想要的零件。

人话group(0) 是"你摸到了啥",group(1) 是"第一个篮子里是啥"。


三、groups():一次性全倒出来

如果你嫌一个一个拿麻烦,可以直接把篮子全打翻

result = re.search(r'(\D+)(\d+)', "蛋炒粉15元")

print(result.groups())
# ('蛋炒粉', '15')   ← 元组,按顺序排好

什么时候用? 你要把结果直接塞进数据库、Excel、字典的时候。

name, price = result.groups()

data = {
    '菜名': name,
    '价格': int(price)
}

人话groups() 就像把快递里的东西全倒桌上,自己挑。


四、findall + 分组:批量拆快递

findall 遇到分组时,行为会变——它不再返回整串,而是只返回每个篮子里的货,装进元组

text = """
蛋炒粉15元
肉炒粉20元
牛排999元
"""

# 两个篮子:(\D+) 装菜名,(\d+) 装价格
results = re.findall(r'(\D+)(\d+)元', text)

print(results)
# [('蛋炒粉', '15'), ('肉炒粉', '20'), ('牛排', '999')]

看到没? 返回的是列表套元组,每个元组就是一组拆好的零件。

直接塞进 pandas:

import pandas as pd

df = pd.DataFrame(results, columns=['菜名', '价格'])
print(df)
#     菜名  价格
# 0  蛋炒粉   15
# 1  肉炒粉   20
# 2   牛排  999

人话findall 加分组,就是流水线批量拆快递,拆完直接装箱。


五、嵌套分组:篮子里套篮子

有时候你要的货,外面还有一层包装。

text = "2026年4月29日"

# 大篮子:(年月日)
# 里面套小篮子:(年) (月) (日)
result = re.search(r'((\d{4})年(\d{1,2})月(\d{1,2})日)', text)

print(result.group(0))  # 2026年4月29日   ← 整串
print(result.group(1))  # 2026年4月29日   ← 大篮子(和0一样)
print(result.group(2))  # 2026            ← 年
print(result.group(3))  # 4               ← 月
print(result.group(4))  # 29              ← 日

数篮子的规则:从左往右,遇到左括号 ( 就计数

((\d{4})年(\d{1,2})月(\d{1,2})日)
12       3        4

人话:括号就是篮子,从左到右数左括号,第几个左括号就是第几个 group


六、命名分组:给篮子贴名字

group(1)group(2) 容易晕,特别是括号多了以后。

命名分组就是给篮子贴个名字,以后按名字取货。

text = '<span class="price">蛋炒粉15元</span>'

# (?P<name>...)  给篮子起名字
pattern = r'<span.*?>(?P<name>\D+)(?P<price>\d+)元</span>'

result = re.search(pattern, text)

print(result.group('name'))   # 蛋炒粉
print(result.group('price'))  # 15

优点:不用数数,代码可读性高。
缺点:写起来麻烦一点,适合复杂正则。

人话:普通分组是"1号篮子、2号篮子",命名分组是"菜名篮子、价格篮子"。


七、实战:从菜单里拆出结构化数据

把今天学的串起来,写一个真正能跑的爬虫片段:

import re

html = """
<div class="item">
    <h3>蛋炒粉</h3>
    <span class="price">15元</span>
    <span class="sales">月销1000+</span>
</div>
<div class="item">
    <h3>肉炒粉</h3>
    <span class="price">20元</span>
    <span class="sales">月销800+</span>
</div>
"""

# 三个篮子:菜名、价格、销量
pattern = re.compile(
    r'<h3>(.*?)</h3>.*?<span class="price">(\d+)元</span>.*?<span class="sales">月销(\d+)\+</span>',
    re.S
)

for match in pattern.finditer(html):
    name = match.group(1)
    price = match.group(2)
    sales = match.group(3)
    
    print(f"菜名:{name},价格:{price}元,销量:{sales}单")

# 输出:
# 菜名:蛋炒粉,价格:15元,销量:1000单
# 菜名:肉炒粉,价格:20元,销量:800单

关键.*? 是非贪婪匹配,摸到第一个就停,不会跨标签乱摸。


八、终极口诀(背下来)

圆括号是篮子,group(0)是整串。

group(1)拿第一篮,groups()全倒出来。

findall遇括号,返回元组列表。

(?P<名字>)贴标签,按名取货不迷路。

非贪婪加问号,跨标签不乱摸。


写在最后

下次再有人问你"正则怎么提取多个字段",你就说:

"加括号,挂篮子,整串用group(0),零件用group(1)(2)(3)。"

他要是问你"findall加分组返回什么",你就说:

"列表套元组,像流水线拆完快递直接装箱。"

散会。