Python定向爬虫实战:商品比价系统从抓取到落地

发布时间:2026/10/28 23:31:24
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;q0.9, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, } def fetch_page(url, retry3): 带重试和随机延时的页面抓取 for i in range(retry): try: # 每次请求间隔 1~3 秒模拟人工浏览节奏 time.sleep(random.uniform(1, 3)) resp requests.get(url, headersHEADERS, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding # 自动纠正编码 return resp.text except requests.RequestException as e: print(f第 {i1} 次请求失败: {e}) if i retry - 1: return None return None这段代码里几个参数值得说清楚。timeout10是防止某个请求卡死拖垮整个流程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(stripTrue) 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(stripTrue) 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(stripTrue) 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, threshold0.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(keylambda 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, pathcomparison_result.json): with open(path, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) print(f比价结果已写入 {path}共 {len(data)} 组商品)threshold0.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, indexFalse) 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(pathsites_config.json): with open(path, r, encodingutf-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(keywordkeyword) 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(stripTrue) if title_tag else , price: normalize_price(price_tag.get_text(stripTrue)) if price_tag else None, shop: shop_tag.get_text(stripTrue) 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, encodingutf-8) as f: data json.load(f) return render_template(index.html, productsdata) app.route(/api/compare) def api_compare(): with open(comparison_result.json, r, encodingutf-8) as f: data json.load(f) return jsonify(data) if __name__ __main__: app.run(debugTrue, port5000)模板文件templates/index.html里用一个简单的表格循环渲染products就行。/api/compare这个接口返回 JSON方便前端用 fetch 异步加载或者给其他系统调用。debugTrue只在开发时开部署时记得关掉。从那以后我每次做完爬虫项目都会先用一条已知结果的数据跑端到端验证确认整条链路没有字段错位或编码问题再放全量跑。这个习惯帮我省过好几次「跑了几千条结果全是错的」的后悔药。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询