本文记录一个完整实践:用 Python 从零实现一个「URL → 干净 Markdown」的网页正文提取工具。重点不是 Hello World,而是真实跑三个网站后遇到的三类坑,以及对应的修复方案。本轮借助 Doubao-Seed-Evolving(字节刚升级、1M 上下文的 Coding 模型)完成初版与两次修复,代码可复现。
一、需求与技术选型
目标:输入一个网页 URL,输出去除导航/广告/侧边栏后的正文 Markdown。
选型:
- readability(readability-lxml)做正文去噪
- markdownify 把清洗后的 HTML 转 Markdown
- BeautifulSoup 做预清理
- Playwright 处理客户端渲染站点
初版代码由 Doubao-Seed-Evolving 在一次对话里生成,下文是提炼后的关键实现与踩坑。
二、第一版实现
这是我输入提示词后AI给的回答:

核心结构长这样(节选):
python
import requests
from bs4 import BeautifulSoup
from readability import Document
import markdownify as md
DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."
def fetch_url(url, timeout=15, verify_ssl=True):
if not url.startswith(("http://", "https://")):
url = "https://" + url
headers = {"User-Agent": DEFAULT_UA}
resp = requests.get(url, headers=headers, timeout=timeout, verify=verify_ssl)
resp.raise_for_status()
return resp.text
def extract_markdown(html, page_url=""):
soup = BeautifulSoup(html, "lxml")
for tag in soup.find_all(["script", "style", "nav", "aside", "footer"]):
tag.decompose()
doc = Document(str(soup), url=page_url)
content_html = doc.summary()
return md(content_html, heading_style="ATX", convert=["table", "pre"])
注意:第一版有个坑------markdownify 不能同传 convert 和 strip,否则运行时直接报错。预清理已处理 script/style,所以删掉 strip 即可。这个修复也是模型在看到报错后给出的。
三、实测三类网站
我选了 3 个不同类型的网页做测试,如实记录结果:
| 网站类型 | 结果 | 现象 |
|---|---|---|
| 技术博客(SSR) | ✅ | 代码块、语言标注、表格全保留 |
| 公众号 | ⚠️ | 正文 OK,标题变 no-title |
| 头条(CSR) | ❌ | 仅 # no-title,正文空 |

三个网页,一个直接成功,一个丢标题,一个彻底空壳。第一版没有框架预设里那么惨,但真实的问题比预设的更有意思,不同的网站暴露出不同的短板。
四、关键修复一:标题提取退化
readability-lxml 的 Document.summary() 不读 og:title,只取 <title>。微信文章标题格式会触发其解析 bug,得到 no-title。

修复:在 extract_markdown 前加一层语义化标题提取。
python
def extract_title(soup, doc):
og = soup.find("meta", property="og:title")
if og and og.get("content"):
return og["content"].strip()
tw = soup.find("meta", attrs={"name": "twitter:title"})
if tw and tw.get("content"):
return tw["content"].strip()
h1 = soup.find("h1")
if h1 and h1.get_text(strip=True):
return h1.get_text(strip=True)
return doc.short_title()
五、关键修复二:客户端渲染站点的空壳
头条这类站点正文由 JS 异步加载,requests 拿到的只是 <div id="root"> 空壳。

修复:加 --render 参数,用 Playwright 无头浏览器等渲染完成再提取。
python
from playwright.sync_api import sync_playwright
def fetch_rendered(url):
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto(url, wait_until="networkidle")
html = page.content()
browser.close()
return html
另:微信/头条图片是懒加载,真实地址在 data-src,需补一段把 data-src 回填到 src,否则全文裂图。
六、最终用法
bash
python extract_article.py https://juejin.cn/post/xxx -o article.md
python extract_article.py https://www.toutiao.com/xxx --render -o toutiao.md
静态站不需要额外依赖,动态站加 --render。
写工具的价值不在第一版多漂亮,而在能不能把踩过的坑固化成可复用的能力。