前言用正则表达式从网页里提取内容是很多人接触爬虫的第一步拿到 HTML 源码用re.findall把链接、标题、价格抠出来。这条路学习成本低但也最容易被用歪——既容易写出脆弱的模式也容易忽视抓取的分寸。本文要先把两件事说清楚。第一正则不是解析 HTML 的正确工具。HTML 不是正则语言标签可能嵌套、属性顺序可变、还有注释和脚本正则只在「结构简单且稳定」的片段上可靠。第二抓取要合规遵守目标站点的robots.txt、如实表明自己的 User-Agent、控制请求频率、不绕过登录或付费限制、不做高并发把对方打挂。反爬机制的存在有它的原因本文只讲「怎么合规地抓」不讲任何绕过手段。下面用一个最小可运行的例子演示「先问 robots、再取页面、用正则提取链接、限速」的完整闭环。示例基于 Python 3.8。一、Python 3 的抓取工具链Python 2 时代流行urllib2这个模块在 Python 3 里已经不存在了它的功能被合并进urllib.request。Python 2.7 已于 2020 年 1 月 1 日停止维护旧教程里的urllib2.urlopen(url)写法一律不能再用本文全部采用 Python 3 的路径用途Python 3 模块说明发起请求urllib.requestRequest/urlopen解析相对链接urllib.parseurljoin(base, link)读取抓取规则urllib.robotparserRobotFileParser正则匹配re只用于结构稳定的片段先关心合规urllib.robotparser.RobotFileParser能读站点根目录的robots.txt用can_fetch(useragent, url)判断某个 URL 允不允许抓。注意它判断的是「给定 UA 下能不能访问」所以 UA 字符串要和实际请求头里的一致。二、完整示例合规地取一个页面的链接# 适用于 Python 3.8import reimport timeimport urllib.requestimport urllib.robotparserfrom urllib.parse import urljoinBASE https://example.com/UA MyCrawler/1.0 (https://example.com/about-bot)# 只匹配 href 属性里的取值单双引号都接受LINK_RE re.compile(rhref[]([^])[], re.IGNORECASE)def allowed(url):查 robots.txt判断当前 UA 是否可以抓取 url。rp urllib.robotparser.RobotFileParser()rp.set_url(urljoin(url, /robots.txt))try:rp.read()except OSError:# 读不到规则时保守处理不抓return Falsereturn rp.can_fetch(UA, url)def fetch(url):发起 GET 请求按响应声明的编码解码成文本。req urllib.request.Request(url, headers{User-Agent: UA})with urllib.request.urlopen(req, timeout10) as resp:charset resp.headers.get_content_charset() or utf-8return resp.read().decode(charset, errorsreplace)def extract_links(html, base):用正则找出所有 href并把相对地址补成绝对地址。hrefs LINK_RE.findall(html)return [urljoin(base, h) for h in hrefs]if __name__ __main__:url BASEif allowed(url):html fetch(url)for link in extract_links(html, url):print(link)time.sleep(1) # 每次请求之间歇一秒控制频率else:print(robots.txt 不允许抓取, url)模式rhref[]([^])[]用三重单引号包住是为了让里面的和都不需要转义。[^]是「一个及以上非引号字符」正好卡住属性值re.IGNORECASE让HREF也能匹配。urljoin(base, href)会处理三种情况绝对地址原样返回/path补成站点根下的绝对地址page.html补成同级绝对地址。少了这一步正则抠出来的往往是一堆相对路径后续没法直接用。三、正则 vs 结构化解析只做「捞链接、抓数字」这类扁平任务正则是够用的一旦结构变复杂就该换工具场景正则rehtml.parser.HTMLParser提取固定属性的值合适模式短也可以但要写子类回调嵌套结构、多层级容易失手按标签层次处理稳容错性缺闭合标签差HTMLParser 也不完美但更可控学习成本低中结论正则用来处理局部、扁平的片段整页结构交给html.parser或专门库。把整页 HTML 塞进一个正则里做「万能匹配」是最常见的失败来源。四、看一眼 robots.txt 长什么样robots.txt是放在站点根目录的一行行规则告诉爬虫「哪些路径可以抓、抓的间隔多少」User-agent: *Disallow: /admin/Disallow: /tmp/Crawl-delay: 5User-agent: MyCrawlerDisallow:大意是对大多数爬虫/admin/和/tmp/不许进每次请求之间至少隔 5 秒。而针对MyCrawler的单条规则写了空的Disallow:表示对这一个 UA 放行全部。RobotFileParser读进来后can_fetch(MyCrawler/1.0, https://example.com/admin/x)会给出是否放行的判断——它匹配的是 UA 的前缀所以传给can_fetch的字符串要与请求头里保持一致。需要留意的是Crawl-delay不是所有解析器都会自动执行实践中仍应自己在代码里time.sleep。五、真的需要解析结构时换用 HTMLParser官方标准库自带html.parser.HTMLParser用回调的方式按标签取数据比正则稳# 适用于 Python 3.8from html.parser import HTMLParserclass LinkGrabber(HTMLParser):def __init__(self):super().__init__()self.links []def handle_starttag(self, tag, attrs):if tag ! a:returnfor name, value in attrs:if name href and value:self.links.append(value)page a href/aA/aa classx href/bB/ag LinkGrabber()g.feed(page)print(g.links) # [/a, /b]HTMLParser会把标签名转成小写、把属性解析成(名字, 值)的列表正好绕开了「属性顺序可变、单双引号混用」这些让正则头疼的问题。代价是要写子类、处理回调。什么时候切换当你的正则开始出现「一层套不进去就加一个.*?」的时候就该换了。常见坑点❌ 把.*用在跨行的 HTML 上结果只匹配到第一行还以为页面没内容。✅ 默认.不匹配换行跨行提取要么加re.DOTALL要么用[\s\S]*?这类显式包含换行的写法。❌ 用re.findall(rhref(.*), html)这类贪心模式一次把从第一个href到行尾最后引号的内容全吞了。✅ 用[^]代替.*把「不能是引号」写进字符集天然截断在属性值末尾。❌ 请求时不写 User-Agent用库的默认值被对方按「未知爬虫」拦掉。✅ 如实填写能标识自己的 UA通常带上用途或联系方式不要伪装成浏览器去骗过校验。❌ 无视robots.txt直接按链接列表批量抓取。✅ 先用RobotFileParser.can_fetch(UA, url)判断只抓允许的部分读不到规则时按不放行处理。❌ 在循环里不 sleep 地连续请求短时间把对方压出高负载。✅ 请求之间加time.sleep串行访问控制并发量给对方留出余量。❌ 直接对resp.read()返回的 bytes 用str模式匹配报TypeError: cannot use a string pattern on a bytes-like object。✅ 先decode成str编码优先取响应头声明的get_content_charset()拿不到再退回utf-8。❌ 抠出href后直接当 URL 用跳转时才发现是/about这样的相对路径。✅ 用urllib.parse.urljoin(base, href)归一成绝对地址再处理。❌ 用正则解析嵌套的div结构页面一改样式就整段失配。✅ 结构复杂的部分改用html.parser.HTMLParser或专门的解析库正则只负责最外层、最稳定的模式。总结环节做法关键点合规检查RobotFileParser.can_fetch读不到规则就不抓发起请求urllib.request 自定义 UA如实标识别伪装解码decode(charset, errorsreplace)正则要在str上跑提取re.findall[^]用字符集截断别用.*归一urljoin相对路径转绝对限速time.sleep串行、低频、不压垮对方用正则写爬虫本质是在做「文本提取」而不是「DOM 解析」所以要挑结构简单、格式稳定的部分下手同时对抓取行为负责。合规、低频、明确身份这三条比任何技巧都重要。