把公开目录变成价格历史的一条管线
人工去看竞争对手,得到的只是一张快照,没有办法和上周比较。我们做的这条管线在渲染后的浏览器里逐页读取公开目录,自己弄清一个站点的类目结构与翻页方式,在信任自己的抽取配方之前先证明它确实能取到真实商品,并且每一轮都写下一个价格点。之后出来的报告是一份差异而不是一堆数据,而且在有人签核之前不会送出去。
- 行业零售与电商
- 合作方式自研平台,内部开发
- 状态端到端在跑,仍有未完成的部分
一个闭环:采集、归一、比较、打包、签核
管线按每个客户各自的计划运行。它先在竞争对手自己的域名下抓取值得读的页面,再采集商品目录本身:一个类目接一个类目、一页接一页,都在渲染后的浏览器里进行。因为用 JavaScript 搭起来的目录,并不在一次普通请求返回的 HTML 里。评价与公开动态汇入同一个存储,所以产出的是一幅竞争对手的画像,而不只是一张价目表。
采集到的一切都会被归一到同一种形态。金额会按欧洲、土耳其和美国的书写方式解析成数字加币种。商品按身份更新,因此新的一轮是刷新而不是复制;首次出现与最近出现的日期,正是让新增与消失可以被算出来的东西。带价格的每一轮还会写下一个价格点,报告因此可以是一份差异而不是一堆数据。
难在哪里
读懂一个竞争对手不是抓取问题。这条管线正是针对下面三种失败设计的。
一张快照没有可比较的对象
人工看价格,得到的清单在抄下来的那一刻就已经旧了,而且背后什么都没有。品类团队真正要问的是上一次之后什么动了,再仔细的抄写也答不上来,因为此前的状态从来没有被保存下来。
目录不在页面源码里
今天的店铺在页面加载之后才搭出商品栅格,把图片挂在懒加载属性上,并按各自平台习惯的方式把一个类目拆到多页。去读类目页的源码,拿到的是一个空壳。
读取失败和空货架看起来一模一样
危险的不是崩溃,而是那种干净收尾、报告什么都没找到、实际上只是网络抖了一下的那一轮。这种结果和真实变化无法区分,还会污染之后的比较。
我们做了什么
一个按计划运行的闭环:够得着目录、弄清它的结构、让数值可比,再把差异变成一个人愿意签字的东西。
真正够得着目录的采集
抓取只留在竞争对手自己的域名内,遵守 robots.txt,自我限速,限定走多远,并且先访问价格页和商品页,再看一般页面。目录采集是另一件事:抽取在渲染后的页面里进行,因为用 JavaScript 搭出来的栅格只有在那里才存在。
- 只在同一域名内抓取,遵守 robots.txt,自我限速,先读商业页面。
- 三个采集引擎自动回退:缺少一个可选依赖只会让抓取降级,而不是失败。
- 查询参数翻页、下一页链接、单页列表和无限滚动,都是靠实地试探判断出来的,而不是靠假设。
模型提出、代码证明的结构
站点真正的类目来自它自己的链接和站点地图,然后再做分类。没有任何东西仅凭这次分类就被采纳:一道解析并抽取的关卡必须证明,提出的配方在它声称适用的页面上确实能取到商品。
- 候选类目来自站点自身的导航和站点地图,每一个在使用前都要通过验证。
- 一个抽取配方只有在被证明能取到商品之后才被信任,分类环节还有一条严格的禁止臆造规则。
- 名称与价格的抽取会沿着一串备选逐级回退,并优先选择最短的干净匹配,避免把整张商品卡当成名称。
真正能拿来比较的数值
页面上的价格是一串文本:带币种符号,带一个在不同国家含义不同的千位分隔符,旁边往往还有一个折扣角标。所有内容在入库之前都会被解析成金额加币种;品牌则来自一份拒绝猜测的人工维护清单。
- 金额按欧洲、土耳其和美国的书写方式解析,币种在旁边一并读出。
- 百分比角标在解析前先被剥掉,最后一个带币种的金额胜出,因此折扣绝不会被当成价格存下来。
- 商品跨轮次按身份去重,重新读取是更新一件商品,而不是复制出一件新的。
一份是差异的报告,和一个为它签字的人
带价格的每一轮都会写下一个历史点,所以下一轮本身就是一次比较。变化报告完全由磁盘上已有的数据算出:不调模型、不走网络、不加新表。渲染之后留下的是待审状态,而签核是唯一会发送的代码路径。
- 涨了什么、跌了什么、哪些是新品、哪些消失了、有哪些新评价,全部确定性地算出来。
- 一份表格数据包和一份幻灯片,相同输入渲染出逐字节相同的文件。
- 在有人签核之前不会有任何东西进入客户的收件箱;没有配置邮件服务时,发送会直接拒绝,而不是记下一次并未发生的投递。
让这份报告值得读的那些决定
四个关于做对、而不是关于好看的选择。
读取失败不是空货架
请求失败、页面加载了却是空的、页面上有商品,在代码里是三种不同的结果,而不是一种。一轮采集会依据这些证据把自己标为完成、部分完成或失败,因此一次短暂的网络故障绝不会以一份干净但空无一物的结果出现。
停止条件归代码所有
缺口检测是基于规则的,每一轮都有硬性上限;可选的模型评审是加在上面的一层,默认关闭。因此在有人刻意打开之前,一次扫描在模型上不产生任何花费,也没有任何语言模型来决定工作什么时候算完成。
不依赖数据库的顺序
时间顺序保存在读取时捕获的微秒级时间戳里,而不是插入顺序里,随机标识只作为最后的并列裁决。因此无论底下用的是哪个数据库,价格历史读出来都一样。
在唯一的收口处做隔离
每一次读写都要经过唯一的作用域收口,并由数据库自身的行级安全兜底;如果服务自己的运行角色可能绕过它,服务会拒绝启动。比较引擎继承这条边界,而不是重新实现一遍。
改变了什么
只讲定性,因为这个项目交出的是一个能跑的闭环,而不是一个被测量过的结果。
历史
有了比较,而不只是快照
第一轮产出一份目录。之后的每一轮产出一份差异,因为背后的价格连同读取日期早已落在磁盘上。
诚实
会承认自己漏了什么的采集轮次
读不到东西的那一轮会说出来,并把自己标为部分完成。下游不必再去猜:某一段是空的,究竟是对手变了,还是一次请求失败了。
已签核
先经过一个人,再到客户
渲染报告和发送报告是刻意分开的两步。从来没有东西因为计划到点就自己发出去;一次健康探测还能不碰网络、也不在模型上产生任何花费地跑完整个闭环。