Python爬虫实战:抓取慢慢买历史价格数据全流程解析
2026/8/31 15:10:52 网站建设 项目流程

简介:本资源是一套面向Python爬虫初学者与电商数据分析从业者的实战工具包,聚焦商品历史价格监控场景,解决慢慢买平台价格数据难以批量获取的痛点。压缩包共22个文件,含2个核心Python脚本(main.py、decode.py)、3个关键JS文件(含转码与注释版本,用于还原前端表单生成逻辑)、14张界面截图(辅助理解请求参数构造与页面结构)、1份配置文件及README说明文档,整体5.85MB,结构清晰,便于按模块理解反爬逻辑与数据处理流程。已有61人学习下载,读者可直接复用完整爬虫框架,掌握模拟JS动态参数生成、Basic Auth认证绕过、时间范围筛选及Excel结构化存储等关键技术点,快速构建自有比价监控系统。 说实话,我第一次动了写慢慢买爬虫的念头,纯粹是因为不甘心。想买个显示器,从六一八蹲到双十一,价格忽上忽下,后来偶然用了慢慢买的历史价格查询,才发现自己之前以为的“好价”其实远远不是最低价。慢慢买这个站把商品在各个平台的历史价格、促销节点都记录得清清楚楚,信息量很大,但没有官方开放接口,想批量分析就只有一个办法——自己写一个Python爬虫。

这个项目说简单也简单,说复杂也确实有不少细节坑。简单在于它不需要登录、不需要维护复杂的会话,数据主要是公开的网页和JSON接口;复杂在于比价站的数据结构、反爬策略、编码问题、字段错位,每一项都够新手折腾一阵子。这篇就是把整个源码的拆解思路、关键代码、我踩过的坑一起整理出来,给正准备练手爬虫的朋友一个能跑、能改、能扩展的参考。

如果你用的是Python 3.8以上版本,有一点requests和BeautifulSoup基础,那这篇正好适合你;哪怕你是刚装好Python没写过几个脚本的新手,跟着章节一步步来,也可以把这个项目跑起来。

1. 先搞清楚慢慢买的数据结构,这决定了爬虫能不能写得顺

1.1 慢慢买不是电商,它是一个价格聚合平台

很多人第一次接触慢慢买,是搜一件商品然后看到它下面那条历史价格曲线。但它本身不卖货,数据来源是淘宝、京东、拼多多这些电商平台的公开页面,由慢慢买自己的爬虫系统去抓取、清洗、聚合之后,再展示成比价结果。

理解这一点很重要:你爬的是“慢慢买整合后的数据”,不是直接去电商平台硬刚。电商平台的反爬等级普遍比较高,验证码、滑块、风控模型一套组合拳;而慢慢买作为一个比价工具,本身需要让搜索引擎和普通用户能访问商品历史价格页,所以它的页面反爬强度相对可控,这就给个人爬虫留出了操作空间。

整个站点的数据结构可以拆成三层:

  • 搜索页:通过关键词返回商品列表,包括商品名称、平台标识、当前价格、商品跳转链接。
  • 商品详情页:展示某一商品的价格曲线、当前在售平台、店铺名称、优惠信息。
  • 历史价格数据页:这是核心,里面是日期、价格、最低价、最高价、促销标记组成的时间序列数据。

对爬虫项目来说,最终目标就是拿到第三层。前两层只是路径,真正的价值数据全部集中在历史价格序列里。

1.2 历史价格页的数据形态:HTML表格和JSON接口并存

慢慢买的历史价格查询入口支持两种方式:一种是站内直接搜索商品,另一种是粘贴商品详情页链接,比如把京东的https://item.jd.com/100012043978.html丢进去,慢慢买会根据平台和商品ID生成对应的历史价格页面。

实际请求历史价格数据时,我遇到过两种返回形态:

第一种是HTML表格,时间、价格、降价幅度、促销标记全部渲染在<table>里,这个对BeautifulSoup很友好,直接定位表格行就能拿到数据。

