上回给你讲了 re 模块的摸金手套——match、search、findall 那些。但你有没有发现一个问题:
你摸出来的东西,是一整坨。
比如摸到 "蛋炒粉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):整串包裹
当你用 search 或 match 摸到东西后,返回的对象是个"包裹"。
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加分组返回什么",你就说:
"列表套元组,像流水线拆完快递直接装箱。"
散会。