去年帮朋友整理一批旧地方志扫描件的卷册目录,第一个让我头疼的不是 OCR 识别文字,而是那些藏在网页侧边栏里的目录树。点开一个卷册,下面还有章、节、目,少的三四层,多的五六层,一层套一层。手动一个个复制粘贴显然不现实,于是我用 Python 爬虫把这类“卷册目录页”完整抓了下来,再整理成树结构,最终落进 SQLite 数据库。今天这篇就把整个过程拆开讲一遍,从页面分析、请求解析、树结构建模,到最后写入 SQLite,全程有代码、有踩坑记录,适合刚入门 Python 爬虫、也适合想系统梳理“树形数据如何落库”的朋友参考。
1. 项目分析与整体设计
1.1 目录页到底长什么样
地方志目录页和普通新闻列表不太一样。新闻列表大多是扁平结构,一个 <ul> 下面全是 <li>,抓下来排个序就完事。而地方志的卷册目录是典型的嵌套结构,HTML 里经常能看到这种写法:
html复制<div class="catalog" id="bookCatalog">
<ul>
<li data-level="1">
<span class="item">卷一 疆域志</span>
<ul>
<li data-level="2"><span class="item">沿革</span></li>
<li data-level="2">
<span class="item">四至</span>
<ul>
<li data-level="3"><span class="item">东至</span></li>
<li data-level="3"><span class="item">西至</span></li>
</ul>
</li>
</ul>
</li>
<li data-level="1">
<span class="item">卷二 建置志</span>
<ul>
<li data-level="2"><span class="item">城池</span></li>
<li data-level="2"><span class="item">廨署</span></li>
</ul>
</li>
</ul>
</div>
有些站点的结构可能不是 span.item,而是 <a>、<p>、<div>,但核心规律是一致的:出现 <ul> 就往下一层,出现 <li> 就是同层的节点。你只要把这条规律抓住,不管标签换成什么都好办。
我在动手写代码之前,习惯先开一个文本编辑器把 HTML 缩进捋一遍,或者直接在浏览器开发者工具里的 Elements 面板看结构。这一步不是浪费时间,而是避免把“目录名”和“子目录容器”搞混,尤其遇到那些嵌套层级深、又混着链接和图标的情况,先看结构再写解析,能少踩一半的坑。
1.2 为什么是“树结构 + SQLite”
目录天然就是一棵树。根节点是整本书,往下是卷,再往下是章、节、目。如果不把它当树处理,而是当作普通表格一行行拍平,后续很难复原“谁的爸爸是谁”。所以我一开始就决定在 Python 里维护一份树结构,每个节点记录自己的 parent_id,这样既能按层级展示,也能随时生成完整的路径字符串,比如 “卷一 疆域志 / 四至 / 东至”。
选 SQLite 的原因也很简单:这是个单人维护的小工具型项目,没必要搭 MySQL 或者 PostgreSQL。SQLite 本身就是单文件数据库,一个 .db 文件拷走就能用,配合 Python 标准库里的 sqlite3,零额外安装成本。对于这种“一天抓一次目录,查出来做成清单”的场景,SQLite 的读写性能完全够用,而且不用担心数据库进程没启动的问题。
有人可能会问,为什么不直接把 JSON 存文件?我在项目里不是没用过 JSON,但 JSON 文件一旦大了,查某个节点要看半天,更新某一层还要整份重写。SQLite 可以做到按 ID 更新、按父子关系查询、按书名过滤,后续要对接网页展示或者 Excel 导出都很方便。这就是标题里强调“SQLite 落库”的价值,落地到一个能检索的存储,而不是把数据停在内存里。
1.3 技术栈怎么选
这个项目的技术选型我做过一次小对比,最终核心组合是:
| 环节 | 可选方案 | 我的选择 | 理由 |
|---|---|---|---|
| HTTP 请求 | requests / httpx / scrapy | requests | 上手快,同步写起来直观,适合中小规模采集 |
| HTML 解析 | BeautifulSoup + lxml / pyquery | BeautifulSoup + lxml | 容错好,选择器写起来方便 |
| 数据存储 | sqlite3 / 文件 JSON / MySQL | sqlite3 | 标准库自带,树查询用递归 CTE 也能撑住 |
| 环境 | venv + pip | venv + pip | 隔离依赖,避免污染系统 Python |
如果你后续想把采集规模做大,可以考虑换 Scrapy,它有调度器、下载中间件和 Item Pipeline,非常成熟。但就“抓一个地方志目录页”这个体量来说,杀鸡用牛刀,requests + BeautifulSoup + sqlite3 已经足够。而且这套组合每一步都能单独调试,出错时定位问题特别快。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 请求与页面解析的完整套路
2.1 先判断数据是静态 HTML 还是异步接口
很多新手上来就写 requests.get(url),拿回来一解析发现目录是空的,就开始怀疑人生。其实先要做一件小事:在浏览器打开目标页面,按 F12 切到 Network 面板,刷新页面后找到第一个文档请求,看它的 Response 里到底有没有目录数据。
如果 Response 里直接能看到“卷一 疆域志”这样的字眼,说明目录是服务端渲染的静态 HTML,直接请求解析就行。如果 Response 只有一段空壳 <div id="app">,那数据大概率是页面加载后通过异步接口拿到的。这时候再去 Network 面板里过滤 XHR/Fetch,找到返回目录数据的接口,直接请求这个接口,往往比等页面渲染更高效。
我这个项目里遇到的是静态 HTML,所以下面的代码都以静态解析为例。异步接口的情况其实也不复杂,本质是把解析对象从 HTML 改成 JSON,