第二种是JSON接口,地址类似https://tool.manmanbuy.com/history.aspx?url=xxx,返回结构是一组日期和价格映射,通常包含datepricelowPricehighPrice这些字段。JSON比HTML好解析,而且字段语义更明确,不容易错位。

注意:慢慢买页面改版过多次,具体URL规则和返回字段可能变化,但核心思路不变——先抓商品页找历史价格入口,再去解析返回的数据。不要一上来就死记某个URL,要动态提取。

1.3 核心字段拆解:日期、价格与促销标记的含义

历史价格数据里最关键的是这四个字段:

字段含义使用场景
日期该条价格记录对应的日期画折线图横轴、判断降价趋势
价格当天商品的实际成交价计算最低价、最高价、均值
低/高价当天价格波动区间判断当天买入性价比
促销标记是否参与秒杀、满减等识别活动价与日常价差异

这里有个容易踩的坑:慢慢买页面上显示的价格,有时是促销计算后的到手价,有时是商品原价;而JSON接口里的price字段,可能是当天的价格,并不是促销后的最终价格。所以写解析逻辑时,不能想当然地认为“页面上显示的就是最低价”。我的做法是:把页面价和接口价分开保存,各存各的,分析的时候再合并判断,避免污染数据。

2. 技术选型:为什么Requests加BeautifulSoup而不是Scrapy

2.1 这是个典型的垂直型爬虫,Requests完全够用

按爬虫的应用场景划分,慢慢买比价爬虫属于典型的垂直型爬虫:只爬一个领域、一个站点、一组特定页面,数据量在百级到千级,不涉及分布式、不涉及增量抓取的海量URL管理。

这种规模的项目用Scrapy属于大炮打苍蝇。Scrapy的优势在爬取效率、中间件体系、调度器,这些能力在一个“查几十个商品历史价格”的场景里完全用不上,反而会引入一堆新的学习成本:Item Pipeline、Spider Middleware、Downloader Middleware,光理解这些概念就够喝一壶了。

我选择的是requests+BeautifulSoup的组合:

  • requests负责发HTTP请求、维护会话、处理Cookie和请求头。
  • BeautifulSoup负责解析HTML,定位表格和链接。
  • lxml作为BeautifulSoup的解析器,速度比默认的html.parser快很多。
  • 数据存储直接用csv模块,不需要上数据库。

这套组合轻量、直观,代码写出来逻辑清晰,特别适合学习爬虫基本流程:构造请求、解析响应、清洗数据、落盘存储。

2.2 环境准备:Python、依赖包与编辑器配置

环境要求不算高,Python 3.8以上就可以了。装依赖只需要一行命令:

pip install requests beautifulsoup4 lxml

如果你后面想拿数据做分析,可以顺手装上pandas:

pip install pandas

编辑器我习惯用VSCode,配好Python扩展之后调试脚本非常方便,尤其是打断点观察页面解析结果的时候,比print大法效率高很多。新手如果还没配过环境,可以在VSCode里装好Python插件,然后Ctrl+Shift+P选择解释器,指向你安装Python的路径就行。

2.3 请求头与Session:让请求看起来像一个正常用户

爬虫被拒,大多数情况不是IP被限制,而是请求头暴露了身份。很多网站的第一个反爬检测就是看User-Agent,如果发现你不是浏览器,直接拒绝服务。

我习惯用一个基础请求头模板:

DEFAULT_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": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", "Connection": "keep-alive", }

其中Referer我一般会根据请求目标动态设置,比如请求慢慢买的历史价格页时,把Referer设为慢慢买商品详情页的地址。这样做是因为部分页面会校验来源,如果Referer是空的或者来自其他网站,可能会被判定为异常请求。

另外建议用requests.Session()来发起请求。Session会自动保存Cookie,两次请求之间会带上之前的Cookie,这对需要先访问首页再访问数据页的流程非常有用,可以避免手动维护Cookie的麻烦。

