Day40 - 正则表达式高级与综合项目
详细讲解
1. 分组高级用法
1.1 捕获分组与非捕获分组
import re
# 捕获分组 - 会保存到 groups() 中
text = "2024-05-01"
match = re.search(r'(\d{4})-(\d{2})-(\d{2})', text)
print(match.groups()) # ('2024', '05', '01')
# 非捕获分组 (?:...) - 不保存到 groups()
match = re.search(r'(\d{4})-(?:\d{2})-(\d{2})', text)
print(match.groups()) # ('2024', '01')
# 使用场景:提高效率(不需要的分组不用保存)
# 当分组多时,非捕获分组性能更好
1.2 命名分组
# (?P<name>pattern) - 命名分组
text = "John Doe <john@example.com>"
match = re.search(r'(?P<name>\w+) (?P<surname>\w+) <(?P<email>\w+@\w+\.\w+)>', text)
if match:
print(match.groupdict())
# {'name': 'John', 'surname': 'Doe', 'email': 'john@example.com'}
# 按名称访问
print(match.group('name')) # 'John'
print(match.group('email')) # 'john@example.com'
# 命名分组在替换中也很方便
text = "2024-05-01"
result = re.sub(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})',
r'\g<month>/\g<day>/\g<year>', text)
print(result) # 05/01/2024
1.3 反向引用
# \1, \2 等 - 引用前面的分组
# 匹配重复的单词
text = "the the cat cat dog"
pattern = r'(\w+) \1' # \1 引用第一个分组
print(re.findall(pattern, text)) # ['the', 'cat']
# 匹配 HTML 标签
text = "<div>content</div> <span>text</span>"
pattern = r'<(\w+)>.*?</\1>' # \1 引用前面的标签名
print(re.findall(pattern, text)) # ['div', 'span']
# 匹配引号
pattern = r'(["\'])(.*?)\1' # \1 引用开头的引号
print(re.findall(pattern, "'hello' and \"world\"")) # ["'", 'hello', '"', 'world']
1.4 条件匹配
# (?(id)yes_pattern|no_pattern) - 如果 id 存在则匹配 yes_pattern,否则匹配 no_pattern
# 匹配带引号或不带引号的单词
text = '"hello" world "python"'
# 如果有引号,则匹配引号内的内容;否则匹配单词
pattern = r'(["\'])?(?(1)(.*?)\1|(\w+))'
matches = re.findall(pattern, text)
print(matches) # [('"', 'hello', ''), ('', '', 'world'), ('"', 'python', '')]
2. re.sub() 高级用法
2.1 基本替换
import re
# 基本替换
text = "Hello World"
result = re.sub(r'World', 'Python', text)
print(result) # Hello Python
# 使用分组引用
text = "2024-05-01"
result = re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2/\3/\1', text)
print(result) # 05/01/2024
# 命名分组引用
result = re.sub(r'(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})',
r'\g<d>-\g<m>-\g<y>', text)
print(result) # 01-05-2024
2.2 替换函数
# 传入函数作为替换参数
def expand_date(match):
"""日期转换函数"""
year, month, day = match.groups()
months = {
'01': 'January', '02': 'February', '03': 'March',
'04': 'April', '05': 'May', '06': 'June',
'07': 'July', '08': 'August', '09': 'September',
'10': 'October', '11': 'November', '12': 'December'
}
return f"{months[month]} {day}, {year}"
text = "Date: 2024-05-01"
result = re.sub(r'(\d{4})-(\d{2})-(\d{2})', expand_date, text)
print(result) # Date: May 01, 2024
# 数字格式化
def format_number(match):
num = int(match.group())
return f"{num:,}" # 添加千分位
text = "金额: 1234567"
result = re.sub(r'\d+', format_number, text)
print(result) # 金额: 1,234,567
2.3 re.subn() - 返回替换次数
# subn() 返回 (新字符串, 替换次数)
text = "aaa bbb aaa ccc aaa"
result, count = re.subn(r'aaa', 'XXX', text)
print(f"替换后: {result}, 替换次数: {count}")
# 替换后: XXX bbb XXX ccc XXX, 替换次数: 3
3. re.split() 高级用法
import re
# 基本分割
text = "apple,banana,cherry"
parts = re.split(r',', text)
print(parts) # ['apple', 'banana', 'cherry']
# 使用分组分割 - 分组会保留在结果中
text = "apple1banana2cherry"
parts = re.split(r'(\d)', text)
print(parts) # ['apple', '1', 'banana', '2', 'cherry']
# 限制分割次数
text = "a:b:c:d:e"
parts = re.split(r':', text, maxsplit=2)
print(parts) # ['a', 'b', 'c:d:e']
# 按多种分隔符分割
text = "apple,bana;na|cherry"
parts = re.split(r'[,;|]', text)
print(parts) # ['apple', 'bana', 'na', 'cherry']
4. 零宽断言(Lookaround)
4.1 正向先行断言 (?=...)
# 匹配后面跟着特定内容的前缀
text = "Windows10 Windows11 Windows12"
# 匹配前面是 Windows,后面是数字的 Windows
result = re.findall(r'Windows(?=\d)', text)
print(result) # ['Windows', 'Windows', 'Windows']
# 更实用的例子:获取价格(数字前的货币符号)
text = "价格: $100, €50, ¥200"
result = re.findall(r'[$€¥]\d+', text)
print(result) # ['$100', '€50', '¥200']
# 匹配后面是 .com 的域名部分
text = "example.com test.org hello.com"
result = re.findall(r'\w+(?=\.com)', text)
print(result) # ['example', 'hello']
4.2 负向先行断言 (?!...)
# 匹配后面不跟着特定内容的前缀
text = "Windows10 WindowsMac Windows11"
# 匹配后面不是数字的 Windows
result = re.findall(r'Windows(?!\d)', text)
print(result) # ['WindowsMac']
# 排除特定结尾
text = ["file.txt", "file.log", "file.txt.bak", "data.csv"]
txt_files = [f for f in text if re.match(r'.*\.txt(?!\.)', f)]
print(txt_files) # ['file.txt']
4.3 正向后行断言 (?<=...)
# 匹配前面跟着特定内容的后缀
text = "价格: 100元, 200元, 300美元"
# 匹配"元"前面的数字
result = re.findall(r'(?<=[元])\d+', text)
print(result) # ['100', '200']
# 提取 HTML 中的文本内容
html = "<div>Hello</div><span>World</span>"
result = re.findall(r'(?<=<[a-z]+>)[^<]+', html)
print(result) # ['Hello', 'World']
# 提取域名后的路径
text = "https://example.com/path1 http://test.org/path2"
result = re.findall(r'(?<=//[^/]+/).+', text)
print(result) # ['/path1', '/path2']
4.4 负向后行断言 (?<!...)
# 匹配前面不跟着特定内容的后缀
text = "100元 200美元 300元 400日元"
# 匹配前面不是"元"的数字
result = re.findall(r'(?<![元])\d+', text)
print(result) # ['200', '400']
# 更实用的例子:提取不是来自某个前缀的内容
text = "abc123 abc456 xyz789"
result = re.findall(r'(?<!abc)\d+', text)
print(result) # ['3456', '789'] - abc123 中的 1 被排除
5. 综合项目实践
5.1 日志分析器
import re
from collections import Counter
from datetime import datetime
class LogAnalyzer:
"""日志分析器"""
# 日志格式正则(示例:Apache 访问日志)
LOG_PATTERN = re.compile(
r'(?P<ip>[\d.]+) ' # IP 地址
r'- - '
r'\[(?P<timestamp>[^\]]+)\] ' # 时间戳
r'"(?P<method>\w+) (?P<path>[^\s]+) [^"]+" ' # 请求方法和路径
r'(?P<status>\d{3}) ' # 状态码
r'(?P<size>\d+)'
)
# 错误日志正则
ERROR_PATTERN = re.compile(
r'\[(?P<timestamp>[^\]]+)\] '
r'\[(?P<level>\w+)\] '
r'(?P<message>.+)'
)
@classmethod
def parse_access_log(cls, log_text):
"""解析访问日志"""
results = []
for line in log_text.strip().split('\n'):
match = cls.LOG_PATTERN.search(line)
if match:
results.append(match.groupdict())
return results
@classmethod
def analyze_traffic(cls, log_text):
"""流量分析"""
entries = cls.parse_access_log(log_text)
# 统计各状态码数量
status_counts = Counter(e['status'] for e in entries)
# 统计各路径访问量
path_counts = Counter(e['path'] for e in entries)
# 统计各 IP 访问量
ip_counts = Counter(e['ip'] for e in entries)
# 计算总流量
total_bytes = sum(int(e['size']) for e in entries)
return {
'total_requests': len(entries),
'status_codes': dict(status_counts),
'top_paths': path_counts.most_common(10),
'top_ips': ip_counts.most_common(10),
'total_bytes': total_bytes
}
@classmethod
def extract_errors(cls, log_text):
"""提取错误信息"""
errors = []
for line in log_text.strip().split('\n'):
match = cls.ERROR_PATTERN.search(line)
if match:
errors.append(match.groupdict())
return errors
# 使用示例
sample_log = '''
127.0.0.1 - - [01/May/2024:10:00:00 +0000] "GET /index.html HTTP/1.1" 200 1234
127.0.0.1 - - [01/May/2024:10:00:01 +0000] "GET /style.css HTTP/1.1" 200 5678
127.0.0.2 - - [01/May/2024:10:00:02 +0000] "GET /api/users HTTP/1.1" 404 0
192.168.1.1 - - [01/May/2024:10:00:03 +0000] "POST /login HTTP/1.1" 200 89
192.168.1.1 - - [01/May/2024:10:00:04 +0000] "POST /login HTTP/1.1" 401 23
'''
analyzer = LogAnalyzer()
result = analyzer.analyze_traffic(sample_log)
print(f"总请求数: {result['total_requests']}")
print(f"状态码统计: {result['status_codes']}")
print(f"Top 路径: {result['top_paths']}")
print(f"Top IPs: {result['top_ips']}")
5.2 文本数据清洗工具
import re
class TextCleaner:
"""文本数据清洗工具"""
@staticmethod
def clean_html(text):
"""移除 HTML 标签"""
# 移除 script 和 style 内容
text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL | re.IGNORECASE)
text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL | re.IGNORECASE)
# 移除所有 HTML 标签
text = re.sub(r'<[^>]+>', '', text)
# 解码 HTML 实体
text = TextCleaner._decode_html_entities(text)
return text.strip()
@staticmethod
def _decode_html_entities(text):
"""解码 HTML 实体"""
entities = {
' ': ' ', '&': '&', '<': '<', '>': '>',
'"': '"', ''': "'", ''': "'"
}
for entity, char in entities.items():
text = text.replace(entity, char)
# 处理数字形式的实体
text = re.sub(r'&#(\d+);', lambda m: chr(int(m.group(1))), text)
return text
@staticmethod
def normalize_whitespace(text):
"""规范化空白字符"""
# 将所有连续空白替换为单个空格
text = re.sub(r'\s+', ' ', text)
# 移除首尾空白
return text.strip()
@staticmethod
def remove_special_chars(text, keep_pattern=''):
"""移除特殊字符"""
if keep_pattern:
pattern = f'[^a-zA-Z0-9{re.escape(keep_pattern)}]'
else:
pattern = r'[^a-zA-Z0-9\u4e00-\u9fff]' # 保留中文
return re.sub(pattern, '', text)
@staticmethod
def extract_phones(text):
"""提取电话号码"""
patterns = [
r'1[3-9]\d{9}', # 手机号
r'\d{3,4}-\d{7,8}', # 固话
r'\+\d{1,3}-\d{3,4}-\d{7,8}' # 国际电话
]
phones = []
for pattern in patterns:
phones.extend(re.findall(pattern, text))
return list(set(phones)) # 去重
@staticmethod
def extract_emails(text):
"""提取邮箱地址"""
pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'
return re.findall(pattern, text)
@staticmethod
def mask_sensitive(text, mask_type='phone'):
"""脱敏处理"""
if mask_type == 'phone':
return re.sub(r'(1[3-9]\d{9})',
lambda m: m.group(1)[:3] + '****' + m.group(1)[7:], text)
elif mask_type == 'email':
return re.sub(r'([a-zA-Z0-9]{2})[a-zA-Z0-9]*@',
r'\1***@', text)
return text
# 使用示例
html_text = '''
<div class="content">
<script>alert("bad");</script>
<p>联系我们: 010-12345678 或 13812345678</p>
<p>邮箱: contact@example.com</p>
<p>地址: 北京市海淀区中关村大街1号</p>
</div>
'''
cleaner = TextCleaner()
text = cleaner.clean_html(html_text)
print(f"清洗后: {text}")
print(f"提取电话: {cleaner.extract_phones(text)}")
print(f"提取邮箱: {cleaner.extract_emails(text)}")
print(f"脱敏电话: {cleaner.mask_sensitive(text, 'phone')}")
5.3 URL 解析器
import re
from urllib.parse import urlparse, parse_qs
class URLParser:
"""URL 解析器"""
# 更完整的 URL 正则
URL_PATTERN = re.compile(
r'^(?P<scheme>https?)://' # 协议
r'(?P<domain>[^/:]+)' # 域名
r'(?::(?P<port>\d+))?' # 端口(可选)
r'(?P<path>/[^?#]*)?' # 路径
r'(?:\?(?P<query>[^#]+))?' # 查询字符串
r'(?:#(?P<fragment>.+))?' # 锚点
)
@classmethod
def parse(cls, url):
"""解析 URL"""
# 尝试用正则解析
match = cls.URL_PATTERN.match(url)
if match:
return match.groupdict()
# 回退到 urllib
parsed = urlparse(url)
return {
'scheme': parsed.scheme,
'domain': parsed.netloc,
'port': parsed.port,
'path': parsed.path,
'query': parsed.query,
'fragment': parsed.fragment
}
@classmethod
def extract_params(cls, url):
"""提取 URL 参数"""
parsed = urlparse(url)
params = parse_qs(parsed.query)
# 转换为简单字典(值列表转单值)
return {k: v[0] if len(v) == 1 else v for k, v in params.items()}
@classmethod
def build_url(cls, scheme='https', domain='', path='/', params=None, fragment=''):
"""构建 URL"""
url = f"{scheme}://{domain}{path}"
if params:
query = '&'.join(f"{k}={v}" for k, v in params.items())
url += f"?{query}"
if fragment:
url += f"#{fragment}"
return url
@classmethod
def extract_domain_info(cls, domain):
"""提取域名信息"""
# 提取子域名
parts = domain.split('.')
if len(parts) >= 3:
subdomain = parts[0]
main_domain = '.'.join(parts[1:])
else:
subdomain = None
main_domain = domain
# 提取顶级域
tld = parts[-1] if parts else ''
return {
'original': domain,
'subdomain': subdomain,
'domain': main_domain,
'tld': tld
}
# 使用示例
urls = [
'https://example.com:8080/path/to/page?id=123&name=test#section',
'http://sub.domain.co.uk/api/v1/users?page=1',
'https://www.test.org'
]
parser = URLParser()
for url in urls:
print(f"\n解析: {url}")
info = parser.parse(url)
print(f" 域名: {info['domain']}")
print(f" 路径: {info.get('path', '/')}")
print(f" 参数: {parser.extract_params(url)}")
if info.get('domain'):
domain_info = parser.extract_domain_info(info['domain'])
print(f" 域名信息: {domain_info}")
5.4 数据验证器
import re
class Validator:
"""常用数据验证器"""
@staticmethod
def is_valid_email(email):
"""验证邮箱"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
@staticmethod
def is_valid_phone(phone, region='CN'):
"""验证手机号"""
if region == 'CN':
# 中国手机号(11位,以1开头,第2位3-9)
pattern = r'^1[3-9]\d{9}$'
elif region == 'US':
# 美国电话(10位)
pattern = r'^\d{10}$'
else:
pattern = r'^\+?\d{6,15}$'
return re.match(pattern, phone) is not None
@staticmethod
def is_valid_url(url):
"""验证 URL"""
pattern = r'^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$'
return re.match(pattern, url) is not None
@staticmethod
def is_valid_ip(ip):
"""验证 IPv4 地址"""
pattern = r'^(\d{1,3}\.){3}\d{1,3}$'
if not re.match(pattern, ip):
return False
# 检查每段是否在 0-255
return all(0 <= int(octet) <= 255 for octet in ip.split('.'))
@staticmethod
def is_valid_date(date_str, format='%Y-%m-%d'):
"""验证日期格式"""
from datetime import datetime
try:
datetime.strptime(date_str, format)
return True
except ValueError:
return False
@staticmethod
def is_strong_password(password):
"""验证密码强度"""
if len(password) < 8:
return False, "密码长度至少8位"
has_upper = bool(re.search(r'[A-Z]', password))
has_lower = bool(re.search(r'[a-z]', password))
has_digit = bool(re.search(r'\d', password))
has_special = bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password))
if not has_upper:
return False, "需要包含大写字母"
if not has_lower:
return False, "需要包含小写字母"
if not has_digit:
return False, "需要包含数字"
if not has_special:
return False, "需要包含特殊字符"
return True, "密码强度合格"
# 使用示例
validator = Validator()
test_cases = [
('email', 'test@example.com'),
('phone', '13812345678'),
('url', 'https://www.example.com'),
('ip', '192.168.1.1'),
('date', '2024-05-01'),
]
for type_name, value in test_cases:
method = getattr(validator, f'is_valid_{type_name}')
result = method(value)
status = "✓" if result else "✗"
print(f"{status} {type_name}: {value}")
# 密码强度验证
password = "Test@1234"
valid, msg = validator.is_strong_password(password)
print(f"\n密码 '{password}': {msg}")
背诵版
高级正则速查
┌─────────────────────────────────────────────────────────────┐ │ 零宽断言(Lookaround) │ ├─────────────────────────────────────────────────────────────┤ │ (?=...) 正向先行 │ 后面跟着... │ │ (?!...) 负向先行 │ 后面不跟... │ │ (?<=...) 正向后行 │ 前面是... │ │ (?<!...) 负向后行 │ 前面不是... │ ├─────────────────────────────────────────────────────────────┤ │ (?P<name>...) 命名分组 │ │ (?:...) 非捕获分组 │ │ \1, \2 反向引用 │ └─────────────────────────────────────────────────────────────┘re.sub 替换函数
# 基本替换
re.sub(pattern, replacement, string)
# 替换函数
def replace_func(match):
return ... # 返回替换文本
re.sub(pattern, replace_func, string)
# 返回替换次数
re.subn(pattern, replacement, string) # 返回 (结果, 次数)
考前记忆
面试重点
-
零宽断言的区别
- 先行:看后面(
(?=)正向,(?!)负向) - 后行:看前面(
(?<=)正向,(?<!)负向) - 不消耗字符,只做条件判断
- 先行:看后面(
-
分组类型
- 捕获分组
(...):保存到 groups - 非捕获分组
(?:...):不保存 - 命名分组
(?P<name>...):命名访问
- 捕获分组
-
反向引用
\1- 引用前面分组的内容
- 用于匹配重复结构
-
sub 替换函数
- 接受 match 对象参数
- 返回替换字符串
-
lookaround 用途
- 精确提取(前后有特定上下文的内容)
- 条件匹配
记忆口诀
零宽断言不占位, 先行看后后看前。 分组捕获要记牢, 反向引用\1到。 sub函数替换强, 日志分析它最棒。测试题
选择题
1. (?=...) 是什么类型的断言?
# A. 正向后行断言
# B. 负向后行断言
# C. 正向先行断言
# D. 负向先行断言
答案:C
2. re.sub() 的第二个参数可以是什么类型?
# A. 只能是字符串
# B. 只能是函数
# C. 字符串或函数
# D. 只能是列表
答案:C
3. 命名分组的语法是什么?
# A. (name:...)
# B. <name:...>
# C. (?P<name>...)
# D. (:name...)
答案:C
4. (?<=...) 是什么类型的断言?
# A. 正向先行
# B. 负向先行
# C. 正向后行
# D. 负向后行
答案:C
5. 反向引用 \1 的作用是什么?
# A. 引用第一个分组
# B. 引用整个匹配
# C. 引用字符串开头
# D. 转义数字
答案:A
编程题
1. 实现一个模板引擎:
import re
class TemplateEngine:
"""简单的模板引擎"""
def __init__(self, template):
self.template = template
def render(self, context):
"""渲染模板"""
result = self.template
# 替换 {{ variable }}
result = re.sub(r'\{\{(\w+)\}\}',
lambda m: str(context.get(m.group(1), '')),
result)
# 替换 {{ var|filter }}
def apply_filter(m):
var, filter_name = m.group(1), m.group(2)
value = context.get(var, '')
return self._apply_filter(value, filter_name)
result = re.sub(r'\{\{(\w+)\|(\w+)\}\}', apply_filter, result)
# 替换 {% if condition %}...{% endif %}
result = self._process_conditionals(result, context)
return result
@staticmethod
def _apply_filter(value, filter_name):
"""应用过滤器"""
filters = {
'upper': lambda v: v.upper(),
'lower': lambda v: v.lower(),
'capitalize': lambda v: v.capitalize(),
'trim': lambda v: v.strip(),
'length': lambda v: len(str(v))
}
return filters.get(filter_name, lambda v: v)(value)
def _process_conditionals(self, template, context):
"""处理条件语句"""
# 匹配 {% if var %}...{% endif %}
pattern = r'\{%\s*if\s+(\w+)\s*%\}(.*?)\{%\s*endif\s*%\}'
def replace_if(match):
var = match.group(1)
content = match.group(2)
if context.get(var):
return content
return ''
return re.sub(pattern, replace_if, template, flags=re.DOTALL)
# 使用
template = """
<h1>{{ title|upper }}</h1>
<p>欢迎, {{ name|capitalize }}</p>
{% if show_footer %}
<footer>版权信息</footer>
{% endif %}
"""
engine = TemplateEngine(template)
context = {
'title': 'Welcome',
'name': 'john doe',
'show_footer': True
}
print(engine.render(context))
2. 实现一个爬虫数据提取器:
import re
class DataExtractor:
"""网页数据提取器"""
@staticmethod
def extract_articles(html):
"""提取文章列表"""
# 提取文章标题和链接
pattern = r'<article[^>]*>.*?<a[^>]*href=["\']([^"\']+)["\'][^>]*>([^<]+)</a>.*?</article>'
articles = []
for match in re.finditer(pattern, html, re.DOTALL):
articles.append({
'url': match.group(1),
'title': match.group(2).strip()
})
return articles
@staticmethod
def extract_images(html):
"""提取图片"""
pattern = r'<img[^>]+src=["\']([^"\']+)["\'][^>]*(?:alt=["\']([^"\']*)["\'])?[^>]*>'
images = []
for match in re.finditer(pattern, html):
images.append({
'src': match.group(1),
'alt': match.group(2) if match.lastindex >= 2 else ''
})
return images
@staticmethod
def extract_metadata(html):
"""提取元数据"""
meta = {}
# title
title_match = re.search(r'<title>([^<]+)</title>', html)
if title_match:
meta['title'] = title_match.group(1)
# meta 标签
meta_pattern = r'<meta[^>]+(?:name|property)=["\']([^"\']+)["\'][^>]+content=["\']([^"\']+)["\']'
for match in re.finditer(meta_pattern, html, re.IGNORECASE):
meta[match.group(1)] = match.group(2)
# keywords
kw_match = re.search(r'<meta[^>]+name=["\']keywords["\'][^>]+content=["\']([^"\']+)["\']',
html, re.IGNORECASE)
if kw_match:
meta['keywords'] = [k.strip() for k in kw_match.group(1).split(',')]
return meta
# 使用
sample_html = '''
<!DOCTYPE html>
<html>
<head>
<title>示例页面</title>
<meta name="description" content="这是一个示例页面">
<meta name="keywords" content="Python, 正则, 爬虫">
</head>
<body>
<article>
<a href="/article/1">第一篇文章</a>
</article>
<article>
<a href="/article/2">第二篇文章</a>
</article>
<img src="/images/1.jpg" alt="图片1">
<img src="/images/2.jpg" alt="图片2">
</body>
</html>
'''
extractor = DataExtractor()
print("文章:", extractor.extract_articles(sample_html))
print("图片:", extractor.extract_images(sample_html))
print("元数据:", extractor.extract_metadata(sample_html))
问答题
Q1: 什么是零宽断言?请举例说明正向和负向的区别。
零宽断言是一种不消耗字符的断言,只用于判断位置前后的条件是否满足。
# 正向先行 (?=...) - 后面必须是...
text = "100元 200美元 300日元"
re.findall(r'\d+(?=元)', text) # ['100'] - 只匹配后面是"元"的数字
# 负向先行 (?!...) - 后面必须不是...
re.findall(r'\d+(?!元)', text) # ['200', '300'] - 后面不是"元"的数字
Q2: 捕获分组和非捕获分组有什么区别?
| 类型 | 语法 | 保存到 groups | 性能 |
|---|---|---|---|
| 捕获分组 | (...) |
是 | 稍慢 |
| 非捕获分组 | (?:...) |
否 | 稍快 |
选择依据:
- 需要使用分组结果 → 捕获分组
- 不需要分组结果 → 非捕获分组(性能更好)
Q3: re.sub() 的替换函数参数是什么?有什么用?
替换函数接收一个 match 对象参数,返回替换字符串。
def replace_func(match):
# match.group(0) - 整个匹配
# match.group(1) - 第一个分组
# match.groupdict() - 命名分组
return replacement
re.sub(pattern, replace_func, text)
常用于:
- 需要根据匹配内容动态计算替换值
- 复杂的数据转换
- 格式化输出