做女装选品和趋势分析的人,大多都有这种体会:爆款来得快,去得也快,今天还在热门榜上的款式,过几天再去看可能已经改款或者直接下架。这个项目最开始的动机,就是想用分布式爬虫把某短视频平台上女装类目的公开商品、短视频和直播间数据成规模地抓下来,放进Hadoop里做离线分析,最终对接一个推荐系统,帮两类人做决策:一类是普通消费者,让他们在大量款式中更快找到适合自己的衣服;另一类是做女装的小商家和运营,希望他们能判断下一阶段上什么款、定什么价位。整条链路大致可以拆成四块:分布式采集、Hadoop存储与计算、数据分析建模、推荐服务。这篇文章会按这个顺序把系统实现讲清楚。无论你是正在做毕业设计的学生,还是想在公司里搭一套趋势分析系统的工程师,下面这些设计思路、字段口径和踩坑记录,都有直接参考价值。
这个项目实际落地之后,最大的感受是:真正难的从来不是某个算法,而是把数据采得全、存得稳、算得准。尤其是女装这类强视觉、强时效、强季节性的类目,数据字段又多又杂,标题里全是口语词和表情符号,直播间和短视频的数据还在持续变化。如果一开始不给数据分好层、定好口径,后面分析出的任何一个指标都可能是错的。下面先从女装数据本身的特殊性说起。
1. 女装数据的特殊性:为什么通用方案在这里容易变形
1.1 女装类目到底有哪些值得分析的数据字段
很多人第一反应是,女装不就是商品标题、价格、销量吗?真做起来会发现远不止这些。这个项目里,我按来源把数据分成了四组:
- 商品维度:商品ID、标题、类目路径、价格和原价、销量、库存、主图链接、SKU结构(尺码、颜色)、面料和版型描述、上架时间、是否支持7天无理由等。
- 内容维度:关联短视频的文案、话题标签、BGM信息、视频时长、点赞数、评论数、收藏数、分享数,以及直播间的开播时长、观看人数、同时在线峰值。
- 达人维度:达人ID、粉丝量、作品数、带货口碑、历史直播场次、场均销量、主要风格定位。
- 行为维度:用户在商品页的曝光、点击、加购、下单、收藏行为,包含时间戳和用户脱敏ID。
这些字段最大的特点是非结构化程度很高。商品标题往往长这样:“法式小众设计感衬衫女2024新款气质通勤高级感上衣”,里面风格词、场景词、年份词、卖点词全堆在一起。短视频文案则充满口语和表情符号,经常出现“谁懂啊家人们”“狠狠拿捏”这种需要专门词典才能解析的表达。如果只做关键词统计,分析结果会非常粗糙。
1.2 单机采集和关系型数据库为什么扛不住
做之前我调研过,很多小型团队用单机Scrapy加MySQL就把数据抓了。问题在于女装数据的两个特性:量大、变化快。
单机爬虫一旦遇到某个任务的请求量变大,很容易拖垮进程,而且没有状态共享,多开几个进程处理不同任务时,去重逻辑只能在本地做,同一件商品很容易被重复抓。同时,女装商品的生命周期极短,部分爆款可能只存活一两周,一年内上架的款式可能达到百万级。如果每天要对百万商品做一次增量更新,单机的带宽和存储会被迅速打满。
存储上,MySQL放业务元数据没问题,但商品详情、视频文案这种长文本和JSON结构,放进关系型数据库里既占空间又不好查。一次想按“近7天销量Top500、价格在100到200之间、标题含通勤关键词”来筛数据,SQL写起来复杂且效率低。这正是Hadoop和Hive这类离线大数据组件适合的场景。
1.3 先想清楚合规边界
做这个项目之前,必须先划定数据采集的边界,这不是套话,而是系统工程里最容易被忽视的一环。我只抓取平台公开可见的信息,比如商品列表页、详情页、达人主页里对外展示的内容,不采集任何用户隐私数据。请求频率严格限制,不给目标平台带来压力,不处理任何需要登录后才能看到的数据,不做验证码对抗,也不去破解任何风控机制。这些数据只用于内部研究和系统原型的验证,不会对外二次售卖。
为什么要把合规写在文章前面?因为在真实的分布式爬虫项目里,最大的风险往往不是技术问题,而是方案本身就踩了线。如果你做类似项目,建议在设计阶段就确定好采集范围、频率上限和数据用途,并把限速逻辑写进框架层。
2. 系统总体架构:采集、存储、计算、推荐的分工边界
2.1 四层架构与数据流向
整个系统的架构可以用一句话概括:爬虫负责把外部数据搬进来,Hadoop负责把数据组织好算清楚,推荐服务负责把分析结果用起来。
数据流是这样走的:分布式爬虫集群从某短视频平台采集原始数据,统一封装成JSON格式,先写入Kafka做缓冲,再由消费任务落到HDFS。之后,离线ETL任务把HDFS里的原始JSON清洗加工,生成Hive的ODS层、DWD层和ADS层表。推荐服务需要的结果表从Hive导出到MySQL或者Redis,供在线接口读取。
这里有一个容易被忽略的点:为什么中间要加Kafka。最初我也想过让爬虫直接写HDFS,后来发现两个问题。一是爬虫进程和HDFS客户端都在同一批机器上,高并发时NameNode的压力会很大;二是如果任务短暂失败,数据会丢。加一层Kafka之后,爬虫只负责把消息投递到Topic,消费端可以灵活控制写入HDFS的速度,且消息只要没被确认就会重发,整个链路的稳定性高很多。
2.2 技术选型:为什么是Hadoop这套,而不是MongoDB或ClickHouse
不少人问过我,既然是JSON数据,为什么不用MongoDB?既然要做分析,为什么不用ClickHouse?我当时的理由很简单:项目的主要场景是每天全量重跑离线指标,数据晚几个小时出结果完全可以接受,但要保证能处理上亿级别的关联查询,同时团队对SQL的熟悉程度远高于写MapReduce代码。Hive加Spark SQL这套组合,正好能满足需求。
选型对比可以列得直白一点:
| 组件方案 | 适合场景 | 在本项目里的问题 |
|---|---|---|
| MongoDB | 灵活存储JSON、快速读写单条 | 做大规模JOIN和聚合统计较弱,按天全量重算成本高 |
| ClickHouse | 极速OLAP查询 | 数据模型偏宽表,复杂ETL和UDF支持不如Spark SQL灵活 |
| HDFS + Hive + Spark SQL | 海量离线批处理、复杂ETL | 查询延迟以分钟计,不适合在线场景,但业务完全能容忍 |
最终选型是:HDFS存原始文件,Hive建数仓分层,Spark SQL做复杂计算,MySQL和Redis承接推荐API的最终结果。这套组合在整个项目里没有明显短板,而且招聘和学习成本都不高。
2.3 先算一笔数据量账
做技术方案时,建议先把数据量估算写明白,不然后面存到一半才发现磁盘不够,就只能尴尬地删数据。以这个项目为参考:日均抓取50万个在售女装商品,每个商品平均关联5条用户行为记录,当天新增行为记录就是250万到300万条。再加上商品详情更新产生的历史快照,一个月下来原始JSON的量大概在10亿行级别,单行JSON平均800字节,仅原始数据一个月就是800GB左右。
如果Hadoop集群配置3个副本,则HDFS实际占用要按2.4TB去规划。清洗后的Parquet列式存储会小很多,大约是原始数据的四分之一。所以我当时配的是3台数据节点,每台4TB可用空间,同时跑NameNode、ResourceManager这些角色。如果你只需要支撑课程设计或者Demo,可以把数据规模压到十分之一,但架构不需要变。
3. 分布式爬虫的工程细节:任务拆解、调度、去重和增量
3.1 任务模型设计
爬虫层第一步不是写Spider,而是设计任务类型。我参考常见电商大数据项目做了一版模型,把采集任务分成四类:
- 类目任务:按女装大类目和子类目遍历商品列表,例如“连衣裙”“T恤”“牛仔裤”。这类任务是地基,决定商品库覆盖是否全面。
- 关键词任务:从热搜词、竞品词、趋势词出发搜索商品,用来捕捉类目里还没被商品库覆盖的新款。
- 达人店铺任务:按达人主页抓取他的在售商品,重点用于达人效应分析。
- 商品详情任务:对每个商品ID回查详情,补充价格变动、评分、SKU结构等信息。
每种任务都带优先级、请求频率、失效时间。任务下发时不直接用URL,而是用统一的参数封装,避免不同任务之间形成重复采集。
3.2 Redis任务队列与动态优先级
任务分发我用了Redis,原因很简单:列表操作是原子的,多个爬虫节点同时消费不会重复拿到同一个任务。具体的做法是:用两个队列,一个待处理队列,一个延时队列。爬虫节点从待处理队列取出任务,处理完写结果,失败则投回延时队列,延时时间按失败次数递增,最多重试3次。
动态优先级用Redis的Sorted Set实现。比如关键词任务如果检测到某个词的搜索热度在涨,就把它的score调高,让爬虫节点优先处理。这里比较实用的一个经验是:不要在一个队列里塞太多任务,宁可把队列按来源切分,比如task:category:女装、task:hotword:女装,否则某个队列堆积时会拖慢其他任务的进度。
3.3 URL规范化与指纹去重
分布式环境下最烦的问题就是重复抓取。同一个商品列表,可能因为排序参数不同、带不带推广参数,生成完全不同的URL。第一版系统里我直接拿URL做去重键,结果发现同样的商品被抓了四五遍。
后来加了一个规范化步骤:先把URL里的query参数按key排序,然后去掉utm_source、share_token这类统计参数,再用 title 和关键参数拼接成唯一指纹。指纹生成用的是md5,存储则用BloomFilter加Redis Set双层校验。BloomFilter负责快速判断“大概率见过”,Redis Set负责精确确认。代码大致是这样:
import hashlib import re from urllib.parse import urlparse, parse_qsl, urlencode def normalize_url(url): parsed = urlparse(url) params = dict(parse_qsl(parsed.query)) params.pop("utm_source", None) params.pop("share_token", None) params = {k: params[k] for k in sorted(params) if params[k]} base = f"{parsed.scheme}://{parsed.netloc}{parsed.path}" return f"{base}?{urlencode(params)}" def gen_fingerprint(url, task_type): normalized = normalize_url(url) raw = f"{task_type}:{normalized}" return hashlib.md5(raw.encode("utf-8")).hexdigest()这个指纹函数在整个生命周期里都很关键。后续做增量更新时,判断一个商品是否要抓取,本质上也是先算指纹、再查存储。
3.4 增量更新策略
女装数据不像新闻,商品不会一天变十次,所以没必要所有商品都高频抓取。我按热度把商品分成了三档:
| 商品热度等级 | 更新频率 | 说明 |
|---|---|---|
| 高热度 | 每6小时一次 | 销量Top榜、直播间热卖款、达人置顶商品 |
| 中热度 | 每天一次 | 有一定互动量但未爆发 |
| 长尾 | 每3天一次 | 销量低、上架时间久、无明显增长信号 |
实现这个策略,依赖的是上一节提到的任务指纹。每次更新前先查上次更新时间字段,如果距离小于该档位对应的间隔,就直接跳过。这个策略让日均抓取量下降了不少,但同时保证了热卖款的数据新鲜度。
3.5 限速、代理与合规的实现
分布式爬虫最容易出现的生产事故就是请求频率失控。我在每个Spider节点加了一个限速中间件,用令牌桶算法保证单节点每秒请求数不超过设定值。同时准备了一个代理池,正常逻辑里每次请求会从代理池随机选一个IP,避免把压力集中在单个出口。
这里要特别说明:代理池的目标是模拟正常用户的分布,不是绕过平台的风控。遇到验证码、滑块这类异常,系统会记录异常日志并立即停止对该任务的抓取,等人工确认后再恢复。这类机制带来的结果就是采集速度看着没那么快,但稳定性很好,整个项目运行期间没有出现过IP被连带封掉的情况。
4. Hadoop链路搭建:HDFS目录规划、Hive建模与离线ETL
4.1 HDFS目录与分区规范
Hadoop这一层最需要提前定好的就是目录和表命名规范。我的做法是:原始JSON统一放在/data/mplatform/ods下,按采集日期分区;清洗后的Parquet文件放在/data/mplatform/dwd下,按业务日期分区。
每个业务表的目录结构长这样:
/data/mplatform/ods/goods_info/dt=2024-05-20/ /data/mplatform/ods/goods_info/dt=2024-05-21/ /data/mplatform/dwd/goods_dwd/dt=2024-05-21/分区字段dt统一用字符串类型,格式固定为yyyy-MM-dd。这样写查询时可以直接用等值条件,Hive能精确裁剪分区,查询速度会快很多。分区字段类型这个细节后面还会再提到,因为它曾经坑了我一次。
4.2 Hive三层模型设计
Hive表我按标准数仓思路分了三层:ODS、DWD、ADS。
ODS层直接映射HDFS上的原始JSON文件,表结构里面只解析出最基础的字段,剩下的内容保留在一个叫做raw_data的字符串字段里。这样做的好处是,一旦后续发现解析逻辑有bug,还能从原始数据重新算,不需要重新抓取。
DWD层做扁平化和标准化,把JSON里真正需要的业务字段全部提取出来,比如价格、销量、点赞数、风格标签、颜色、面料等。DWD层的表结构完全按分析需求设计,字段类型明确,枚举字段都做了注释。
ADS层放的是最终指标表,比如“每日女装价格带销售统计”“近7天风格热度榜单”“达人带货月度排行”。ADS层表一般很小,算完后导出到MySQL,供推荐系统或者Dashboard直接读取。
Hive建表的一个关键示例是DWD层商品表:
CREATE EXTERNAL TABLE dwd.goods_dwd ( goods_id STRING, title STRING, category_path STRING, price DOUBLE, original_price DOUBLE, sales_volume BIGINT, style_tags ARRAY<STRING>, color STRING, fabric STRING, like_count BIGINT, comment_count BIGINT, collect_count BIGINT, share_count BIGINT, on_sale_time STRING ) PARTITIONED BY (dt STRING) STORED AS PARQUET LOCATION '/data/mplatform/dwd/goods_dwd';4.3 ETL脚本核心清洗规则
离线ETL是整个项目里最枯燥但最核心的部分。清洗规则我总结成几个固定动作:
第一,去重。同一商品同一天可能从多个入口抓到,先按照goods_id加dt做去重,保留信息最全的一条。第二,类型标准化。价格字段统一转成DOUBLE,销量转成BIGINT,时间统一转成yyyy-MM-dd HH:mm:ss。第三,文本清洗。标题里的表情符号、多余空格、网址都去掉,同时把“衣服”“女装”这类无意义高频词剔除。第四,空值兜底。价格为空时置为0,销量为空时置为0,不会因为一句NULL把整个聚合结果算崩。
清洗这一步看着简单,实际上决定了下游指标的可信度。我在做价格带分析时曾经发现9.9元以下的商品特别多,排查才发现是部分商品价格字段里混入了货币符号,清洗时没处理干净。这类脏数据不清理,最后得出的任何结论都不能信。
4.4 小文件合并与数据倾斜的规避
Hadoop上有两个经典问题,女装数据项目也躲不开:小文件过多和数据倾斜。
小文件问题主要出在采集端:每个Spider节点每写一批数据就生成一个文件,如果文件特别多,Hive跑任务时Map数量会爆炸,启动开销比计算还大。解决办法有两个:一是把Kafka消费端的写入改为按时间攒批,比如每5分钟写一个文件;二是定期对ODS层做小文件合并,用INSERT OVERWRITE重新写一遍分区,让一个分区只保留少量大文件。
数据倾斜则更容易出现在按风格标签做聚合的场景。某些风格词,比如“气质”“显瘦”,出现的频率远高于其他词,导致个别Reduce任务处理的数据量巨大。通用的解法是给热点key加随机前缀打散,再分两阶段聚合。这种写起来比较复杂,好在这个项目里倾斜现象不算严重,简单调大spark.sql.shuffle.partitions就缓解了。
5. 数据分析到底算些什么:热度、价位和人群偏好
5.1 价格带与销量分布的交叉分析
基础的分析先从价格带开始。把价格分成“0到99”“100到199”“200到499”“500到999”“1000以上”五档,再看每个档位内的销量占比、互动量占比。
这段分析用的SQL很简单,但结论非常直观:
SELECT CASE WHEN price < 100 THEN '0-99' WHEN price < 200 THEN '100-199' WHEN price < 500 THEN '200-499' WHEN price < 1000 THEN '500-999' ELSE '1000+' END AS price_band, COUNT(DISTINCT goods_id) AS goods_cnt, SUM(sales_volume) AS total_sales FROM dwd.goods_dwd WHERE dt = '2024-05-20' GROUP BY 1 ORDER BY total_sales DESC;实际跑下来发现,销量最集中的价格带并不是最低价的9.9元档,而是100到200元这个区间。原因是平台的用户以年轻女性为主,她们对39元包邮这类低价款的质量信任度偏低,愿意为设计感和质感付更高价格。这个结论直接影响了后面推荐系统的排序特征权重。
5.2 风格、颜色、面料的标签化
女装分析离不开款式标签。我们没有一开始就上复杂的模型,而是先用“词典加正则”的方式做了一套标签体系。词典里每类风格都配了一批种子词:法式对应“法式”“复古”“碎花”,通勤对应“通勤”“上班”“气质”,辣妹对应“辣妹”“修身”“短款”,韩系对应“韩系”“简约”“宽松”。
按这套逻辑给商品打风格标签,再统计每个风格下的销量占比和价格中位数,能清楚看到不同风格的天花板。分析结果里,法式和通勤风格的价格中位数明显高于其他风格,而辣妹风格的销量波动最大,几乎完全跟着热点走。这类标签体系的精度虽然不如深度学习模型,但胜在逻辑清晰、好解释,作为第一版绰绰有余。
5.3 达人效应与内容特征
达人维度的分析,核心是想搞清楚一个问题:商品卖得好,到底是靠商品本身,还是靠达人的流量。我用商品的核心卖点字段和达人的粉丝量做了交叉分析。结论是:头部达人能带火任何价位的商品,但腰部和尾部达人的带货能力差异很大,粉丝量相近的达人可能效果差出好几倍。进一步看,视频的完播率和评论互动率与商品转化率的相关性比粉丝量更高,这说明内容质量比粉丝规模更重要。
这个发现对推荐系统的帮助也很大:给用户做推荐时,不能只看商品销量,还要看商品关联的视频内容质量。于是推荐特征里加入了“关联视频互动率”和“达人垂直度”两个字段。
5.4 换季、上新与直播时段规律
时间规律的分析要分成两个口径看。一个是季节口径,以月为单位看风格词的搜索热度变化:每到3月和9月,通勤装和外套类目的热度会提前两周出现;换季商品的销量峰值并不是在季节开始的当月,而是在季节开始前一周到两周。另一个是直播时段口径,晚上8点到11点是女装直播间的黄金时段,这个时间段的转化率平均比其他时段高出30%以上。
这类规律的价值在于指导业务动作:上新排期要提前,直播时段要卡准。对系统来说,它们会被转化成时间特征,加入推荐排序模型和运营看板。
6. 推荐系统设计:给谁用、推荐什么、怎么落地
6.1 两类用户和两种推荐场景
这个项目的推荐系统一开始就不是只服务消费者。我把它拆成两个场景:C端消费者场景和B端运营场景。
C端推荐做的是“相似款发现”和“猜你喜欢”。用户在浏览一件法式衬衫时,系统给他推荐其他风格相近、价格区间相仿的女装商品。B端推荐做的是“选品参考”,比如给运营人员推荐下一阶段可能成为爆款的潜力商品清单,推荐依据是历史爆款的共性特征与当前热度的交叉。
两类场景底层共享同一套商品特征和用户特征,但推荐目标和排序指标不同。C端更看重点击和收藏,B端更看重潜力的爆发概率。
6.2 行为日志的采集与清洗
推荐系统最重要的输入是行为数据。系统通过前端埋点采集商品曝光、点击、收藏、加购、购买五类行为,每一条行为都带上时间戳、用户脱敏ID、商品ID、行为类型和所在场景。数据统一进入Kafka,再落到HDFS,离线清洗成一张“用户行为宽表”。
这张宽表是ItemCF和排序模型的基础。清洗时要注意两个细节。一是行为时间窗口:计算相似度时,只用最近90天的行为,太旧的行为会引入过期偏好。二是行为权重:一次点击权重为1,收藏为3,加购为5,购买为10,这样在计算商品相似度时,强意向行为能发挥更大作用。
6.3 基于Spark的ItemCF实现
第一版推荐算法选的是ItemCF,也就是“用户喜欢A商品,那么和A相似的商品B也该推荐给用户”。ItemCF在商品数量大但用户行为相对稀疏的场景下,效果比较稳定,而且实现不复杂。用Spark实现的核心步骤是:先按用户分组收集他交互过的商品列表,再统计两两商品在同一用户行为中共现的次数,作为商品相似度。
以下是简化的核心计算逻辑:
from pyspark.sql import functions as F interactions = spark.sql(""" SELECT user_id, goods_id, behavior_weight FROM dwd.user_behavior_dwd WHERE dt = '2024-05-20' """) # 按用户聚合商品列表 item_groups = interactions.groupBy("user_id").agg( F.collect_list("goods_id").alias("goods_list") ) # 生成同用户下的商品共现对 def gen_pairs(goods_list): goods = sorted(set(goods_list)) for i in range(len(goods)): for j in range(i + 1, len(goods)): yield (goods[i], goods[j], 1) yield (goods[j], goods[i], 1) pairs = item_groups.rdd.flatMap( lambda r: gen_pairs(r["goods_list"]) ).toDF(["goods_a", "goods_b", "cnt"]) sim_matrix = pairs.groupBy("goods_a", "goods_b").agg(F.sum("cnt").alias("co_cnt"))得到商品相似度矩阵之后,保存到Redis里,线上服务直接查。ItemCF要注意的问题是矩阵可能非常稀疏,所以相似度计算时加入了“惩罚热门商品”的因子:如果一个商品被几乎所有用户交互过,它和任何商品都相似,不代表它有价值,需要降低它的权重。
6.4 新品冷启动与长尾保底
ItemCF没法解决新上架商品的推荐问题,因为新商品没有任何用户行为。这时候要用内容特征做相似度兜底。具体做法是提取新商品的风格标签、颜色、面料、价格带,和已有的商品做一次向量相似度计算,找到最相近的5到10个老商品,然后参考它们的用户群体去推荐。
长尾商品则靠“探索流量”保底:在推荐列表里留出5%的位置,随机放一些非热门但用户可能感兴趣的款式,这就相当于给长尾商品曝光机会。从实际效果看,探索位虽然点击率略低,但收藏率反而更高,说明用户对“没见过的款”有真实的兴趣。
6.5 排序模型轻量化实践
候选集有了,最后一步是排序。如果用深度学习模型,冷启动和部署成本都会偏高,所以我选了轻量化的逻辑回归加GBDT组合:先用逻辑回归做一版基础排序,保证结果稳定可解释;再在线上积累一两周数据后,用XGBoost或LightGBM训练一版点击率预估模型。
排序特征主要包括:商品价格与用户历史购买均价的差、商品风格与用户偏好风格的匹配度、商品近7天互动率、关联视频完播率、达人历史带货评分。这套特征集合覆盖了价格、内容、达人三个层面,特征工程做完之后,线上点击率比第一版规则排序有明显提升。
7. 实测阶段的高频问题与完整排查路径
7.1 去重指纹冲突导致重复采集
项目运行到第二周,我发现HDFS里出现了大量重复商品,同一个商品ID有多个采集记录。一开始以为是Scala解析的问题,后来逐条对比原始JSON才发现,有些商品详情来源于同一个商品的不同SKU入口,url里带的参数顺序不同,导致指纹不一样。
排查链路是这样的:先抽一条重复记录对比URL,发现参数分布不一样;接着查指纹生成函数,确认参数排序逻辑在部分URL上没生效,原因是URL里带了中文编码和未转义字符,parse_qsl解析时丢了一部分值。修复方案是先把URL做一次unquote再解析,并把解析失败的任务纳入异常队列。从那以后,重复率从4%降到了0.1%以下。
7.2 分区剪裁失效导致Hive查询慢
某一天凌晨的ETL任务跑得特别慢,一个简单的聚合查询耗时比平时多了快20分钟。排查时先看了执行计划,发现扫描的分区数目不对,明明只需要一天的数据,却扫了全部七天的分区。
原因最后定位在分区字段的数据类型上。分区字段在底表里被定义成了TIMESTAMP类型,而我在查询时用dt = '2024-05-20'做等值匹配,Hive无法把字符串直接转成时间戳,也就没法做分区剪裁。修复方法是把建表语句的dt STRING类型固定下来,并且强制要求所有查询都直接使用字符串常量。这个坑非常容易踩,建议大家在设计表结构时就把分区字段的类型当成一种约定写进规范。
7.3 中文文案乱码与emoji干扰
分析风格标签时,发现一部分商品的标题清洗后变成了一堆乱码,另一部分标题里的表情符号直接污染了关键词统计。源头有两个:采集时某些响应没有声明编码格式,导致HTML解析时按错了字符集;清洗时没有把emoji和其他特殊字符提前剔除。
修复方案分两层。采集层统一强制使用UTF-8解码,遇到解码异常就重试一次。清洗层在文本处理步骤里先做一次字符过滤:只保留中英文、数字和常用标点,其他字符一律去掉,再用一个自定义的服装词典做分词。经过这轮修复,标题分析的质量大幅提升,风格标签统计也不再出现那些诡异的杂音。
7.4 销量字段口径不一致导致指标失真
分析销量趋势的时候,发现不同商品的销量量级差异巨大,有些商品一天卖几千件,有些商品卖几万件,一开始以为是正常现象,后来抽样对比才发现在售商品页显示的销量和直播间场景里显示的销量根本不是同一个口径:有的含预售,有的不含,有的算的是近30天总计,有的算的是一天实时数据。
这个问题最终是靠DWD层的口径标准化解决的。我把销量拆分成了几个字段:实时销量、近30天累计销量、直播间销量,并在每个字段的注释里写明统计口径。下游分析只使用统一口径的字段,例如做价格带分析只用近30天累计销量,做实时热度榜用实时销量。这个教训的重要程度几乎超过所有技术问题,因为口径不一致导致的错误会直接让业务方做错误决策。
如果让我重新做一遍这个项目,我会把三分之一的力气提前花在数据口径定义上。先写清楚每个字段从哪里来、按什么规则计算、能用于什么场景,然后再开始写爬虫和算法。项目推进到后期,最花时间的不是Hadoop调优,也不是推荐模型迭代,而是不断核对数据、清洗数据、修正口径。最后再分享一个小技巧:做这类大数据系统,最好先用一个月的小规模样本把整条链路跑通,哪怕数据只有几万条,也要让SQL、HDFS、推荐接口、看板全部联通,再放量去抓全量数据。这样既能验证架构,也能提前把所有脏数据的坑踩完,后面的活会轻松很多。