☰
基于Python定向爬虫的商品比价系统:同款匹配与价格归一化实践
2026/10/3 2:45:52 网站建设 项目流程

简介:这是基于Python与定向爬虫实现的商品比价系统毕业设计源码包,面向计算机相关专业学生和爬虫入门开发者,适合用于课程设计、毕业设计以及电商数据采集实战项目。压缩包共16个文件,以Python脚本为主,涵盖爬虫抓取、GUI交互界面和数据库操作等核心模块,另含SQLite数据文件与说明文档;整包约25KB,结构紧凑,便于快速部署和二次开发。资源已有37人学习下载,对需要完整项目参考的人群有较高借鉴价值。读者可从源码中梳理定向爬虫采集、数据清洗与存储、比价结果展示的完整链路,理解目标电商平台的页面解析与反爬应对思路;同时,借助清晰的模块划分和异常处理方式,能够快速搭建自己的比价系统原型,并进一步体会项目从需求分析到系统测试的落地过程。

1. 比价系统的成败不在爬虫抓不到,而在同款匹配和价格归一化

提到“基于 Python 和定向爬虫的商品比价系统”,很多人第一反应是“爬得越多、比得越准”。我在做第一版时就这么干过,结果不是更准,而是更乱:同一个手机,一个平台标题写“Apple iPhone 15 (A3092) 128GB 蓝色 双卡双待”,另一个平台写“苹果 iPhone 15 128GB 蓝色 5G 手机(A3092)”,直接按标题比较,匹配率有 30% 就算不错。这套系统真正的技术重心不在爬虫,而在“同款匹配”和“价格归一化”:把不同平台对同一商品的各种写法归一成同一件商品,再把原价、优惠、运费折算成同一个到手价。落地之后能解决的问题很具体:给电商运营做价格监控、给学生项目做数据支撑、给选品团队看行情趋势。下面这条路径按“建模 → 抓取 → 匹配 → 存储 → 排错”的顺序讲,重点放在能复现的代码和参数上。

2. 先建模再写爬虫:比价系统的数据来源与最小可运行原型

2.1 先定义商品字段:比价数据模型里每一列都在解决一个问题

定向爬虫(有的资料里叫聚焦爬虫)和通用爬虫最大的区别,不是用了什么框架,而是“提前知道要什么”。通用爬虫会顺着链接一层层往外扩散,爬大量无关页面;定向爬虫拿到的是种子 URL 列表,只抓固定的目标站点、固定的页面类型,并且抽取字段时直接落到一张设计好的表结构上。这样做的优势是可控、低负载、解析规则可以按平台单独维护;代价是每个平台都要单独适配,页面一改版就得跟一次。

我一般先列一张字段清单再写代码,这张清单也是数据库表结构的雏形:

字段示例值在比价系统里解决什么问题
product_fingerprintiphone15-128gb-blue跨平台匹配主键,同款商品共用一个值
platform_id1标识来源平台
product_titleApple iPhone 15 128GB 蓝色原始标题,保留排查依据
spec_key128gb/blue规格签名,同款匹配的辅助输入
origin_price5999.00页面原价
coupon_amount400.00优惠金额
freight0.00运费,包邮也要单独记录
final_price5599.00最终到手价,计算公式见第 3 章
page_urlhttps://...详情页链接,用于人工复核
crawled_at2024-01-15 10:23:00数据新鲜度判断

这里容易犯的第一个错,是“把最终提取结果和原始数据混在一起”。页面上的“券后价”是促销模块的结果,不是独立字段,如果只存一个券后价,一旦计算口径变了,所有历史数据都得重算。我的做法是:每一列都既是业务数据,也是排查证据。final_price算错了,我可以拿origin_price、coupon_amount、freight反推;如果只存一个最终价,就只能对着一个可疑数字猜。

字段定了之后,爬虫代码才会好写。所谓“定向”,就是每个目标平台配一套自己的解析规则:去哪个列表页找种子 URL、详情页的哪个节点放标题、价格在哪个元素里、优惠信息在哪个模块里。这些规则通常写成配置,而不是散落在抓取代码里,因为规则一定会变,配置化之后每次只改配置,不动主流程。

2.2 定向爬虫的最小原型:requests 加 BeautifulSoup 抓取详情页字段

以单个商品详情页为例,最小实现如下。这段代码假设平台把价格放在span.price节点里,真实站点可能不同,需要先打开开发者工具确认一次。

