PAYATHON 2026

如何用 Python 的 BeautifulSoup 过滤文章正文前后的无关文本?

技术老齐

先用 BeautifulSoup 定位文章正文所在的 HTML 容器,再提取文字。如果正文中夹杂着推荐阅读、广告等内容,可以按标签或 CSS 选择器删除。

BeautifulSoup 不会自动识别正文,提取结果主要取决于是否选对了正文节点。

为什么会提取出导航和页脚?

这一行会提取整个 <body> 中的文本:

text = soup.body.get_text()

<body> 通常包含导航栏、文章、推荐内容、评论区和页脚。get_text() 会收集所选节点下的文本,因此这些内容都会出现在结果中。

删除 script、style 只能去掉脚本和样式。导航栏和推荐阅读通常使用普通 HTML 标签,仍会保留下来。后面的 strip()、split() 和 join() 只负责整理空白,无法区分正文和其他内容。

先确认正文所在的节点

在浏览器中打开文章,右键正文第一段,选择“检查”,再沿着父节点向上查找,找到包含全部正文的容器。

假设页面结构如下:

<div class="article-body">
    <p>正文第一段。</p>

    <div class="related-content">
        <p>推荐阅读:另一篇文章。</p>
    </div>

    <p>正文第二段。</p>
</div>

可以用 .article-body 定位正文容器,删除其中的 .related-content,然后提取剩余的 <p>。

.article-body 和 .related-content 都是示例选择器。题目只提供了提取后的纯文本,无法据此确定 ZDNet 页面的实际 HTML 结构,具体选择器需要检查抓取到的 HTML 后再填写。

修改后的代码

这个函数只发送一次请求,可以通过参数为不同页面指定正文选择器和排除规则:

import requests
from bs4 import BeautifulSoup


def get_text(url, body_selector, remove_selectors=()):
    response = requests.get(
        url,
        headers={"User-Agent": "Mozilla/5.0"},
        timeout=20,
    )
    response.raise_for_status()

    soup = BeautifulSoup(response.content, "html.parser")
    article = soup.select_one(body_selector)

    if article is None:
        raise ValueError(
            f"未找到正文容器,请检查选择器:{body_selector}"
        )

    # 删除正文容器内的脚本、样式等节点。
    for node in article.select("script, style, noscript"):
        node.decompose()

    # 删除已确认不属于正文的模块。
    for selector in remove_selectors:
        for node in article.select(selector):
            if node.parent is not None:
                node.decompose()

    paragraphs = []

    for paragraph in article.find_all("p"):
        text = paragraph.get_text(" ", strip=True)
        text = " ".join(text.split())

        if text:
            paragraphs.append(text)

    if not paragraphs:
        raise ValueError(
            "正文容器内没有提取到段落,请检查 HTML 结构"
        )

    return "\n\n".join(paragraphs)

如果实际页面符合前面的示例结构,可以这样调用:

text = get_text(
    "https://example.com/article",
    body_selector=".article-body",
    remove_selectors=(".related-content",),
)

print(text)

输出为:

正文第一段。

正文第二段。

抓取题目中的 ZDNet 页面时,需要把 URL 换成文章地址,并把两个选择器改成该页面实际使用的选择器。这里尚未验证该页面当前的结构,示例类名不能直接作为 ZDNet 的提取规则。

正文中仍有“参见”或推荐阅读怎么办?

题目中的干扰内容还穿插在正文段落之间,选中正文容器后,可能仍需清理。

检查这些模块是否有独立的 class、id 或外层标签,再将对应的选择器加入 remove_selectors。decompose() 会删除节点及其全部内容,使用前要确认其中没有需要保留的段落。

正文可能包含必要的链接,因此不要直接删除所有 <a> 标签。也不要仅凭 SEE:、“参见”等文字就删除整段。如果推荐链接和正文在同一个段落中,需要进一步定位链接及其包装节点,避免连同正文一起删除。

使用时需要注意什么?

  • 页脚、推荐阅读和评论也可能使用 <p>,所以仅查找所有 <p> 还不够,需要先将查找范围缩小到正文容器。
  • 代码目前只保留段落。如果还需要保留文章中的小标题、列表或代码块,就要按实际结构提取 h2、h3、li、pre 等元素。代码块不宜使用同样的空白压缩方式。
  • get_text(" ", strip=True) 会在文本节点之间加入空格,适合一般英文段落,但未必能保留原始排版,也不能代替正文过滤。示例中的固定宽度换行属于显示格式,可以在提取完成后另行处理。
  • 不同网站需要不同的规则。<article>、[itemprop="articleBody"] 可以作为检查线索,但页面不一定使用这些标记,<article> 中也可能包含标题和推荐模块。
  • 提取时要以请求返回的 HTML 为准。requests 不会执行 JavaScript。如果浏览器中能看到正文,响应 HTML 中却没有,仅更换 BeautifulSoup 选择器无法提取出来。

原代码先用 requests.get(),随后又用 urlopen() 请求同一地址,重复发送了请求,保留一种请求方式即可。raise_for_status() 能让 HTTP 错误直接暴露出来,比统一返回字符串 "none" 更方便排查。

备注:内容仅供参考。