资讯动态

Python定向爬虫实现商品比价系统:从源码拆解到实战优化

发布时间:2026/10/26 22:38:12 来源:尧图企业网站定制
简介面向计算机相关专业毕业设计这份基于Python和定向爬虫的商品比价系统源码解决了从多个电商平台自动抓取价格信息并对比展示的需求。项目不仅演示了定向爬虫与通用爬虫的区别还完整实现了数据获取、数据清洗、数据存储、商品比价和用户界面五大模块适合作为毕设参照或Python爬虫进阶练习。压缩包内共十六个文件包含十个Python源文件、三个编译缓存文件、一个SQLite数据库、一个TXT说明文档和一个Markdown文档包体仅二十六KB结构精简。当前已有一百四十六人学习。通过这份源码可以学习到requests库与BeautifulSoup解析库的实战搭配以及多文件项目的工程组织技巧数据库文件和底部说明文档则呈现了表结构设计与部署要点。源码中还针对请求间隔、代理、模拟登录等反爬策略做了代码层面处理有助于理解真实场景中的爬虫稳定性方案对理解和完成类似爬虫类毕设极具参考价值。1. 定向爬虫商品比价系统一套能跑通的毕设源码把《基于Python和定向爬虫的商品比价系统》这套毕设源码解压之后第一眼看到的不是文档而是一排gui.py文件和好几个同名crawl.py、database.py外加一个info.db。这不是乱这是真实项目迭代留下的痕迹。它的核心目标很直接用定向爬虫抓取电商平台上的商品价格存进数据库再通过图形界面让用户输入关键词看比价结果。对正在做毕设或课程设计的 Python 学习者来说这套源码的价值在于它同时覆盖了爬虫、数据持久化、桌面 GUI 三条主线且没有上框架纯标准库加少量第三方库就能跑。它不是什么黑匣子而是一份能把“书本知识”落成“可运行程序”的完整样例。2. 源码结构拆解文件清单、info.db 与模块复用逻辑2.1 从文件清单看工程组织方式整个 zip 解压后典型的目录结构是根目录下有一个code文件夹里面放着main.py、crawl.py、database.py、gui.py和README.md同时在根目录或单个功能目录里又分别出现了one gui.py、two gui.py、three gui.py以及配套的crawl.py、spider.py、database.py。第一次拿到这种结构的人很容易懵我拆过不少毕设包这种“同名字段在不同目录里各来一份”的现象非常普遍它通常意味着作者在迭代过程中没有做目录归并。文件/目录作用推测说明code/main.py程序入口一般负责初始化数据库并启动 GUIcrawl.py爬虫核心发送 HTTP 请求、解析 HTML、提取价格database.py数据持久化封装 SQLite 的增删改查one gui.py第一版界面功能最简单通常是验证流程two gui.py第二版界面开始与 database、crawl 联动three gui.py第三版界面引入 spider.py结构更清晰spider.py爬虫类封装把请求、解析、容错封装成类info.dbSQLite 数据库文件存放抓取到的商品与价格记录__pycache__Python 缓存目录说明打包时未清理运行缓存我的建议是先读README.md再看main.py最后对照three gui.py去看整个流程。很多毕设 README 写得极简只有“如何运行”几句但这套源码的价值恰恰在代码本身README 没写清楚的部分要靠读代码补。如果你打算在此基础上改造成自己的毕设最省力的做法是直接以three gui.py和它同目录的spider.py为基底不要从one gui.py开始那个版本太原始了。动手运行之前先把依赖确认掉。这个项目基本只需要requests、beautifulsoup4、lxml三个第三方库数据库用的sqlite3是 Python 标准库GUI 用的tkinter也是标准库。理论上 Python 3.8 以上都能跑不需要额外安装 MySQL 之类的重型组件。pip install requests beautifulsoup4 lxml执行完这三行依赖就齐了。这里刻意不装scrapy因为这套源码的处理逻辑用 requests 就能讲清楚强行引入 Scrapy 反而会把代码结构带偏。如果你是新手建议用 pip 安装而不是直接下载 whl 文件pip 会自动处理依赖版本关系。2.2 info.dbSQLite 是比 MySQL 更务实的选题现在很多毕设一上来就写“基于 MySQL 数据库”但实际交出来的工程用的却是 SQLite这套源码就是一个典型。SQLite 单文件、免安装、Python 标准库直接支持对商品比价这种数据量不超过几万条的桌面应用来说完全够用。它最大的好处是你不需要让答辩老师现场配一个数据库服务双击main.py就能看到数据落盘。进入database.py数据库操作基本离不开这几件事建表、插入、查询、去重。常规的表结构会围绕商品、平台、价格、抓取时间四个字段展开。假设表名叫products字段设计通常是这样的import sqlite3 def init_db(db_pathinfo.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS products ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, platform TEXT NOT NULL, price REAL, url TEXT, crawl_time TEXT ) ) conn.commit() conn.close()这段代码里IF NOT EXISTS很关键每次启动程序都会执行一次初始化如果没有这个判断第二次运行就会因为表已存在而报错。AUTOINCREMENT让主键自动增长虽然性能上不如纯 INTEGER但胜在省事不需要手动维护 id。price用REAL而不是TEXT是因为后续要比价排序数字类型才能直接用 SQL 做ORDER BY price ASC字符串排序会得到“9.9 100”的可笑结果。插入数据时我一般会额外加一条去重约束。比价系统的重复数据主要来自同一个商品链接被多次抓取如果不做处理info.db会越跑越脏。一个常见的做法是用平台加商品链接做唯一键INSERT OR REPLACE INTO products (name, platform, price, url, crawl_time) VALUES (?, ?, ?, ?, ?)INSERT OR REPLACE是 SQLite 的特色语法遇到相同唯一键就先删后插保证同一 URL 只会保留最新一次价格。如果你在源码里没看到这层逻辑也正常很多第一版数据库模块只做了单纯的INSERT这就把去重的责任甩给了爬虫层。实际使用时我更推荐在初始化表结构时直接加UNIQUE约束让数据库自己把关CREATE TABLE IF NOT EXISTS products ( ... url TEXT UNIQUE )加了UNIQUE之后再配合INSERT OR REPLACE数据质量就有保障了。2.3 同名模块反复出现三版 GUI 的演进信号这套源码里crawl.py出现了多次database.py也出现多次三个 GUI 文件分别对应三个开发阶段我先把这个演进关系说透。第一版通常是“功能验证版”。one gui.py里大概率只放了一个输入框和一个按钮点击按钮后同步调用爬虫界面会卡住几秒然后把结果显示在文本框里。这个版本能跑但体验很差因为网络请求阻塞了 tkinter 主线程。第二版开始拆模块。two gui.py会主动from database import insert_product这样的导入语句把存储逻辑独立出去同时可能在按钮回调里加入基本的异常处理。但它仍然可能没有解决界面卡顿的问题因为线程还没有引入。第三版是关键的一版。three gui.py同目录出现了spider.py说明作者开始用面向对象方式组织爬虫代码把每次请求封装成一个Spider类里面包含请求头、超时时间、重试次数这些参数。这一版才真正像一个“系统”而不是几段脚本的拼凑。我在拆这类工程时有个习惯先找出最后版本的文件组合再反推前面的版本。如果你准备拿这套源码改造成自己的毕设不需要逐行理解one和two重点看第三个组合就够了。之前的版本可以当作“开发过程文档”来读它们能帮你理解作者在哪些地方走了弯路哪些需求是后补的。3. 定向爬虫怎么写从 crawl.py 到 spider.py 的抓取逻辑3.1 定向爬虫与通用爬虫的本质区别很多资料把爬虫分成通用爬虫和定向爬虫听起来像是学术分类但落到工程上差别很实在。通用爬虫的目标是从一个种子 URL 出发不断提取链接、层层递归抓遍全网定向爬虫则相反它预先知道“我要抓哪个网站、哪个页面、哪些字段”代码写死了目标站点的 URL 模板和解析规则。这套商品比价系统的“定向”体现在三个地方第一起始 URL 是固定的。比如要抓某个电商平台商品搜索页的 URL 模板通常是https://xxx.com/search?keyword关键词爬虫只需要把关键词拼进 URL不需要做全网链接发现。第二解析规则是固定的。每个商品卡片在 HTML 里的位置是确定的选择器写死就能取出价格。第三字段是固定的。只需要商品名、价格、平台、URL 四个字段不抓评论、不抓销量、不抓图片。所以定向爬虫的工程量比通用爬虫小得多但容错要求更高——因为页面结构一变选择器就失效。在写这套源码的爬虫模块时一个容易被忽略的点是同一个网站在 PC 端和移动端的 HTML 结构完全不同定向爬虫必须锁定一种终端形态。常见做法是在请求头里写死User-Agent把它伪装成 Chrome 浏览器。3.2 requests BeautifulSoup 的抓取模板crawl.py和spider.py的底层基本都是这个套路requests 发请求BeautifulSoup 解析提取目标节点后清洗文本。下面是一段在比价场景里非常通用的抓取函数模板和这套源码的思路一致import requests import time import random from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 } def fetch_price(url, timeout8, retry3): for attempt in range(retry): try: resp requests.get(url, headersHEADERS, timeouttimeout) if resp.status_code ! 200: continue resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, lxml) price_node soup.select_one(.price-now) if price_node is None: return None return price_node.get_text().strip() except requests.RequestException: time.sleep(random.uniform(1, 3)) return None这里有几个参数值得逐一说清楚。timeout8是请求超时时间单位秒超过 8 秒没响应就抛异常避免程序卡死。retry3是重试次数应对偶发的网络波动。resp.apparent_encoding是根据页面内容反推编码的兜底方案直接resp.text有时会乱码尤其目标站点可能是 GBK 编码。select_one(.price-now)是 CSS 选择器用法点号代表 class实际项目里要根据目标页面的 HTML 结构改成#price、span.price之类这套源码的解析规则也得按当时页面的结构来页面改版后这里是要改的第一处。还有一个细节很多人忽略拿到价格文本后要strip()去掉空白并且价格里大概率带了“¥”符号和“元”字存储前必须清洗。清洗规则在各源码项目里五花八门常见的是用正则把非数字小数点字符全部剔除import re def clean_price(raw_text): match re.search(r\d(\.\d)?, raw_text.replace(,, )) return float(match.group()) if match else None这段代码里\d(\.\d)?匹配整数或小数的价格replace(,, )用来处理“1,299.00”这种千分位写法。清洗之后才能安全地存进数据库的 REAL 字段也才能参与后续的比价排序。3.3 定向爬虫的容错设计不要假设页面永远不变定向爬虫最大的坑不是写不出来而是写完之后跑两周突然抓不到数据了。原因绝大多数是目标网站改版或者加了反爬机制。我在看这套源码时最关心的就是爬虫模块有没有做三层容错请求层、解析层、存储层。请求层容错包括捕获超时和连接异常、设置重试次数、两次请求之间加随机 sleep。很多毕设源码只做第一层也就是try...except但没有 sleep导致请求频率过高被对方服务器限流。比较稳妥的间隔是random.uniform(1, 2)秒既不会太慢也不会因为单位时间请求过多触发反爬。解析层容错的核心是“节点不存在时不要崩溃”。select_one找不到节点会返回None不处理的话下一行.get_text()直接抛AttributeError。上面的模板里已经做了if price_node is None: return None这是底线写法更稳妥一点是针对同一页面写多个备用选择器比如price_node soup.select_one(.price-now) or soup.select_one(.sale-price) or soup.select_one(del)一个选择器失效就自动尝试下一个这是实战里最实在的兜底方案。注意备选节点一定要放到一个or链里任何一个生效都直接短路返回不会逐个遍历。存储层容错主要是幂等性也就是前面说的INSERT OR REPLACE同一商品 URL 重复抓取不会产生脏数据。把这三层容错做完这套系统才谈得上“能持续运行”而不是“能跑一次”。4. 比价与展示tkinter 三版 GUI 的取舍与查询流程4.1 三版 GUI 的定位哪个版本值得继续用标题里的gui.py用的是 Python 标准库 tkinter没有引入 PyQt、PySide 这类重型框架。对毕设来说这是合理选择tkinter 开箱即用跨平台表现一致且代码量少答辩时容易讲清楚每一行控件逻辑。one gui.py基本就是一个输入框加一个按钮点击后同步执行爬虫界面假死到请求返回为止。two gui.py开始把数据库操作独立成函数调用但仍然可能是同步执行。three gui.py是三个版本里结构最好的它引入了spider.py并把爬虫放到子线程运行界面不再卡死。如果你要基于这套源码继续开发我建议直接删掉one gui.py和two gui.py的引用关系以three gui.py为准。不过要留意一点three gui.py的 UI 控件通常还停留在“能看但不好看”的程度控件之间没有严格的布局管理用grid()和pack()混排。改造的方向是用ttk组件替换原生控件并统一布局策略。4.2 查询流程拆解按钮回调、子线程与消息队列tkinter 有个经典的坑爬虫这种阻塞操作不能直接放在按钮回调里跑。因为 tkinter 是单线程事件循环回调函数里一旦出现网络请求整个界面就会冻住鼠标点了没反应窗口拖动都卡。这套源码的第三版应该已经做了线程处理做法是标准的“子线程跑爬虫主线程轮询队列”模式import threading import queue import tkinter as tk msg_queue queue.Queue() def start_compare(): keyword entry.get().strip() if not keyword: msg_queue.put(关键词不能为空) return t threading.Thread(targetdo_compare, args(keyword,), daemonTrue) t.start() def do_compare(keyword): result crawl_and_compare(keyword) msg_queue.put(result) def poll_queue(): try: while True: item msg_queue.get_nowait() result_text.insert(tk.END, str(item) \n) except queue.Empty: pass root.after(100, poll_queue)这段代码的逻辑是start_compare只负责校验输入并创建线程真正的爬虫和比价逻辑跑在do_compare里。子线程不能直接操作 UI 控件否则会报RuntimeError: main thread is not in main loop之类的错误所以它把结果放进queue主线程通过root.after(100, poll_queue)每 100 毫秒轮询一次队列取到结果再写入文本框。root.after是 tkinter 自带的事件调度方法第一个参数是毫秒数第二个是回调函数。轮询间隔 100 毫秒足够感知结果到达又不会造成 CPU 空转。这个模式比threading StringVar的写法更稳妥因为队列天然支持多条消息排队不会丢数据。4.3 输入校验与空结果处理比价系统的用户输入是搜索关键词直接拼进 URL 会带来两个问题空白字符和特殊字符。关键词前后的空格会影响 URL 合法性所以entry.get()之后必须做.strip()。特殊字符方面中文关键词要做 URL 编码requests 库会在传参时自动处理但如果关键词里带了空格就要显式转换from urllib.parse import quote keyword_encoded quote(keyword) search_url fhttps://xxx.com/search?keyword{keyword_encoded}quote会把中文转成%E4%BA%A7%E5%93%81这样的编码空格转成%20保证 URL 语义正确。如果不做这步某些电商平台会拒绝请求或者返回空结果页。拿到各平台的价格数据后比价逻辑通常是这样把所有价格收集到一个列表里按数字升序排序标记出最低价、最高价和平均价。存储层已经用了 REAL 类型的 price 字段所以这一步用 Python 原生的sorted()就能完成def compare_prices(products): if not products: return 未找到相关商品 sorted_items sorted(products, keylambda x: x[price]) lines [] for item in sorted_items: lines.append(f{item[platform]}: {item[price]} 元) lines.append(f-- 最低: {sorted_items[0][platform]} {sorted_items[0][price]} 元) lines.append(f-- 最高: {sorted_items[-1][platform]} {sorted_items[-1][price]} 元) avg_price sum(i[price] for i in sorted_items) / len(sorted_items) lines.append(f-- 平均: {avg_price:.2f} 元) return \n.join(lines)这里的keylambda x: x[price]指定按价格排序avg_price保留两位小数展示。整套流程的核心就是把“抓到的原始数据”转换成“排序后的用户可读结论”这一层逻辑虽然在代码里只有几行但在系统设计上属于独立的比价模块值得单独写成函数方便后续扩展促销信息、价格走势等功能。5. 避坑排查SQLite 并发、编码与页面改版的实战记录这一章的每条记录都是拆这种毕设源码时最容易踩中的真实问题。按“现象 → 原因 → 解决”的顺序写之前我自己改这类项目时也在这几个坑里栽过。5.1 界面按钮点了没反应程序直接卡死现象在one gui.py里点击“开始比价”窗口立即变成“未响应”状态过十几秒才恢复甚至直接白屏。原因按钮回调函数里直接调用了requests.get()网络请求。tkinter 的主线程被阻塞事件循环无法处理窗口绘制和鼠标事件表现就是界面冻结。解决把爬虫逻辑扔进threading.Thread子线程主线程只负责创建线程和轮询消息队列。参考 4.2 节的模板确保 UI 控件只在主线程中被修改。判断标准很简单回调函数里出现requests.get()且没有threading就一定会卡。5.2 抓下来的中文全部变成乱码现象info.db里存的商品名是“鍟嗗搧”这种无意义字符或者 GUI 文本框里显示的是问号。原因目标网站返回的 HTML 用的是 GBK/GB2312 编码而resp.text默认按requests猜测的编码解码。requests的编码猜测逻辑并不总是准尤其对中文网站经常猜错。解决强制指定编码方法是在解析之前加一行resp.encoding resp.apparent_encoding。apparent_encoding是根据响应内容二进制特征反推出来的编码对中文网页命中率高。如果还不行就手动指定resp.encoding gbk以目标站点页头meta charset...声明为准。乱码问题一定在解析之前解决存进数据库之后再清洗就来不及了。5.3 info.db 打开以后里面是空的或者数据少得离谱现象程序跑完没有报错crawl.py也正常输出了日志但用 DB Browser 打开info.db表格里只有零星几条记录。原因SQLite 连接没有提交事务。conn.execute()之后必须conn.commit()才会真正落盘如果代码只做了execute没做commit程序退出后所有写入全部丢失。另一部分原因是爬虫解析失败返回None但上层代码没有过滤就跳过了存储。解决写操作统一走“连接 → 执行 → commit → close”四步且把commit()放在循环外。批量插入几十条数据时循环内逐条 commit 会比批量 commit 慢几十倍性能差距很明显。在调试阶段每执行完一次抓取就手动打开数据库看一眼确认数据落盘再继续。5.4 换一台电脑跑不起来报 No module named requests现象源码在自己的机器上运行正常拷到舍友电脑或答辩机器上一运行就提示ModuleNotFoundError。原因当前机器的 Python 环境是全局环境依赖库已经装过但项目本身没有提供requirements.txt换机器后全新环境自然缺依赖。另外zip 包里的__pycache__目录说明打包者直接拷贝了运行目录没有重新构建干净环境。解决在项目根目录执行pip freeze requirements.txt导出依赖清单换机器后pip install -r requirements.txt一键还原。这里有个默认的坑pip freeze会把全局环境下所有包都导进去包括和项目无关的库更好的做法是虚拟环境安装再导出python -m venv venv venv\Scripts\activate pip install requests beautifulsoup4 lxml pip freeze requirements.txt用虚拟环境隔离依赖之后交出去的源码就自带完整的运行说明答辩老师拿到就能复现。5.5 爬虫前两周正常某天突然返回空结果现象日志显示 HTTP 状态码正常select_one也没有抛异常但价格字段一直None数据库不再写入新记录。原因电商平台的页面结构改了。商品价格节点的 class 名从.price-now改成了.sale-price写死的 CSS 选择器失效。也有可能是网站加了反爬校验返回了空页面或验证页而状态码恰好是 200。解决先打印resp.text[:500]看页面开头内容判断是验证页还是正常页面。如果是结构变化更新选择器即可如果是反爬页面优先降低请求频率而不是增加请求次数。另一个兜底方案是准备多个备用选择器用or链逐个尝试上一节已经给了写法。这个坑无法完全避免只能通过日志和监测来尽早发现。5.6 参数经验表并发、超时与请求间隔参考参数建议值说明timeout6~10 秒低于 5 秒容易误判慢请求为超时retry2~3 次超过 3 次会显著拉长整体耗时请求间隔1~3 秒随机固定间隔更容易被识别为脚本行为单次查询平台数3~5 个再多 GUI 界面等待时间过长数据库批量提交每 50 条一次兼顾内存与磁盘 IO 效率这套参数不是拍脑袋定的。请求间隔取 1~3 秒的随机值比固定 2 秒更贴近人工操作节奏能在效率和风险之间取得平衡。超时时间 8 秒是默认值目标站点慢的时候可以放宽到 10 秒但不要超过 15 秒否则异常感知太迟钝。6. 进阶线程池与定时更新把它改造成持续可用的工具基础版源码跑通只是第一步离“能用”还有一段距离。这三个改造方向在毕设答辩里也是加分项并发抓取、定时更新、结果导出。6.1 用 ThreadPoolExecutor 替代手动线程目前源码里大概是每查一个平台就手动开一个threading.Thread平台多了线程数失控界面轮询队列也扛不住。我会改成线程池from concurrent.futures import ThreadPoolExecutor, as_completed def crawl_all_platforms(keyword, urls, max_workers3): results [] with ThreadPoolExecutor(max_workersmax_workers) as pool: future_map {pool.submit(fetch_price, url): url for url in urls} for future in as_completed(future_map): price future.result() if price is not None: results.append({platform: future_map[future], price: price}) return resultsmax_workers3意味着同一时间最多 3 个 HTTP 请求在跑既能加快速度又不会把目标站点打挂。as_completed按完成顺序返回结果先返回的先处理不强制等待慢请求。6.2 定时更新与 Excel 导出商品价格是会变动的比价系统的价值建立在数据新鲜度上。用schedule库做定时抓取是最轻量的方案import schedule import time def job(): keyword 笔记本电脑 urls build_search_urls(keyword) products crawl_all_platforms(keyword, urls) save_products(products) print(f{time.strftime(%Y-%m-%d %H:%M:%S)} 更新完成) schedule.every(30).minutes.do(job) while True: schedule.run_pending() time.sleep(1)schedule.every(30).minutes.do(job)定义每 30 分钟执行一次time.sleep(1)让主循环每秒检查一次任务表。价格变动这种粒度用 30 分钟足够。导出结果用 pandas 加 openpyxl一行就能生成表格import pandas as pd def export_to_excel(products, filenamecompare_result.xlsx): df pd.DataFrame(products) df.to_excel(filename, indexFalse)indexFalse避免多出一列行号导出的表格可以直接塞进毕设的测试报告。从那以后我每次拿到这类毕设源码都会先看数据库有没有做去重、爬虫有没有放子线程、依赖有没有导出这三道检查跑完才敢把代码往正式环境里放。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