记录把「文献里引用的标本」正式接进昆虫物种数据库的一天。
关键词:默认隔离、链接而不是复制、回收站、没有 GPS 的分布怎么画。
一、起因:文献里的标本该住在哪
文献里常常直接引用标本,一行记录大概是这样:
2♂,云南 昆明 西山区 西山,2018.08.11,采集人 A、B
这些标本是真实存在的研究材料。但如果直接把它们录进标本库,问题立刻出现:
- 它们不是站主亲手检视过的馆藏标本,却会在统计里冒充实体;
- 分布地图上会多出一批「本库从没见过」的点;
- 一旦想区分「我研究过的」和「文献提到的」,就再没有依据了。
站主给的两条底线:
绝不删除馆藏标本。
来自文献的标本默认隔离。
这两句话基本决定了后面所有的架构选择。
二、数据模型:两个预置字段 + 一张回收站表
2.1 隔离靠一个字段
标本表里其实早就预留了两个字段(站主自己设计的,之前一直没有数据):
| 字段 | 含义 |
|---|---|
specimens_LitSpec | 1 = 这条记录来自文献(文献标本) |
specimens_LitList | 这号标本被哪些文献引用(JSON 数组,可以多篇) |
于是「默认隔离」就变成一句可复用的过滤条件:
def not_lit_specimen():
"""默认隔离"来自文献的标本"的过滤条件。"""
return or_(Specimens.litspec.is_(None), Specimens.litspec != 1)
这里有一个必须写对的地方:
-- 错:会漏掉所有 NULL
WHERE specimens_LitSpec != 1
-- 对:
WHERE specimens_LitSpec IS NULL OR specimens_LitSpec != 1
SQL 里 NULL != 1 的结果是 NULL(不是真)。所以只要写成第一种,所有没标过的老标本会一次性从统计里消失 —— 这种错不会抛异常,只会在数字里静静地沉下去。
2.2 回收站是一张新表
这个库一直有「改动前先备份」的习惯,今天把「删除的文献标本」也纳入了体系:
| 列 | 用途 |
|---|---|
trash_Kind | 类型,如 lit_specimen |
trash_RefText | 来源文献:年份 + 作者 + 标题(字符串) |
trash_DeletedAt | 删除时间 |
trash_Payload | 完整数据(JSON),便于日后人工恢复 |
RefText 为什么不用文献 ID?因为文献一旦删除,ID 就作废了,将来拿这个 ID 去找,只会找到另一个不相干的文献。用「年份+作者+标题」拼成字符串,哪怕文献不在了,人也看得懂这条数据是从哪来的。
三、录入:三种命中,一个按钮
录入一篇文献的标本时,系统依次判定:
| 情况 | 处理 | 是否计入统计 |
|---|---|---|
| 库里已有完全一致的馆藏标本(物种 / 采集地 / 采集信息 / 采集人一致) | 直接建立链接 | 计入(它确实是库里的实体,也确实被用在文献里) |
| 命中一条已有的文献标本 | 直接链接 | 不计入 |
| 都没有 | 新建一条文献标本(LitSpec = 1) | 不计入 |
「完全一致」这四个字在实践中非常麻烦:采集地、日期、采集人只要有一个空格、一个标点不同,程序就认为不是同一条。
所以录入窗口上多了一个「搜索并链接已有标本」的按钮:不要求你把字符串抄对,而是让你搜出来点一下。补全联想与标本录入页同款,并且把文献标本的取值也一起纳入;但字段本身是自由输入 —— 文献里出现的记录,不一定在本库有对应标本。
四、展示:三处入口
4.1 文献详情页:「本文献标本」
- 默认收起,展开后按物种分组:物种名占一行,下面是这号标本的卡片;
- 一行可以放多张卡片,鼠标悬停可以预览标本照片(有图片时);
- 点卡片直接跳到标本详情页;
- 卡片上不再显示 GPS 字符串 —— 人眼无法从一串经纬度里读出「云南昆明西山」,这和删掉「详情 ↗」按钮是同一个理由:让条目本身就是入口。
4.2 标本详情页:「📚 本号标本相关文献」
只有当这号标本真的参与过文献时,按钮才出现;点开就是引用它的文献清单。没有数据的标本不会多出一个空栏目。
4.3 阶元库页面:「文献标本」
与「同种团」「检视标本」并列的新栏目:点开列出该阶元下的文献标本,并按国家 / 行政区域分组。
这一栏和地图的「本种 / 属 / 种团文献坐标」共用同一个数据入口 —— 数据还没录入,图层与栏目先把位置留出来,录入之后不需要再改代码。
五、删除:让数据可回收
删除一篇文献时,涉及三类东西,处理方式完全不同:
| 对象 | 处理 |
|---|---|
| 馆藏标本 | 只解除与文献的链接,标本本体绝不动 |
| 文献标本 | 进回收站(含来源字符串、删除时间、完整数据) |
| 文献本身 | 正常删除 |
「绝不删除馆藏标本」在实现层面就是一句话:删除流程里没有一条 SQL 能删掉 specimens 里的馆藏记录。
六、B8:没有 GPS 的分布怎么画
文献标本最麻烦的地方是 GPS 常常没有,但采集地写得清清楚楚。所以这一类分布分三种画法:
| 数据情况 | 画法 |
|---|---|
| 有 GPS | 橙色空心点位 |
| 没有 GPS,但采集地能识别到省 | 该省的多边形填色 |
| 没有 GPS,采集地是国外 | 国家色块 |
| 都识别不出 | 归入「其它 / 未识别」,不猜 |
地名匹配这件事,上一篇已经踩过一次坑(「内蒙古」被识别成「蒙古」)。这次的要求更实际:文献里的地名不一定写中文或英文。
现在的策略是:
- 简体中文:沿用原有逻辑;
- 正式英文 / 拼音:用词边界匹配(
(?<![a-z])…(?![a-z])),避免Yunnan命中Yunnanomonticola这类属名; - 其它语言:暂不处理 —— 宁可归进「未识别」,也不猜。
顺带一提,匹配前同样做了「先遮蔽中国省名、再做国家匹配」的那一步,否则「内蒙古」还会被算进蒙古。
七、几处小修
「解除关联」改成右键菜单。 一个按钮贴在一行条目上,鼠标划过就能点到,误碰的代价是「这号标本突然不属于这篇文献了」。改成右键菜单之后,误操作需要两个明确动作。
物种胶囊右键菜单。 现在有两项:📄 添加标本信息 和 🗑 删除物种关联。后者只把物种从这篇文献的「涉及物种」里摘掉,不删物种、不动标本。
雄性数量字段的历史数据清理。 这个字段最初的设计是:空值 = 未指定,-1 = 无雄性,0 = 该号标本全是雌性。后来 0 的含义在一次升级中被确定下来,但数据没有跟着改 —— 一批原本代表「未指定」的 0 留在库里,等于把「不知道」写成了「确定全雌」。这次把它们改回空值:86 条,改动前有整库备份。
地图弹窗文案。 行政区划显示改为「省 地级市 县/区」,更符合中文语境(原来只写省)。
八、用一张标签照片做最后一道校验
标本标签的照片是可以「读」的。今天试了一次图片核对:
照片读出的编号 / 采集地 / 日期 / 采集人
↕ 逐项比对
数据库里这一条记录
结果完全吻合。
它替代不了人工复核,但作为交叉校验很有用:一条记录如果在库里是错的,照片会立刻把它指出来。
九、写在最后
今天真正的工作量不在功能上,而在口径上:
- 「来自文献」和「馆藏」是两种东西,必须在数据层面分开,而不是在报表里绕开;
- 「默认隔离」的价值在于默认值是安全的 —— 忘记加条件的代码,得到的是「保守」的结果,而不是「污染」的结果;
- 删除前先想「以后怎么把它找回来」:回收站里存的不是 ID,而是人看得懂的来源。
以及一条今天又验证了一遍的规律:口径的每一次漂移,都会在数据里留下痕迹,而痕迹不会自己消失。 雄性字段那 86 条数据就是一次漂移的化石 —— 逻辑改对了,数据还得另外去修。
文献标本的真实数据目前还没有开始录入(库里 0 条)。今天把位置、图层、入口、回收站先建好了:录入之后,地图上会多出一层「橙色空心点 + 省多边形 + 国家色块」的文献坐标,而统计里的数字不会因此动一下。
本文由 Codex 整理,记录 2026-10-07 的 InsectDB 文献标本接入工作。