恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
MarketClaw:市场数据采集与分析工具的设计与实现
首页
资讯中心
/
MarketClaw:市场数据采集与分析工具的设计与实现
MarketClaw:市场数据采集与分析工具的设计与实现
发布时间:2026/10/17 5:29:29
创新实训项目做了几周我们组的选题终于定下来并且跑通了第一版。这个项目叫 MarketClaw名字是我起的Market 是市场Claw 是爪子合起来就是“从市场里抓数据的一只爪子”。听起来有点野但干的事很明确——做一个面向公开市场信息的数据采集与分析工具把散落在不同渠道的商品价格、销量、用户评价和行业动态抓下来清洗成统一格式再通过图表和关键词趋势展示出来。第一期我们完成了从任务配置到统计报表输出的完整闭环这篇文章就把立项时怎么想的、架构怎么拆的、第一版怎么落地的以及中途踩过的一堆坑一次性写清楚。如果你也在做类似的“采集分析”类课程项目或者个人作品这篇应该有参考价值。1. 项目到底在解决什么问题1.1 一句话说清 MarketClaw 是什么MarketClaw 的定位是一个轻量级市场情报采集工具。所谓市场情报不只是“某个商品现在卖多少钱”还包括价格波动、促销周期、同类商品分布、用户评价里的高频关键词这些信息。传统做法是人肉打开网页逐个记录效率低且容易漏而 MarketClaw 做的事情就是把这块工作自动化你只需要在配置里写清楚“我要盯哪个渠道、哪些关键词、多久采集一次”剩下的采集、清洗、入库、统计都由系统完成。第一版我们只做了最核心的链路任务管理、数据抓取、字段清洗、数据存储、基础统计展示。目标用户是两类一类是电商相关方向做价格跟踪和竞品观察的同学或运营人员另一类是给毕业论文或市场调研收集结构化数据的同学。不是要做一套商业级别的爬虫框架而是提供一个门槛低、看得懂、能自己改的采集分析方案。1.2 需求是怎么来的这个选题不是导师直接给的而是我们几个组员一起从课程项目、个人经历和实际痛点中聊出来的。大家发现很多调研类作业其实非常依赖网络公开数据但收集过程极其痛苦复制粘贴容易出错抓下来的页面结构五花八门同一个商品在不同平台的价格字段叫法完全不同整理成 Excel 的时间比写分析报告还长。更痛的是数据没有一个统一口径。比如“价格”这个字段有的地方叫“售价”有的叫“到手价”有的还分“定金”和“尾款”如果直接拿原始字符串去分析得出任何结论都是不可靠的。所以我们就想做一个工具能让采集规则和清洗规则分离用一套代码去适配多个渠道。这其实就是 MarketClaw 最早的启发数据采集不难难的是把不同来源的数据变成同一种“语言”。1.3 第一期要交付到什么程度作为创新实训的第一个阶段我们没有野心太大定的目标是“打通链路验证可行性”具体交付四件事一是可配置的采集任务系统不写死采集对象所有 URL、关键词、采集周期从配置文件读取二是适配至少两类不同结构的公开页面并输出统一格式的 CSV 和数据库记录三是做一套简单的数据看板接口能按天查看采集量、按关键词查看热度四是完整的项目文档和启动说明保证下个学期接手的同学能直接跑起来。今天看这个目标非常匹配“先跑通、再优化”的思路事实也证明先把链路打通比一开始就追求分布式、高并发要务实得多。2. 技术选型与架构设计的取舍2.1 技术栈选型为什么是 Python 全家桶技术栈是我们花时间讨论比较多的部分。最开始有人提议用 Java 配合现成的爬虫框架也有人提议用 Node.js说前后端语言统一更方便。最后我们统一选择了 Python理由很直接一是生态成熟度高。采集、解析、清洗、训练每一个环节都有现成库不用事事从零造轮子尤其我们组里有几个同学是第一次接触这个方向用 Python 降低上手门槛。二是语法表达力强。我们经常要临时调整清洗规则Python 写起来快改起来也快不像静态语言改一次要重新编译、重启服务。三是数据分析顺手。项目后面规划了要做关键词趋势、价格预测这些必然用到 pandas 甚至 sklearn数据链路与算法链路共用一种语言可以减少数据格式转换成本。具体选型是 requests BeautifulSoup/lxml 做抓取和解析pandas 做清洗SQLite 做第一版存储Flask 做展示接口。这个组合虽然朴素但每个人都能快速上手。特别说明一下数据库没有一开始就上 MySQL 或者 PostgreSQL是因为第一版数据量不会很大SQLite 单文件部署方便后期要换也能平滑迁移。2.2 模块划分四个核心模块的边界架构上我们参考了常见的数据管道模型把系统拆成了四个模块每个模块职责单一接口边界尽量清晰采集器模块负责执行抓取任务它的输入是一个“任务对象”输出是“原始页面 HTML 或者 JSON 文本”它不关心字段怎么清洗也不需要知道数据最终存到哪。解析清洗模块负责从原始文本中提取结构化字段这一层也是全项目最核心的地方。每个数据源有独立的解析器但所有解析器都返回统一的字段集合名称、价格、销量、上架时间、来源链接、抓取时间戳。存储模块接收清洗后数据负责去重、入库和简单的聚合计算。去重逻辑非常关键后面踩坑部分细说。展示模块提供 REST 接口和最小前端页面前端第一版不追求炫酷能用表格和基础折线图展示趋势即可。四个模块之间通过内存数据结构和文件传递不引入消息队列原因很简单第一版的并发量达不到需要 MQ 的程度引入额外组件只会增加部署复杂度。2.3 设计文档先行目录结构和接口约定很多课程项目失败的原因不是技术难而是几个人对“数据长什么样”没有达成共识。我们花了一个晚上定义统一数据格式并且在项目根目录写了一份开发约定文档里面规定了几件事采集任务统一用 JSON 描述必须包含 task_id、source、url、keywords、schedule 五个字段清洗后的数据统一为 CSV 列名集合列名使用英文下划线命名法所有时间字段统一为 Asia/Shanghai 时区的 ISO 8601 字符串。这个决定在后续开发中帮了大忙。因为四个模块是并行开发的如果接口格式没有提前定好联调时百分之百会出现“你返回的是列表我拿到的是字典”这种问题。事实证明花 40 分钟写一份约定文档能省下来回扯皮的功夫至少两天。项目目录结构大概是这样的供参考marketclaw/ configs/ # 采集任务配置文件 collectors/ # 采集器实现 parsers/ # 各渠道解析器 cleansers/ # 清洗规则 storage/ # 数据库操作与聚合逻辑 api/ # Flask 接口层 frontend/ # 简单展示页面 outputs/ # CSV 输出目录 main.py # 入口 requirements.txt3. 第一期核心功能的落地实现3.1 采集模块先写一个能跑的最小骨架采集器部分没有一上来就做插件化、动态加载而是用最直接的方式从 JSON 配置里读任务列表逐个执行请求把结果保存到临时目录。核心逻辑是这个样子import requests import json from urllib.parse import urljoin class SimpleCollector: def __init__(self, task_config): self.task_id task_config[task_id] self.url task_config[url] self.keywords task_config[keywords] self.headers { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36 ) } def fetch(self): resp requests.get(self.url, headersself.headers, timeout10) resp.raise_for_status() return resp.text def save_raw(self, content, filename): with open(filename, w, encodingutf-8) as f: f.write(content)这段代码非常简单但有几个细节是第一批进坑的地方。timeout 必须设置否则某个长时间无响应的页面会把整个任务卡死User-Agent 要仿真这是最基本的请求头配置保存原始文件时强制指定 utf-8 编码避免在 Windows 环境写入默认 gbk 导致文件乱码。实际使用中直接把所有页面一次性抓到内存再解析其实有隐患。我们发现某些渠道页面非常大单页超过 2MB如果任务量上来内存占用会明显上升。所以爬取逻辑做了一点调整requests.get 拿 response 之后立刻判断最大长度超过阈值先写入临时文件再通过文件流传给解析器。这个优化第一版里不算复杂但让系统面对异常大页面的时候不会直接挂掉。另外有一个容易被忽视的点采集器要记录每次请求的元信息包括状态码、耗时、抓取时间。别小看这个日志后面排查“为什么某个任务没数据”的时候这些元信息是最直接的证据。3.2 解析清洗从原始页面到统一字段解析清洗模块是技术含量最高的部分也是体验“数据处理乐趣”的地方。我们第一批支持的两个渠道结构差别很大一个是列表页返回 JSON 接口一个是相对传统的 HTML 页面。JSON 那个渠道解析起来很简单直接用 json.loads 然后取值HTML 那个就需要用 BeautifulSoup 定位节点。统一字段集合我们定义了这样几个核心字段字段名类型说明source_namestring数据来源渠道item_namestring商品或条目名称item_pricefloat数值化后的价格item_salesint销量或热度缺失时填 0item_linkstring原文链接collected_atstring抓取时间ISO 8601这里边的难点在于 price 字段的清洗。页面里的原始价格通常长这样“¥299.00”“299元起”“到手价 269”。我们要抽取纯数字并且考虑是否有“起”字暗示最低价。第一版规则用了正则加条件判断比较粗暴但完成了基本功能import re def clean_price(raw_price): if not raw_price: return None s str(raw_price).strip() numbers re.findall(r\d\.?\d*, s.replace(, )) if not numbers: return None vals [float(n) for n in numbers] return min(vals)为什么取最小值因为带“起”字的场景表示“最低从这里开始”而页面有时会同时出现划线价和现价取数值中的最小值通常更接近实际可购买价。这个逻辑当然不是完美的但作为第一版足够合理。清洗还有一个必须处理的坑字段缺失。我们约定了所有缺失不能抛异常而是填 None 或对应类型的默认值。原因很简单一个页面解析失败不应该影响整轮任务解析器要具备“部分失败”的容忍度。实现上每个字段都有 try except 兜底单条记录清洗失败会打印警告并继续。3.3 存储与去重不要让同一份数据反复出现存储层我们用了 SQLite建了三张表分别是采集任务表、商品快照表和统计聚合表。商品快照表是最核心的字段和统一字段基本一致额外加了一个主键 id 和唯一索引唯一索引。唯一索引打在 source_name、item_link、collected_at 这三个字段的组合上实现逻辑CREATE TABLE IF NOT EXISTS item_snapshot ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_name TEXT NOT NULL, item_name TEXT, item_price REAL, item_sales INTEGER, item_link TEXT, collected_at TEXT, UNIQUE(source_name, item_link, collected_at) );用 item_link 做唯一值索引很关键。同一商品在不同时间抓取会有不同 collected_at但因为带了日期字段所以同一天同一链接只保留一条不会出现重复插入。这里有个小坑后面会讲item_link 有时候带 URL 访问参数看起来不同实际是同一个页面需要在解析清洗环节做链接规范化。第一批数据入库后我们做了一次简单统计发现销售量的覆盖率只有 60%很多页面根本没有销量数据。所以我们调整了展示层的逻辑销售量为空的条目不参与平均销量计算避免“疑似零销量”导致统计失真。这算是一个比较细的数据质量意识。3.4 展示模块先表格后图表不追求一次到位展示模块第一版目标很克制能查看每轮任务抓取了多少条有效数据、能按关键词看条目数量排名、能看某个商品的价格随时间的小趋势。接口设计就三个GET /api/tasks 返回任务列表和最近一次抓取状态GET /api/items?keywordxx 返回匹配关键词的商品列表GET /api/trend?item_idxx 返回指定商品的历史价格序列。前端用了最简单的 HTML ECharts没有上前后端分离的工程化方案。不是不会而是第一版要控制复杂度。纯静态页面可以直接双击打开配合 Flask 返回 JSON看一眼就能懂。ECharts 的折线图效果很直观价格趋势一画出来大家就觉得项目“有点东西”了。实际上真正的数据价值也确实在这里体现当散落的页面文本变成一条时间序列曲线信息密度完全不同。4. 踩坑与排查实录4.1 乱码问题的根源不在 Python而在不知道页面是什么编码刚开始跑第一个页面时打印前一百个字符全是乱码。我们第一反应是 requests 没设置编码于是在代码里写死 resp.encoding utf-8结果还是乱。后来用 head 命令查看响应头发现这个页面声明的是 gbk而页面 meta 标签里却写的是 utf-8两者冲突。最终正确的处理方式是优先按照响应头里的 Content-Type charset 解码如果响应头里没有 charset就用页面 meta 声明的编码如果 meta 也没有再用默认 utf-8 尝试不行就换 gbk。因为我们已经确定自己只处理中文站所以把猜编码的权重放到了 gbk 和 utf-8 二选一没有引入第三方编码检测库。如果你处理的网站比较杂可以考虑接一个检测库一般都能覆盖。排查这类问题的经验是先在浏览器里打开页面右键查看源代码看最前边的 meta 标签写了什么再对比 requests 响应头里的 Content-Type。绝大多数乱码都是编码声明不一致导致的和脚本语言本身没啥关系。4.2 请求频率与合规性问题必须一开始就考虑这个项目从立项开始就明确了一条底线只采集公开的、非登录状态的页面数据严格遵守每个渠道的 robots 声明单次请求间隔不低于一个阈值对返回 429 或 503 的渠道做退避重试而不是强行冲频率。实际开发中我们把每轮任务的请求间隔抽成了配置项默认 3 秒一次。这样一来一轮任务的耗时变长但换来的是安全性和可持续性。做采集类项目最容易犯的错误是觉得“多线程跑得快”就疯狂并发结果就是被限制甚至带来合规风险。我们内部反复强调一个观点MarketClaw 是工具不是攻击手段所有数据采集都合法合规只用于学习和研究场景。项目的意义在于数据处理链路而不在于绕过访问策略。退避重试逻辑第一版长这样import time def fetch_with_retry(url, retries3): for attempt in range(retries): try: resp requests.get(url, timeout10) if resp.status_code 429: wait (attempt 1) * 10 time.sleep(wait) continue resp.raise_for_status() return resp.text except requests.RequestException as e: if attempt retries - 1: raise time.sleep(5 * (attempt 1))这种简单的指数退避足够用也确实在测试时触发过几次 429等了几秒后重试就恢复了。4.3 链接规范化的坑同一个商品三个不同 URL我们去重逻辑最初只做“完整链接比较”结果统计时发现同一个商品存在多条记录排查原因发现是 URL 上有不同的追踪参数。有的是 utm_source有的是 spm 标识还有的是 session 参数。这些参数不影响页面内容却让链接字符串完全不同。解决方案是在清洗层的 item_link 处理里加一步 URL 规范化去掉 query 中已知的追踪参数保留核心路径参数。这一步做完之后去重表的唯一索引才真正发挥作用。顺便提一句规范化 URL 的函数要写得保守一点只删除明确列为追踪参数的字段不要试图猜所有 query 参数的意义否则可能误删有用信息导致页面打不开。4.4 同一轮任务只跑一次幂等性提前规划这个坑是在调测试脚本时发现的。我们第一次运行入库 138 条第二次跑同一批任务居然入库 268 条仔细一看原来第二次运行时 upsert 逻辑写成了先 delete 再 insert而 delete 条件漏了 collected_at导致把之前的记录删了又重新插入索引看着没重复实际历史数据被覆盖了。后来把入库逻辑统一成“先按组合唯一键查存在则更新价格和销量不存在则插入”实现好之后再跑数据量稳定在预期范围内。这类“重复执行同一轮任务”在开发调试时非常常见如果你的系统没有幂等性保障每次调试都会产生脏数据分析结果自然不可信。幂等设计的心得就是任何写操作都要问自己一句“如果这个操作执行两次结果应该一样”把这条原则放在代码评审里能堵住一大部分数据质量问题。5. 第一期小结与后续方向第一期从立项到跑通大概用了三周课余时间。三周前我们还在争论用什么数据库、要不要做消息队列三周后看到价格趋势曲线在页面上画出来那个成就感比想象中强。这期项目给我最大的体会是不要一开始求大求全把最小闭环先跑起来后面所有的优化都有真实数据支撑决策就不会靠猜。下阶段我们有三个明确的方向。一是扩展渠道数量让解析器从硬编码走向插件式新增一个渠道只需要实现统一接口即可二是把关键词热度做成时间序列在现有数据基础上做简单趋势预测三是把展示模块完善成带筛选条件的完整报表。这些都是基于第一期数据的自然延伸。最后分享两个实际经验。第一项目开发文档要写到“能让人不打小抄也能跑起来”的程度包括依赖安装命令、配置项说明、样例任务文件这个文档会在你放假一个月后救你一命。第二如果团队里分工明确强烈建议在开发前用半小时把最少两个模块之间的接口例子写出来贴在仓库联调阶段会顺畅很多。MarketClaw 的名字还在那只爪子如今算是有了一副骨架。下一次写第二期的时候希望它能抓得更深一点。