Python定向爬虫实战:商品比价系统从抓取到匹配全流程 简介这份资源是面向计算机专业学生与Python初学者的一套商品比价系统完整实现可直接用于毕业设计、课程设计或大作业场景。项目基于Python 3.6与定向爬虫技术围绕商品价格采集、存储与展示展开帮助读者理解从数据抓取到图形界面呈现的完整链路。压缩包共16个文件以10个py源码文件为核心另含3个pyc编译文件、1个txt说明、1个db数据库文件与1个md文档整体约27KB体积轻便便于携带与二次修改。内容按实现方式分为三个文件夹first采用面向对象思路整合数据库与图形界面second以面向过程方式实现数据库与图形界面third同样面向过程但改用文件保存数据并搭配图形界面便于对比不同架构的取舍。目前已有1024人学习读者可借此掌握爬虫编写、数据持久化与GUI开发等关键技能并参考目录结构快速搭建自己的比价系统。1. 从零搭一套商品比价系统定向爬虫到底比通用爬虫强在哪电商页面每天都在变通用爬虫今天能跑通明天可能就抓到一堆空字段。我见过太多毕业设计卡在这一步代码写了几百行跑出来的价格全是None。问题不在 Python 水平而在「定向」两个字没吃透。定向爬虫的核心思路是——先锁定目标站点的页面结构、字段位置和翻页规律再针对性地写解析规则而不是指望一套通用选择器打天下。这套基于 Python 和定向爬虫的商品比价系统要解决的就是把多个平台的商品标题、价格、销量、店铺抓下来清洗后存库再按商品名做匹配比价。适合正在做计算机毕业设计、想找一个能跑通又有技术含量的题目的同学也适合想入门爬虫但被通用框架劝退的开发者。下面按「选型 → 抓取 → 清洗 → 比价 → 避坑 → 进阶」的顺序把每个环节的参数和坑讲清楚。2. 定向爬虫的选型与请求层搭建为什么不用 Scrapy 全家桶2.1 定向爬虫和通用爬虫的本质区别通用爬虫比如 Scrapy 默认配置假设你面对的是未知站点所以它把大量精力花在调度、去重、分布式上。但毕业设计场景里目标站点通常就三五个页面结构固定真正花时间的是「解析规则怎么写」和「反爬怎么绕」。定向爬虫的做法是用requests发请求用BeautifulSoup或lxml做解析自己控制请求频率和重试逻辑。这样做的好处是代码量小、调试直观、每个字段的来源都能追溯。我一般会先做一件事打开目标商品列表页按 F12 看 Network 面板确认数据是服务端渲染还是接口返回。如果是接口返回 JSON直接请求接口比解析 HTML 省一半工作量。这一步决定了后面所有代码的写法千万别跳过。2.2 请求头、Cookie 与频率控制的最小实现下面是一个定向请求的最小可用模板包含请求头伪装、超时重试和随机延时import requests import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry 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, Referer: https://www.example.com/, # 替换为目标站点首页 } def build_session(): session requests.Session() retry Retry(total3, backoff_factor1, status_forcelist[429, 500, 502, 503]) session.mount(https://, HTTPAdapter(max_retriesretry)) session.headers.update(HEADERS) return session def fetch(session, url, paramsNone): try: resp session.get(url, paramsparams, timeout10) resp.raise_for_status() return resp except requests.RequestException as e: print(f[fetch error] {url} - {e}) return None def polite_sleep(): time.sleep(random.uniform(1.5, 3.5)) # 随机延时降低被封概率逻辑说明Retry负责在 429/5xx 时自动重试backoff_factor1表示重试间隔按 1s、2s、4s 递增。polite_sleep的随机区间建议设在 1.5 到 3.5 秒之间太短容易触发风控太长抓取效率低。参数方面timeout10是经验值超过 10 秒基本说明对方在限流重试也没意义。提示Cookie 不要硬编码在代码里建议单独存一个cookies.json登录后导出再加载避免过期后满屏报错。2.3 翻页策略URL 规律 vs 接口分页翻页有两种常见做法。第一种是 URL 里带page1,2,3直接循环拼接第二种是接口返回has_next字段靠它判断是否继续。我一般优先用第二种因为更稳定。如果站点用 AJAX 加载可以在 Network 面板找到真实接口通常返回 JSON解析起来比 HTML 快得多。翻页时记得加一个最大页数保护比如max_page50防止死循环把 IP 跑废。3. 商品字段解析与数据清洗价格、标题、销量的提取规则3.1 用 CSS 选择器定位价格和标题定向爬虫的解析规则要针对每个站点单独写。以常见的商品列表页为例价格通常在span classprice或>from bs4 import BeautifulSoup import re def parse_list_page(html): soup BeautifulSoup(html, lxml) items [] for card in soup.select(div.product-card): title_el card.select_one(a.title) price_el card.select_one(span.price) sales_el card.select_one(span.sales) if not title_el or not price_el: continue # 跳过结构不完整的卡片 title title_el.get(title) or title_el.get_text(stripTrue) price_text price_el.get_text(stripTrue) price extract_price(price_text) sales parse_sales(sales_el.get_text(stripTrue)) if sales_el else 0 items.append({title: title, price: price, sales: sales}) return items def extract_price(text): match re.search(r(\d\.?\d*), text.replace(,, )) return float(match.group(1)) if match else None def parse_sales(text): text text.strip() if 万 in text: return int(float(re.search(r(\d\.?\d*), text).group(1)) * 10000) match re.search(r(\d), text) return int(match.group(1)) if match else 0逻辑说明extract_price用正则提取第一个数字兼容「99.00」「99元」「99.00」等格式。parse_sales处理「1.2万」这类中文单位统一转成整数。参数上select里的类名必须和实际页面一致建议先用浏览器控制台document.querySelector验证一遍再写进代码。3.2 数据清洗去重、异常价格过滤和标题归一化抓下来的原始数据不能直接入库。常见问题有三个同一商品重复出现、价格出现 0 或 999999 这种异常值、标题里带促销后缀导致比价匹配失败。清洗步骤建议这样写def clean_items(items): seen set() cleaned [] for item in items: if item[price] is None or item[price] 0 or item[price] 100000: continue title_key re.sub(r[【】\[\]促销秒杀包邮], , item[title]).strip() if title_key in seen: continue seen.add(title_key) item[title] title_key cleaned.append(item) return cleaned逻辑说明价格过滤区间设 0 到 100000覆盖绝大多数商品。标题归一化去掉「【】」「促销」「秒杀」「包邮」等干扰词让不同平台的同一商品能匹配上。这一步做完数据质量会明显提升。3.3 存储方案SQLite 够用别急着上 MySQL毕业设计阶段SQLite 完全够用零配置、单文件、方便打包。建表语句如下CREATE TABLE products ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, price REAL NOT NULL, sales INTEGER DEFAULT 0, platform TEXT, url TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_title ON products(title);逻辑说明idx_title索引是为了后面比价查询时按标题匹配更快。platform字段记录来源平台方便后续按平台筛选。如果数据量超过 10 万条再考虑 MySQL否则 SQLite 的读写性能足够。4. 比价匹配与查询接口怎么让同一商品在不同平台对上号4.1 基于标题相似度的匹配策略比价的核心是「同一商品在不同平台怎么识别」。最简单的方法是标题精确匹配但实际标题差异很大所以要用相似度。常见做法是用difflib.SequenceMatcher或jieba分词后算 Jaccard 相似度。下面是一个轻量实现import jieba from difflib import SequenceMatcher def similarity(a, b): return SequenceMatcher(None, a, b).ratio() def match_products(target_title, candidates, threshold0.6): results [] for item in candidates: score similarity(target_title, item[title]) if score threshold: results.append((score, item)) results.sort(keylambda x: x[0], reverseTrue) return results逻辑说明threshold0.6是经验值低于 0.6 基本不是同一商品高于 0.85 可能是完全相同的标题。如果标题很长建议先用jieba分词再算 Jaccard效果更稳。参数可以按实际数据调先跑 100 条看准确率再定。4.2 比价查询接口用 Flask 暴露一个最小 API为了让系统能演示可以用 Flask 写一个查询接口from flask import Flask, request, jsonify import sqlite3 app Flask(__name__) app.route(/compare) def compare(): keyword request.args.get(q, ).strip() if not keyword: return jsonify({error: empty query}), 400 conn sqlite3.connect(products.db) cur conn.cursor() cur.execute(SELECT title, price, platform, url FROM products WHERE title LIKE ?, (f%{keyword}%,)) rows cur.fetchall() conn.close() return jsonify([{title: r[0], price: r[1], platform: r[2], url: r[3]} for r in rows]) if __name__ __main__: app.run(debugTrue, port5000)逻辑说明LIKE查询适合小数据量数据多了要换成全文索引或向量匹配。debugTrue只用于开发部署时关掉。接口返回 JSON前端可以直接渲染成比价表格。4.3 定时更新与增量抓取比价系统不能只跑一次。常见做法是用schedule库或系统 cron 定时执行抓取脚本每次只抓有变化的商品。增量抓取的关键是记录上次抓取时间只请求更新过的页面。如果站点没有更新时间字段就全量抓但用INSERT OR REPLACE去重。5. 定向爬虫避坑清单封 IP、字段错位、编码乱码怎么排查5.1 请求被 403 或返回验证码页现象resp.status_code返回 403或者返回的 HTML 里出现「验证」「滑动」等字样。原因请求头不完整、频率过高、IP 被标记。解决补全User-Agent、Referer、Accept等头把延时调到 3 秒以上如果持续被封换用代理池或降低抓取频率。注意不要用免费代理稳定性极差。5.2 价格字段抓成空或抓到错误数字现象price全是None或者抓到了「原价」「划线价」而不是实际售价。原因选择器命中了多个元素或者页面结构和你看到的不一样。解决在浏览器控制台用document.querySelectorAll确认选择器唯一性优先取>import math def rank_products(items, price_weight0.7, sales_weight0.3): if not items: return [] max_price max(i[price] for i in items) for item in items: price_score 1 - (item[price] / max_price) sales_score math.log(item[sales] 1) item[score] price_weight * price_score sales_weight * sales_score return sorted(items, keylambda x: x[score], reverseTrue)逻辑说明price_score把价格归一化到 0 到 1价格越低分越高。sales_score用对数平滑避免销量差距过大导致权重失衡。参数price_weight和sales_weight可以根据答辩演示的重点调整比如想突出低价就调高price_weight。最后说个血泪经验定向爬虫的代码不要一次写太多站点先把一个站点跑通、跑稳再复制到第二个。我见过太多人同时开五个站点结果每个都半途而废。先把一个站点的抓取、清洗、入库、比价全流程跑通再扩展这才是最省时间的路径。希望帮到你。本文还有配套的精品资源点击获取