3. 核心代码逻辑:从单商品查询到批量抓取

3.1 完整流程设计

整个爬虫的流程是:

  1. 输入一个商品详情页链接。
  2. 请求该链接,获取页面HTML。
  3. 从HTML中提取历史价格入口的真实URL。
  4. 请求历史价格URL,拿到JSON或HTML表格。
  5. 解析并清洗数据。
  6. 写入CSV文件。

其中第二步和第三步最关键。历史价格入口不是固定写死的,不同平台、不同商品ID会生成不同的查询URL,最好的办法是动态获取,而不是自己拼接。

3.2 动态提取历史价格入口

在商品详情页里,历史价格入口通常是一个带history关键词的链接。用BeautifulSoup查找非常方便:

import re import requests from bs4 import BeautifulSoup def get_history_page_url(product_url): resp = requests.get(product_url, headers=DEFAULT_HEADERS, timeout=10) if resp.status_code != 200: print(f"[error] 商品页无法访问,状态码: {resp.status_code}") return None resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "lxml") a_tag = soup.find("a", href=re.compile(r"history", re.I)) if a_tag and a_tag.get("href"): href = a_tag["href"] return href if href.startswith("http") else "https://www.manmanbuy.com" + href return None

这里有个细节:resp.apparent_encoding是requests根据页面内容自动检测的编码,比直接设定gbkutf-8更稳妥。慢慢买页面历史上出现过GBK和UTF-8混用的情况,直接用resp.text容易乱码,所以我在拿到响应后第一时间处理编码。

3.3 解析JSON与HTML表格两种数据格式

拿到历史价格页面后,先判断返回的是JSON还是HTML。JSON直接解析:

def parse_history_json(resp): try: data = resp.json() except ValueError: return [] if isinstance(data, list): return data return data.get("datas") or data.get("items") or []

HTML表格则用BeautifulSoup解析,但要注意字段长度判断,避免脏数据:

def parse_history_table(resp): resp.encoding = resp.apparent_encoding soup = BeautifulSoup(resp.text, "lxml") rows = [] for tr in soup.select("table tr"): tds = tr.find_all("td") if len(tds) < 4: continue rows.append({ "date": tds[0].get_text(strip=True), "price": tds[1].get_text(strip=True), "low": tds[2].get_text(strip=True), "high": tds[3].get_text(strip=True), }) return rows

我在这两个函数里都做了防御性处理:JSON解析失败就返回空列表,表格的列数不够就跳过。这样即使页面结构发生变化,程序也不会直接崩溃,最多是某一条数据缺失。

3.4 存储与批量调度

存储用CSV,加上utf-8-sig编码,这样用Excel打开时不会出现中文乱码:

import csv def save_to_csv(rows, filepath): if not rows: return with open(filepath, "w", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=list(rows[0].keys())) writer.writeheader() writer.writerows(rows)

批量抓取时,最重要的是控制节奏。我的经验是每两个请求之间随机等待2到5秒:

import random import time def sleep_random(): time.sleep(random.uniform(2, 5))

不控制频率的后果很直接:轻则请求被拒,重则IP被网站临时封禁。而且从道德层面讲,没有限制的快速请求会给对方服务器造成不必要的压力,这对一个以“比价数据”为核心业务的网站来说,是非常不友好的行为。

4. 真实踩坑记录:编码错乱、字段错位和请求被拒

4.1 页面编码问题:GBK还是UTF-8?

我第一次跑通请求的时候,打印出来的页面内容全是乱码,所有中文都变成了类似æ´å°的乱字符。后来排查发现,慢慢买的某些历史价格页面返回的是GBK编码,而requests默认会用HTTP头里的charset去解码,一旦那个值不对,就会乱码。

解决方法有两种:

# 方法一:用apparent_encoding自动检测 resp.encoding = resp.apparent_encoding # 方法二:手动指定gbk解码 html_text = resp.content.decode("gbk", errors="ignore")

