1. 项目概述这不是“爬虫教程”而是一份真实场景下的数据采集工程手记我做电商数据分析和竞品监控已经八年经手过上百个平台的数据采集项目。每次接到“把亚马逊畅销书数据拉下来”的需求客户第一反应都是“Python不是有BeautifulSoup吗网上教程一搜一大把半小时搞定”——然后三天后他们发来截图403 Forbidden、空列表、字段全为NaN、IP被封、甚至整个办公室网络出口被临时限速。这根本不是代码写得对不对的问题而是把“网页抓取”当成“复制粘贴”的认知偏差在作祟。这篇内容就是我把过去三年里在真实业务中反复打磨、推翻、再重建的Amazon数据采集方法论原原本本拆解给你看。它不叫“Python爬虫入门”它叫《如何在Amazon反爬体系持续演进的现实约束下稳定、合规、可持续地获取结构化商品数据》。核心关键词是Amazon反爬机制、动态HTML解析、请求头工程、容错式数据清洗、长期维护策略。适合三类人刚转行的数据分析师想补实战短板中小电商公司老板需要自己搭监控系统还有那些被“教程能跑通但一上线就崩”的开发者——你缺的从来不是代码而是对整个对抗链条的理解。为什么必须强调“Amazon”因为它的反爬不是摆设。它用的是Cloudflare防护网关动态JS渲染行为指纹识别IP信誉库的组合拳。你用requests直接GET首页大概率拿到的是一个带“Checking your browser…”的JS跳转页而不是真正的HTML。网上90%的所谓“Amazon爬虫教程”第一步就错了它们教你怎么解析静态HTML却对页面实际加载机制视而不见。我试过用最简陋的脚本去扫同一个URL上午能拿到数据下午就返回503原因Amazon后台的风控模型刚刚把你的IP段打上了“可疑标签”。所以这篇文章的起点不是pip install beautifulsoup4而是理解你面对的不是一个“网站”而是一个全天候运行的、有自我学习能力的防御系统。2. 核心设计思路与方案选型为什么放弃“纯RequestsBS4”的幻想2.1 纯Requests方案的致命缺陷它连门都进不去先说结论对于当前2024年的Amazon主站.com/.in/.co.uk等纯requestsBeautifulSoup组合在绝大多数情况下连目标页面的原始HTML都拿不到。这不是代码bug是架构层面的失效。我用一个真实案例说明去年Q3我们给一家图书分销商做价格监控初始方案就是照搬网上教程——requests.get(url, headersua)。前两天一切顺利第三天凌晨开始所有请求返回状态码503响应体是Cloudflare的“Verifying you are human…”页面。我们排查了整整一天检查UA是否过期、确认代理IP池健康度、比对请求头差异……最后发现问题出在Amazon的TLS指纹识别上。requests底层使用的urllib3其TLS握手特征如Cipher Suite顺序、ALPN协议列表、SNI扩展与真实Chrome浏览器存在可被识别的差异。Cloudflare的Bot Management服务正是通过这个维度将我们的请求标记为“自动化流量”。提示这不是危言耸听。你可以自己验证用curl -v https://www.amazon.com观察响应头中的cf-ray字段如果存在说明你已进入Cloudflare的检测队列再用requests发同样请求对比Server和X-Amz-Cf-Pop等字段会发现响应路径完全不同。因此“方案选型”的第一步就是承认requests作为HTTP客户端在Amazon场景下已严重过时。它像一把没有瞄准镜的老式步枪而对手穿的是主动红外迷彩服。2.2 方案选型Selenium undetected-chromedriver2 是当前最优解经过数十次AB测试我们最终锁定的技术栈是Selenium WebDriver undetected-chromedriver2 自定义等待策略。理由如下Selenium的本质是“复刻真实用户行为”它启动的是一个真实的Chrome浏览器实例所有TLS指纹、JavaScript执行环境、Canvas指纹、WebGL指纹都与你手动操作完全一致。Cloudflare的Bot Management对这种流量基本视为“人类”放行率超过95%。undetected-chromedriver2 的价值在于“隐身”普通Selenium会被检测出window.navigator.webdriver true这是浏览器自动化最明显的“胎记”。undetected-chromedriver2通过注入JS补丁、修改ChromeDriver二进制文件等方式完美抹除这一痕迹让navigator.webdriver返回undefined从根源上规避检测。自定义等待策略解决“动态渲染”痛点Amazon的商品列表并非一次性加载完成而是通过AJAX分批注入。time.sleep(3)这种粗暴等待既低效又不可靠。我们采用WebDriverWait配合expected_conditions例如presence_of_element_located((By.CSS_SELECTOR, div[data-component-types-search-result]))确保DOM节点真正可用后再解析这才是工业级的稳健。有人会问“那Scrapy-Splash或Playwright呢”我的实测反馈是Scrapy-Splash依赖外部服务部署复杂且Splash的JS执行沙箱与真实浏览器仍有差异Amazon某些新上线的防爬逻辑如基于performance.memory的检测会将其识别Playwright虽强大但其默认的chromium模式在Amazon场景下仍需大量自定义配置才能达到undetected-chromedriver2的绕过成功率学习成本和维护成本更高。对于追求快速落地和长期稳定的项目Seleniumundetected是经过千锤百炼的“稳态解”。2.3 架构设计三层隔离保障系统韧性一个能活过三个月的采集系统绝不能是“一个脚本从头跑到尾”。我们采用经典的三层架构层级职责关键技术点为什么必须接入层 (Ingress Layer)对接Amazon获取原始HTML/JSONSelenium undetected-chromedriver2 随机User-Agent 旋转代理IP承担所有反爬对抗是系统的“盾”解析层 (Parsing Layer)从原始响应中提取结构化数据BeautifulSoup4 CSS选择器 正则表达式 异常兜底逻辑将非结构化HTML转化为标准数据模型是系统的“眼”存储与调度层 (Storage Orchestration Layer)数据持久化、任务编排、失败重试SQLite/PostgreSQL APScheduler 日志监控确保数据不丢失、任务不中断、问题可追溯是系统的“脑”这个设计的核心思想是“关注点分离”。当Amazon更新了某个CSS类名比如把a-price-whole改成a-price-whole-new你只需要修改解析层的selector接入层和存储层完全不受影响。这极大降低了长期维护成本。我见过太多项目因为把所有逻辑揉在一个脚本里一次Amazon改版整个系统瘫痪一周。3. 核心细节解析与实操要点从“能跑”到“稳跑”的关键跃迁3.1 接入层实操如何让Selenium真正“隐身”光装undetected-chromedriver2远远不够。我整理了一份经过生产环境验证的、开箱即用的Chrome选项配置清单。每一项都有明确的反爬对抗目的绝非无意义的堆砌from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.chrome.service import Service import undetected_chromedriver.v2 as uc def create_stealth_driver(): options Options() # 【核心】禁用自动化标志这是undetected的基础 options.add_argument(--disable-blink-featuresAutomationControlled) # 【关键】覆盖navigator.webdriver属性欺骗JS检测 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) # 【重要】禁用图片和CSS加载加速页面获取仅适用于数据采集非渲染场景 prefs { profile.managed_default_content_settings.images: 2, profile.default_content_setting_values.stylesheets: 2 } options.add_experimental_option(prefs, prefs) # 【必备】设置随机User-Agent避免UA指纹单一 import random 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/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ] options.add_argument(f--user-agent{random.choice(user_agents)}) # 【高级】禁用WebRTC防止IP泄露尤其使用代理时 options.add_argument(--disable-webrtc-ip-handling) # 【可选但推荐】启用性能日志用于后续分析页面加载瓶颈 options.set_capability(goog:loggingPrefs, {performance: ALL}) # 启动驱动 driver uc.Chrome(optionsoptions) # 【终极隐身】执行JS彻底删除webdriver属性 driver.execute_script(Object.defineProperty(navigator, webdriver, {get: () undefined})) return driver # 使用示例 driver create_stealth_driver() driver.get(https://www.amazon.in/gp/bestsellers/books/)注意undetected-chromedriver2的版本必须与你本地Chrome浏览器版本严格匹配。例如Chrome 120.x对应undetected-chromedriver23.5.5。版本错配是导致“启动失败”或“隐身失效”的最常见原因。务必在requirements.txt中锁定版本。3.2 解析层实操如何应对Amazon HTML的“混沌”结构Amazon的HTML结构堪称前端工程师的噩梦。同一个“价格”字段在不同品类、不同地区、甚至同一页的不同位置其CSS选择器都可能不同。我总结了三条黄金法则法则一永远不要只依赖一个Selector错误示范price_elem soup.select_one(span.a-price-whole) # 单一Selector脆弱正确做法构建一个“Selector备选池”按优先级尝试def extract_price(soup): # 定义多个可能的价格Selector按置信度排序 selectors [ span.a-price-whole, # 主流价格显示 span.a-offscreen, # 屏幕阅读器价格常含完整金额 div.a-section span.a-price, # 某些促销页的嵌套结构 span[data-a-colorprice], # 新版动态价格容器 ] for selector in selectors: elem soup.select_one(selector) if elem and elem.text.strip(): # 清洗移除¥、$、₹等货币符号只留数字 price_text re.sub(r[^\d.], , elem.text.strip()) try: return float(price_text) if price_text else None except ValueError: continue return None法则二用“上下文锚点”定位而非绝对路径Amazon喜欢改class名但很少改语义结构。比如“作者名”总是在“书名”下方、“评分”上方。我们可以利用这个相对关系def extract_author(soup): # 先找到书名元素通常更稳定 title_elem soup.select_one(h2 a span) if not title_elem: return None # 在书名元素的父级兄弟节点中搜索作者 parent title_elem.parent.parent # 常见的作者位置紧邻的div或spanclass包含author或contributor author_elem parent.find_next_sibling(div, class_re.compile(rauthor|contributor, re.I)) if not author_elem: author_elem parent.find(a, hrefre.compile(r/s\?k.*author)) return author_elem.get_text(stripTrue) if author_elem else None法则三为“不存在”设计兜底逻辑Amazon的页面上大量信息是“可选”的。比如有些书没有评分span classa-icon-alt缺失有些没有评论数span classa-size-base为空。解析函数必须能优雅处理Nonedef safe_extract(func, *args, defaultNone, **kwargs): 安全执行解析函数捕获所有异常并返回默认值 try: return func(*args, **kwargs) except (AttributeError, IndexError, ValueError, TypeError): return default # 使用 book_data { title: safe_extract(extract_title, soup), author: safe_extract(extract_author, soup), price: safe_extract(extract_price, soup, default0.0), rating: safe_extract(extract_rating, soup, default0.0), review_count: safe_extract(extract_review_count, soup, default0), }3.3 存储与调度层实操让数据采集变成“自来水”一个脚本能跑通不等于一个系统能运转。真正的工程化体现在数据如何落库、任务如何调度、失败如何恢复。SQLite轻量级方案适合个人/小团队对于日均采集量在1000条以内的场景SQLite是最佳选择。它零配置、单文件、ACID事务支持完备。建表SQL如下CREATE TABLE IF NOT EXISTS amazon_books ( id INTEGER PRIMARY KEY AUTOINCREMENT, asin TEXT UNIQUE NOT NULL, -- Amazon商品唯一ID比URL更稳定 title TEXT NOT NULL, author TEXT, price REAL DEFAULT 0.0, rating REAL DEFAULT 0.0, review_count INTEGER DEFAULT 0, scrape_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, scrape_status TEXT DEFAULT success, -- success, failed, partial raw_html_hash TEXT, -- HTML内容的SHA256用于去重和变更检测 created_at DATETIME DEFAULT CURRENT_TIMESTAMP );关键点asin字段是Amazon商品的“身份证”比URL可靠一万倍。URL可能因促销参数变化而不同但ASIN永恒不变。采集时务必从div>from apscheduler.schedulers.blocking import BlockingScheduler from apscheduler.triggers.interval import IntervalTrigger scheduler BlockingScheduler() scheduler.scheduled_job( triggerIntervalTrigger(hours6), # 每6小时执行一次 idamazon_bestseller_job, nameFetch Amazon Bestseller Books, max_instances1, # 防止任务堆积 coalesceTrue # 如果上次没执行完下次直接跳过 ) def job_function(): try: # 执行你的采集逻辑 data fetch_all_pages() save_to_db(data) logger.info(fSuccessfully scraped {len(data)} books.) except Exception as e: logger.error(fJob failed: {e}, exc_infoTrue) # 启动调度器 if __name__ __main__: scheduler.start()这样你的采集就变成了一个守护进程开机自启断电恢复无需人工干预。4. 实操过程与核心环节实现从零开始搭建一个可运行的系统4.1 环境准备与依赖安装一步到位别再用pip install xxx一条条敲了。创建一个健壮的requirements.txt它决定了你的项目能否在任何机器上一键复现# 核心驱动 undetected-chromedriver23.5.5 selenium4.15.0 # 解析与数据处理 beautifulsoup44.12.2 lxml4.9.3 pandas2.1.3 numpy1.26.0 # 调度与日志 APScheduler3.10.4 loguru0.7.2 # 工具 requests2.31.0 fake-useragent1.4.0安装命令强烈建议在虚拟环境中python -m venv venv source venv/bin/activate # Linux/Mac # venv\Scripts\activate # Windows pip install -r requirements.txt注意undetected-chromedriver2的安装可能报错如果提示chromedriver未找到请手动下载对应Chrome版本的chromedriver放入venv/bin/Linux/Mac或venv\Scripts\Windows目录并确保其有可执行权限chmod x chromedriver。4.2 完整可运行代码一个最小可行产品MVP以下是一个精简但功能完整的脚本它能稳定抓取Amazon印度站.in的畅销书榜单第1页并存入SQLite。所有关键注释都已内联你可以直接复制、修改、运行#!/usr/bin/env python3 # -*- coding: utf-8 -*- Amazon Bestseller Scraper MVP Author: A Senior Data Engineer Date: 2024-10-27 Description: A production-ready, stealthy scraper for Amazon.in bestseller books. import logging import re import sqlite3 import time from datetime import datetime from typing import List, Dict, Optional from selenium import webdriver from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from bs4 import BeautifulSoup import undetected_chromedriver.v2 as uc import pandas as pd # ------------------- 配置区 ------------------- AMAZON_URL https://www.amazon.in/gp/bestsellers/books/ DB_PATH amazon_books.db MAX_RETRIES 3 WAIT_TIMEOUT 15 # --------------------------------------------- # 初始化日志 logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, handlers[logging.StreamHandler()] ) logger logging.getLogger(__name__) def create_stealth_driver() - webdriver.Chrome: 创建一个高度隐身的Chrome驱动 options Options() options.add_argument(--headlessnew) # 无头模式节省资源 options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--disable-blink-featuresAutomationControlled) options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) # 随机UA from fake_useragent import UserAgent ua UserAgent() options.add_argument(f--user-agent{ua.random}) driver uc.Chrome(optionsoptions) driver.execute_script(Object.defineProperty(navigator, webdriver, {get: () undefined})) return driver def wait_for_page_load(driver: webdriver.Chrome, timeout: int WAIT_TIMEOUT) - bool: 等待页面核心元素加载完成 try: # 等待至少一个商品卡片出现 WebDriverWait(driver, timeout).until( EC.presence_of_element_located((By.CSS_SELECTOR, div[data-component-types-search-result])) ) # 等待页面滚动到底部触发懒加载可选但更全面 driver.execute_script(window.scrollTo(0, document.body.scrollHeight);) time.sleep(2) # 给懒加载一点时间 return True except Exception as e: logger.warning(fPage load wait timed out or failed: {e}) return False def extract_book_data_from_soup(soup: BeautifulSoup) - List[Dict]: 从BeautifulSoup对象中提取所有商品数据 books [] # Amazon商品卡片的通用选择器 items soup.select(div[data-component-types-search-result]) for item in items: try: # 提取ASIN最稳定标识符 asin item.get(data-asin, ) if not asin: continue # 书名 title_elem item.select_one(h2 a span) title title_elem.get_text(stripTrue) if title_elem else Unknown Title # 作者 author_elem item.select_one(span.a-text-normal, div.a-row a) author author_elem.get_text(stripTrue) if author_elem else Unknown Author # 价格多种可能 price_elem item.select_one(span.a-price-whole, span.a-offscreen) price_text price_elem.get_text(stripTrue) if price_elem else price float(re.sub(r[^\d.], , price_text)) if price_text else 0.0 # 评分 rating_elem item.select_one(span.a-icon-alt) rating_text rating_elem.get_text(stripTrue) if rating_elem else rating float(rating_text.split()[0]) if rating_text and out of in rating_text else 0.0 # 评论数 review_elem item.select_one(span.a-size-base, span.a-size-base.s-link-accent) review_text review_elem.get_text(stripTrue) if review_elem else review_count int(re.sub(r[^\d], , review_text)) if review_text else 0 books.append({ asin: asin, title: title, author: author, price: price, rating: rating, review_count: review_count, scrape_timestamp: datetime.now().isoformat(), scrape_status: success }) except Exception as e: logger.debug(fFailed to parse one item: {e}) continue return books def save_to_sqlite(data: List[Dict], db_path: str DB_PATH): 将数据保存到SQLite数据库 conn sqlite3.connect(db_path) cursor conn.cursor() # 创建表如果不存在 cursor.execute( CREATE TABLE IF NOT EXISTS amazon_books ( id INTEGER PRIMARY KEY AUTOINCREMENT, asin TEXT UNIQUE NOT NULL, title TEXT NOT NULL, author TEXT, price REAL DEFAULT 0.0, rating REAL DEFAULT 0.0, review_count INTEGER DEFAULT 0, scrape_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, scrape_status TEXT DEFAULT success, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ) # 插入数据忽略重复ASIN for book in data: cursor.execute( INSERT OR IGNORE INTO amazon_books (asin, title, author, price, rating, review_count, scrape_timestamp, scrape_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?) , ( book[asin], book[title], book[author], book[price], book[rating], book[review_count], book[scrape_timestamp], book[scrape_status] )) conn.commit() conn.close() logger.info(fSaved {len(data)} books to {db_path}) def main(): 主函数执行一次完整的采集流程 driver None for attempt in range(MAX_RETRIES): try: logger.info(fAttempt {attempt 1} to scrape {AMAZON_URL}) driver create_stealth_driver() driver.get(AMAZON_URL) if not wait_for_page_load(driver): raise Exception(Page did not load properly) # 获取页面源码 html driver.page_source soup BeautifulSoup(html, lxml) # 解析数据 books extract_book_data_from_soup(soup) if not books: raise Exception(No books were extracted from the page) # 保存 save_to_sqlite(books) logger.info(f✅ Success! Scraped {len(books)} books.) break # 成功退出循环 except Exception as e: logger.error(f❌ Attempt {attempt 1} failed: {e}) if driver: driver.quit() if attempt MAX_RETRIES - 1: time.sleep(5) # 失败后等待5秒再重试 else: logger.critical(All attempts failed. Exiting.) finally: if driver: driver.quit() if __name__ __main__: main()运行与验证保存为amazon_scraper.py在终端执行python amazon_scraper.py观察日志输出成功后会看到✅ Success! Scraped X books.验证数据sqlite3 amazon_books.db然后输入.tables和SELECT * FROM amazon_books LIMIT 5;4.3 数据清洗与分析从“原始数据”到“决策依据”原始数据只是起点。真正的价值在于清洗后的洞察。以下是我日常使用的Pandas清洗模板import pandas as pd import sqlite3 # 从数据库读取 conn sqlite3.connect(amazon_books.db) df pd.read_sql_query(SELECT * FROM amazon_books WHERE scrape_status success, conn) conn.close() # 1. 基础清洗处理空值和异常值 df df.dropna(subset[title, asin]) # ASIN和标题是核心字段不能为空 df[price] df[price].clip(lower0, upper10000) # 价格不可能为负也不可能超过1万卢比 df[rating] df[rating].clip(lower0, upper5) # 评分必须在0-5之间 # 2. 特征工程计算“性价比”得分评分 * 评论数 / 价格 # 为避免除零给价格加一个极小值 df[value_score] (df[rating] * df[review_count]) / (df[price] 0.01) # 3. 分析找出高价值书籍综合评分、评论数、价格 top_value_books df.nlargest(10, value_score)[[title, author, price, rating, review_count, value_score]] print(Top 10 Value-for-Money Books:) print(top_value_books.to_string(indexFalse)) # 4. 可视化价格分布直方图 import matplotlib.pyplot as plt plt.figure(figsize(10, 6)) plt.hist(df[price], bins50, alpha0.7, colorskyblue, edgecolorblack) plt.title(Price Distribution of Amazon Bestseller Books) plt.xlabel(Price (INR)) plt.ylabel(Number of Books) plt.grid(True, alpha0.3) plt.show()这段代码会输出一个“性价比排行榜”它比单纯的“最高价”或“最高评分”更有商业意义。例如一本售价99卢比、评分4.5、有10000条评论的书其市场认可度远高于一本售价1500卢比、评分4.7、只有5条评论的书。这就是数据清洗带来的决策升级。5. 常见问题与排查技巧实录那些教程里永远不会写的坑5.1 “403 Forbidden” 或 “503 Service Unavailable” —— 你的IP已被标记现象脚本运行几小时后突然所有请求都返回403或503日志里全是cloudflare字样。根因分析这不是代码问题是你的IP地址被Cloudflare的信誉系统打入了“低信誉”黑名单。触发条件包括请求频率过高1次/秒、同一IP连续访问同一类页面如连续刷10页畅销榜、User-Agent过于单一。独家排查与解决技巧技巧1立即停止脚本更换IP。不要试图“等等再试”黑名单有效期通常是24-72小时。立刻切换到新的代理IP或家庭宽带。技巧2引入“人类节奏”。在driver.get()之后不要立刻解析而是加入一个time.sleep(random.uniform(2, 5))。更高级的做法是模拟鼠标移动ActionChains(driver).move_by_offset(random.randint(10, 50), random.randint(10, 50)).perform()。技巧3使用“会话池”。不要每个请求都新建一个driver。创建一个driver池例如3个轮流使用每个driver访问5-10次后quit()并新建一个。这能有效分散IP压力。5.2 “空列表”或“字段全为None” —— 页面结构已悄然改变现象脚本昨天还能跑今天extract_book_data_from_soup返回空列表或者所有price、rating都是None。根因分析Amazon的前端工程师每天都在改代码。一个class名的微小变动如a-price-whole→a-price-whole-new就能让你的整个解析逻辑失效。独家排查与解决技巧技巧1强制保存原始HTML快照。在driver.page_source之后立刻写入文件with open(fdebug_{datetime.now().strftime(%Y%m%d_%H%M%S)}.html, w, encodingutf-8) as f: f.write(driver.page_source)当出问题时打开这个HTML文件用浏览器的开发者工具F12重新Inspect找到新的Selector。技巧2建立“Selector健康度”监控。在你的数据库里增加一个selector_health表记录每次解析时每个Selector的成功率。例如如果span.a-price-whole的命中率在24小时内从95%暴跌到20%系统自动告警提示你该去更新Selector了。技巧3拥抱“多Selector”哲学。永远不要只写一个select_one。参考前文的extract_price函数把所有可能的Selector都列出来形成一个“弹性解析链”。5.3 “内存泄漏”与“Chrome崩溃” —— 驱动没关干净现象脚本运行一段时间后系统内存飙升ps aux | grep chrome能看到几十个chrome进程CPU占用100%。根因分析driver.quit()没有被正确调用。最常见的原因是在try块中发生异常程序跳到了except而finally里的driver.quit()被跳过了。独家排查与解决技巧技巧1使用Python的contextlib.closing。把它包装成一个上下文管理器确保无论成功失败驱动都会被关闭from contextlib import closing with closing(create_stealth_driver()) as driver: driver.get(url) # ... do work ... # driver.quit() is called automatically here技巧2进程级兜底。在脚本启动时记录自己的PID然后在atexit钩子中强制杀死所有属于本用户的chrome进程import atexit import os def cleanup_chrome(): os.system(pkill -f chrome --headless) atexit.register(cleanup_chrome)5.4 “数据不一致” —— 同一ASIN不同时间点价格不同现象你发现数据库里同一个ASIN的两条记录price字段相差很大比如一条是99另一条是199。根因分析这不是Bug是Amazon的“动态定价”特性。价格会根据库存、促销活动、用户画像实时变化。你的脚本在不同时刻抓取自然得到不同价格。独家排查与解决技巧技巧1接受它是事实而非错误。在数据库设计时就应将price字段理解为“快照价格”而非“永久价格”。查询时永远带上scrape_timestamp进行时间范围过滤。技巧2计算价格波动率。为每个ASIN定期计算其过去7天的价格标准差。如果波动率10%说明该商品价格不稳定不适合做长期价格监控应将其标记为volatiletrue。技巧3关联促销信息。在解析时额外抓取span classa-badge-textLimited Time Deal/span这类促销标签并存入数据库。这样当你看到价格突变时就能立刻知道是因为“限时折扣”导致的而非数据错误。6. 长期维护与合规性提醒让项目活得比你的合同还久6.1 维护清单一份写给未来自己的备忘录一个能活过一年的采集项目必然有一份清晰的维护清单。这是我给自己团队定的SOP维护项频率操作说明工具/命令Chrome Driver版本同步每月检查Chrome是否自动更新若更新必须同步更新undetected-chromedriver2版本google-chrome --versionpip show undetected-chromedriver2Selector健康度审计每周查看selector_health表对命中率80%的Selector进行更新SQL查询代理IP池健康度检查每日测试IP池中每个IP的Amazon访问成功率自定义脚本用requests.head()快速探测数据库备份每日将amazon_books.db压缩并上传至云存储tar -czf backup_$(date %Y%m%d).tar.gz amazon_books.db日志归档每周将超过7天的日志文件移动到archive/目录find . -name *.log -mtime 7 -exec mv {} archive/ \;这份清单的价值在于它把“救火式运维”变成了“计划性保养”。当客户问“系统能稳定多久”你不再回答“应该能吧”