import requests from bs4 import BeautifulSoup def fetch_product_detail(url: str) -> dict: headers = { # 常规做法是写一个通用的桌面浏览器 UA,不要带“spider”字样 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/124.0 Safari/537.36", } # timeout 必须显式设置,防止某个慢页面把整个任务拖住 resp = requests.get(url, headers=headers, timeout=10) resp.raise_for_status() # 有些站点是 gbk 编码,不要依赖自动检测,显式指定最稳 resp.encoding = "utf-8" soup = BeautifulSoup(resp.text, "html.parser") title_node = soup.select_one("div.sku-name") price_node = soup.select_one("span.price") return { "product_title": title_node.get_text(strip=True) if title_node else "", "final_price": float(price_node.get_text(strip=True)) if price_node else None, "page_url": url, } if __name__ == "__main__": print(fetch_product_detail("https://example.com/item/10001.html"))

逻辑说明:requests.get发起请求,BeautifulSoup按 CSS 选择器抽取节点。select_one拿不到节点时返回 None,所以每次都要判空,不能直接调用get_text(),否则一条页面改版就会让整个抓取任务中断。价格文本里常带“¥”“促销价”之类的前缀,我在生产代码里会先做一次re.sub把非数字和点号去掉,再转float,否则float("¥59.9")会直接抛异常。

参数说明:timeout=10是请求超时上限,慢接口建议调到 15~20 秒,但不要超过 30,因为一次重试会拖慢整批任务,超时设太宽反而让故障不容易暴露。headers里的 User-Agent 按真实浏览器写,目的是让服务端把你当成正常访客。resp.encoding在页面声明乱掉时一定要显式指定,不然中文标题抓出来是一串乱码,后面做同款匹配时全是无效输入。

这段代码是单个页面的抓取,还没到“系统”。真正落地时,每个平台都会有一个类似的parse_detail(html) -> dict函数,主流程只负责调度和写库,不关心某个平台的价格到底在哪个节点里。

2.3 请求节流与失败重试:三个必调参数让爬虫活得更久

爬虫写好后,比价系统真正在线上跑的时候,出问题最多的反而是最简单的“请求间隔、重试、超时”。我把它们做成三个必调参数,放在配置里全局生效:

# config.py 或 config.json,两个文件选一个,json 更适合不改代码就调整 CRAWL_SETTINGS = { "request_interval": 2.5, # 每次请求之间至少间隔 2.5 秒 "retry_times": 3, # 失败后最多重试 3 次 "retry_backoff": 1.5, # 重试间隔按 1.5 倍递增 "batch_size": 20, # 每批只处理 20 个商品,处理完先写库 "max_error_count": 10, # 单批连续失败 10 次就停止该批任务 }

在请求函数里,这三个参数这样生效:time.sleep(request_interval)放在下一次请求之前;遇到网络错误或非 200 状态时,按retry_backoff递增等待时间,第一次等 1.5 秒,第二次等 2.25 秒,第三次等 3.375 秒;一批商品抓完后先写库、做一次简短停顿,再进入下一批。

request_interval是保护自己的关键参数。单机、单 IP、单线程情况下,2~3 秒的间隔足够大多数公开商品页接受;如果稳定出现 4xx 或验证类响应,不是把间隔调大到 10 秒就能解决的,而是先检查是不是请求了不该碰的接口。batch_size决定失败影响的粒度:每批 20 个商品,处理完就写库,任何一批出错都不会推倒重来。max_error_count是我后来加的,因为发生过“某个页面反复超时,整个 while 循环卡在重试里,日志刷了几万行”的情况。定向爬虫的意义就在这:宁可慢,不能乱。

3. 商品同款匹配与价格归一化:把“看起来像同一件”变成“确实是同一件”

3.1 标题完全相同为什么是伪命题

比价系统的第一关是抓取,第二关是同款匹配。我见过不少项目里直接用“标题完全一致”做匹配,结果匹配率低到怀疑人生。因为平台标题不是数据,是文案。同一个商品在不同平台几乎不会出现两个一模一样的标题:有加品牌自营标识的,有把容量和颜色顺序换掉的,有全角括号、半角括号混用的,还有把“5G 手机”这种通用词放在前还是放在后的区别。

平台 A:Apple iPhone 15 (A3092) 128GB 蓝色 移动联通电信5G手机 双卡双待 平台 B:苹果 iPhone 15 128GB 蓝色 5G 手机(A3092)

这两行文本里,真正能用来判定同一件商品的,是“品牌 + 型号 + 容量 + 颜色 + 网络制式”这些关键规格。其余部分是平台加的营销词和通用描述。所以匹配的第一步不是直接比较整条标题,而是从标题里抽规格、做归一化,再比较归一化后的结果。

3.2 型号归一化与相似度匹配:一段可直接复制的 Python 实现

归一化的目标是把不稳定的写法变成稳定写法:全角转半角、括号统一、英文品牌保持小写、规格词排序、去掉“正品、官方、旗舰店、包邮、全新”这类噪音词。一个够用的实现如下:

import re STOP_WORDS = {"官方", "旗舰", "正品", "全新", "包邮", "自营", "移动联通电信", "双卡双待", "5g手机"} def normalize_title(title: str) -> str: # 全角括号转半角,统一大小写,再去掉所有空格 text = title.lower().replace("(", "(").replace(")", ")") text = text.replace(" ", "") # 去掉噪音词 for w in STOP_WORDS: text = text.replace(w, "") # 只保留中文、英文、数字,其余标点全部去掉 text = re.sub(r"[^\w\u4e00-\u9fa5]", "", text) return text def match_same_product(t1: str, t2: str) -> bool: n1, n2 = normalize_title(t1), normalize_title(t2) if not n1 or not n2: return False # 优先做包含判定:一方完全包含另一方,且长度差较小 longer, shorter = (n1, n2) if len(n1) >= len(n2) else (n2, n1) if shorter in longer and len(longer) - len(shorter) <= 6: return True # difflib 是标准库,不需要额外安装 from difflib import SequenceMatcher return SequenceMatcher(None, n1, n2).ratio() >= 0.88 # 跑一下上面两个标题 t1 = "Apple iPhone 15 (A3092) 128GB 蓝色 移动联通电信5G手机 双卡双待" t2 = "苹果 iPhone 15 128GB 蓝色 5G 手机(A3092)" print(normalize_title(t1)) print(normalize_title(t2)) print(match_same_product(t1, t2)) # 期望输出 True

逻辑说明:normalize_title先统一括号和大小写,再剔除噪音词。match_same_product先做包含判定,再做相似度兜底。包含判定解决的是“规格顺序不同但文本几乎相同”的情况;相似度阈值 0.88 解决的是“个别营销词不同”的情况。

参数说明:阈值 0.88 不是拍脑袋定的,是拿两个平台的实际标题跑了一批样本之后取的值:同一商品的相似度集中在 0.95 以上,不同商品集中在 0.6 以下,中间留出了 0.85~0.95 的模糊区间。如果你匹配的是标准化程度高的 3C 数码,阈值可以放到 0.85 并加一道规格抽检;如果是服装这种规格词模糊的类目,0.88 会产生不少误判,建议先做规格抽取,把“颜色 + 尺码”拼成spec_key,再让相似度只作用在标题主干上。任何时候都不要省略“先判空”:页面偶尔抓到空标题,空字符串和任何字符串算相似度都是 0,结果就是那一条商品永远匹配不上,看起来像没抓到,其实是匹配模块在空数据上翻车了。

这段代码的局限也很明显:它判断不出“型号不同、文字相似”的商品,比如iPhone 15和iPhone 15 Pro,归一化后文本高度重合,相似度可能超过阈值。解决这个问题需要在归一化阶段就把型号抽出来比较,通常的做法是维护一份型号正则表,例如re.search(r"iphone\s?\d+\s?pro?", n1),再把型号加入spec_key参与匹配。比价系统里最怕的不是“匹配不上”,而是“匹配错”,宁可漏配,不要错配。

3.3 价格归一化:原价、券后价、运费分开存,比价才不会失真

同款匹配解决的是“是不是同一件”,价格归一化解决的是“多少钱才是真的到手价”。我见过最多的问题是拿页面上的“原价”当最终价格,结果比价结果里全是虚高价,用户一打开详情页就发现不对。

把手价统一折算成一条公式:final_price = origin_price - coupon_amount + freight。定向爬虫抓页面时,把价格链路拆开存:origin_price(原始价)、coupon_amount(优惠金额)、freight(运费)、final_price(到手价),是否包邮单独标记。

这里有几个容易忽略的点。一是“满减”通常需要在促销模块里单独解析,不能只取一个券后价,因为有的优惠对特定规格不生效。二是“包邮”不等于运费为 0,很多店铺对偏远地区包邮条件不一样,但页面默认展示的运费是 0,这个要结合收货地址参数去看;如果拿不到就按商家展示为准,同时打一个shipping_unknown标记。三是有的平台把“预约价”“拼团价”放在活动模块而不是详情模块,爬虫如果只抓详情模块,拿到的就是错误的原价。

我的习惯是:宁可一个商品到手价显示“待确认”,也不要拿一个错误价格去比价。比价系统的口碑建立在“我报的价格基本准确”上,数据有缺失可以更新,算错则会让用户彻底失去信任。所以字段设计里final_price允许为 NULL,不做默认值;展示层遇到 NULL 就显示“待确认”,而不是用origin_price偷偷顶上。

4. 数据落地与查询:一套 SQLite 表结构让比价系统先跑起来

4.1 三张表的字段设计与索引

数据量没到百万级之前,用 SQLite 做落地就够了。它的优势不是性能,而是“一个文件、零部署、备份就是复制一个文件”。线上跑比价系统最怕的不是慢,而是改表字段要迁库,SQLite 在这方面反而让我少踩很多坑。表结构按功能拆三张:

-- 商品主体表:一个 fingerprint 对应一个跨平台商品 CREATE TABLE product ( id INTEGER PRIMARY KEY AUTOINCREMENT, fingerprint TEXT, -- 同款匹配后回填,允许为空 title TEXT NOT NULL, spec_key TEXT, category TEXT, UNIQUE(fingerprint, spec_key) ); -- 平台在售信息表:同一商品在每一个平台的快照,一商品一平台一行 CREATE TABLE sku_offer ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, platform_id INTEGER NOT NULL, origin_price REAL, final_price REAL, -- 允许 NULL,表示待确认 freight REAL, coupon_amount REAL, page_url TEXT, crawled_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(product_id, platform_id), FOREIGN KEY (product_id) REFERENCES product(id) ); -- 历史价格表:每次抓取都追加一行,是画价格曲线的数据源 CREATE TABLE price_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, sku_offer_id INTEGER NOT NULL, final_price REAL NOT NULL, crawled_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (sku_offer_id) REFERENCES sku_offer(id) );

