从零到接单 07:正则表达式入门——文本处理的瑞士军刀

pythontutorialbeginner自动化爬虫接单正则表达式

系列目录:本文是「从零到接单:Python 自动化与爬虫实战」系列的第 7 篇。正则表达式是文本处理的终极利器——爬虫从 HTML 中提取信息、清洗脏数据、验证用户输入、解析日志……都离不开它。虽然语法看起来有点"天书",但掌握核心规则后,你会发现它无比强大。


一、正则表达式是什么?

正则表达式(Regular Expression,简称 regex)是一套模式匹配语言,用来在一段文本中查找、提取、替换符合特定规则的字符串。

举个例子:

  • 从一堆文字中找所有手机号 → 1[3-9]\d{9}
  • 从 HTML 中提取所有链接 → href="(.*?)"
  • 验证邮箱格式 → [\w.-]+@[\w.-]+\.\w+

看起来像乱码?一步步来。


二、Python re 模块基础

import re

Python 中正则表达式通过 re 模块使用,四种最常用的方法:

| 方法 | 用途 | 返回值 | |------|------|--------| | re.search() | 搜索第一个匹配 | Match 对象或 None | | re.findall() | 查找所有匹配 | 列表 | | re.sub() | 替换所有匹配 | 新字符串 | | re.match() | 从开头匹配 | Match 对象或 None |

import re

text = "我的手机号是 13812345678,备用号是 15987654321"

# search:找第一个
result = re.search(r"1[3-9]\d{9}", text)
if result:
    print(result.group())  # 13812345678

# findall:找所有
phones = re.findall(r"1[3-9]\d{9}", text)
print(phones)  # ['13812345678', '15987654321']

# sub:替换
masked = re.sub(r"\d{4}$", "****", "13812345678")
print(masked)  # 1381234****

# match:从开头匹配
print(re.match(r"你好", "你好,世界!"))  # <re.Match object>
print(re.match(r"世界", "你好,世界!"))  # None(不在开头)

💡 建议始终用 原始字符串 r"...",避免 \d 被 Python 当成转义字符。


三、正则核心语法

1. 基本字符匹配

# 普通字符直接匹配
re.findall(r"cat", "cat dog cat bird")  # ['cat', 'cat']

# 点号 . 匹配任意单个字符(除换行)
re.findall(r"c.t", "cat cot c@t cut")   # ['cat', 'cot', 'c@t', 'cut']

# 转义特殊字符
# 特殊字符:. ^ $ * + ? { } [ ] \ | ( )
re.findall(r"\$", "价格:$99.99")        # ['$']
re.findall(r"\d+", "价格:99.99")        # ['99', '99']

2. 字符类 [ ]

# [abc] 匹配 a 或 b 或 c
re.findall(r"[aeiou]", "hello world")   # ['e', 'o', 'o']

# [a-z] 匹配 a 到 z 的小写字母
re.findall(r"[a-z]", "Hello World")     # ['e', 'l', 'l', 'o', 'o', 'r', 'l', 'd']

# [0-9] 匹配数字
re.findall(r"[0-9]", "电话:010-12345678")  # ['0', '1', '0', '1', '2', ...]

# [^abc] 匹配不是 a、b、c 的字符(取反)
re.findall(r"[^aeiou]", "hello")        # ['h', 'l', 'l']

3. 预定义字符类(最常用)

| 符号 | 含义 | 等价于 | |------|------|--------| | \d | 数字 | [0-9] | | \D | 非数字 | [^0-9] | | \w | 字母/数字/下划线 | [a-zA-Z0-9_] | | \W | 非字母数字下划线 | [^a-zA-Z0-9_] | | \s | 空白字符(空格、制表、换行) | [ \t\n\r\f] | | \S | 非空白 | [^ \t\n\r\f] |

text = "价格:¥99.99,数量:5件"

# 提取所有数字
print(re.findall(r"\d+", text))      # ['99', '99', '5']

# 提取所有中文字符
print(re.findall(r"[\u4e00-\u9fff]+", text))  # ['价格', '数量', '件']

4. 量词:指定重复次数

| 符号 | 含义 | |------|------| | * | 0 次或多次 | | + | 1 次或多次 | | ? | 0 次或 1 次 | | {n} | 恰好 n 次 | | {n,} | 至少 n 次 | | {n,m} | n 到 m 次 |

# + 一次或多次
re.findall(r"\d+", "abc 123 4567")    # ['123', '4567']

# * 零次或多次
re.findall(r"go*", "go g goo g")      # ['go', 'g', 'goo', 'g']

# ? 零次或一次(可选)
re.findall(r"colou?r", "color colour") # ['color', 'colour']

# {n} 精确 n 次
re.findall(r"\d{3}", "12 345 6789")   # ['345', '678']

# {n,m} n到m次
re.findall(r"\d{2,4}", "1 12 123 1234 12345")  # ['12', '123', '1234', '1234']

5. 贪婪 vs 非贪婪(爬虫重中之重!)

html = "<div>第一个</div><div>第二个</div>"

# 贪婪匹配(默认):尽可能多
print(re.findall(r"<div>.*</div>", html))
# ['<div>第一个</div><div>第二个</div>']  ← 一次匹配了整个!

# 非贪婪匹配(加 ?):尽可能少
print(re.findall(r"<div>.*?</div>", html))
# ['<div>第一个</div>', '<div>第二个</div>']  ← 正确!

🔥 这是爬虫最重要的正则技巧! 在 HTML 中提取内容,几乎永远用 .*?(非贪婪)。

6. 锚点:匹配位置

| 符号 | 含义 | |------|------| | ^ | 字符串开头 | | $ | 字符串结尾 | | \b | 单词边界 |

