1. 项目概述从零到一用Python爬虫监控电商价格最近在帮朋友做一个市场调研需要持续跟踪几个电商平台上特定商品的价格波动。手动去查效率太低而且没法做到实时。用现成的价格监控工具要么功能太臃肿要么价格不菲要么数据导出不方便。作为一个Python老手我第一个想到的就是自己写个爬虫。这听起来像是“Hello World”级别的任务但真做起来从绕过反爬虫到数据清洗再到稳定运行每一步都有不少门道。今天我就把自己这次“电商商品价格抓取”项目的完整思路、代码实现和踩过的坑毫无保留地分享出来。无论你是刚学Python想找个实战项目练手还是已经有一定基础但被反爬虫搞得头疼这篇文章都能给你提供一套可直接“抄作业”的解决方案。这个项目的核心目标很明确给定一个电商商品页面的URL我们的程序要能自动、准确、稳定地抓取到商品名称和当前价格并把数据规整地保存下来便于后续分析。我们会使用最经典的requests库发起请求用BeautifulSoup或lxml来解析HTML页面提取我们需要的信息。整个过程我会重点讲解如何应对网站结构变化、如何设置请求头模拟真实浏览器、如何处理动态加载内容等实际开发中必然会遇到的问题。你会发现一个健壮的爬虫远不止几行find和find_all那么简单。2. 核心思路与技术选型为什么是Requests BeautifulSoup在动手写代码之前我们先得把技术路线定下来。爬虫技术栈五花八门从简单的同步请求到复杂的异步框架从静态解析到无头浏览器模拟。对于“抓取商品价格”这个场景我的选择是Requests BeautifulSoup 正则表达式/Selectors。下面我详细解释一下为什么这么选以及备选方案的考量。2.1 主技术栈解析轻量、高效、易上手Requests库是Python HTTP客户端库的“事实标准”。它的API设计极其优雅发起一个GET请求只需要requests.get(url)一行代码。对于我们这个项目它的核心优势在于简单直观学习成本极低文档丰富社区支持强大。连接池与会话保持通过Session对象可以自动处理Cookies保持登录状态如果需要的话并复用TCP连接提升效率。灵活的请求头设置这是对抗基础反爬虫的关键我们可以轻松伪装成浏览器。超时与重试机制可以设置请求超时时间并配合第三方库如tenacity实现自动重试增强程序的健壮性。BeautifulSoup库是一个用于从HTML或XML文件中提取数据的Python库。它能够将复杂的HTML文档转换成一个复杂的树形结构然后让我们用类似find(‘div’, class_‘price’)这样直观的方式来定位元素。选择它的原因解析器灵活它本身不解析但支持html.parser内置、lxml快和html5lib容错强等多种解析器。我们通常搭配lxml解析器速度上有很大优势。导航与搜索API强大find(),find_all(),select()CSS选择器等方法组合使用能应对绝大多数页面解析需求。容错性好即使页面HTML有些许不规范它也能较好地处理不至于直接崩溃。为什么不直接用正则表达式正则表达式re库在提取有固定模式的文本比如价格数字时非常高效但用它来解析复杂的、嵌套的HTML结构很容易写出脆弱且难以维护的“天书”代码。我们的策略是用BeautifulSoup定位到包含价格信息的HTML标签再用正则表达式从标签文本中精确提取价格数字。这样结合了二者的优点。2.2 备选方案与高级场景考量我们的主方案适用于静态页面即商品价格直接写在返回的HTML源码里。但现在很多电商网站尤其是大型平台为了性能和用户体验会采用动态加载技术如Ajax, JavaScript渲染。你直接拿到的初始HTML里可能没有价格价格是通过后续的JavaScript请求获取并填充的。如何判断很简单在浏览器里打开商品页右键“查看网页源代码”然后搜索商品价格。如果搜不到但页面上明明显示着价格那基本就是动态加载了。应对动态加载有几种进阶方案分析网络请求打开浏览器的开发者工具F12切换到“网络”(Network)选项卡刷新页面观察有哪些XHR/Fetch请求特别是那些返回JSON数据的。价格信息很可能就在其中一个JSON响应里。直接模拟这个请求往往比解析HTML更简单、更稳定。使用Selenium或Playwright这两个工具可以自动化控制一个真实的浏览器。它们能完整执行页面上的JavaScript等所有内容渲染完毕后再获取完整的HTML。这种方法最“笨”但也最通用能解决几乎所有前端渲染问题。缺点是速度慢、资源消耗大不适合大规模、高频次的爬取。使用Pyppeteer或SplashPyppeteer是Puppeteer的Python版本控制无头ChromeSplash是一个带有HTTP API的JavaScript渲染服务。它们比Selenium轻量一些但同样属于浏览器自动化范畴。注意对于价格监控这种通常不需要高频请求例如每分钟一次都算高了的场景如果静态分析失效我建议优先尝试方案1分析网络请求。直接请求数据接口效率最高也最不容易被识别为爬虫。本文后续的实战部分会以静态页面为主但会穿插讲解如何寻找并模拟Ajax请求。3. 环境准备与基础爬虫搭建理论说完了我们开始动手。首先确保你的Python环境已经就绪3.6以上版本均可。我们将通过命令行来安装必要的库。3.1 创建虚拟环境与安装依赖我强烈建议为每个项目创建独立的虚拟环境避免包版本冲突。# 1. 创建项目目录并进入 mkdir price_monitor cd price_monitor # 2. 创建虚拟环境这里使用venv你也可以用conda python -m venv venv # 3. 激活虚拟环境 # 在Windows上 venv\Scripts\activate # 在macOS/Linux上 source venv/bin/activate # 4. 安装核心库 pip install requests beautifulsoup4 lxml # 5. 可选但推荐的库用于格式化输出、处理数据 pip install pandas安装完成后我们可以用pip list确认一下。接下来我们从一个最简单的爬虫骨架开始。3.2 编写第一个爬虫获取页面与解析HTML假设我们要抓取一个模拟的电商商品页为了教学我们可以用一个静态页面做示例比如某个开源项目提供的demo页面或者自己写一个简单的HTML。这里我们先以概念性代码展示流程。import requests from bs4 import BeautifulSoup import re def fetch_product_price(url): 抓取指定URL的商品价格和名称 # 1. 发送HTTP请求 response requests.get(url) # 检查请求是否成功 response.raise_for_status() # 2. 解析HTML内容 # 使用lxml解析器需要提前安装lxml库 soup BeautifulSoup(response.content, lxml) # 3. 提取商品名称这里需要根据实际网页结构调整选择器 # 假设商品标题在一个h1标签里且class为‘product-title’ title_tag soup.find(h1, class_product-title) product_name title_tag.get_text(stripTrue) if title_tag else 未找到商品名 # 4. 提取商品价格这是核心也是最容易出问题的地方 # 假设价格在一个span标签里class为‘price’ price_tag soup.find(span, class_price) if price_tag: price_text price_tag.get_text(stripTrue) # 使用正则表达式提取数字部分包括小数点 # 这个正则匹配如 199.00, $299, 1,299.50 等格式 price_match re.search(r[\d,]\.?\d*, price_text) product_price price_match.group() if price_match else 价格格式异常 # 移除可能存在的逗号千位分隔符并转换为浮点数 try: product_price float(product_price.replace(,, )) except ValueError: product_price price_text # 转换失败则保留原文本 else: product_price 未找到价格 # 5. 返回结果 return { name: product_name, price: product_price, url: url } # 测试代码需要替换为真实的、允许爬取的测试URL if __name__ __main__: # 示例URL请勿用于实际爬取仅作格式参考 test_url https://httpbin.org/html # 这是一个返回示例HTML的测试网站 result fetch_product_price(test_url) print(f商品名称: {result[name]}) print(f商品价格: {result[price]})这段代码定义了一个核心函数fetch_product_price它完成了爬虫最基础的流程请求 - 解析 - 提取 - 返回。但把它扔到真实的电商网站上99%的概率会立刻失败或者返回一堆乱码。接下来我们就来给它穿上“盔甲”让它能在真实的网络环境中生存下来。4. 应对反爬虫让爬虫看起来像“真人”电商网站为了保护数据、减轻服务器压力都会部署反爬虫机制。我们的爬虫必须进行伪装核心在于HTTP请求头Headers和请求行为的模拟。4.1 精心构造请求头一个从Python的Requests库直接发出的请求其User-Agent头通常是python-requests/2.xx.x这等于在脸上写着“我是爬虫”。我们需要把它改成普通浏览器的样子。import requests from bs4 import BeautifulSoup import re import time import random # 定义一个常用的浏览器User-Agent列表 USER_AGENTS [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15, Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0 ] def get_headers(): 生成一个随机的、完整的浏览器请求头 headers { User-Agent: random.choice(USER_AGENTS), Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, br, # 注意Requests自动处理gzip解码这里写上没关系 Connection: keep-alive, Upgrade-Insecure-Requests: 1, Sec-Fetch-Dest: document, Sec-Fetch-Mode: navigate, Sec-Fetch-Site: none, Sec-Fetch-User: ?1, Cache-Control: max-age0, } return headers def fetch_product_price_advanced(url): 增强版爬虫添加请求头、异常处理、延迟 headers get_headers() try: # 使用Session可以自动管理cookies提升效率 session requests.Session() response session.get(url, headersheaders, timeout10) # 设置10秒超时 # 更健壮的状态码检查 if response.status_code ! 200: print(f请求失败状态码{response.status_code}) return None # 检查返回内容是否是HTML content_type response.headers.get(Content-Type, ) if text/html not in content_type: print(f返回内容非HTML: {content_type}) return None soup BeautifulSoup(response.content, lxml) # ... 后续解析逻辑与之前相同 ... # 在返回前添加一个随机延迟模拟人类浏览间隔 time.sleep(random.uniform(1, 3)) # 延迟1到3秒 return result except requests.exceptions.Timeout: print(f请求超时: {url}) return None except requests.exceptions.RequestException as e: print(f网络请求异常: {e}) return None except Exception as e: print(f解析或其他异常: {e}) return None关键点解析User-Agent轮换从列表中随机选择一个避免单一Agent被识别。完整的Headers除了User-Agent其他如Accept、Accept-Language等也尽量模仿浏览器。Sec-Fetch-*系列头是较新浏览器才有的加上它们能让请求看起来更“现代”。使用Sessionrequests.Session()会保持一个会话自动处理服务器返回的Cookies并在后续请求中带上这对于一些需要简单会话状态的网站很有用。超时与异常处理网络是不稳定的必须设置timeout并捕获各类异常超时、连接错误等防止程序因单个请求失败而崩溃。随机延迟time.sleep(random.uniform(1, 3))是最重要的道德与策略性措施。不加延迟地疯狂请求会对目标服务器造成压力也极易触发IP封禁。延迟模拟了人类阅读页面的时间。4.2 处理Cookies与登录态有些商品信息如会员价可能需要登录后才能看到。如果只是简单的登录我们可以先用浏览器手动登录然后从开发者工具的“网络”选项卡中复制出Cookie请求头的值直接放到我们的爬虫Headers里。def fetch_with_cookie(url): headers get_headers() # 将从浏览器复制的Cookie字符串粘贴在这里 headers[Cookie] 你的完整Cookie字符串 response requests.get(url, headersheaders) # ... 后续处理 ...重要警告这种方法获取的Cookie有有效期。更稳定的自动化登录需要模拟登录表单的POST请求这涉及到分析登录接口、处理可能的验证码等复杂度会大大增加。对于价格监控如果目标数据不需要登录应尽量避免处理登录。4.3 应对IP封锁与验证码如果严格遵守了延迟策略一般中小网站不会触发IP封锁。但对于大型电商平台即使有延迟单个IP持续访问也可能被限制。这时需要考虑代理IP池使用付费或免费的代理IP服务在每次请求时轮换不同的IP。Requests库使用代理很简单proxies {‘http’: ‘http://10.10.1.10:3128’, ‘https’: ‘http://10.10.1.10:1080’}然后在get方法中传入proxiesproxies参数。免费代理质量极不稳定商用项目建议考虑付费服务。验证码识别如果遇到验证码项目难度会陡增。可以尝试使用OCR库如ddddocr、pytesseract识别简单图形验证码或接入第三方打码平台。但遇到滑块、点选等复杂验证码通常意味着你的爬虫行为已被识别最好的策略是进一步降低请求频率或研究是否有官方API可用。5. 实战解析真实电商页面结构现在我们进入最核心也最繁琐的部分如何从一堆HTML标签中找到我们想要的价格和名称。没有通用的选择器每个网站的HTML结构都不同。我们必须像侦探一样仔细分析目标页面的结构。5.1 使用浏览器开发者工具定位元素以Chrome浏览器为例打开目标商品页面。右键点击商品价格选择“检查”(Inspect)。开发者工具会打开并自动定位到价格对应的HTML元素。仔细观察这个元素的特征它是什么标签span,div,p它有什么class或id属性它的父级元素有什么特征尝试在“元素”(Elements)面板中右键该节点选择“复制” - “复制selector” 或 “复制XPath”。这能给你一个初步的定位路径。5.2 编写健壮的选择器不要依赖复制出来的绝对路径如#root div main div:nth-child(2) div div span它们非常脆弱页面结构稍有变动就会失效。我们应该寻找最接近目标元素且具有唯一性或稳定性的class或属性。示例分析假设我们分析发现价格被包裹在一个div class”product-price”里里面有一个span class”currency”/span和一个span class”value”299.00/span。脆弱的写法price soup.find(div, class_product-price).find(span, class_value).text更健壮的写法price_container soup.find(div, class_product-price) if price_container: # 方法1: 直接取容器内所有文本再用正则提取数字 full_text price_container.get_text(stripTrue) price_match re.search(r[\d,]\.?\d*, full_text) # 方法2: 如果结构稳定可以尝试找特定的子元素 # value_span price_container.find(span, class_value) # price_text value_span.text if value_span else full_text product_price price_match.group() if price_match else None使用CSS选择器BeautifulSoup的select()方法支持CSS选择器语法有时更简洁。# 选择 class 为 ‘product-price’ 的 div 下的 class 为 ‘value’ 的 span price_span soup.select_one(div.product-price span.value)5.3 处理多种价格格式和状态真实场景比想象复杂原价/折扣价页面可能同时显示¥399和¥299。你需要明确你要抓取的是当前售价通常是更突出的那个。可能需要查找class包含sale、current、final等关键词的元素。无货/预售商品无货时价格区域可能显示“暂无报价”或“即将开售”。你的代码需要判断如果提取不到数字应该返回一个特殊值如None或字符串“无货”而不是报错。价格区间某些商品如手机可能有不同配置价格显示为¥5999 - ¥7999。你的正则表达式需要能捕获这种模式并决定是取最低价、最高价还是平均值。一个更健壮的提取函数可能长这样def extract_price_from_text(text): 从混杂的文本中提取价格数字 if not text: return None # 匹配数字、逗号千位分隔符、小数点 # 例如匹配 1,299.00, 899, $99.99, 5999-7999 matches re.findall(r[\d,]\.?\d*, text) if not matches: return None # 清理千位分隔符并转换为浮点数 cleaned_numbers [] for m in matches: try: num float(m.replace(,, )) cleaned_numbers.append(num) except ValueError: continue if not cleaned_numbers: return None # 根据业务逻辑返回如果只有一个数字就返回它如果是区间返回列表或平均值 if len(cleaned_numbers) 1: return cleaned_numbers[0] else: # 例如返回区间 return cleaned_numbers # 或者返回平均值 # return sum(cleaned_numbers) / len(cleaned_numbers)6. 数据存储、调度与工程化抓取到数据后我们需要把它存下来并且让整个流程可以定期自动运行。6.1 数据存储从CSV到数据库对于起步CSV文件是最简单的选择。import csv from datetime import datetime import os def save_to_csv(data, filenameproduct_prices.csv): 将数据追加保存到CSV文件 file_exists os.path.isfile(filename) # 添加抓取时间戳 data[fetch_time] datetime.now().strftime(%Y-%m-%d %H:%M:%S) fieldnames [fetch_time, name, price, url] with open(filename, a, newline, encodingutf-8-sig) as f: # utf-8-sig解决Excel打开中文乱码 writer csv.DictWriter(f, fieldnamesfieldnames) if not file_exists: writer.writeheader() # 文件不存在时写入表头 writer.writerow(data) print(f数据已保存: {data})当数据量变大或需要复杂查询时应该迁移到数据库。SQLitePython内置或轻量级的MySQL/PostgreSQL都是好选择。使用sqlite3库示例import sqlite3 def init_db(db_nameprices.db): conn sqlite3.connect(db_name) c conn.cursor() c.execute(CREATE TABLE IF NOT EXISTS price_history (id INTEGER PRIMARY KEY AUTOINCREMENT, fetch_time TIMESTAMP, product_name TEXT, price REAL, url TEXT)) conn.commit() conn.close() def save_to_db(data, db_nameprices.db): conn sqlite3.connect(db_name) c conn.cursor() c.execute(INSERT INTO price_history (fetch_time, product_name, price, url) VALUES (?, ?, ?, ?), (datetime.now(), data[name], data[price], data[url])) conn.commit() conn.close()6.2 任务调度实现自动化监控我们不可能手动运行脚本。在Linux服务器上最经典的方式是使用Cron。在Windows上可以使用任务计划程序。首先我们将主逻辑封装成一个脚本比如monitor.py# monitor.py import json def main(): # 从配置文件读取要监控的商品URL列表 with open(products.json, r, encodingutf-8) as f: product_list json.load(f) for product in product_list: url product[url] print(f正在抓取: {product[name]}) data fetch_product_price_advanced(url) # 使用我们之前写的增强版函数 if data: save_to_csv(data) # 或 save_to_db(data) print(f成功抓取: {data[name]} - 价格: {data[price]}) else: print(f抓取失败: {product[name]}) # 每个商品抓取后等待一段时间避免请求过快 time.sleep(random.uniform(2, 5)) if __name__ __main__: main()products.json文件内容[ { name: 某品牌手机, url: https://www.example.com/product/12345 }, { name: 某型号笔记本电脑, url: https://www.anotherexample.com/item/67890 } ]然后在Linux服务器上使用crontab -e命令添加定时任务。例如每天上午10点和晚上10点各运行一次0 10,22 * * * cd /path/to/your/price_monitor /path/to/your/venv/bin/python monitor.py /path/to/log/monitor.log 21在Windows上可以通过“任务计划程序”创建一个基本任务设置触发时间和启动程序为C:\path\to\your\venv\Scripts\python.exe C:\path\to\your\price_monitor\monitor.py。6.3 日志与错误监控一个能长期运行的程序必须有完善的日志记录。使用Python内置的logging模块。import logging # 配置日志 logging.basicConfig( levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(price_monitor.log, encodingutf-8), logging.StreamHandler() # 同时在控制台输出 ] ) logger logging.getLogger(__name__) # 在代码中使用 try: response session.get(url, headersheaders, timeout10) logger.info(f成功请求 {url}, 状态码: {response.status_code}) except requests.exceptions.Timeout: logger.error(f请求超时: {url})7. 常见问题排查与进阶技巧即使代码写得再仔细在实际运行中还是会遇到各种问题。这里记录一些我踩过的坑和解决方法。7.1 问题一返回乱码或中文显示不正常现象打印出来的网页内容或提取的中文是乱码。原因Requests 自动推断的编码有误。解决response requests.get(url) # 方法1手动指定编码如果知道的话比如‘utf-8’或‘gbk’ response.encoding utf-8 # 方法2使用chardet库自动检测需安装 pip install chardet import chardet encoding chardet.detect(response.content)[encoding] response.encoding encoding if encoding else utf-8 soup BeautifulSoup(response.text, lxml)7.2 问题二find方法返回None但页面上明明有元素现象选择器看起来没错但就是找不到元素。排查步骤检查请求是否成功打印response.status_code和response.text的前几百字符确认页面内容被正确下载。检查是否为动态内容如前所述用“查看网页源代码”确认元素是否存在。如果不存在需要分析网络请求。检查选择器是否准确在浏览器控制台使用document.querySelector(‘你的选择器’)测试你的CSS选择器是否有效。注意BeautifulSoup的select语法与浏览器JavaScript基本一致。注意class名称的动态部分有些网站的class名可能包含随机哈希值如price_abc123。这时不要用完整的class可以用soup.find(‘div’, class_re.compile(‘price_’))进行部分匹配。元素可能在iframe中如果元素在iframe里你需要先定位到iframe的src然后单独请求那个src地址。7.3 问题三请求被重定向或返回验证页面现象状态码是200但返回的内容是“请输入验证码”或“访问过于频繁”的页面。解决首要方案加大延迟降低频率。这是最根本的解决办法。time.sleep(random.uniform(5, 15))。检查请求头是否完整。特别是Referer来源页和Accept-Language有时加上它们能提高通过率。Referer可以设置为该电商网站的主页或分类页URL。考虑使用IP代理。模拟更完整的浏览器行为有些网站会检查JavaScript环境。可以尝试使用requests-html或Selenium这类能执行JS的库但这会显著增加复杂度。7.4 进阶技巧使用API接口替代HTML解析如果通过浏览器开发者工具的“网络”选项卡发现价格数据是通过一个单独的API请求通常是XHR类型返回JSON格式获取的那么恭喜你找到了“捷径”。步骤在“网络”选项卡中筛选XHR或Fetch请求。逐个查看请求的“响应”内容寻找包含价格字段的JSON。复制这个请求的URL、方法通常是GET、请求头特别是可能有的Authorization,X-Requested-With等。在你的爬虫中直接模拟这个请求。import requests import json def fetch_price_via_api(product_id): # 这个API URL、参数、请求头都需要从浏览器中实际分析得来 api_url fhttps://www.example.com/api/product/{product_id}/price api_headers { User-Agent: ..., Accept: application/json, # 有时需要特定的Referer或Origin Referer: fhttps://www.example.com/product/{product_id}, # 如果API需要认证可能需要添加Token # Authorization: Bearer xxxx } response requests.get(api_url, headersapi_headers) if response.status_code 200: data response.json() # 从JSON中提取价格例如 data[price][current] price data.get(price, {}).get(current) return price return None这种方式获取的数据干净、结构化且请求量远小于加载整个HTML页面是爬虫的首选方案。7.5 法律与道德边界最后也是最重要的一点我们必须清楚爬虫的边界遵守robots.txt在网站根目录下如https://www.example.com/robots.txt的这个文件指明了网站允许和禁止爬虫访问的路径。使用Python的urllib.robotparser可以解析它。虽然这不是法律强制但这是互联网的通行礼仪。尊重版权与数据所有权抓取的数据用于个人分析或研究通常问题不大但未经授权用于商业目的、大规模复制或重新发布很可能构成侵权。不要对网站造成负担设置合理的延迟避免并发大量请求这是作为一个“好爬虫”的基本素养。查看网站的服务条款很多网站会在其服务条款中明确禁止爬虫行为。写爬虫是一项在技术、耐心和规则间寻找平衡的工作。从简单的requests.get开始逐步添加错误处理、反爬策略、数据存储和调度最终构建一个稳定可靠的价格监控系统这个过程本身就是一个极好的学习项目。希望这篇超详细的指南能帮你避开我当年踩过的那些坑顺利抓取到你需要的价格数据。记住当爬虫遇到困难时放慢速度、仔细分析网络请求往往是解决问题的关键。