如果页面里中文已经乱码,后续用正则或BeautifulSoup去匹配中文关键词就会完全失效。所以拿到响应后的第一件事就是确认编码,这个步骤不能偷懒。

4.2 表格字段错位:按位置取数是有风险的

HTML表格解析最常踩的坑是“看起来有规律,实际不规律”。历史价格表里有时候某一行没有促销标记,如果代码写死了“第五列是促销标记”,这一行解析结果就会整体前移,导致日期对不上价格。

我的处理方式是对每一行的td数量做判断,少于预期列数就跳过,或者用None填充缺失字段:

td_list = tr.find_all("td") date = td_list[0].get_text(strip=True) if len(td_list) > 0 else None price = td_list[1].get_text(strip=True) if len(td_list) > 1 else None low = td_list[2].get_text(strip=True) if len(td_list) > 2 else None high = td_list[3].get_text(strip=True) if len(td_list) > 3 else None

Python的三元表达式可以非常优雅地处理这种“某列可能缺失”的场景,代码也很直观。

4.3 请求被拒的完整排查链路

有一次我批量查询40个商品,前10个很正常,第11个开始连续返回403。我当时的排查过程是这样的:

  1. 先打印状态码,确认返回403 Forbidden。
  2. 打印响应内容前200个字符,发现返回的是一个安全验证页面,而不是商品数据。
  3. 检查请求头,User-AgentReferer都是有的,排除请求头缺失问题。
  4. 对比前10个请求和后30个请求的时间间隔,发现前面请求太快,触发了网站的风控。
  5. 把每次请求之间的睡眠时间从1秒调整到3到5秒,403再也没出现过。

这次排查让我确认了一个原则:请求头解决“你是谁”的问题,请求频率解决“你像不像人”的问题。很多爬虫一开始只关注请求头,忽略了频率,结果还是被限制。我认为在写爬虫时,频率控制应该和请求头伪装同等重要。

4.4 页面价格和接口价格不一致

还有一次,我解析出来的历史最低价和慢慢买页面上显示的最低价格对不上。页面显示某天是2699元,我拿到的最低价却是3099元。

排查后发现,页面渲染的价格是“促销到手价”,也就是叠加满减、优惠券之后的价格;而接口返回的是“商品原价”或“日常价”。两者都是真实数据,只是口径不同。

所以我在存数据时增加了一个price_type字段,区分是“页面展示价”还是“接口原始价”。后续做降价分析时,我会优先用促销到手价来算“是否是好价”,用接口原始价来判断“价格中枢”,两个口径互不干扰。

5. 反爬与合规:哪些手段可以用,哪些不能碰

5.1 网站反爬的四个层级

做爬虫的人必须知道,反爬不是一个开关,而是一层层叠加的防护:

第一层是请求头校验,检测User-AgentReferer等字段,看你是不是正常浏览器。

第二层是频率控制,单位时间内请求次数超过阈值,直接限制访问。

第三层是动态参数,页面里的接口URL带有signtoken这样的加密参数,需要执行JavaScript才能拿到,常规requests请求拿不到有效值。

第四层是验证码,包括图形验证码、滑块验证码等,这是最重的一层,几乎能把绝大多数爬虫挡在门外。

对于慢慢买这个项目,目前基本只需要应对前两层。第三层和第四层大概率不会遇到。

5.2 我的建议:合法合规地提高稳定性

在实际项目中,我采取的做法包括:

  • 设置友好的请求频率,单次查询间隔不低于3秒。
  • 优先使用页面提供的正常入口,不绕过、不破解任何参数。
  • 抓取只用于个人学习和商品选购参考,不批量下载、不转售数据。
  • 如果网站页面结构或接口发生变化,就调整代码适配,而不是想着去逆向加密逻辑。
  • 请求失败时做有限次数的重试,避免无限重试给对方服务器制造压力。

