简介这是一套面向计算机相关专业学生与开发者的电商比价系统毕业设计完整资料基于Python爬虫技术实现多平台商品数据采集与价格对比适合用作毕设、课程设计或爬虫入门进阶练习。压缩包共137个文件约26.68MB包含20个py源码文件、6个html页面、6个xml配置、3个csv数据样本以及大量png、gif、jpg界面截图与演示素材另有md说明文档和pickle数据文件结构完整、便于按模块查阅。目前已有186人学习关注。项目源码均经本地编译运行验证答辩评审分达95分以上并附详细文档与全部资料读者可据此掌握爬虫请求、数据解析、存储与比价逻辑的完整实现思路也可在现有代码基础上修改扩展功能用于毕设、课设或项目初期立项演示。1. 电商比价系统到底比的是什么从三个价格字段说起打开任何一个电商 App同一款商品在不同平台的价格能差出一顿饭钱。我去年帮一个做小家电的朋友盯过一款空气炸锅京东标价 399天猫旗舰店满减后 349拼多多百亿补贴直接 299。三个平台三个价格同一款货。这就是电商比价系统要解决的核心问题把散落在不同平台的商品价格抓回来对齐到同一个商品维度上让你一眼看出该在哪下单。这个标题里的关键词是 Python 爬虫、电商比价系统、毕业设计。它适合两类人一类是正在找计算机毕业设计选题的学生想要一个能跑通、有界面、能写进论文的完整项目另一类是刚学完 requests 和 xpath、想找个真实场景练手的 Python 入门者。比价系统的技术栈不复杂但涉及的知识面很全——HTTP 请求、页面解析、数据清洗、商品匹配、可视化展示一条链路走下来对爬虫的理解会比刷十道练习题扎实得多。需要提前说清楚的是这个系统抓的是公开的商品页面数据不涉及登录态、不碰用户隐私、不做高频请求。毕业设计做这个方向核心价值在于把「抓取→清洗→匹配→展示」这条链路讲清楚而不是追求抓多少条数据。2. 商品数据从哪来三个平台的页面结构与抓取策略2.1 搜索列表页和详情页分别抓什么电商平台的商品数据分两层。搜索列表页给你的是商品标题、价格、销量、店铺名、商品链接这些信息足够做初步比价。详情页则包含规格参数、优惠券信息、历史价格走势数据更全但请求量更大。我一般建议毕业设计阶段只抓搜索列表页。原因很直接列表页的 HTML 结构相对稳定一个搜索关键词返回 40 到 60 条商品翻几页就能拿到几百条数据足够支撑系统的演示和论文的数据量要求。详情页的抓取会引入额外的反爬风险和解析复杂度性价比不高。以京东搜索页为例商品卡片通常在ul.gl-warp li.gl-item这样的结构里每个 li 包含价格、标题、店铺等字段。天猫和拼多多的结构不同但思路一致找到商品卡片的 CSS 选择器逐个提取字段。2.2 requests xpath 的最小抓取脚本下面是一个抓取京东搜索结果的骨架代码跑通它只需要装好 requests 和 lxmlimport requests from lxml import etree import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Accept-Language: zh-CN,zh;q0.9, } def fetch_page(keyword, page1): 抓取京东搜索列表页返回 HTML 文本 url https://search.jd.com/Search params { keyword: keyword, enc: utf-8, page: page * 2 - 1, # 京东页码是奇数递增 } resp requests.get(url, paramsparams, headersHEADERS, timeout10) resp.encoding utf-8 return resp.text def parse_items(html): 从列表页 HTML 中提取商品信息 tree etree.HTML(html) items [] for li in tree.xpath(//li[contains(class,gl-item)]): title li.xpath(.//div[classp-name]//text()) price li.xpath(.//div[classp-price]//i/text()) shop li.xpath(.//div[classp-shop]//a/text()) items.append({ title: .join(title).strip(), price: price[0] if price else 0, shop: shop[0].strip() if shop else , }) return items if __name__ __main__: html fetch_page(空气炸锅) for item in parse_items(html): print(item) time.sleep(random.uniform(1, 3)) # 随机延时降低请求频率这段代码的逻辑分三步构造请求参数、发送 GET 请求拿到 HTML、用 lxml 解析出商品字段。page参数的换算是因为京东的搜索页码按奇数递增第 1 页对应 page1第 2 页对应 page3以此类推。参数方面有几个必须调的User-Agent要伪装成真实浏览器否则京东会返回一个空页面timeout设 10 秒避免某个请求卡死拖垮整个脚本time.sleep的随机延时是必须的固定间隔的请求很容易被识别为爬虫。我一般设 1 到 3 秒的随机值既不会太慢也不会触发频率限制。2.3 天猫和拼多多的抓取差异天猫的搜索页需要处理一个额外问题部分商品价格是通过 JavaScript 动态渲染的直接请求 HTML 拿不到价格文本。常见的做法是改用移动端接口或者用 Selenium 渲染后再解析。毕业设计阶段如果时间紧可以只做京东和天猫两个平台拼多多的反爬策略相对严格抓取难度更高。天猫商品卡片的 xpath 大致是//div[contains(class,product)]价格字段在//span[classprice]里。拼多多的移动端页面结构变动频繁建议用requests请求移动端搜索接口返回的是 JSON 数据解析起来反而比 HTML 简单。三个平台的抓取策略可以总结成一张表平台请求方式解析方式主要难点京东requests 请求 PC 搜索页lxml xpath页码奇数递增需伪装 UA天猫requests 请求移动端接口JSON 解析部分字段需登录态拼多多requests 请求移动端接口JSON 解析接口参数有签名校验提示三个平台的抓取频率都要控制。我一般单平台每分钟不超过 10 次请求三个平台交替抓取避免对单一平台造成压力。3. 价格数据怎么存怎么对齐从原始字段到可比价格3.1 数据表结构设计抓回来的原始数据不能直接拿来比价需要先落到数据库里做清洗。我一般用 SQLite毕业设计够用不用额外装 MySQL。核心表就两张商品表和价格记录表。CREATE TABLE product ( id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, -- 平台标识jd / tmall / pdd title TEXT NOT NULL, -- 商品标题 shop_name TEXT, -- 店铺名 product_url TEXT, -- 商品链接 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE price_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, product_id INTEGER NOT NULL, price REAL NOT NULL, -- 当前价格 crawl_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (product_id) REFERENCES product(id) );商品表和价格记录表分开存的好处是同一个商品可以有多条价格记录方便后续做价格走势分析。platform字段用来区分数据来源product_url作为商品的唯一标识之一。3.2 商品标题清洗与关键属性提取不同平台的商品标题写法差异很大。京东的标题通常是「品牌 型号 规格 卖点」天猫的标题可能带更多营销词拼多多的标题往往更短。直接拿标题做匹配准确率很低。我的做法是先从标题里提取关键属性品牌、型号、规格。比如「美的空气炸锅家用 5L 大容量智能触控 MF-KZ50E101」这条标题品牌是「美的」容量是「5L」型号是「MF-KZ50E101」。提取方法可以用正则匹配常见规格模式import re def extract_attrs(title): 从商品标题中提取品牌、容量、型号等关键属性 attrs {} # 提取容量数字 L / ml / 升 cap re.search(r(\d(?:\.\d)?)\s*(?:L|升|ml|毫升), title, re.I) if cap: attrs[capacity] cap.group(1) # 提取型号字母 数字组合长度 4 以上 model re.search(r[A-Z]{2,}[-]?[A-Z0-9]{4,}, title) if model: attrs[model] model.group(0) # 提取品牌取标题前两个字符作为候选 attrs[brand] title[:2] return attrs这段代码的逻辑是先定义几个常见的属性模式然后用正则逐个匹配。capacity匹配数字加容量单位model匹配字母数字组合的型号串brand简单取标题前两个字符作为候选。实际项目中品牌识别需要维护一个品牌词表用关键词匹配会更准。参数方面正则的re.I让匹配不区分大小写因为有些标题会写「5l」而不是「5L」。型号匹配的长度阈值设 4 是为了过滤掉「USB」这类短词。3.3 跨平台商品匹配的三种策略商品匹配是比价系统里最玄学的部分。同一个商品在不同平台的标题不一样图片不一样甚至连型号写法都有差异。我试过三种策略各有适用场景。第一种是标题相似度匹配。用difflib.SequenceMatcher计算两个标题的相似度超过阈值就认为是同一商品。优点是实现简单缺点是阈值不好定设高了漏匹配设低了误匹配。第二种是关键属性匹配。先提取品牌、型号、容量等属性属性完全一致才认为是同一商品。准确率高但要求属性提取足够准而且很多商品标题里根本没有型号信息。第三种是图片哈希匹配。下载商品主图计算感知哈希pHash哈希值接近的认为是同一商品。这个方法准确率最高但需要额外下载图片增加了请求量和存储开销。毕业设计阶段我建议用第一种加第二种的组合先用属性匹配筛一遍属性匹配不上的再用标题相似度兜底。阈值设 0.75 左右这个值是我试了几百条数据后觉得比较平衡的点。from difflib import SequenceMatcher def match_score(title_a, title_b): 计算两个商品标题的相似度返回 0 到 1 之间的值 return SequenceMatcher(None, title_a, title_b).ratio() def is_same_product(item_a, item_b, threshold0.75): 判断两个商品是否匹配 # 属性匹配优先 attrs_a extract_attrs(item_a[title]) attrs_b extract_attrs(item_b[title]) if attrs_a.get(model) and attrs_a[model] attrs_b.get(model): return True # 标题相似度兜底 return match_score(item_a[title], item_b[title]) thresholdSequenceMatcher的ratio()返回的是两个序列的相似度值在 0 到 1 之间。threshold参数控制匹配的严格程度我一般从 0.8 开始试如果发现漏匹配太多就降到 0.7误匹配太多就升到 0.85。4. 比价结果怎么展示从命令行到可视化界面4.1 用 Flask 搭一个最小比价页面数据存好了匹配也做完了接下来要让人看到结果。毕业设计如果只输出命令行结果答辩时不好演示。用 Flask 搭一个简单的 Web 界面工作量不大效果直观。from flask import Flask, render_template, request import sqlite3 app Flask(__name__) def get_db(): conn sqlite3.connect(price.db) conn.row_factory sqlite3.Row return conn app.route(/) def index(): keyword request.args.get(kw, 空气炸锅) conn get_db() rows conn.execute( SELECT p.title, p.platform, p.shop_name, pr.price FROM product p JOIN price_record pr ON p.id pr.product_id WHERE p.title LIKE ? ORDER BY pr.price ASC, (f%{keyword}%,) ).fetchall() conn.close() return render_template(index.html, itemsrows, keywordkeyword) if __name__ __main__: app.run(debugTrue)这段代码的核心是一个路由函数接收搜索关键词从数据库里查出匹配的商品和价格按价格升序排列传给模板渲染。sqlite3.Row让查询结果可以像字典一样按列名访问模板里写起来更方便。模板文件index.html用一个简单的表格展示结果就行列分别是商品标题、平台、店铺、价格。价格最低的那一行可以用 CSS 标个颜色一眼就能看出来。4.2 价格走势图的绘制比价系统如果只有当前价格价值有限。加上历史价格走势才能看出某个商品现在是真便宜还是假便宜。用 matplotlib 画折线图把同一个商品在不同时间点的价格连起来。import matplotlib matplotlib.use(Agg) # 无界面环境下使用 import matplotlib.pyplot as plt import sqlite3 def plot_price_trend(product_id, outputtrend.png): 绘制指定商品的价格走势图 conn sqlite3.connect(price.db) rows conn.execute( SELECT price, crawl_time FROM price_record WHERE product_id ? ORDER BY crawl_time, (product_id,) ).fetchall() conn.close() times [r[1] for r in rows] prices [r[0] for r in rows] plt.figure(figsize(10, 4)) plt.plot(range(len(prices)), prices, markero) plt.xticks(range(len(times)), times, rotation45, fontsize8) plt.ylabel(价格 (元)) plt.title(商品价格走势) plt.tight_layout() plt.savefig(output) plt.close()matplotlib.use(Agg)这行是必须的因为 Flask 运行在无图形界面的环境里不设这个后端会报错。figsize控制图片尺寸rotation45让横轴的时间标签倾斜显示避免重叠。如果横轴标签太密集可以每隔几个显示一个用plt.xticks的切片控制。4.3 定时抓取与数据更新比价系统的数据要持续更新才有意义。用schedule库或者系统的 crontab 定时跑抓取脚本每天更新一次价格。import schedule import time def daily_crawl(): 每日抓取任务 keywords [空气炸锅, 扫地机器人, 电动牙刷] for kw in keywords: html fetch_page(kw) items parse_items(html) save_to_db(items) time.sleep(random.uniform(2, 5)) schedule.every().day.at(08:00).do(daily_crawl) while True: schedule.run_pending() time.sleep(60)schedule.every().day.at(08:00)设定每天早上 8 点执行抓取。time.sleep(60)让主循环每分钟检查一次是否有任务要执行。这个方案适合部署在本地或者一台长期运行的服务器上。注意定时抓取的时间点不要设得太密集一天一次足够。频繁抓取不仅增加被封的风险对毕业设计来说也没有必要。5. 避坑指南爬虫比价系统最容易翻车的五个地方5.1 请求返回 200 但页面是空的现象requests.get()返回的状态码是 200但resp.text里找不到任何商品信息xpath 提取出来全是空列表。原因电商平台识别出了爬虫特征返回了一个验证页面或者空壳页面。常见触发条件是 User-Agent 缺失、请求频率过高、缺少 Referer 头。解决检查请求头是否完整至少带上User-Agent和Accept-Language。降低请求频率加随机延时。如果还是不行尝试加上Referer头值设为平台的搜索页地址。5.2 价格字段抓到了但全是「暂无报价」现象商品标题和店铺名都能抓到但价格字段要么是空要么是「暂无报价」。原因部分商品的价格是通过 JavaScript 异步加载的初始 HTML 里没有价格文本。另外有些商品确实没有报价比如下架商品或需要登录才能看价格的商品。解决先确认目标页面的价格是否在初始 HTML 里。如果不在改用移动端接口或者用 Selenium 渲染。如果是商品本身没有报价在数据清洗阶段过滤掉这些记录。5.3 商品匹配把不同商品判成同一个现象比价结果里出现了明显不相关的商品比如「美的空气炸锅」和「美的电饭煲」被匹配到了一起。原因标题相似度阈值设得太低或者属性提取不准确导致误匹配。品牌取前两个字符的做法在遇到「美的」和「美菱」时也会出错。解决提高相似度阈值从 0.75 调到 0.85 试试。品牌识别改用品牌词表匹配不要简单取前两个字符。型号匹配要求完全一致不要做模糊匹配。5.4 数据库写入速度越来越慢现象刚开始抓取时写入很快跑了几千条之后每次插入都要等好几秒。原因每次插入都自动提交事务没有用批量插入。SQLite 在频繁小事务写入时性能下降明显。解决用executemany批量插入或者手动控制事务攒够 100 条再提交一次。def save_batch(conn, items): 批量插入商品数据 conn.executemany( INSERT INTO product (platform, title, shop_name, product_url) VALUES (?, ?, ?, ?), [(i[platform], i[title], i[shop], i[url]) for i in items] ) conn.commit()5.5 定时任务跑着跑着就断了现象schedule脚本运行几个小时后自动停止没有报错信息。原因通常是某个请求超时没有捕获异常导致整个脚本崩溃。或者是网络波动导致连接中断。解决给每个请求加上 try-except捕获requests.exceptions.RequestException记录日志后继续执行下一个任务。不要让单个请求的失败影响整个定时任务。def safe_fetch(keyword): 带异常处理的抓取函数 try: html fetch_page(keyword) return parse_items(html) except requests.exceptions.RequestException as e: print(f抓取失败: {keyword}, 错误: {e}) return []6. 让比价更准一点商品匹配的调优技巧商品匹配的准确率直接决定了比价系统好不好用。我调了几轮之后总结出一个比较稳的流程先做属性提取再做分层匹配最后人工抽检。属性提取阶段品牌识别不要用「取前两个字符」这种偷懒做法。维护一个品牌词表把常见的家电、数码、日用品牌列进去用关键词匹配。型号提取的正则要针对不同品类调整比如家电的型号通常是「字母数字」组合而服装的型号可能是「S/M/L」这种尺码。分层匹配的思路是第一层用型号精确匹配型号一致且品牌一致的直接判定为同一商品第二层用品牌容量关键规格匹配比如「美的 5L 空气炸锅」第三层用标题相似度兜底阈值设 0.8。三层都匹配不上的标记为「待确认」不纳入比价结果。抽检环节不能省。我一般随机抽 50 组匹配结果人工看一眼有没有明显的误匹配。如果误匹配率超过 10%就回去调阈值或者补充属性提取规则。这个迭代过程可能要重复三四轮但每轮都能明显感觉到匹配质量在提升。还有一个细节不同平台的商品标题里品牌名的写法可能不一样。比如「美的」和「Midea」是同一个品牌但字符串匹配完全对不上。解决办法是在品牌词表里同时维护中文名和英文名匹配时做归一化处理。最后说一个我踩过的坑不要试图追求 100% 的匹配准确率。电商商品数据本身就存在大量噪声标题不规范、规格缺失、同一商品多个变体这些问题靠规则很难完全解决。毕业设计阶段匹配准确率做到 85% 以上就足够支撑论文的结论了。把精力放在把流程跑通、把数据讲清楚上比死磕匹配算法更划算。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?