Chapter 10 / 10

实战:文本解析与替换

用正则表达式解决真实工作中的文本处理问题——日志、CSV、HTML、性能优化

实战一:解析 Nginx 访问日志

import re
from collections import Counter

# Nginx 默认日志格式:
# 127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /index.html HTTP/1.0" 200 2326

LOG_PATTERN = re.compile(r"""
    (?P<ip>\S+)\s+          # IP 地址
    \S+\s+                  # ident(通常为 -)
    (?P<user>\S+)\s+        # 认证用户
    \[(?P<time>[^\]]+)\]\s+ # 时间
    "(?P<method>\w+)\s+     # HTTP 方法
    (?P<path>\S+)\s+        # 请求路径
    (?P<proto>\S+)"\s+      # 协议
    (?P<status>\d{3})\s+    # 状态码
    (?P<size>\d+|-)         # 响应大小
""", re.VERBOSE)

def parse_log(log_file):
    status_counter = Counter()
    with open(log_file) as f:
        for line in f:
            m = LOG_PATTERN.match(line)
            if m:
                status_counter[m.group('status')] += 1
    return status_counter

实战二:提取并清理 HTML

# 提取纯文本(移除 HTML 标签)
def strip_html(html):
    # 先移除 script 和 style 块
    text = re.sub(r'<(?:script|style)[^>]*>.*?</(?:script|style)>',
                  '', html, flags=re.DOTALL | re.IGNORECASE)
    # 再移除所有标签
    text = re.sub(r'<[^>]+>', '', text)
    # 合并多余空白
    text = re.sub(r'\s+', ' ', text).strip()
    return text

# 提取所有链接
def extract_links(html):
    return re.findall(r'href="([^"]*)"', html, re.IGNORECASE)

# 将相对链接转为绝对链接
def absolutize_links(html, base_url):
    return re.sub(
        r'href="(?!https?://)([^"]*)"',
        lambda m: f'href="{base_url}/{m.group(1).lstrip("/")}"',
        html
    )

实战三:处理 CSV 数据

# 解析 CSV(处理引号内含逗号的情况)
CSV_FIELD = re.compile(r'"(?:[^"]|"")*"|[^,]+')

def parse_csv_line(line):
    fields = CSV_FIELD.findall(line)
    result = []
    for f in fields:
        if f.startswith('"'):
            f = f[1:-1].replace('""', '"')  # 去引号,处理转义
        result.append(f.strip())
    return result

# 示例
parse_csv_line('name,"Doe, John",age,30')
# → ['name', 'Doe, John', 'age', '30']

实战四:代码中的批量重构

# 将 Python 2 print 语句改为 Python 3 函数
def fix_print(code):
    return re.sub(
        r'^(\s*)print\s+(.+)$',
        r'\1print(\2)',
        code,
        flags=re.MULTILINE
    )

# 将驼峰命名改为下划线命名
def camel_to_snake(name):
    s1 = re.sub(r'([A-Z]+)([A-Z][a-z])', r'\1_\2', name)
    return re.sub(r'([a-z\d])([A-Z])', r'\1_\2', s1).lower()

camel_to_snake('getUserName')    # → 'get_user_name'
camel_to_snake('parseHTTPRequest') # → 'parse_http_request'

实战五:模板渲染

# 简单模板引擎:替换 {{变量名}}
def render_template(template, variables):
    def replacer(m):
        key = m.group(1).strip()
        return str(variables.get(key, m.group(0)))
    return re.sub(r'\{\{\s*(\w+)\s*\}\}', replacer, template)

template = "Hello, {{name}}! You have {{count}} messages."
render_template(template, {'name': 'Alice', 'count': 5})
# → "Hello, Alice! You have 5 messages."

性能优化技巧

1. 预编译常用模式

# ❌ 每次调用都重新编译
for line in lines:
    re.search(r'\d+', line)

# ✅ 预编译,复用
DIGIT = re.compile(r'\d+')
for line in lines:
    DIGIT.search(line)

2. 优先用否定字符类代替懒惰量词

# 较慢(有回溯)
".*?"

# 更快(无回溯)
"[^"]*"

3. 避免嵌套量词

# 危险,可能造成 ReDoS
(\w+\s*)+ 

# 安全替代
\w+(?:\s+\w+)*

4. 善用 fullmatch 替代 ^ $

# 验证格式时,fullmatch 更清晰高效
re.fullmatch(r'\d{4}', s)          # 推荐
re.match(r'^\d{4}$', s)            # 等价但冗余

调试正则表达式

import re
re.compile(r'(\d+)-(\w+)', re.DEBUG)
# SUBPATTERN 1 0 0
#   MAX_REPEAT 1 MAXREPEAT
#     IN
#       RANGE (48, 57)  ← 数字 0-9
# LITERAL 45              ← -
# ...

课程总结

恭喜完成正则表达式完全指南!你现在掌握了: