Python定向爬虫实战:商品比价系统从抓取到落地
简介:这份资源是面向计算机专业学生与Python爬虫初学者的毕业设计/课程设计完整方案,围绕「基于Python和定向爬虫的商品比价系统」展开,帮助读者解决从数据抓取到界面展示的全流程实现问题。压缩包共16个文件,以10个py源码为主,辅以pyc编译文件、txt说明、db数据库与md文档,整体约27KB,体量轻便、便于快速阅读与二次修改。资源按三种实现思路分文件夹组织:面向对象版本同时接入数据库与图形界面,面向过程版本分别提供数据库存储与文件存储两种方案,便于对比不同编程范式的取舍与代码组织方式。内容涵盖爬虫抓取、数据持久化、GUI交互等核心模块,适合直接作为毕设参考或大作业模板,也可用于练习定向爬虫与数据库操作。目前已有1024人学习下载,具备一定的参考热度与验证基础。
1. 从一份能跑通的商品比价系统说起:它到底解决了什么问题
电商比价这件事,手动做一次两次还行,一旦要盯十几个平台、上百个商品,人肉刷新就彻底不现实了。这份毕业设计资源的核心,就是一套基于 Python 的定向爬虫加比价逻辑的完整实现——它不是那种只爬一个静态页面的玩具 demo,而是把「定向抓取 → 数据清洗 → 价格归一 → 比价排序 → 结果落地」这条链路串了起来。适合正在做计算机毕业设计、需要一套能讲清楚架构又能实际跑起来的同学,也适合想入门定向爬虫的开发者拿来拆解。定向爬虫和通用爬虫最大的区别在于:它只盯目标站点的特定字段,不做全站遍历,所以结构更清晰、反爬对抗压力更小、代码量也更可控。这份资源能帮你省掉从零搭框架的时间,把精力放在比价算法和业务逻辑上。
2. 定向爬虫的抓取层怎么搭:请求、解析、字段抽取三步走
2.1 为什么选 requests + BeautifulSoup 而不是上 Scrapy
很多人一提到爬虫就条件反射上 Scrapy,但对于商品比价这种定向场景,Scrapy 的调度器、去重队列、中间件体系反而是负担。这份资源走的是轻量路线:requests 负责发请求,BeautifulSoup 负责解析 HTML,字段抽取用 CSS 选择器直接定位。好处是调试直观——你可以在交互环境里一行行试选择器,确认拿到正确的价格节点再写进代码。常见做法是先用浏览器开发者工具定位到价格所在的 DOM 节点,复制它的 class 或 id,再映射到 BeautifulSoup 的 select 方法上。
选型理由说白了就一条:定向爬虫的目标页面结构是已知的、有限的,不需要通用爬虫那套自适应发现机制。你明确知道要去哪个页面、取哪个字段,那用最薄的工具链就够了。Scrapy 适合的是「不知道有多少页面、需要广度优先遍历」的场景,跟比价系统的需求不匹配。
2.2 请求头的伪装与请求频率控制
直接裸 requests.get 大概率被拒。下面是一个我常用的请求封装,包含请求头设置和随机延时:
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/120.0.0.0 Safari/537.36" ), "Accept-Language": "zh-CN,zh;q=0.9", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", } def fetch_page(url, retry=3): """带重试和随机延时的页面抓取""" for i in range(retry): try: # 每次请求间隔 1~3 秒,模拟人工浏览节奏 time.sleep(random.uniform(1, 3)) resp = requests.get(url, headers=HEADERS, timeout=10) resp.raise_for_status() resp.encoding = resp.apparent_encoding # 自动纠正编码 return resp.text except requests.RequestException as e: print(f"第 {i+1} 次请求失败: {e}") if i == retry - 1: return None return None这段代码里几个参数值得说清楚。timeout=10是防止某个请求卡死拖垮整个流程,10 秒是个经验值,超过这个时间基本说明对方服务器不打算正常响应了。resp.apparent_encoding是让 requests 根据内容自动推断编码,比手动指定resp.encoding = 'utf-8'更稳,因为有些站点返回的是 gbk 但你不知道。随机延时用random.uniform(1, 3)而不是固定 sleep,是因为固定间隔的请求模式很容易被识别为脚本行为。
2.3 用 CSS 选择器抽取商品字段
拿到 HTML 之后就是解析。下面是一个典型的商品信息抽取函数:
def parse_product(html, url): """从商品详情页 HTML 中抽取结构化字段""" soup = BeautifulSoup(html, "html.parser") product = {} # 商品标题——不同平台 class 名不同,这里以通用结构举例 title_tag = soup.select_one("h1.product-title") or soup.select_one(".sku-name") product["title"] = title_tag.get_text(strip=True) if title_tag else "" # 价格——注意有些平台价格在 JS 变量里而非 DOM 中 price_tag = soup.select_one(".price .now") or soup.select_one("[class*=price]") if price_tag: # 去掉货币符号和千分位逗号,只保留数字和小数点 raw = price_tag.get_text(strip=True) product["price"] = float( raw.replace("¥", "").replace(",", "").strip() ) else: product["price"] = None # 店铺名 shop_tag = soup.select_one(".shop-name") or soup.select_one(".J-hove-wrap") product["shop"] = shop_tag.get_text(strip=True) if shop_tag else "" product["url"] = url return product逻辑说明:select_one返回第一个匹配元素,适合字段唯一的场景。价格清洗那一步是必须的——页面上显示的是「¥1,299.00」,直接 float 会报错,得先把货币符号和千分位逗号去掉。product["price"] = None的处理是为了后续比价时能过滤掉无效数据,而不是让程序在 float 转换处直接崩掉。
参数方面,html.parser是 Python 内置解析器,不需要额外装 lxml,虽然速度慢一点但对毕业设计这种数据量完全够用。如果你要处理几千个页面,换成lxml解析器会快不少,代价是多装一个依赖。
3. 比价逻辑与数据落地:从原始价格到可排序的比价结果
3.1 价格归一化:为什么不能直接比数字
不同平台的标价方式差异很大。有的显示「¥1299」,有的显示「1299.00 元」,有的搞促销写「券后 ¥1199」,还有的用「起」字标最低价。如果直接抓文本转 float,要么报错要么比出错误结果。归一化要做三件事:去掉货币符号和单位、处理促销价与原始价的关系、统一保留两位小数。
import re def normalize_price(raw_text): """将各种格式的价格文本统一为 float""" if not raw_text: return None # 提取第一个连续的数字(含小数点) match = re.search(r"(\d+\.?\d*)", raw_text.replace(",", "")) if match: return round(float(match.group(1)), 2) return None这个正则(\d+\.?\d*)能匹配「1299」「1299.00」「0.99」等格式。replace(",", "")先去掉千分位逗号,避免「1,299」被截断成 1。round(..., 2)统一精度,防止浮点误差导致 1299.00 和 1299.0 被判定为不同价格。
3.2 比价排序与结果存储
比价的核心逻辑其实很简单:把同一商品在不同平台的归一化价格放在一起,按升序排列,标出最低价来源。但这里有个容易翻车的点——商品匹配。同一个商品在不同平台的标题可能完全不同,你不能靠标题字符串相等来判断是不是同一商品。
常见做法是用商品编码(如 ISBN、型号)做精确匹配,如果没有编码就用标题的相似度做模糊匹配。毕业设计里通常用后者,配合一个简单的关键词提取:
import json from difflib import SequenceMatcher def similarity(a, b): """计算两个标题的相似度,返回 0~1""" return SequenceMatcher(None, a, b).ratio() def build_comparison(products, threshold=0.6): """将商品按相似度分组,组内按价格排序""" groups = [] for p in products: placed = False for g in groups: if similarity(p["title"], g[0]["title"]) >= threshold: g.append(p) placed = True break if not placed: groups.append([p]) result = [] for g in groups: g.sort(key=lambda x: x["price"] if x["price"] else float("inf")) result.append({ "title": g[0]["title"], "lowest_price": g[0]["price"], "lowest_shop": g[0]["shop"], "all_offers": [{"shop": x["shop"], "price": x["price"], "url": x["url"]} for x in g] }) return result # 落地为 JSON 文件 def save_result(data, path="comparison_result.json"): with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) print(f"比价结果已写入 {path},共 {len(data)} 组商品")threshold=0.6是相似度阈值,低于这个值就不认为是同一商品。这个值调高会漏掉同商品(因为标题措辞差异大),调低会误合并不同商品。0.6 是个保守起点,实际用的时候建议拿十几条真实数据跑一遍看分组效果再微调。float("inf")是给价格为 None 的条目排到最后,避免 None 参与比较时报 TypeError。
3.3 用 pandas 做结果汇总与导出
如果你需要把比价结果导出成 Excel 交毕业设计报告,pandas 是最省事的:
import pandas as pd def export_to_excel(comparison_result, path="比价结果.xlsx"): rows = [] for item in comparison_result: for offer in item["all_offers"]: rows.append({ "商品": item["title"], "店铺": offer["shop"], "价格": offer["price"], "链接": offer["url"], "是否最低": offer["price"] == item["lowest_price"] }) df = pd.DataFrame(rows) df.to_excel(path, index=False) print(f"已导出 {len(df)} 条记录到 {path}")这段代码把嵌套的比价结果拍平成表格,每行是一个「商品-店铺-价格」组合,加了一列「是否最低」方便筛选。to_excel需要 openpyxl 支持,装一下pip install openpyxl就行。
4. 避坑与排查:定向爬虫比价系统最容易翻车的五个地方
4.1 价格抓到的是「划线价」而不是「实际售价」
现象:比价结果里所有商品价格都偏高,跟页面上看到的不一致。原因:商品详情页通常同时存在原价(划线价)和促销价,CSS 选择器如果命中了原价节点就会取错。解决:在浏览器里右键检查价格元素,确认你选中的是实际售价的 DOM 节点;如果价格由 JavaScript 动态渲染,requests 拿到的 HTML 里可能根本没有这个值,需要考虑抓接口或换用能执行 JS 的方案。
4.2 请求被重定向到登录页或验证页
现象:程序不报错,但解析出来的字段全是空的。原因:目标站点检测到非浏览器请求,返回了验证页面而非商品页,状态码可能还是 200。解决:在 fetch_page 里加一个内容校验——检查返回的 HTML 里是否包含预期的关键词(如商品标题节点),不包含就判定为异常页面并记录 URL,后续人工排查。
4.3 编码问题导致中文标题变成乱码
现象:抓下来的商品标题显示为「æ£åè¡£æ」之类的乱码。原因:响应头里的 charset 与实际内容编码不一致,或者站点根本没声明编码。解决:用resp.apparent_encoding自动推断,或者在 requests 拿到响应后手动试resp.encoding = 'gbk'和resp.encoding = 'utf-8'看哪个正常。
4.4 相似度阈值设太低导致不同商品被合并
现象:比价结果里「iPhone 15 128G 黑色」和「iPhone 15 256G 黑色」被归为同一组。原因:标题相似度太高,SequenceMatcher 区分不了容量差异。解决:在相似度判断之前先做关键属性提取(容量、颜色、型号),属性不一致的直接跳过匹配。或者把阈值提高到 0.8 以上,代价是可能漏掉一些措辞差异大的同商品。
4.5 程序跑到一半因为某个页面超时卡死
现象:整个爬取流程停住不动,也不报错。原因:某个请求没有设 timeout,或者重试逻辑里没有兜底退出条件。解决:确保每个 requests.get 都带 timeout 参数;重试次数用完后果断返回 None 并跳过该 URL,不要让单点失败拖垮整个批次。我一般还会在循环里加一个计数器,每处理 50 个 URL 打印一次进度,方便判断是卡住了还是在慢慢跑。
5. 进阶技巧:把比价系统从「能跑」推到「好用」
5.1 用配置文件管理目标站点和选择器
硬编码选择器的问题是:目标站点一改版,你就得翻代码找哪里要改。更好的做法是把站点配置抽成 JSON 或 YAML:
# sites_config.json { "site_a": { "base_url": "https://example-a.com", "search_url": "https://example-a.com/search?q={keyword}", "selectors": { "title": "h1.product-title", "price": ".price .now", "shop": ".shop-name" } }, "site_b": { "base_url": "https://example-b.com", "search_url": "https://example-b.com/s?keyword={keyword}", "selectors": { "title": ".sku-name", "price": "[class*=price]", "shop": ".J-hove-wrap" } } }import json def load_sites(path="sites_config.json"): with open(path, "r", encoding="utf-8") as f: return json.load(f) def crawl_by_config(keyword, sites): """按配置驱动抓取,新增站点只需改 JSON""" all_products = [] for site_name, cfg in sites.items(): url = cfg["search_url"].format(keyword=keyword) html = fetch_page(url) if not html: print(f"[{site_name}] 抓取失败,跳过") continue # 用该站点的选择器解析 soup = BeautifulSoup(html, "html.parser") sel = cfg["selectors"] title_tag = soup.select_one(sel["title"]) price_tag = soup.select_one(sel["price"]) shop_tag = soup.select_one(sel["shop"]) product = { "title": title_tag.get_text(strip=True) if title_tag else "", "price": normalize_price(price_tag.get_text(strip=True)) if price_tag else None, "shop": shop_tag.get_text(strip=True) if shop_tag else "", "url": url, "site": site_name } all_products.append(product) return all_products这样加一个新平台只需要在 JSON 里加一段配置,不用动 Python 代码。答辩的时候这个设计也是个加分项——它体现了「开闭原则」,对扩展开放、对修改关闭。
5.2 验证爬取结果是否正确的三个检查点
写完代码别急着跑全量,先用小样本验证。我一般会做三个检查:第一,手动打开一个目标页面,把程序抓到的标题、价格、店铺跟页面上肉眼看到的做对比,确认字段没错位;第二,打印前 5 条结果的原始 HTML 片段,确认选择器命中的是你想要的节点而不是父级容器;第三,用一条已知价格的商品跑完整流程,看最终比价结果里的最低价是否跟预期一致。这三个检查花不了十分钟,但能省掉后面大量调试时间。
5.3 把结果做成一个简单的 Web 界面
毕业设计答辩时,一个能交互的界面比命令行输出有说服力得多。用 Flask 搭一个最小可用的比价展示页:
from flask import Flask, render_template, jsonify import json app = Flask(__name__) @app.route("/") def index(): with open("comparison_result.json", "r", encoding="utf-8") as f: data = json.load(f) return render_template("index.html", products=data) @app.route("/api/compare") def api_compare(): with open("comparison_result.json", "r", encoding="utf-8") as f: data = json.load(f) return jsonify(data) if __name__ == "__main__": app.run(debug=True, port=5000)模板文件templates/index.html里用一个简单的表格循环渲染products就行。/api/compare这个接口返回 JSON,方便前端用 fetch 异步加载或者给其他系统调用。debug=True只在开发时开,部署时记得关掉。
从那以后我每次做完爬虫项目,都会先用一条已知结果的数据跑端到端验证,确认整条链路没有字段错位或编码问题,再放全量跑。这个习惯帮我省过好几次「跑了几千条结果全是错的」的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取