逻辑说明:product表管“商品是什么”,sku_offer表管“在哪卖、卖多少”,price_log表管“价格怎么变”。sku_offer上的唯一索引UNIQUE(product_id, platform_id)确保同一个平台对同一个商品只保留最新一条在售快照,价格变化写入price_log。price_log只追加、不更新、不删除,方便做趋势分析和误判回查。

调参说明:fingerprint在第一次匹配前是空值,空值和空值之间不参与唯一约束,这意味着“多个不同商品都为空指纹”时,UNIQUE(fingerprint, spec_key)拦不住重复。我的做法是先让fingerprint允许 NULL,抓取结束跑一轮“指纹回填”,回填前清理空指纹,把空指纹的记录单独列出来人工看。SQLite 默认外键检查是关闭的,建表后执行一次PRAGMA foreign_keys = ON;,否则误删父记录时子表会出现孤儿数据。

为什么不直接用 MySQL?单机比价系统的主要吞吐在“定时抓取 + 查询”,SQLite 在几百 GB 内都够;MySQL 需要服务进程、账号权限、备份策略,部署成本一下子变高。等数据量真的涨到需要并发写的时候,再把sku_offer和price_log两张表迁到 MySQL,product表留在 SQLite 也问题不大,因为它是低频读写的参照表。

