链接提取

简而言之:链接提取是爬虫从已抓取页面中提取 URL,以便将其加入队列并访问的步骤。爬虫在种子 URL 之外发现的所有内容都源于此步骤。

链接提取可以将单个页面的抓取转化为完整的爬取过程。获取页面后,爬虫工具会解析页面,识别其中包含的每个 URL,将这些 URL 解析为绝对形式,然后传递至 URL 待爬取队列。如果爬虫的链接提取能力不佳,无论其他配置优化得多么完善,都会漏掉网站中的大量内容。

链接所在的位置:

  1. 锚元素:锚标签上的 href 属性包含了大多数可发现的链接。
  2. 链接元素:link 元素包含规范 URL、分页提示、其他语言版本和 Feed 地址。
  3. 框架和 iframe:src 属性指向嵌入式文档,而这些文档可能包含更多链接。
  4. 表单:action 属性会显示端点,但访问这些端点需要提交数据,而不是使用普通的 GET 请求。
  5. 站点地图:XML 站点地图可直接提供 URL 列表,无需解析页面标记。
  6. 嵌入式 JSON:API 响应以及 script 标签内的状态对象通常包含不会出现在可见标记中的 URL。

解析相对 URL:

真实 HTML 中的大多数链接都是相对 URL,必须先根据其所在页面进行解析,才能抓取。

href 值 基于 https://example.com/shop/shoes/ 解析后的结果
boots.html https://example.com/shop/shoes/boots.html
/about https://example.com/about
../bags/ https://example.com/shop/bags/
//cdn.example.com/img.jpg https://cdn.example.com/img.jpg
?sort=price https://example.com/shop/shoes/?sort=price

如果存在 base 元素,它会覆盖页面 URL,作为解析基准。忽略该元素会生成看似合理、但在整个网站中都会返回 404 的 URL。

实际提取过程:

from bs4 import BeautifulSoup
from urllib.parse import urljoin

def extract_links(html, page_url):
    soup = BeautifulSoup(html, "lxml")

    base_tag = soup.find("base", href=True)
    base = urljoin(page_url, base_tag["href"]) if base_tag else page_url

    links = set()

    for tag in soup.find_all("a", href=True):
        href = tag["href"].strip()
        if href.startswith(("mailto:", "tel:", "javascript:", "#")):
            continue
        links.add(urljoin(base, href))

    for tag in soup.find_all("link", href=True):
        if tag.get("rel") and "next" in tag.get("rel"):
            links.add(urljoin(base, tag["href"]))

    return links

系统会丢弃非 HTTP 协议和纯片段链接,遵循 base 元素,并以集合形式返回结果,因此同一页面上的重复链接会立即合并。

解析器会漏掉的链接:

静态 HTML 解析只能看到服务器发送的内容。越来越多的链接只有在浏览器运行 JavaScript 后才会出现。

客户端路由。单页应用通常通过 JavaScript 路由器渲染导航,因此初始 HTML 中不会包含大多数路由的锚标签。

无限滚动。其他结果通过滚动触发的后台请求加载,绝不会出现在首次响应中。

点击处理程序。通过 onclick 处理程序而非 href 进行导航的元素,对查找锚链接的解析器而言是不可见的。

混淆链接。有些网站会特意通过 JavaScript 拼接 URL,以防静态解析器发现这些链接。

针对上述每种情况,解决方法都是先渲染页面再进行提取,这正是 JavaScript 渲染所提供的功能。Bright Data 的浏览器 API会在真实浏览器中执行页面,使渲染后的链接可供提取。

筛选提取的内容:

并非每个发现的链接都应加入队列。在 URL 进入待爬取队列前,提取流程通常会执行多项检查。

  1. 协议检查:丢弃所有不使用 HTTP 或 HTTPS 的链接。
  2. 范围检查:针对域名和路径边界应用爬取范围规则。
  3. 规范化:在进行任何比较前,先通过 URL 规范化将 URL 转换为规范形式。
  4. 去重:丢弃已经爬取或已加入队列的 URL。
  5. robots.txt 检查:确认此 User-Agent 可以访问该路径。

在提取阶段而非抓取阶段执行这些检查,可以缩小待爬取队列,并避免浪费带宽去发现某个 URL 根本就不该加入队列。

Nofollow 与爬虫行为:

rel="nofollow" 属性最初是作为提供给搜索引擎的信号,用于告知搜索引擎不要通过链接传递排名权重。它从来都不是爬取指令,并且搜索引擎在许多情况下仍会抓取 nofollow URL。

对于数据采集爬虫而言,没有义务遵循该属性,但它通常是一项有用的提示。标记为 nofollow 的链接往往来自用户生成内容、付费推广位或低价值分页,因此将它们设为较低优先级,而不是直接排除,通常能取得更好的平衡。

准备好开始了吗?