简介:基于Python和定向爬虫的商品比价系统毕业设计源码包,面向计算机相关专业毕设学生及爬虫入门进阶者,展示从定向数据采集、持久化存储到比价结果可视化展示的完整实现流程。包内共16个文件,以10个Python脚本为绝对主体,覆盖爬虫核心逻辑、GUI交互界面、数据库操作等模块,同时包含运行生成的字节码文件、SQLite数据库及说明文档,压缩包仅26KB,结构紧凑易于阅读。已有146人学习下载,适合快速参考项目框架或进行毕设二次开发。通过源码可掌握requests+BeautifulSoup定向爬取、数据清洗入库、界面比价展示等关键技术,系统性地拆解了商品关键词搜索、价格信息抽取、多平台对比等功能;README文档帮助快速梳理运行方式,GUI脚本让操作直观,数据库文件便于理解存储结构,对希望以较小成本完成高质量毕设项目的同学极具参考价值。
1. 从手动开五个标签页比价,到一条命令跑完的毕设项目
以前比价是这么干的:打开京东、淘宝、苏宁、国美,挨个搜商品名,再人工对比谁便宜。这套流程最烦的不是点鼠标,而是每次都要重来一遍。这个毕设项目把整条链路固化成了代码——spider.py负责定向抓取、database.py负责落库去重、GUI 负责交互,一条命令启动后,爬虫自动跑完四个平台,结果直接进 SQLite 的info.db。从数据获取、清洗、存储到展示,全是 Python 生态里最常用的方案:requests发请求、BeautifulSoup解析 HTML、sqlite3持久化、tkinter做界面。对正在做毕设、想快速搭一个爬虫全流程 Demo 的人,或者刚入门爬虫、想看清一个工程怎么串起来的开发者,这份源码的价值在于:它把「爬虫 + 存储 + 展示」三件事完整串了起来,不是孤立的爬虫脚本,而是一个能跑的系统。
2. 拆目录与定向爬虫的调度入口:先看懂 main.py 怎么指挥全局
拿到 zip 解压后,先别急着跑one gui.py,先把工程结构捋一遍。根目录下有几个关键文件:main.py、spider.py、crawl.py、database.py、one gui.py、three gui.py、info.db。main.py是调度入口,它做了两件事:先启动一个子进程跑爬虫抓数据,再启动 GUI 让用户搜索比价。
2.1 main.py 怎么同时拉起爬虫和界面
我一般会先打开main.py看启动逻辑。常见的毕设工程里,main.py写的是进程调度,crawl.py负责循环抓取,spider.py封装单页解析。下面这段代码是这类工程的典型骨架:
import multiprocessing import time from crawl import crawl_process from database import init_db def gui_process(): # 延迟启动,等爬虫先写入一部分数据 time.sleep(3) import one_gui one_gui.run() if __name__ == "__main__": init_db() p1 = multiprocessing.Process(target=crawl_process) p2 = multiprocessing.Process(target=gui_process) p1.start() p2.start() p1.join() p2.join()这里用multiprocessing而不是threading,原因很实际:爬虫里的requests.get()是 IO 密集型操作,但解析 HTML 和写 SQLite 会有 GIL 锁竞争;而且爬虫进程一旦阻塞,GUI 还能保持响应,这是多进程比多线程更稳的地方。init_db()会在数据库里建好products表,crawl_process()是爬虫的循环入口,gui_process()里先sleep(3)是给爬虫争取首轮抓取的时间,避免启动后界面上什么都搜不到。
2.2 spider.py 的定向抓取逻辑
spider.py是整个项目的核心,它做的事是:构造请求头、发送 HTTP 请求、解析 HTML、抽取商品名和价格。定向爬虫和通用爬虫的最大区别就在「定向」两个字——它只针对预定义的 URL 模板和 CSS 选择器,不关心页面上的其他内容。
import requests from bs4 import BeautifulSoup HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept-Language": "zh-CN,zh;q=0.9", } def fetch_page(url): try: resp = requests.get(url, headers=HEADERS, timeout=5) resp.raise_for_status() resp.encoding = resp.apparent_encoding return resp.text except requests.RequestException as e: print(f"[spider] 请求失败: {url}, 错误: {e}") return None def parse_product(html, platform): if not html: return None soup = BeautifulSoup(html, "lxml") title_tag = soup.select_one("a.title, h3 a, .p-name a") price_tag = soup.select_one(".price, .p-price, span.price") if not title_tag or not price_tag: return None return { "platform": platform, "title": title_tag.get_text(strip=True), "price": price_tag.get_text(strip=True), }fetch_page里有两个细节值得注意:timeout=5防止某个平台响应慢时把整个爬虫拖死;resp.apparent_encoding用于应对返回内容编码不标准的情况,很多电商页面不声明 charset 或声明错误,用apparent_encoding可以让 BeautifulSoup 拿到正确的中文文本。parse_product里select_one的选择器是多个候选方案兜底,第一优先匹配上就用哪个,这是毕设项目最务实的做法——预置两到三套选择器,总有一套能命中。
2.3 反爬参数怎么设才不触发风控
定向爬虫最容易翻车的地方是请求频率和请求头。常规的电商反爬会检查 User-Agent、Referer、访问频率、代理 IP 的段位。表里列的是毕设场景下稳妥的参数配置,按这个设基本不会触发验证码。
关键参数 推荐值 说明 请求间隔 2-5 秒 用time.sleep(random.uniform(2, 5))模拟人工 请求头 完整浏览器头 缺 Referer 时会被部分平台拒绝 超时 3-8 秒 超过直接放弃本轮,下次再抓 重试次数 2 次 只在网络错误时重试,反爬返回不重试
实际操作里,抓完一个平台后sleep(3)再抓下一个,比抓一件商品 sleep 一下效率高得多,而且风控特征是「短时间内大量请求」,平台级间隔通常够用。
3. SQLite 持久化与数据清洗:database.py 怎么解决重复商品和脏数据
爬虫抓下来的原始数据不能直接进 GUI,原因很直接:不同平台对价格的表述格式不一样,有的写¥299.00,有的写299元,有的带促销标签;同一件商品在不同平台标题也可能有「官方标配」「旗舰版」这类后缀,直接比较会把数据搞乱。database.py的职责就是把它们规整成统一的表结构,再写进 SQLite。
3.1 建表与写入时的字段规范化
先看database.py里最常见的建表和写入实现:
import sqlite3 DB_PATH = "info.db" def init_db(): conn = sqlite3.connect(DB_PATH) cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, platform TEXT NOT NULL, title TEXT NOT NULL, price REAL NOT NULL, url TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) conn.commit() conn.close() def save_product(item): conn = sqlite3.connect(DB_PATH) cur = conn.cursor() cur.execute(""" INSERT OR REPLACE INTO products (platform, title, price, url) VALUES (?, ?, ?, ?) """, (item["platform"], item["title"], item["price"], item.get("url", ""))) conn.commit() conn.close()建表时用price REAL而不是TEXT,是为了后面能直接MIN(price)、AVG(price)做聚合运算;如果爬虫返回的是带 ¥ 的字符串,写入前先转浮点。INSERT OR REPLACE的去重逻辑靠什么触发?完整版里通常会在platform + title上建唯一索引,这样同一平台同一商品重复抓取时不会产生冗余行。created_at字段存储抓取时间,给增量更新留了余地,后面要加「24 小时内价格波动」时直接用这个时间戳筛。
3.2 价格清洗和标题归一化的实操方法
原始页面里的价格字符串基本不能直接用,save_product之前必须做清洗。常见做法是封装一个clean_price函数:
import re def clean_price(raw_price): if not raw_price: return None # 只保留数字、小数点和负号,其余字符全扔 match = re.search(r"(\d+\.?\d*)", raw_price.replace(",", "")) if not match: return None try: return float(match.group(1)) except ValueError: return Nonereplace(",", "")处理的是千分位分隔符,比如1,299.00先变成1299.00再去掉 ¥。re.search只提第一个数字段,因为促销文案「到手价 269 元,原价 329 元」里的第一个数字才是真实价格。清洗后的price是float类型,MySQL 里最常用DECIMAL(10,2),SQLite 里直接存 REAL 就够——毕设数据量到不了浮点精度溢出的量级。标题归一化我一般只做两件事:全角转半角、去掉多余空格,不做分词,因为分词可能把品牌名拆乱反而影响比价。
3.3 SQLite 和 MySQL、CSV 的选型边界
info.db用的是 SQLite 文件型数据库,适合这个项目的原因有三个:单文件部署、无需独立服务、Python 标准库直接支持。但如果毕设答辩要说清选型利弊,可以这么讲:SQLite 适合个人项目和数据量在百万行以内的场景;MySQL 适合多机访问和更大并发;CSV 只适合导数据,不适合查询。表里是三者的边界对比:
对比维度 SQLite MySQL CSV 部署成本 零配置,单文件 需独立服务 无 并发写 单写者 多写者 不支持 查询能力 标准 SQL 完整 SQL 需逐行遍历 适用场景 毕设/原型 生产环境 数据交换
出现sqlite3.OperationalError: database is locked时,说明爬虫进程和 GUI 进程同时在写库。解决方案在main.py的多进程设计里已经体现:爬虫进程负责写入,GUI 进程只读;如果两个进程都写,就把sqlite3.connect的timeout参数调大,比如sqlite3.connect(DB_PATH, timeout=10)。
4. 从 tkinter 界面到数据展示:one gui.py 怎么查库并比价
one gui.py是系统的交互入口,它实现的是典型的搜索型比价界面:上面是搜索框,中间是结果表格,点击搜索后从info.db里查数据,把同一商品在各平台的价格并排展示。tkinter 虽然是 Python 自带的 GUI 库,写复杂界面会繁琐,但做毕设的展示型界面完全够用,部署时不需要额外装包。
4.1 搜索框、结果表格和触发查询的核心代码
GUI 工程里one gui.py的骨架通常长这样:
import tkinter as tk from tkinter import ttk import sqlite3 def search_product(keyword): conn = sqlite3.connect("info.db") cur = conn.cursor() cur.execute(""" SELECT platform, title, price, created_at FROM products WHERE title LIKE ? ORDER BY price ASC """, (f"%{keyword}%",)) rows = cur.fetchall() conn.close() return rows def on_search(): keyword = entry.get().strip() if not keyword: return for row in tree.get_children(): tree.delete(row) for row in search_product(keyword): tree.insert("", "end", values=row) app = tk.Tk() app.title("商品比价系统") frame = tk.Frame(app) frame.pack(pady=10) entry = tk.Entry(frame, width=40) entry.pack(side="left") tk.Button(frame, text="搜索", command=on_search).pack(side="left") tree = ttk.Treeview(app, columns=("platform", "title", "price", "time"), show="headings") tree.heading("platform", text="平台") tree.heading("title", text="商品标题") tree.heading("price", text="价格") tree.heading("time", text="抓取时间") tree.pack(fill="both", expand=True) app.mainloop()LIKE ?配合f"%{keyword}%"是模糊查询的标准写法,传参时用?占位符而不是直接拼字符串,能避开 SQL 注入——毕设代码里这个习惯也会被答辩老师认可。ORDER BY price ASC保证结果从低价到高价排列,用户一眼看到最低价来自哪个平台。tree是 ttk.Treeview 表格组件,tree.insert的values参数和 SQL 查询列顺序要严格一致,否则列头和数据对不上。
4.2 重要交互逻辑的说明
搜索按钮绑定的on_search做了三件事:先取搜索框内容并去掉首尾空格;然后清空表格现有数据;最后执行查询并逐行插入。这里每次搜索都会重新查库而不是从内存缓存里读,因为是毕设项目,数据量不大,直查 SQLite 的延迟可以忽略。
需要留意的是 GUI 和爬虫进程的协同方式。main.py里爬虫进程先跑、GUI 延迟 3 秒启动,如果爬虫还没抓到商品用户就开始搜索,结果是空表。合理的兜底方案是加一行提示:tree.insert("", "end", values=("暂无数据", "请稍后重试", "", ""))。另外app.mainloop()会阻塞当前线程,如果直接在 GUI 线程里调用爬虫抓取函数,界面会卡死,表现是窗口无响应。毕设常见做法是简单点——搜索时只查已有数据,不触发抓取,数据由crawl_process在后台持续填充。
4.3 GUI 工程的结构对比:one gui 和 three gui 差在哪
压缩包里有两个 GUI 文件,one gui.py通常是单独的搜索窗口,three gui.py往往是加了图表或明细页面的增强版。两者共用一个info.db,UI 上差异主要在布局复杂度。one gui.py适合演示核心流程,逻辑短,答辩时好讲解;three gui.py如果你打开后发现它有matplotlib嵌图,说明它用FigureCanvasTkAgg把比价柱状图画进了 tkinter 窗口,那部分要单独确认matplotlib是否在环境里,否则 import 阶段就会报ModuleNotFoundError。
运行环境上,建议 Python 3.8 到 3.11 之间,tkinter是标准库,直接import即可,但BeautifulSoup和lxml需要手动安装:
pip install beautifulsoup4 lxml requestslxml比html.parser解析速度快,电商大页面效果明显,但 Windows 上安装 lxml 偶尔会失败,改用pip install lxml --prefer-binary能解决大部分编译问题。
5. 定向爬虫的比价核心逻辑与最后一公里的数据校验
比价不是「找出最低价」这么简单,真正的核心是:数据可信度、实时性和多平台口径统一。这个系统虽然是一个毕设工程,但该有的比价逻辑并没有被简化——抓取数据落库后,GUI 查询时做聚合分析,三行 SQL 就能算出最低价、最高价和均价。
5.1 多平台价格聚合查询与归一化处理
假设save_product抓到的price已经洗净成float,那么比价查询可以这么写:
SELECT platform, MIN(price) AS min_price, ROUND(AVG(price), 2) AS avg_price FROM products WHERE title LIKE '%iPhone%' GROUP BY platform ORDER BY min_price ASC;ROUND(AVG(price), 2)保留两位小数,GROUP BY platform让每个平台独立计算统计值,ORDER BY min_price直接给出最便宜的平台排第一。GUI 里如果要把这个查询嵌进去,search_product需要扩展成两个查询:一个查明细,一个查聚合——明细给表格逐行展示,聚合给顶部标签展示。
5.2 价格归一化时最容易踩的坑
不同平台会把「满减」写在价格后面,有人贪省事直接把整段文字转 float,结果ValueError: could not convert string to float。我以前也踩过这个坑,后来定了一条硬规则:入库前不做促销计算,只存页面上标出的第一价格。标269就存269,标券后 269就存269,标269起还是取269。这样虽然丢掉了促销价信息,但换来的是所有平台口径一致,比价才有可比性。如果未来要扩展「到手价」逻辑,需要在clean_price里提前把「券后」「到手」这类关键词作为权重标记存一列,而不是改解析规则。
5.3 爬虫运行后的正确验证顺序
跑完main.py后,不要只盯着 GUI 窗口看,按这套动作检查爬虫健康度:
# 第一步先查库总行数,看抓取是否有数据进入 sqlite3 info.db "SELECT COUNT(*) FROM products;" # 第二步看每个平台的抓取条数是否均衡,某个平台为 0 说明选择器或反爬拦截 sqlite3 info.db "SELECT platform, COUNT(*) FROM products GROUP BY platform;" # 第三步看价格字段是否有异常值(NULL 或 0) sqlite3 info.db "SELECT * FROM products WHERE price IS NULL OR price <= 0;"如果某个平台行数为 0,多半是页面结构改了导致select_one的选择器没命中。排查方法是先手动打开该平台对应 URL,用浏览器开发者工具检查商品标题和价格的实际 class 名,把spider.py里对应平台的 CSS 选择器更新掉。如果价格字段全为 0,则是clean_price没匹配到数字段,打印原始raw_price一眼就能定位。这三个验证操作做完,系统才算真的能交。
5.4 扩展比价维度时的保留接口
数据表里已有的url字段不只是摆设,GUI 里可以给结果表格绑定「点击跳转」事件,把url传给webbrowser.open()。created_at时间戳可以做「近 7 天价格走势」,只需加一条WHERE created_at >= datetime('now', '-7 days')。这个项目的核心架构里,spider.py负责抓,database.py负责洗,GUI 唯一做的事就是查——职责严格分离,加新平台时只需要在crawl.py里加一个 URL 模板和一组解析规则,不动表结构、不动 GUI,这就是它作为毕设工程结构上最值得保留的设计。
本文还有配套的精品资源,点击获取