4.2 用一条 SQL 算出每个商品的“全网最低价”

比价查询的核心是“找出每个 fingerprint 下,final_price最低的那条平台记录”。直接用GROUP BY加MIN()拿不到完整的平台信息,我习惯用窗口函数:

WITH ranked AS ( SELECT p.fingerprint, p.title, s.platform_id, s.final_price, s.page_url, ROW_NUMBER() OVER ( PARTITION BY p.fingerprint ORDER BY s.final_price ASC -- 同一个商品按到手价排序 ) AS rn FROM product p JOIN sku_offer s ON p.id = s.product_id ) SELECT fingerprint, title, platform_id, final_price, page_url FROM ranked WHERE rn = 1 -- 只保留每个商品的最低价 ORDER BY final_price ASC;

逻辑说明:窗口函数ROW_NUMBER() OVER (PARTITION BY ... ORDER BY ...)对每个 fingerprint 内的记录按价格升序编号,rn = 1就是该商品的最低价格行,之后再按全网最低价升序输出。这个查询一次完成“取最低 + 带上最低价对应的平台和链接”,不用子查询回表两遍,SQLite 3.25 及以上版本支持窗口函数。

如果没有窗口函数可用,退化成GROUP BY fingerprint配合MIN(final_price)也能出结果,但要额外自连接才能拿到平台链接,查询会慢一些。我的建议是直接用窗口函数,它在排错时也更好读:rn > 1的记录就是非最低价,可以单独拿出来看价差。

