← 返回项目档案

项目复盘 · 2020

长尾工程资产:小型采集器、转换脚本与服务快照

把二十余个低提交、单用途或快照型仓库放回同一条数据链,分析它们采用的技术、真正可复用的设计,以及难以继续维护的原因。

工程考古数据采集ETL技术债务

GitLab 中有一批不适合单独写成产品案例的仓库。它们有的只有一次提交,有的是针对单一来源的采集脚本,有的是把运行环境和生成文件一起保存的代码快照。单看每个仓库,容易得到“逻辑很少”的结论;放回整条数据链后,却能看出团队当时如何快速补齐数据能力。

本文不按仓库数量制造项目数量,而是按职责把它们组织为采集适配器、传输与转换、研究计算、查询服务和前端原型五层。

长尾仓库在数据链中的位置
外部来源采集适配器原始响应与抓取批次字段清洗
字段清洗传输与转换结构化存储查询服务
结构化存储研究计算指标与关系结果查询服务
查询服务前端原型研究人员
失败记录隔离队列修复与重放传输与转换

纳入本篇的资产清单

这里的“长尾”指不适合作为独立产品叙述,并不代表代码没有价值。部分仓库也在对应复杂项目文章中作为主证据,本篇只分析其可复用的工程模式。

  • 通用和单来源采集:finance/demolambdax/autohomelambdax/baidu_newslambdax/crunchbaselambdax/ycy-zk个人空间-周川/news_spider个人空间-周川/stock个人空间-周川/industry_research个人空间-周川/social_organization个人空间-周川/company个人空间-陆川/company_news
  • 教育与社交资料补充:lambdax/collage_weibo个人空间-林宁/forwardpathway个人空间-林宁/instagram_collage个人空间-林宁/twitter_collage
  • 文书与存储迁移:个人空间-林宁/DBtransflambdax_go/wenshu_doc_synclambdax_go/wenshu_doc_filterlambdax/company个人空间-周言/neijinglambdax/courtlambdax/wenshu个人空间-陆川/court。文书主链仍以“海纳”文章为准。
  • 服务、计算与原型快照:lambdax/ksslambdax/selenium_shuzhilambdax/wikidb_weblambdax/bigtable_apilambdax/vue-company个人空间-林禾/fund个人空间-江叙/DataStasticsfinance/third_party个人空间-江舟/shtTest
  • 产业链的并行解析与演示:industry-chain/parser/industry-chainindustry-chain/parser/解析器分支个人空间-周川/industry-chain个人空间-陆川/industry-chainlambdax/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 到结束,用独立错误通道和取消上下文汇总失败,并为目标表设置唯一键来支持重跑。

下面是文书同步仓库的源码节选(已脱敏),连接配置和业务标识均未保留。代码展示源端如何用游标递归分页;最后一项被用作下一页起点,因此调用方还必须验证存储接口是否会重复返回边界项:

go
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 报告、产业链、数据生产线和内部工具存在交叉。

代码可以证明技术路线和局部实现,不能证明所有脚本持续运行、数据长期更新或服务进入生产。仓库提交数也不能直接代表工作量:一次导入可能包含大量第三方文件,频繁提交也可能只是配置和日志调整。本文不公开真实命名空间、人员身份、内部地址、凭据或采集目标的敏感细节。