最稳妥做法是遍历所有标签并检查href属性是否存在,用.get_text(strip=True)提取文本;href需转绝对URL并过滤无效链接;lxml更快但需注意编码和HTML修复。

用 BeautifulSoup.find_all('a') 提取链接和文本最稳妥
直接遍历所有 <a> 标签是通用性最强的做法,BeautifulSoup 能处理不规范 HTML(比如缺失闭合标签、嵌套错乱),比正则或 lxml 的严格解析更容错。注意必须检查 href 属性是否存在,否则会触发 KeyError;同时用 .get_text(strip=True) 取文本,避免换行符和多余空格干扰。
常见错误是写成 link.text 后遇到 <a href="..."><img></a> 这类无文本的标签,结果得到空字符串甚至 None——这时候 .get_text() 仍能返回空字符串,而 .text 在某些版本中可能抛异常。
-
href值可能是相对路径(如/about、./contact.html),需用urllib.parse.urljoin(base_url, href)转为绝对 URL - 跳过
href为空、以javascript:、mailto:、#开头的链接(除非业务明确需要) - 若页面含大量广告或导航栏重复链接,可加简单去重:用
set()存urlparse(href).netloc + urlparse(href).path
用 lxml.etree.HTML 解析更快但要注意命名空间和编码
当页面体积大(>1MB)、并发量高时,lxml 比 BeautifulSoup 快 3–5 倍,尤其适合批量抓取。但它默认不修复 HTML 结构,遇到 <meta charset="gb2312"> 这类声明时,若未显式指定编码,.xpath('//a') 可能解析失败或乱码。
典型报错是 UnicodeDecodeError: 'utf-8' codec can't decode byte 0xd6 in position 123,根源常是响应头没传 Content-Type 或网页用 GBK 编码但你用 UTF-8 解析。
立即学习“Python免费学习笔记(深入)”;
- 务必先用
response.content(而非response.text)构造etree.HTML(),再手动指定编码:etree.HTML(html_bytes, parser=etree.HTMLParser(encoding='gbk')) - XPath 表达式写
//a[@href]比//a更安全,排除无href的伪链接 - 取文本推荐
elem.xpath('string(.)'),它等价于“展开所有子节点后拼接文本”,比elem.text_content()更接近浏览器渲染逻辑
过滤无效链接和提取锚文本要分两步做
很多人试图在一次循环里既清洗 URL 又提取干净文本,结果逻辑缠绕、边界 case 漏判。实际应拆成:先收集原始 <a> 元素,再逐个处理其 href 和文本内容。这样便于调试,也方便后续加规则(比如只保留站内链接、过滤特定关键词)。
容易被忽略的是“关联文本”的定义模糊性。例如 <a href="x">点击<span class="tip">了解更多</span></a>,有人要纯“点击”,有人要“点击了解更多”。没有统一答案,得按需求定策略。
- 若只要最外层直接文本:用
elem.text.strip() if elem.text else '' - 若要全部可见文本(含子标签内):用
elem.get_text(strip=True)(BS4)或elem.xpath('string(.)').strip()(lxml) - 过滤掉长度 » | «)的文本,这类大概率是装饰性符号而非真实锚文本
避免因 JavaScript 渲染导致链接漏采
纯静态解析拿不到由 document.write()、Vue.mount() 或 fetch() 动态插入的 <a> 标签。如果你发现目标链接在浏览器里可见,但用 requests + BeautifulSoup 却抓不到,基本就是这个原因。
这时候不能硬上 Selenium——90% 的场景,只需检查 Network 面板,找到加载链接数据的真实 API 接口(比如 /api/nav?site=xxx),直接请求该接口更轻量、更稳定。
- 先禁用 JS 刷新页面,确认链接是否仍在源码中;不在,就说明是动态渲染
- 用浏览器开发者工具的 “Network → Filter → XHR/Fetch” 找带
link、url、menu字样的请求 - 若必须用无头浏览器,优先选
Playwright(比 Selenium 启动快、API 更简洁),且只在必要时调用page.content(),别全程挂载
lxml 会静默失败,连报错都没有——这时候得靠 chardet.detect() 先猜编码,再喂给解析器。



