4.3 定时增量更新:只抓需要更新的商品

第一个能跑的版本不需要用调度框架,一个while True加time.sleep的入口就足够。系统抓取的主循环代码如下:

import json import time from crawler import fetch_product_detail from storage import upsert_price, get_stale_products def main(): cfg = json.load(open("config.json")) while True: # 只取出超过配置时间没有更新过的商品 stale_list = get_stale_products(max_age=cfg["crawl_interval"]) for item in stale_list: record = fetch_product_detail(item["page_url"]) upsert_price(item["id"], record) time.sleep(cfg["request_interval"]) time.sleep(60) # 每轮之间等待一分钟,防止空轮高频访问 if __name__ == "__main__": main()

逻辑说明:get_stale_products负责从数据库里找出crawled_at距今超过crawl_interval的商品,这样不会每次都重抓所有商品,而是按需增量更新。upsert_price写入当前价格并在price_log里追加一条历史。这个主循环把定时系统拆成了“查过期数据 → 单条抓取 → 单条写库”三个可以单独验证的环节,比在一个大循环里抓完再批量写库更容易排错。

config.json最小结构长这样:

{ "crawl_interval": 21600, "request_interval": 2.5, "batch_size": 20 }

crawl_interval单位是秒,21600 表示 6 小时更新一次。价格敏感的商品可以缩短到 3600,非敏感商品可以放到 43200,这个值直接影响目标站点的请求压力,不要为了“数据新鲜”把间隔设得太短。增量更新能正常工作的前提,是所有抓取都带crawled_at时间戳,并且中断重启后从数据库状态继续,而不是从头再来。

我吃过一次亏:最初把stale_list设计成“按关键词搜索返回的新商品列表”,导致每天抓的是新商品而不是已跟踪商品,价格历史完全断档。所以这个循环的核心逻辑应该是“先读库拿更新列表,再请求新链接”,而不是“先请求,再判断要不要入库”。

5. 避坑专项:比价系统真实运行中常见的 5 个翻车点

5.1 页面改版导致解析全空

现象:昨天还正常抓取的站点,今天跑完所有商品都是空标题、空价格,数据库里全是空值,日志没有报错。

原因:目标平台前端改版,把价格节点从span.price挪到了某个div里,或者给节点加了动态class。定向爬虫的优势是解析稳定,劣势是“一改全废”。

解决:解析规则配置化,选择器放配置而不是写在代码里;每批抓取后做一次“字段完整率”校验,标题完整率低于 90% 就自动告警并停止覆盖该站点的数据。页面改版不可预测,能预测的是“一定会改”。我自己的习惯是每次改版时保留上一版解析配置,出问题时一键回退,这个习惯帮我省掉了不止一次手工返工的麻烦。

5.2 请求返回 200 但价格字段为空

现象:HTTP 状态码正常,页面也抓到了,唯独价格节点是空的。浏览器里打开明明有价格。

原因:价格被放在页内 JavaScript 动态加载的接口里,初始 HTML 只有商品主图、标题、描述,价格是页面脚本再异步请求一个价格接口后渲染出来的。用requests拿到的原始 HTML 里没有这个价格数据。

解决:打开浏览器的开发者工具,翻 XHR 请求列表,找到价格接口,直接请求这个公开接口替代解析 HTML;如果接口带签名参数,看返回的 HTML 里是否内嵌了一段window.__INITIAL_STATE__之类的 JSON,里面有完整的初始数据。两个方案都拿不到时,才把价格标记为“待二次确认”,而不是凭空猜测。无头浏览器能兜底但不优先,它带来的 CPU、内存开销和等待时间会在批量抓取时被无限放大,这也是很多人从这个坑跳进下一个坑的原因。

5.3 响应里出现验证页,程序“假装成功”

现象:请求返回 200,解析出的内容是一段验证脚本或安全提示文案,字段完整率骤降,但程序没有报错。

