用 requests + BeautifulSoup 抓网页正文:能跑和好用之间

米阳 2025-8-9 4 8/9

抓一篇文章的正文,写起来只要十行。但要抓得干净、稳定、不被人拦,需要考虑的东西不少。

最简版本

import requests
from bs4 import BeautifulSoup

r = requests.get(url, timeout=10, headers={"User-Agent": "Mozilla/5.0"})
r.encoding = r.apparent_encoding
soup = BeautifulSoup(r.text, "html.parser")
title = soup.find("h1").get_text(strip=True)
body = soup.find("article").get_text("\n", strip=True)

能跑,但 get_text 会把导航、广告、评论区全带上。

去掉噪音

先删掉明显不需要的标签:

for tag in soup(["script", "style", "nav", "header", "footer", "aside", "form", "iframe"]):
    tag.decompose()

for sel in [".comment", ".ad", ".sidebar", ".related", ".share", "#comments"]:
    for tag in soup.select(sel):
        tag.decompose()

找正文容器

不同站点结构不同,盲目找 article 常常找不到。更可靠的办法是按「文本密度」打分:

def densest_block(soup):
    best, best_score = None, 0
    for div in soup.find_all(["div", "article", "section"]):
        text = div.get_text(strip=True)
        if len(text) < 200:
            continue
        links = sum(len(a.get_text(strip=True)) for a in div.find_all("a"))
        score = len(text) - links * 2      # 链接文字多说明是导航
        score *= len(div.find_all("p"))    # 段落多说明是正文
        if score > best_score:
            best, best_score = div, score
    return best

这个启发式对付大多数内容站够用,比写死选择器稳。

保留结构

get_text 会丢掉段落、标题、列表的层次。想保留的话,把 HTML 转成 Markdown 更合适:

import markdownify
md = markdownify.markdownify(str(container), heading_style="ATX", strip=["a"])

正文里的图片也要处理:延迟加载的图片真实地址在 data-src 而不是 src:

for img in container.find_all("img"):
    src = img.get("data-src") or img.get("data-original") or img.get("src")
    if src and src.startswith("//"):
        src = "https:" + src
    elif src and src.startswith("/"):
        src = urljoin(url, src)
    img["src"] = src

这一步不做,抓下来的文章图片全是 404 或者空白占位图。

反爬的应对

  • UA:默认的 python-requests/2.x 一眼就能识别,带上正常的浏览器 UA
  • Referer:有些站检查来源,加上原站地址
  • 频率:加 time.sleep(1~3),别把人家站点打挂,也避免被封
  • Cookie:登录才有内容的站点,需要带 Cookie
  • 重试:网络抖动很正常,用 requests.adapters.HTTPAdapter 配重试
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=Retry(total=3, backoff_factor=0.5)))

判断内容质量

抓下来要能判断「值不值得留存」:

  • 正文字数(少于 500 字可能是列表页或摘要页)
  • 中文字符占比(低于 30% 可能是英文或乱码)
  • 段落数(少于 3 段多半是抓错了)

编码问题

中文站点最常见的坑是乱码。r.encoding 优先用响应头里的 charset,没有就从 HTML 的 meta 里猜,最后才是 apparent_encoding。用 r.text 之前一定要设对,否则后面全错。

合规

只抓公开内容,遵守 robots.txt,加来源声明,不整站镜像。抓别人的内容是为了自己学习整理,不是为了搬运流量——这条线要清楚。

- THE END -

米阳

8月09日21:15

最后修改:2025年8月9日
0

非特殊说明,本博所有文章均为博主原创。

共有 0 条评论