1. 项目背景与整体设计思路
1.1 为什么选地方志目录页做爬虫实战
地方志这东西,很多人可能只在图书馆或者政府网站上见过。它本质上是一个地方的历史百科全书,记载了某个行政区域从古至今的地理、人物、风俗、物产、建制沿革等信息。因为内容庞杂,一部完整的地方志通常被拆分成很多卷册,比如“建置卷”“人物卷”“艺文卷”“山川卷”,每一卷下面可能还有分册。所以地方志网站的卷册目录页,天然就是一棵树。
我在做地方志数字化整理的时候,碰到的第一个真实需求就是:把网站上的目录结构完整保存下来。这个需求本质上和做电商网站的商品分类抓取、做文档站点的侧边栏抓取没有区别,都是把网页上层级分明的树状结构解析出来,再存储到本地数据库里。所以别看标题里写的是“地方志”,这套思路换到任何一个树形目录场景都能直接用。
选这个案例还有一个现实理由:地方志网站的目录页结构相对规范,没有知乎、微博那种复杂的登录验证和动态加载机制,适合作为从零到一理解“树结构 + 爬虫 + 落库”完整链路的教学样本。你先把这套基本功练扎实,以后遇到反爬更强的站点,至少在解析和存储层面不会慌。
1.2 技术选型:为什么是 requests + SQLite
整个项目的技术栈很简单:requests 负责抓取网页,标准库 html.parser 或者 BeautifulSoup 负责解析,SQLite 负责存储。没有用 Scrapy,也没有用 MySQL,这不是偷懒,而是刻意为之。
requests 是 Python 生态里最基础、最常用的 HTTP 客户端库,API 设计简洁,出问题容易排查。用它可以让你把注意力集中在爬虫本身的逻辑上——怎么分析页面结构、怎么设计树模型、怎么处理数据关联——而不是被框架的配置项牵着走。等你把 requests 这套流程跑顺了,再去看 Scrapy 的中间件、Downloader 这些概念,会轻松很多。
SQLite 的选择也很有讲究。一个本地爬虫项目,数据量撑死几十万条记录,用 MySQL 属于杀鸡用牛刀,还得额外装服务端、配账号权限。SQLite 是一个嵌入式关系型数据库,就是一个单文件,Python 自带的 sqlite3 库就能直接操作,连安装都省了。而且 SQLite 支持标准的 SQL 语法,你在这里写熟了的 INSERT、UPDATE、SELECT,换到 MySQL、PostgreSQL 上一样通用。用 SQLite 做爬虫的本地存储,是性价比极高的方案。
提示:如果你的爬虫项目是多人协作、需要并发写入,或者未来数据量会增长到千万级别,那就老老实实上 MySQL 或者 PostgreSQL。SQLite 适合单机、个人使用、数据量可控的场景,这一点要心里有数。
1.3 树结构:这个项目里最核心的抽象
很多人一听到“树结构”就想到数据结构课本里的二叉树、红黑树,觉得高深莫测。其实在爬虫场景里,树结构就是一个“父子关系”的层级模型。地方志总集是根节点,下面挂着卷,卷下面挂着册,册里面可能还有章节,这就是一棵多叉树。
在代码里表达这棵树,有两种主流方式。一种是内存式,用 Python 的类或者字典嵌套来表示节点关系,边爬边在内存里构建整棵树,最后统一入库。另一种是关系式,直接把父子关系映射到数据库表里,每条记录通过 parent_id 字段指向它的父节点,通过递归查询还原树形。两种方式不冲突,实际项目中往往是结合使用:内存里临时构建,落库时保持 parent_id 关联。
我在这个项目里选择了一个更务实的方案:用 Node 类表示节点,用 parent_id 维护层级,边爬边存。也就是每解析出一个节点,立即写入 SQLite,不等待整棵树构建完成。这样做的优势是:一是防止爬到一半程序崩溃导致内存数据全部丢失,二是数据库里始终有一份最新的完整快照,方便随时查看进度。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 目标页面分析与目录树建模
2.1 目录页的 HTML 结构长什么样
动手写代码之前,最重要的功课是分析目标页面的结构。这一步如果省了,后面全是坑。我这次的目标是某地方志网站的卷册目录页,用浏览器的“检查”功能看完结构后,发现它的目录是用嵌套的无序列表(ul/li)实现的,典型的树形布局。
简化后的 HTML 结构大致长这样:
html复制<ul>
<li data-id="1">
<span class="folder">地方志总集</span>
<ul>
<li data-id="2">
<span class="folder">建置卷</span>
<ul>
<li data-id="3"><a href="/book/3" class="file">上篇</a></li>
<li data-id="4"><a href="/book/4" class="file">中篇</a></li>
</ul>
</li>
<li data-id="5">
<span class="folder">人物卷</span>
<ul>
<li data-id="6"><a href="/book/6" class="file">列传一</a></li>
</ul>
</li>
</ul>
</li>
</ul>
看到没有,class 属性里已经给出了语义提示:folder 代表有子节点的分类,file 代表叶子节点(具体卷册)。data-id 是每个节点的唯一标识。这种结构在文档管理类网站中非常常见,算是最友好的爬虫目标之一。
2.2 设计 Node 类:让节点自带“层级基因”
分析完 HTML,接下来的工作就是设计数据模型。我定义了一个 Node 类,它承担两个职责:一是保存节点的自身属性,二是表达节点在树中的位置。核心字段包括 id、名称、父节点 id、类型、链接地址、排序值。
为什么要额外设计一个 sort_order 字段?因为在 HTML 里遍历的兄弟节点顺序就是它们在页面上的展示顺序,如果后续要在前端还原目录树,没有排序字段就只能按 id 排序,而 id 的分配顺序不一定等于展示顺序,这会导致树形展示错乱。这是一个我在实际项目中吃过亏才加上的字段,建议新手从一开始就留好这个位。
Node 类的实现如下:
python复制class Node:
def __init__(self, node_id, name, parent_id, node_type, url=""):
self.id = node_id
self.name = name
self.parent_id = parent_id
self.node_type = node_type
self.url = url
self.children = [] # 便于内存中构建树形
def add_child(self, child_node):
self.children.append(child_node)
def to_dict(self):
return {
"id": self.id,
"name": self.name,
"parent_id": self.parent_id,
"node_type": self.node_type,
"url": self.url
}
这里我额外给 Node 加了一个 children 列表。虽然落库时主要靠 parent_id 表达层级,但爬虫运行过程中需要在内存里快速组装父子关系,children 列表会让这个过程非常高效。比如你可以很容易地判断“这个节点有没有子节点”“某个节点下面的全部子节点有哪些”