原因:目标站点把该访问行为识别为高频访问,返回给客户端的是一个验证页而不是商品页。验证页的状态码也可能是 200,所以“没报错”不等于“抓到了”。

解决:第一优先级是降低请求频率和同轮内对同域名的并发数,定向爬虫单机单线程通常足够;第二优先级是检查是不是把必要的公开访问参数少带了,比如某些站点不携带基础 Cookie 时不展示价格。页面返回时加一个“校验断言”:如果解析结果里没有标题也没有价格节点,直接记日志并暂停该站点的任务,不要继续往下写空数据。这里不建议为了“通过验证”去走各类绕过手段,合规、低频、按公开规则访问,才是能长期跑的策略。

5.4 原价当成到手价,比价结果没有参考价值

现象:比价结果里价格普遍偏高,用户去实际页面看一眼,发现实际到手价比系统报的低不少。

原因:优惠券、满减、会员价在详情页的不同模块,有的甚至要用户手动领券后才展示。“价格”不是一个值,而是一套促销规则。

解决:在解析层就拆多个字段:原价、促销价、优惠金额、运费。促销价能取到就取促销价,取不到就保留原价并标记promotion_unknown。比价展示时,把“最终价”和“原始价”两列都显示出来,让用户看到系统没有乱算。页面如果同一 SKU 有多个促销(券后价、满减价),按“到手价最低”原则计算,并把计算公式对应的文案原样存下来,方便日后排查。

5.5 同款商品重复入库,比价列表里出现两条“最低价”

现象:同一个商品在结果页出现两次,一个指纹是空、一个指纹是乱码,价格可能还不一致。

原因:关键词搜索返回了同一商品的多个链接,或者同一链接被不同关键词各抓了一遍,但初次入库时 fingerprint 还没回填,唯一索引没有拦住。

解决:入库前先做一步“链接去重”,同一page_url只保留一条记录;抓取结束后再跑一次“指纹回填”任务,将空指纹按照第 3 章的匹配结果更新。sku_offer表建唯一索引,回填指纹时如果碰到UNIQUE(product_id, platform_id)冲突,做UPDATE而不是INSERT。如果这段逻辑没写对,会出现一种很隐蔽的现场:比价结果显示这个商品的最低价是昨天的,但数据库里明明是今天抓的——因为新记录没有更新旧记录,而查询时取到了旧数据。这个现象排查起来非常费时间,我后来在抓取入库函数里加了“先按page_url查,再决定 INSERT 还是 UPDATE”的固定逻辑,才算根治。

6. 可信比价的验证方法:从“能跑”到“敢用”的三步收尾

一套比价系统写完,只有“能跑”是不够的。线上跑两周后,确定它准不准,我通常做三个验证动作。

第一个动作是“抽检脚本”:从价格表里随机取 20 个商品,人工打开详情页核对“最终到手价”是否与系统一致。这个动作不要只在开发期做一次,建议每两周做一次,重点看相似度阈值有没有因为页面改版而悄悄退化。

import random import sqlite3 conn = sqlite3.connect("price.db") rows = conn.execute( "SELECT id, page_url, final_price FROM sku_offer " "ORDER BY RANDOM() LIMIT 20" ).fetchall() for row in rows: print(row) # 逐个打开 page_url,人工核对 final_price

第二个动作是“价格曲线”:从price_log里按商品拉出历史价格,用 Python 简单的列表绘图就能画出一条趋势线,并做一个“比最新价低 5% 就提醒”的最小逻辑。这一步不需要引入消息队列,一个sqlite3查询加一段if price <= threshold的判断就够了。它能验证的不只是“数据有没有抓对”,还有“这套比价系统有没有持续提供价值”的真实使用场景。

第三个动作是把“字段完整率”纳入每次抓取的日志输出:标题、价格、运费三个关键字段任何一个为空,整条记录标黄。线上跑三周后统计标黄率,超过 5% 说明解析规则或请求频率需要调整,而不是去改匹配阈值。

做这套系统两年,我最大的教训是早期把最终价格直接存在product表里。后来想画历史价格曲线时,才发现过去的数据已经被覆盖,只能从头回填price_log。这个坑让我养成了一个习惯:先想清字段会不会被覆盖,再写 create table——宁可多一张日志表,也不让历史消失在覆盖里。这个系统值不值得做,我的答案是值得,但一定要先做匹配和后做爬虫一样重要;数据可靠了,比价结果才有人敢用。希望这套“先建模、再做同款匹配、最后用日志表验证”的思路帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询