mongodb 无模式特性天然适配爬虫数据的动态字段,但需注意类型统一、唯一索引防重、$set 安全更新及入库前扁平化与类型归一。

MongoDB 存储爬虫数据时,字段缺失或动态新增会直接写入,无需预定义 Schema
爬虫抓取的数据天然不规整:有的页面有 price,有的只有 original_price 和 discount;有的商品带 specifications 数组,有的空着甚至根本没这个字段。MongoDB 的无模式(schema-less)特性正好匹配这种场景——你不需要提前声明哪些字段必须存在、类型是什么。只要 BSON 格式合法,任意结构的文档都能插入。
实操建议:
- 直接用
collection.insertOne()或collection.insertMany()写入原始 JSON 对象,别先做字段校验或补全空值(除非业务强依赖) - 避免在应用层强行统一字段名(比如把所有价格字段都映射成
price_final),除非后续查询/聚合严重依赖一致性 - 注意:字段名大小写敏感,
url和URL是两个不同字段,爬虫中间件若未标准化,容易导致重复存、查不到 - 如果字段嵌套过深(如
data.product.details.attributes.list.0.value),会影响索引效率和可读性,建议在入库前做一层扁平化(例如提取到顶层attr_color、attr_size)
用 updateOne() + $set 处理增量更新时,只覆盖有值字段,不删旧字段
爬虫常需“补全”或“修正”已有记录:比如第一次只抓了标题和 URL,第二次才拿到发布时间和作者。这时用 updateOne() 配合 $set 是最安全的选择——它只会设置传入的字段,其他字段原封不动保留。
常见错误现象:
- 误用
replaceOne():整个文档被新对象替换,所有未传字段(包括之前存的timestamp_crawled、source_id)都会丢失 - 用
$set但传了null值:MongoDB 会真把该字段设为null,而不是忽略;后续find({field: {$exists: true}})就查不到它了 - 对数组字段盲目
$set:比如想追加标签,却写成{$set: {tags: ["new"]}},结果把原有标签全清空
正确做法:
- 只传确定要更新的字段,不传的字段不出现
- 需要追加数组元素用
$push,去重追加用$addToSet - 更新时间戳建议固定用
{$set: {updated_at: new Date()}},避免依赖客户端时间不准
字段类型混用(字符串 vs 数字)会导致查询失效或排序错乱
MongoDB 不强制字段类型,但一旦同个字段存了 "123"(字符串)和 123(数字),find({price: {$gt: 100}}) 就可能漏掉字符串型价格——因为字符串和数字比较规则不同,且 BSON 类型优先级影响排序结果。
使用场景:
- 价格、评分、库存、年份等本应为数字的字段,被爬虫解析失败后存成空字符串
""或短横线"-" - 日期字段混存 ISO 字符串(
"2024-05-20")和Date对象,$dateToString聚合会报错
实操建议:
- 入库前做轻量类型归一:用
parseInt()/parseFloat()转数字,失败则设为null(不是空字符串);日期用new Date(str)尝试解析,无效则丢弃或记日志 - 建索引前确认字段类型一致性,否则
price_1索引对字符串型price无效 - 用
aggregate()查数据时,可用$type过滤:{$match: {price: {$type: "double"}}}快速定位异常值
不要依赖 MongoDB 自动去重,_id 不等于业务唯一键
新手常以为 MongoDB 会像关系库那样按某几个字段自动去重。其实不会:_id 默认是 ObjectId,每次插入都生成新值,即使内容完全一样也会存两份。真正要防重复,得靠唯一索引(unique index)或应用层判断。
容易踩的坑:
- 只给
url字段建了唯一索引,但爬虫有时带跟踪参数(?utm_source=xxx),导致同一页面存多次 - 用
upsert: true但没配好filter条件,比如{url: "a.com"}匹配到了多个旧文档,结果只更新其中一条,其余残留 - 唯一索引字段含
null值:MongoDB 允许多个文档在该字段上为null,不违反唯一约束
实操建议:
- 业务唯一标识(如商品 ID、文章 slug)单独提出来,作为
_id或建唯一复合索引(如{source: 1, item_id: 1}) - URL 去参标准化:入库前用
new URL(url).origin + new URL(url).pathname提纯 - 批量插入时用
insertMany(..., {ordered: false})+ 捕获writeErrors,比逐条 upsert 更快且可控
字段动态、结构松散是优势,但类型混乱、唯一性失控、更新逻辑错位,才是让数据变脏的真正起点。留心那些“看起来能跑通”的写法,它们往往在三个月后开始反噬查询性能和分析口径。










