项目复盘 · 2020
长尾工程资产:小型采集器、转换脚本与服务快照
把二十余个低提交、单用途或快照型仓库放回同一条数据链,分析它们采用的技术、真正可复用的设计,以及难以继续维护的原因。
GitLab 中有一批不适合单独写成产品案例的仓库。它们有的只有一次提交,有的是针对单一来源的采集脚本,有的是把运行环境和生成文件一起保存的代码快照。单看每个仓库,容易得到“逻辑很少”的结论;放回整条数据链后,却能看出团队当时如何快速补齐数据能力。
本文不按仓库数量制造项目数量,而是按职责把它们组织为采集适配器、传输与转换、研究计算、查询服务和前端原型五层。
纳入本篇的资产清单
这里的“长尾”指不适合作为独立产品叙述,并不代表代码没有价值。部分仓库也在对应复杂项目文章中作为主证据,本篇只分析其可复用的工程模式。
- 通用和单来源采集:
finance/demo、lambdax/autohome、lambdax/baidu_news、lambdax/crunchbase、lambdax/ycy-zk、个人空间-周川/news_spider、个人空间-周川/stock、个人空间-周川/industry_research、个人空间-周川/social_organization、个人空间-周川/company、个人空间-陆川/company_news。 - 教育与社交资料补充:
lambdax/collage_weibo、个人空间-林宁/forwardpathway、个人空间-林宁/instagram_collage、个人空间-林宁/twitter_collage。 - 文书与存储迁移:
个人空间-林宁/DBtransf、lambdax_go/wenshu_doc_sync、lambdax_go/wenshu_doc_filter、lambdax/company、个人空间-周言/neijing、lambdax/court、lambdax/wenshu、个人空间-陆川/court。文书主链仍以“海纳”文章为准。 - 服务、计算与原型快照:
lambdax/kss、lambdax/selenium_shuzhi、lambdax/wikidb_web、lambdax/bigtable_api、lambdax/vue-company、个人空间-林禾/fund、个人空间-江叙/DataStastics、finance/third_party、个人空间-江舟/shtTest。 - 产业链的并行解析与演示:
industry-chain/parser/industry-chain、industry-chain/parser/解析器分支、个人空间-周川/industry-chain、个人空间-陆川/industry-chain、lambdax/show-how。它们的业务语义与完整产品边界在“产业链知识图谱”文章中说明。
采集层:为不同来源选择不同执行环境
最轻量的采集器使用 Python 请求和 HTML 解析,处理新闻、行业报告、社会组织、公司网站、股票与境外投融资数据。结构相对完整的新闻项目采用 Scrapy,把 Spider、Middleware、Pipeline 与日/月调度分开;汽车口碑项目则组合 Selenium、字体解析、代理检查、Cookie 管理、消息队列和多进程任务,以应对动态页面和反爬变化。
Node.js 仓库主要处理需要浏览器行为的教育榜单与社交资料。Puppeteer 负责登录或动态页面,Cheerio 解析文档,队列控制请求,MongoDB 保存中间结果。微博工具甚至把表格补全、登录更新、账号标识对齐和定时采集拆成连续步骤。Go 采集快照则用并发请求、代理池、失败重试和压缩响应解析提高吞吐。
这批仓库的技术选择基本符合来源特性:静态页面用轻量请求,复杂交互用浏览器自动化,高并发下载用 Go。但每个来源都单独实现配置、日志、代理和重试,公共能力没有沉淀为统一框架,维护成本随来源数量线性增长。
传输与转换层:让原始记录进入可查询存储
数据转换仓库用 Python 在 SSDB、Couchbase、MongoDB GridFS 与普通 MongoDB 之间搬运对象,说明当时的数据不只包含结构化字段,也包含文档或较大的二进制内容。法律文书同步器使用 Go,从键值存储按日期范围读取文书标识,通过带缓冲通道分发给多个工作协程,再批量写入关系数据库。
这段同步设计是小仓库中很清晰的技术亮点:源端遍历、并行消费、批量写入和进度报告彼此分离,连接也并发建立。它比逐条串行复制更适合大规模迁移。不过结束信号与超时轮询耦合,错误由共享变量传递,缺少上下文取消、幂等键和失败批次落盘;进程中断后能否准确续跑仍不明确。
具体实现能解释这个风险怎样产生。程序并行建立源端键值库与目标 MySQL 连接,以日期键范围枚举文书 ID;容量为 并发数 × 10 的通道承担背压,每个工作协程累计 10 条后写库,空闲 1 秒则刷新不足 10 条的尾批。生产者结束后只向一个带缓冲的 end 通道写入标记,所有消费者通过检查通道长度退出。这个技巧在当前单生产者结构下大体可用,却不是可靠的完成协议:连接错误共用同一个 err 变量形成竞态,批量写入错误没有向上返回,time.After 在循环内反复创建定时器,通道也没有关闭语义。更稳妥的重建应由生产者关闭任务通道,消费者 range 到结束,用独立错误通道和取消上下文汇总失败,并为目标表设置唯一键来支持重跑。
下面是文书同步仓库的源码节选(已脱敏),连接配置和业务标识均未保留。代码展示源端如何用游标递归分页;最后一项被用作下一页起点,因此调用方还必须验证存储接口是否会重复返回边界项:
func RangeKeys(
db *ssdb.Client,
name, begin, end string,
cb func(name, key string) error,
) error {
result, err := db.Do("hkeys", name, begin, end, keysLimit)
if err != nil {
return err
}
keys := []string{}
for _, raw := range result[1:] {
keys = append(keys, string(raw))
}
for _, key := range keys {
if err = cb(name, key); err != nil {
return err
}
}
if len(keys) != keysLimit {
return nil
}
return RangeKeys(db, name, keys[len(keys)-1], end, cb)
}境外投融资转换器则把组织、人物、融资、投资、并购、上市和关系分别建模,再从周期性 CSV 快照导入 MongoDB,并计划维护增量数据库。模型拆分有利于保留关系语义,但更新流程仍依赖人工修改日期配置,失效记录清理也留在待办中。
这里的关键不只是 CSV 入库,而是“快照覆盖”与“事件增量”不能混用同一判断。公司简介、地址等属性适合按快照比较;融资轮次、并购和上市则应以事件主键追加或订正。如果只按本次文件是否出现来删除旧记录,上游迟到、分片或字段缺失都可能被误判为业务事实消失。资产化时至少要保存来源批次、抓取时间、事件时间、原始行哈希和生效区间。
研究计算层:专利、PDF 与排名原型
一个 Java 专利处理快照包含多个年代和格式版本的 XML 解析入口,使用 Jackson XML、XOM、MongoDB 与 Elasticsearch,并通过 Maven Shade 打成可执行程序。它体现了批量专利处理的真实难点:同一种数据在不同时期有不同结构,需要版本化解析器,再统一清洗和索引。
多版本解析器的正确抽象应是“格式探测器 + 版本适配器 + 统一领域对象”,而不是由调用者猜年份选择主类。每条输出要保留检测到的格式版本、解析器版本和原始文件定位;不认识的节点进入隔离队列而不是静默丢弃。这样当专利字段映射修正时,才能只重放受影响的版本,并量化修正前后记录差异。
行业研究脚本同时包含报告采集、PDF 下载、目录抽取、文本和图片处理,以及面向不同来源的适配目录。早期排名快照则直接用 Java 类分别实现大学、商学院、医学院、电影、图书和其他榜单,并连接百科与数据库数据。它们验证了“从列表和知识网络生成排名”的方向,却把多个实验平铺在仓库根目录,缺少统一包结构、构建描述和固定测试数据。
finance/demo 更像算法与数据源试验台:股票获取、新闻消息发送、汽车和金融网站采集、指数预测及分类评估共处一个仓库。它证明多种路线被快速验证过,但没有稳定模块边界,不能视为一个可部署的金融产品。
查询服务层:从公司主表到投资关系接口
几个 Java 服务快照展示了同一能力的演进。较早的百科语义服务使用 Jersey Servlet,提供搜索、定义、类别比较、树结构和 Wikifier 等接口;公司聚合服务使用 Spring MVC,分别从 Elasticsearch、MongoDB 和 MySQL 读取应用、专利、投融资和工商数据;后来的公司大表服务加入 MyBatis-Plus、Shiro、缓存、日志、角色与资源管理,并提供公司、关键词、导出和数据源接口。
基金相关微服务使用 Spring Boot、MyBatis、MySQL、PageHelper 和 Elasticsearch,围绕公司基础信息、投资关系、融资历史和名称联想提供四类接口。它还用 AOP 记录同名公司查询,说明团队已经注意到实体消歧问题。这个做法适合作为质量观测入口,比在业务代码中散落日志更集中。
代码同时暴露出原型期缺陷:部分查询假定公司一定存在,融资投资者合并对固定长度列表继续追加,可能触发运行时异常;分页参数固定在服务内部,错误与空结果也没有稳定契约。仓库有连续提交,但测试大多围绕启动或日志,现有证据不足以确认其生产稳定性。
前端与依赖快照:能运行不等于可维护
公司详情原型使用 Vue 2、Vue Router、Vuex、Axios 和 Element UI,采用标准的 Vue CLI 结构,可以消费上面的公司接口。它说明数据团队曾尝试把公司主表从接口推进到可浏览产品,但九次提交和短开发周期只支持“交互原型”的判断。
另一些仓库几乎全部是第三方代码、虚拟环境、编译产物、驱动程序或抓取结果。例如热点采集快照把完整 Python 虚拟环境提交进仓库,Java 项目保留 target 和大型依赖目录,依赖仓保存分词和其他第三方组件,还有一个仓库完全为空。这些内容有助于复原当时运行环境,却显著增加体积、许可证识别和安全修复难度,也让“文件很多”无法代表自研逻辑很多。
横跨这些仓库的技术亮点
- 根据来源特性在 Scrapy、Selenium、Puppeteer 与 Go 并发之间做了现实选择,而非强迫所有任务使用同一种工具。
- 文书迁移把遍历、缓冲、并发和批量入库拆开,已经具备数据管线的基本形态。
- 投融资导入按实体与关系建模,保留了比单张结果表更丰富的语义。
- 专利处理为多版本 XML 保留独立入口,承认上游格式会随年代变化。
- 公司查询服务通过分层 DAO 聚合多种存储,并开始把同名实体作为可观测的数据质量问题。
- 社交资料采集采用“表格清单—账号标识补全—平台采集—定时更新”的分阶段流程,便于定位缺失发生在哪一步。
共同的技术债务
- 配置、代码、样本、日志、抓取结果、编译产物和第三方依赖混放,难以界定可复现的最小源码。
- 多个历史仓库曾把连接信息写入配置或代码历史;相关值应视为可能泄露并轮换,不能因系统停用而忽略。
- 自动化测试、数据契约、失败队列、运行指标和持续集成普遍不足,README 多数只描述启动动作。
- 同一类 MySQL、MongoDB、代理、日志和重试代码在多个采集器中重复实现,修复无法同步传播。
- 一次性快照缺少来源版本和数据生成时间,后来者难以判断代码、样本与数据库是否匹配。
- 采集器的持续使用还受来源条款、个人信息和访问限制约束,技术可行不能替代合规评估。
- 批处理普遍把“函数返回”当作成功,缺少输入数、接受数、拒绝数、写入数与校验数的守恒检查;静默跳过会比进程报错更难发现。
更适合的资产化方式
这些代码不需要全部改造成微服务。更合理的整理方式是保留薄适配器,将调度、限速、重试、原始响应归档、字段校验、指标和秘密管理下沉为共享运行层。一次性分析则应封装为带输入版本、参数和输出清单的批处理任务。
每个仓库至少应回答五个问题:数据从哪里来,允许怎样使用;输入样例和字段版本是什么;怎样从断点恢复;结果写到哪里并由谁消费;怎样判断本次运行成功。这样,小工具即使不再维护,也仍是可理解、可审计的历史资产。
对这类历史快照,恢复规格时还应固定四类验收样例:一个正常输入、一个字段缺失输入、一个重复输入和一个上游格式变化输入。采集器要证明限速与重试不会重复写入,转换器要证明数量守恒,查询服务要证明空结果与重名结果稳定,前端原型则要记录它依赖的接口版本。这样的少量“黄金样例”比试图立即补齐整套测试更适合工程考古。
证据边界
本文逐项登记了 37 个单用途、组件型或快照型仓库,覆盖金融与汽车演示、新闻和社交采集、境外投融资转换、公司资料、行业报告、股票、专利 XML、百科语义服务、公司聚合接口、数据存储迁移、早期排名、前端原型、产业链解析、第三方依赖与空仓库。它们多数没有独立 Jira 或 Confluence 项目,部分与海纳、大学排名、AI 报告、产业链、数据生产线和内部工具存在交叉。
代码可以证明技术路线和局部实现,不能证明所有脚本持续运行、数据长期更新或服务进入生产。仓库提交数也不能直接代表工作量:一次导入可能包含大量第三方文件,频繁提交也可能只是配置和日志调整。本文不公开真实命名空间、人员身份、内部地址、凭据或采集目标的敏感细节。