㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~ ㊙️本期爬虫难度指数:⭐ (入门级) 🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。
全文目录:
-
-
- 🌟 开篇语
- 0️⃣ 前言(Preface)
- 1️⃣ 摘要(Abstract)
- 2️⃣ 背景与需求(Why)
- 3️⃣ 合规与注意事项(必写)
- 4️⃣ 技术选型与整体流程(What/How)
- 5️⃣ 环境准备与依赖安装(可复现)
- 6️⃣ 核心实现:请求层(Fetcher)
- 7️⃣ 核心实现:解析层(Parser)
- 8️⃣ 数据存储与导出(Storage)
- 9️⃣ 运行方式与结果展示(必写)
- 🔟 常见问题与排错(排雷指南)
- 1️⃣1️⃣ 进阶优化(打造企业级 RAG 语料)
- 1️⃣2️⃣ 总结与延伸阅读
- 🌟 文末
-
- ✅ 专栏持续更新中|建议收藏 + 订阅
- ✅ 互动征集
- ✅ 免责声明
-
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。 运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO 欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战,主理专栏 《Python爬虫实战》:从采集策略到反爬对抗,从数据清洗到分布式调度,持续输出可复用的方法论与可落地案例。内容主打一个“能跑、能用、能扩展”,让数据价值真正做到——抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间:如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。 💕订阅后更新会优先推送,按目录学习更高效💯~
0️⃣ 前言(Preface)
👋 大家好!今天我们要挑战一个极其有趣且实用的项目:使用 Python 的 requests 和 BeautifulSoup,深入某个开源数据库(如类 Redis/PostgreSQL 的在线文档)的侧边栏导航,把它的“层级目录树”完整扒下来,并自动提取每个章节的摘要,最终产出一份完美的结构化 JSON 文件。
🔥 读完这篇你能获得:
1️⃣ 摘要(Abstract)
本文将演示如何针对由 Sphinx/MkDocs/Docusaurus 等框架生成的静态数据库文档站点,开发一个层级目录爬虫。我们将优先抓取全局侧边栏(Sidebar)获取章节的名称、层级与链接,随后并发访问各详情页提取首段简介,最终输出为标准的 db_docs_tree.json 文件。
2️⃣ 背景与需求(Why)
🤔 为什么要爬文档树? 在学习复杂的开源数据库时,官方文档往往庞大且碎片化。通过爬虫将目录“树状 JSON 化”,不仅可以生成离线版的高清知识导图,更是目前构建 AI 智能客服(让 AI 知道哪个知识点在哪个章节)的基石。
🎯 目标字段清单:
- Chapter_Name (章节名):如 “1.2 安装指南”
- Path (路径):用于记录它的父级关系,如 “入门 / 安装”
- Level (层级):树的深度(1级、2级、3级目录…)
- Link (链接):该章节的绝对 URL
- Summary (简介):进入该链接后,正文部分的第一段有效文本 <p>
3️⃣ 合规与注意事项(必写)
在开源世界遨游,规矩不能忘:👮♂️
- 尊重 robots.txt:官方文档通常是完全开放索引的,但礼貌性检查 robots.txt 依然是专业爬虫工程师的素养。
- 温和的并发(频率控制):文档站点多由开源社区用爱发电(有些部署在 GitHub Pages 或 ReadTheDocs),请务必加入延时(time.sleep),不要用高并发把人家服务器打宕机了。
- 遵守开源协议:抓取的数据仅限个人学习或内部知识库使用,若要公开展示,请遵循目标站点的 CC-BY-SA 或 MIT 等开源授权协议(中立表达,合法合规)。
4️⃣ 技术选型与整体流程(What/How)
🛠️ 技术选型:静态解析为主 绝大多数文档网站(如 ReadTheDocs)首次加载时侧边栏 HTML 已经存在于源码中。因此,我们不需要沉重的无头浏览器。 选型方案:Requests (请求) + BeautifulSoup (DOM解析) + 递归算法 (组装树)。
🔄 整体执行流程:
[Fetch] 请求文档主页 HTML
⬇️
[Parse] 定位 Sidebar(侧边栏),递归解析嵌套的 <ul> 和 <li>
⬇️
[Build] 构建包含层级 (Level) 和路径 (Path) 的扁平字典列表
⬇️
[Fetch & Parse] 遍历列表,访问每个 Link,提取正文首段作为 Summary
⬇️
[Storage] 将丰富后的数据持久化为嵌套或扁平的 JSON 文件
5️⃣ 环境准备与依赖安装(可复现)
推荐使用 Python 3.9+,我们需要非常纯粹的基础库组合。
📦 依赖安装:
pip install requests beautifulsoup4
📂 项目结构:
db_docs_crawler/
├── spider.py # 核心代码
├── utils.py # 辅助工具(如清洗 HTML 标签)
└── output/ # 存放最终导出的树形文件
└── db_docs_tree.json
6️⃣ 核心实现:请求层(Fetcher)
文档站点通常没有严格的反爬,但由于可能会一次性请求几百个页面,**网络稳定性(重试机制)**至关重要。
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
def get_session():
"""配置具有重试机制的会话"""
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
})
# 遇到 429(限流) 和 5xx 错误时,自动进行指数退避重试
retries = Retry(total=3, backoff_factor=1.5, status_forcelist=[429, 500, 502, 503, 504])
session.mount('http://', HTTPAdapter(max_retries=retries))
session.mount('https://', HTTPAdapter(max_retries=retries))
return session
client = get_session()
def fetch_page(url):
try:
res = client.get(url, timeout=10)
res.raise_for_status()
res.encoding = 'utf-8'
return res.text
except Exception as e:
print(f"❌ 请求失败 [{url}]: {e}")
return None
7️⃣ 核心实现:解析层(Parser)
这是本项目的灵魂所在!文档的侧边栏通常是多重嵌套的 <ul> 列表,我们需要写一个递归函数来遍历它。
(注:以下代码基于典型的 Sphinx/MkDocs 侧边栏结构编写,实战需视情况微调 DOM 选择器)
from bs4 import BeautifulSoup
from urllib.parse import urljoin
BASE_DOC_URL = "https://docs.mock-database.com/en/latest/"
def parse_navigation_tree(html, base_url):
"""解析左侧导航树,返回带有层级关系的列表"""
soup = BeautifulSoup(html, 'html.parser')
# 假设侧边栏被包裹在 class="wy-menu-vertical" 或 <nav class="sidebar"> 中
nav_container = soup.select_one('.sidebar-nav') or soup.select_one('nav')
if not nav_container:
return []
chapters = []
def traverse_ul(ul_element, current_level=1, parent_path=""):
"""递归遍历 UL 标签"""
if not ul_element: return
# 遍历当前 UL 下的所有 LI
for li in ul_element.find_all('li', recursive=False): # 只找第一层儿子,防止重复
a_tag = li.find('a', recursive=False)
if not a_tag: continue
chapter_name = a_tag.text.strip()
# 拼接相对路径为绝对 URL
link = urljoin(base_url, a_tag.get('href', ''))
# 构建当前的完整路径 (如 "Home / Setup / Installation")
current_path = f"{parent_path} / {chapter_name}" if parent_path else chapter_name
# 记录当前节点
# 注意:这里的 link 可能带有锚点 #,比如 page.html#section,我们保留它
chapters.append({
"Chapter_Name": chapter_name,
"Path": current_path,
"Level": current_level,
"Link": link,
"Summary": "" # 占位,稍后填充
})
# 如果当前 LI 内部还有 UL,说明有子章节,递归深入!
nested_ul = li.find('ul', recursive=False)
if nested_ul:
traverse_ul(nested_ul, current_level + 1, current_path)
# 寻找第一层入口 UL 开始递归
root_ul = nav_container.find('ul')
traverse_ul(root_ul)
return chapters
def extract_summary(html):
"""提取正文页面的第一段作为简介"""
if not html: return "无法获取页面"
soup = BeautifulSoup(html, 'html.parser')
# 典型文档的正文区包裹在 class="document" 或 <main> 中
content_area = soup.select_one('.document') or soup.select_one('main')
if content_area:
# 找到第一个有效的非空段落
for p in content_area.find_all('p'):
text = p.text.strip()
if len(text) > 20: # 过滤掉极短的无意义文本
return text[:200] + "…" # 截取前 200 字
return "无有效简介"
8️⃣ 数据存储与导出(Storage)
我们将爬取到的扁平化但带有层级(Level)标识的数据结构,存储为 JSON 文件。这比 CSV 更适合表达树形关系。
import json
import os
def save_to_json(data, filename="db_docs_tree.json"):
if not data:
print("📭 数据为空,取消保存!")
return
os.makedirs("output", exist_ok=True)
file_path = os.path.join("output", filename)
# 简单的去重(有些文档锚点不同但指向同一页,按需决定是否去重)
unique_data = {item['Path']: item for item in data}.values()
with open(file_path, 'w', encoding='utf-8') as f:
# ensure_ascii=False 保证中文正常显示,indent=4 美化输出
json.dump(list(unique_data), f, ensure_ascii=False, indent=4)
print(f"💾 文档目录树已成功生成并保存至: {file_path}")
9️⃣ 运行方式与结果展示(必写)
这是主程序,它将“拉取树”和“拉取摘要”两大步骤完美串联。
import time
def main():
print("🚀 数据库文档章节爬虫启动!")
# 步骤 1: 抓取主页获取全局导航树
print(f"🌳 正在解析全局导航树: {BASE_DOC_URL}")
home_html = fetch_page(BASE_DOC_URL)
chapters = parse_navigation_tree(home_html, BASE_DOC_URL)
print(f"✅ 导航解析完成,共发现 {len(chapters)} 个章节节点。")
# 步骤 2: 遍历节点,抓取详情页摘要 (如果节点过多,实战中可按需截断)
for i, chapter in enumerate(chapters):
link = chapter["Link"]
# 如果带有 '#' 锚点,说明页面内容跟父级是同一页,可避免重复请求
if '#' in link.split('/')[–1]:
chapter["Summary"] = "见父级页面"
continue
print(f" ├── [{i+1}/{len(chapters)}] 正在提取摘要: Level {chapter['Level']} – {chapter['Chapter_Name']}")
page_html = fetch_page(link)
chapter["Summary"] = extract_summary(page_html)
time.sleep(1) # 频率控制,做个乖宝宝 ☕
# 步骤 3: 存储
save_to_json(chapters)
if __name__ == "__main__":
# main() # 取消注释即可运行
pass
📄 预期输出的 JSON 文件示例 (db_docs_tree.json):
[
{
"Chapter_Name": "Getting Started",
"Path": "Getting Started",
"Level": 1,
"Link": "https://docs.mock-database.com/en/latest/start.html",
"Summary": "Welcome to the MockDB documentation. This guide will help you install and configure the database…"
},
{
"Chapter_Name": "Installation",
"Path": "Getting Started / Installation",
"Level": 2,
"Link": "https://docs.mock-database.com/en/latest/install.html",
"Summary": "MockDB can be installed on Linux, macOS, and Windows. We highly recommend using Docker for the quickest setup…"
}
]
🔟 常见问题与排错(排雷指南)
在实际爬取不同框架构建的文档时,你极有可能遇到这些坑:💣
相对路径地狱:
- 现象:获取到的 Link 是 ../setup/install.html,直接访问报 404。
- 解法:永远使用 urllib.parse.urljoin(current_page_url, extracted_href)!这是处理相对路径的银弹。
动态渲染的文档站(如 Docusaurus v2 某些模式):
- 现象:网页源码里没有 <ul>,只有 <div id="__docusaurus"></div> 空壳。
- 解法:此时不必死磕 HTML。按 F12 打开 Network,寻找类似 search-index.json 或 route.js 的接口,很多现代前端框架会在底层打包一份完整的站点 JSON,直接抓那个更香!
循环嵌套导致递归死循环:
- 现象:代码跑着跑着报 RecursionError 栈溢出。
- 解法:警惕网页中存在的互相引用(比如顶部导航栏和侧边栏互相指向)。通过控制 recursive=False (只找子节点) 或维护一个 visited_urls 集合来避免死循环。
1️⃣1️⃣ 进阶优化(打造企业级 RAG 语料)
如果你的终极目的是做知识库,还可以这样秀操作:✨
- Markdown 还原:利用 html2text 库,把 HTML 正文直接转换为 Markdown 格式存储,这是 LLM 最喜欢的阅读格式。
- 并发加速 (Asyncio / ThreadPool):如果有上千个章节,可以用 concurrent.futures 开启 10 个线程并行抓取摘要,速度提升 10 倍以上。
- 接入向量数据库:爬完之后直接调用 OpenAI 的 Embedding API,将 Summary 转成向量存入 Milvus 或 Pinecone,立刻拥有一个属于你的智能文档问答助手!
1️⃣2️⃣ 总结与延伸阅读
🎉 搞定!在这个项目中,我们不仅仅是在写爬虫,更是在解析结构、理解逻辑。利用递归算法将 DOM 树无损转换为 JSON 树,这种“降维打击”的能力在数据工程中非常核心。
下一步,你可以尝试把抓取到的 db_docs_tree.json 导入到 Elasticsearch 中,自己手撸一个比官方更好用的文档全文搜索工具!
🌟 文末
好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~ 三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
✅ 专栏持续更新中|建议收藏 + 订阅
墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以“入门 → 进阶 → 工程化 → 项目落地”的路线持续更新,争取让每一期内容都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~
✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?
评论区留言告诉我你的需求,我会优先安排实现(更新)哒~
⭐️ 若喜欢我,就请关注我叭~(更新不迷路) ⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力) ⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)
✅ 免责声明
本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。
使用或者参考本项目即表示您已阅读并同意以下条款:
- 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
- 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
- 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
- 使用或者参考本项目即视为同意上述条款,即 “谁使用,谁负责” 。如不同意,请立即停止使用并删除本项目。!!!