我不建议去逆向签名参数,也不建议破解验证码。一方面这样做违反网站服务条款,有法律风险;另一方面,如果你的爬虫依赖“破解验证码”才能运行,那它的脆弱性会超出你的想象,网站风控策略一天三变,你就要跟着改三天。

这里特别说明:爬虫技术本身是中立的,但使用方式有边界。请只访问公开数据,遵守网站的robots.txt和《用户协议》。写爬虫的正确心态是“用技术解决自己的问题”,而不是“用技术占别人便宜”。

5.3 扩展反爬应对思路:真的需要更多手段时怎么办

如果你的项目规模变大,需要抓取更多商品,我不建议继续在这个爬虫上堆代码,而是考虑以下方案:

  • 使用官方API:慢慢买没有面向个人的开放API,但部分比价数据服务商提供付费接口,如果有预算,这是最稳妥的方案。
  • 优化抓取策略:利用URL去重,避免重复请求相同商品。
  • 数据二次利用:把历史数据存下来做成自己的小型价格数据库,后续查询直接读本地数据,不需要每次都访问网站。

核心逻辑是:每一次对目标网站的请求都有成本,尽量让每次请求的价值最大化。

6. 从“查询工具”到“价格监控提醒”:这个项目还能怎么扩展

6.1 定时抓取与邮件提醒

爬虫写完之后,如果只是手动跑一次,能解决“查一个商品的历史价格”的问题,但解决不了“长期监控降价”的问题。我后面做了一个扩展:用APScheduler定时执行抓取任务,每天跑一次,判断当前价格是否低于我的心理价位。

代码核心就一段:

from apscheduler.schedulers.blocking import BlockingScheduler scheduler = BlockingScheduler() scheduler.add_job(run_daily_check, "cron", hour=9, minute=30) scheduler.start()

当检测到价格低于阈值时,用smtplib给自己发一封提醒邮件。这里不贴完整邮件代码了,核心就是用Python标准库的smtplib连上你的邮箱SMTP服务器,把价格信息拼成邮件正文发出去。

这一步做完,整个项目就从“被动查询”变成了“主动监控”。我自己的体会是,这个转变才真正让爬虫产生了使用价值。

6.2 用pandas做价格趋势分析

抓下来的CSV数据如果只存不用,那和一仓库没整理的废纸没什么区别。我后来用pandas简单做了一下分析:

import pandas as pd df = pd.read_csv("price_history.csv") df["price"] = pd.to_numeric(df["price"], errors="coerce") min_price = df["price"].min() avg_price = df["price"].mean()

这样就能快速得到历史最低价、平均价、价格中位数。如果你想更直观,可以再用matplotlib画一张价格走势折线图,把促销标记对应的日期高亮出来,一眼就能看出这个商品的促销节奏。

6.3 我对这个项目后续的想法

项目写到这个程度,其实已经具备了一个完整的垂直型爬虫项目该有的所有环节:数据采集、解析、存储、调度、告警、分析。

但如果要往更大规模扩展,比如抓取成千上万个商品,我会建议重新设计架构:用数据库替代CSV,用消息队列做任务分发,用分布式代理池管理IP,用Docker部署定时任务。这时候就不是一个脚本能解决的问题了,而是需要一个团队或至少一个资深工程师持续维护。

对我来说,这个项目的价值更多在于“以最小的成本解决了一个实际的购物决策问题”。买大件商品之前,先用这个爬虫拉一遍历史价格,心里基本就有底了:到底该现在买、等大促再买、还是直接换一个型号。这种确定性,是逛几个购物网站无法得到的。

最后说一个我用下来的小技巧:解析历史价格数据时,不要只关心最低价和最高价,一定要把促销标记一起存下来。有些商品的价格波动非常有规律,比如每个月都有一次满减,标记清楚后你会发现“所谓的历史最低价”,其实每个月都会出现一次,完全不用抢在某个特定时间点入手。这种洞察,才是比价数据真正值钱的地方。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询