# ^ 开头
print(re.findall(r"^\d+", "123abc456"))    # ['123']

# $ 结尾
print(re.findall(r"\d+$", "123abc456"))    # ['456']

# \b 单词边界
print(re.findall(r"\bcat\b", "cat category concat"))
# ['cat']  ← 只匹配独立的 cat

7. 分组与捕获

text = "姓名:张三,年龄:25,城市:北京"

# 用分组提取
result = re.search(r"姓名:(.+?),年龄:(\d+),城市:(.+)", text)
if result:
    print(result.group(0))  # 完整匹配:姓名:张三,年龄:25,城市:北京
    print(result.group(1))  # 第一个分组:张三
    print(result.group(2))  # 第二个分组:25
    print(result.group(3))  # 第三个分组:北京
    print(result.groups())  # 所有分组:('张三', '25', '北京')

# 命名分组(更清晰)
result = re.search(r"姓名:(?P<name>.+?),年龄:(?P<age>\d+),城市:(?P<city>.+)", text)
if result:
    print(result.group("name"))  # 张三
    print(result.group("age"))   # 25
    print(result.groupdict())    # {'name': '张三', 'age': '25', 'city': '北京'}

四、常用正则表达式速查

# 手机号
phone_pattern = r"1[3-9]\d{9}"

# 邮箱
email_pattern = r"[\w.-]+@[\w.-]+\.\w+"

# 网址(URL)
url_pattern = r"https?://[^\s]+"

# 身份证号(18位)
id_pattern = r"\d{17}[\dXx]"

# IP 地址
ip_pattern = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"

# 中文
chinese_pattern = r"[\u4e00-\u9fff]+"

# 日期 YYYY-MM-DD
date_pattern = r"\d{4}-\d{2}-\d{2}"

# HTML 标签中的内容
html_content_pattern = r">([^<]+)<"

# HTML 属性值
attr_pattern = r'(\w+)="([^"]*)"'

五、实战练习

练习 1:提取网页中所有链接

import re

html = '''
<html>
<body>
    <a href="https://example.com/page1">链接1</a>
    <a href="https://example.com/page2">链接2</a>
    <a href="/about">关于我们</a>
    <img src="/images/logo.png" alt="Logo">
</body>
</html>
'''

# 提取所有 a 标签的 href 和文本
links = re.findall(r'<a\s+href="([^"]*)"[^>]*>(.*?)</a>', html)
for href, text in links:
    print(f"链接:{href},文字:{text.strip()}")

练习 2:清洗文本中的特殊字符

import re

def clean_text(text):
    """清洗文本:去掉HTML标签、多余空白、特殊字符"""
    # 去掉 HTML 标签
    text = re.sub(r"<[^>]+>", "", text)
    # 去掉多余空白
    text = re.sub(r"\s+", " ", text)
    # 去掉首尾空格
    text = text.strip()
    return text

dirty = "<p>  这是一段  <b>脏</b>  数据   </p>"
print(clean_text(dirty))  # 这是一段 脏 数据

练习 3:解析日志文件

import re

log = """
[2026-07-12 10:15:30] [INFO] 爬虫启动,目标URL:https://example.com
[2026-07-12 10:15:32] [INFO] 正在抓取第1页...
[2026-07-12 10:15:35] [WARNING] 响应速度慢:2.5s
[2026-07-12 10:15:40] [ERROR] 第3页抓取失败:Connection timeout
[2026-07-12 10:15:45] [INFO] 爬虫结束,共抓取200条数据
"""

pattern = r"\[([\d\- :]+)\] \[(\w+)\] (.+)"
matches = re.findall(pattern, log)

# 统计各级别日志数量
from collections import Counter
levels = Counter(m[1] for m in matches)
print(levels)  # Counter({'INFO': 3, 'WARNING': 1, 'ERROR': 1})

# 找出所有 ERROR
for time, level, msg in matches:
    if level == "ERROR":
        print(f"[{time}] {msg}")

六、正则调试技巧

正则写错了怎么调?两种方法:

  1. 在线工具regex101.com,粘贴文本和正则,实时看匹配结果
  2. Python 内调试
import re

def debug_regex(pattern, text):
    """辅助调试正则"""
    matches = re.finditer(pattern, text)
    found = False
    for m in matches:
        found = True
        print(f"匹配: '{m.group()}' | 位置: {m.start()}-{m.end()}")
        for i, g in enumerate(m.groups(), 1):
            print(f"  分组{i}: '{g}'")
    if not found:
        print("没有找到匹配!")

debug_regex(r"\d{3,4}", "电话:010-12345678")

总结

| 模式 | 含义 | 示例 | |------|------|------| | . | 任意字符 | c.t → cat, cot | | \d | 数字 | \d+ → 123, 4567 | | \w | 字母数字下划线 | \w+ → hello, name_1 | | \s | 空白 | \s+ → 空格, 换行 | | * | 0次或多次 | ab* → a, ab, abb | | + | 1次或多次 | ab+ → ab, abb | | ? | 非贪婪 | .*? 爬虫必用 | | [] | 字符类 | [aeiou] → 元音 | | () | 分组 | (\d+) 捕获数字 | | ^$ | 开头/结尾 | ^Hello 以 Hello 开头 | | (?P<name>) | 命名分组 | (?P<year>\d{4}) |

正则表达式是爬虫的"眼睛"——帮你看清网页里有什么,并把有用的信息摘出来。下一篇,我们将正式进入爬虫世界,学习 HTTP 协议与 requests 库


练习

  1. 写一个函数 extract_emails(text),从一段文本中提取所有邮箱地址
  2. 写一个函数 extract_phone_numbers(text),提取所有中国大陆手机号
  3. 用这两个函数处理一段包含联系方式的模拟文本

Comments

Sign in to leave a comment.