抓一篇文章的正文,写起来只要十行。但要抓得干净、稳定、不被人拦,需要考虑的东西不少。
最简版本
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 -
最后修改:2025年8月9日
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:https://mi-blog.cn/archives/698
共有 0 条评论