
如果你试过用 requests 直接抓京东的商品页面大概率会碰到这样一幕请求发过去返回的不是 HTML而是一句参数错误或者一个跳转到登录验证的页面。这不是你的代码写错了而是京东的 web 端反爬已经升级到了非常系统的程度——账号维度、设备维度、IP 维度、行为维度四层交叉验证。标题里提到的 Selenium 代理 IP 组合正是目前实战中对付这套体系最主流、最可控的方案。这篇文章我会完整拆解这套方案。内容包括京东反爬到底卡在哪些环节、Selenium 怎么配置才能不裸奔、代理 IP 池怎么自建和调度、完整的爬虫代码怎么落地、以及我实测中反复踩过的坑。无论你是刚开始学爬虫的新手还是已经写过不少脚本但总被验证码劝退的老手这篇文章都能给你一份可以直接跑的方案。1. 京东反爬体系的真面目你不是在跟一个验证码战斗很多人以为京东的反爬就是出个滑块验证码滑过去就行。实际上,京东的风控是分层设计的每一层都有自己的触发条件。我花了很长时间抓包、对比、测试才把整个体系摸清楚。1.1 第一道防线账号态与设备指纹京东对无登录状态的匿名请求非常敏感。你用 requests 直接访问商品详情页服务端会检查请求头里的 Cookie、User-Agent、Accept-Language 等字段还会通过一段 JS 脚本采集浏览器指纹信息包括 Canvas 指纹、WebGL 渲染结果、字体列表、屏幕分辨率、时区、语言等。这些信息会被组合成一个加密的指纹串放在请求的 header 或者参数里。如果你用的是纯 requests这个指纹串要么不存在要么是伪造的。京东服务端一比对发现这个浏览器没有正常的 JS 执行环境当场就给你返回 250 之类的错误码或者直接重定向到验证页面。这也是为什么 Selenium 成了绕过的关键——它本质上是一个真正的浏览器内核会完整执行 JS生成的指纹和真实用户没有区别。所以后面讲到配置时我会特别强调保持浏览器特征完整。1.2 第二道防线行为轨迹检测与滑块验证即使你带着看似正常的 Cookie 和指纹顺利访问到了页面京东风控还会观察你在页面上的操作行为。鼠标是不是直接瞬移过去点击、页面滚动是不是瞬间到底、停留时间是不是完全一样……这些行为数据都会被埋点脚本上报。触发滑块验证的条件我记得很清楚我曾经用同一个 IP 连续访问 20 个商品详情页且每次间隔不超过 3 秒第 21 次请求直接弹滑块而且那个滑块难度极高不是拖过去就行。京东的滑块会检测你拖动的轨迹是不是符合人类特征如果轨迹是匀速直线、没有微小的抖动判为机器操作再次弹窗甚至冻结 IP 一段时间。所以后面我设计的爬虫在两次访问之间加入了随机延时并且把 Selenium 驱动的鼠标动作模拟成人手的轨迹而不是直接 execute_script 瞬移点击。1.3 第三道防线IP 地域与访问频次风控这就是代理 IP 存在的意义。京东对一个 IP 的访问频率有严格的限制具体阈值虽然不公开但我实测下来纯数据中心 IP 在 1 分钟内请求同一类 URL 超过 40 次触发异常的概率明显上升。一旦 IP 被标记你后面不管怎么换设备特征这个 IP 发出的请求都会进入慢速验证流程页面加载时间增加甚至直接拒绝服务。另外地域也是风控的一部分。比如你在 A 城市有正常数据中心节点突然用 B 城市的代理大量访问且 B 城市的 IP 段刚好是爬虫重灾区风控评分会立刻拉高。代理 IP 的作用就是把这些请求分散到不同的 IP 上让每个 IP 的访问量都看起来正常。提示我之前遇到过用某个免费代理 IP 池结果里面混着大量已经被京东标记的脏 IP换上之后反而更容易触发验证码。所以代理 IP 不是有就能用必须做可用性检测和评分这个后面会详细讲。2. Selenium 配置里的隐形陷阱启动成功不等于不被识别Selenium 本身是一个自动化测试工具它启动的浏览器和正常用户打开的浏览器是有区别的。最典型的就是window.navigator.webdriver属性正常浏览器这个值是false或undefined而 Selenium 驱动启动的浏览器里它是true。京东的埋点脚本只要检查这一项就能判断你是不是自动化脚本。2.1 环境准备与版本匹配先说我本地跑通这个项目的环境组合组件版本说明Python3.10.x3.8 以上基本都行Selenium4.20.04.x 的 API 和 3.x 差别较大Chrome 浏览器125.x注意和 chromedriver 版本对应chromedriver125.x 同版本严格匹配否则启动直接报错Selenium 4.6 以上可以使用 Selenium Manager 自动匹配版本不手动下载 driver。但国内网络环境下偶尔会拉取失败我最后的做法是手动下载 chromedriver 放到项目目录下用Service类显式指定路径稳得一批。安装依赖pip install selenium requests pandas lxml注意lxml是解析 HTML 用的速度比BeautifulSoup快不少。后面解析京东商品数据时这个性能优势能感受到。2.2 隐藏 Selenium 特征的完整配置这是我项目里驱动初始化的核心代码每个参数都有实际意义。你必须用完整版而不是网上那些阉割版from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options def create_driver(proxyNone): chrome_options Options() # 1. 基础反检测参数 chrome_options.add_argument(--disable-blink-featuresAutomationControlled) chrome_options.add_experimental_option(excludeSwitches, [enable-automation]) chrome_options.add_experimental_option(useAutomationExtension, False) # 2. 设置一个真实的 User-Agent别用默认的 chrome_options.add_argument( user-agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 ) # 3. 禁用自动化提示条 chrome_options.add_experimental_option(excludeSwitches, [enable-logging]) # 4. 代理设置后面会传进来 if proxy: chrome_options.add_argument(f--proxy-serverhttp://{proxy}) # 5. 窗口大小设置成真实分辨率避免被检测出无头特征 chrome_options.add_argument(--window-size1920,1080) # 6. 不加载图片加快页面加载速度同时降低带宽占用 prefs {profile.managed_default_content_settings.images: 2} chrome_options.add_experimental_option(prefs, prefs) service Service(./chromedriver) # 显式指定driver路径 driver webdriver.Chrome(serviceservice, optionschrome_options) # 7. 最关键的CDP级伪装覆盖navigator.webdriver属性 driver.execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument, { source: Object.defineProperty(navigator, webdriver, { get: () undefined }); // 补充伪装其他常用属性 Object.defineProperty(navigator, plugins, { get: () [1, 2, 3, 4, 5] }); Object.defineProperty(navigator, languages, { get: () [zh-CN, zh, en] }); }) return driver这段代码里有几个点我要单独拿出来说--disable-blink-featuresAutomationControlled这个参数会关闭 Chromium 的自动化控制标记是基础中的基础不加的话很多网站通过window.chrome对象就能识别出 Selenium。execute_cdp_cmd(Page.addScriptToEvaluateOnNewDocument)这个方法是在每个新文档加载之前注入一段 JS。我这里把navigator.webdriver重定义为undefined实际上就是欺骗了浏览器的 JS 环境。这个方法比简单的driver.execute_script可靠得多因为后者在页面加载完成后再执行埋点脚本已经跑完了该采集的特征已经采集到了。为什么我不建议用无头模式Headless虽然无头模式省资源但navigator.webdriver之外无头模式还有很多特征比如navigator.plugins为空数组、window.chrome对象缺失、User-Agent里带HeadlessChrome字样。而且很多网站会用 Canvas/Bochs 指纹检测无头模式下 GPU 渲染路径不同指纹不一致。综合下来为了稳定性我宁可在一台小服务器上多开几个窗口也不开无头模式。2.3 页面加载策略别让等待变成煎熬Selenium 默认的页面加载策略是normal也就是等页面完全加载完才返回。京东的商品页图片多、脚本多一个页面等 5 秒以上很常见。更坑的是某些广告资源可能永远加载不完导致脚本卡死。我用的是eager策略也就是 DOM 加载完成后就返回不等图片和样式表chrome_options.page_load_strategy eager但是页面加载策略改为eager后有个副作用部分通过 JS 异步渲染的内容还没出现。这时候我通常会加一个显性等待from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, //div[classp-price])) )这个等待会一直轮询直到找到价格节点才继续往下走。比固定time.sleep(5)高效得多网络稍差时也稳定得多。3. 自建代理 IP 池比买现成接口更可控代理 IP 这块很多新手容易犯一个错误直接买一个代理 API 接口每次从接口拉一个 IP 出来用。这种方式的问题是接口提供的 IP 很多都被别人反复用过可能早就被京东标记了。而且免费 IP 池的质量参差不齐代理响应速度慢的话整个爬虫速度会被拖死。我自己搭的是一个自采集 自验证 自评分的小型代理池。3.1 代理来源免费与付费的取舍说实话完全免费的代理源质量堪忧。我测试过热门的免费代理网站每天更新几万个 IP但能用的不到 10%能用的里面很大一部分是透明代理Connect 到京东会直接暴露真实 IP等于没有代理。我现在的做法是用付费的代理作为主力同时搭配几个免费源做补充。付费代理我选的是按量计费的那种几块钱能买几百个 IP用完了再充成本可控。选服务商时看两点是否提供 API 拉取、是否套餐内 IP 可以自动去重。把拉到的 IP 放到池子里统一走下面这套验证逻辑。3.2 IP 可用性检测与评分每个新拉取的 IP 都必须经过三道验证验证通过的才放进可用队列import requests def validate_proxy(proxy): 返回一个0到100之间的评分 test_urls [ https://www.jd.com/, https://www.baidu.com/ ] proxies { http: fhttp://{proxy}, https: fhttp://{proxy}, } score 0 for url in test_urls: try: resp requests.get(url, proxiesproxies, timeout5) if resp.status_code 200: score 40 except Exception: pass if score 80: return 0 # 第三道验证检查出口IP是否和代理声明一致 try: resp requests.get(https://httpbin.org/ip, proxiesproxies, timeout5) external_ip resp.json().get(origin, ) if external_ip: score 20 except Exception: pass return score这里我设置了一个 50 分的阈值低于这个的就直接丢弃。剩下的 IP 还要继续观察它在京东上的表现——如果连续 3 次请求都触发验证码这个 IP 直接拉黑。3.3 Selenium 中代理注入方式别用 WebDriver 的 DesiredCapabilities 老方法在 Selenium 4.x 里设置代理有两种方式第一种是DesiredCapabilities老教程里常见但 Selenium 4 里已经部分弃用而且配置起来麻烦。第二种就是我上面create_driver代码里用的chrome_options.add_argument(f--proxy-serverhttp://{proxy})这种方式直接传给 Chrome 内核兼容性最好。这里有个细节代理认证问题。如果你的代理是带用户名密码的--proxy-server参数无法直接带上认证信息。我当初卡在这里很久——报错显示代理认证失败但浏览器又没弹窗让我输密码。解决方法是使用 Chrome 扩展方式动态注入认证信息import zipfile def create_proxy_auth_extension(proxy_host, proxy_port, proxy_username, proxy_password): 生成带认证信息的Chrome扩展用于代理认证 manifest_json { version: 1.0.0, manifest_version: 2, name: Chrome Proxy, permissions: [ proxy, tabs, unlimitedStorage, storage, all_urls, webRequest, webRequestBlocking ], background: { scripts: [background.js] }, minimum_chrome_version: 22.0.0 } background_js var config { mode: fixed_servers, rules: { singleProxy: { scheme: http, host: %s, port: parseInt(%s) }, bypassList: [foobar.com] } }; chrome.proxy.settings.set({value: config, scope: regular}, function() {}); function callbackFn(details) { return { authCredentials: { username: %s, password: %s } }; } chrome.webRequest.onAuthRequired.addListener( callbackFn, {urls: [all_urls]}, [blocking] ); % (proxy_host, proxy_port, proxy_username, proxy_password) plugin_file proxy_auth_plugin.zip with zipfile.ZipFile(plugin_file, w) as zp: zp.writestr(manifest.json, manifest_json) zp.writestr(background.js, background_js) return plugin_file然后在create_driver里加上chrome_options.add_extension(plugin_file)。这个扩展的原理是注册了一个onAuthRequired事件拦截器在 Chrome 收到代理认证要求时自动填入用户名密码不需要人工干预。说实话这块是我这个项目里花时间最多的地方。因为我之前用 API 拉取的代理大多都带账号密码不解决这个认证问题后面跑起来就是噩梦——你看着浏览器打开的页面就是加载不出来全是代理认证框。3.4 IP 轮换策略与频率控制代理池准备好了不能只是换一个 IP 继续跑那样很快又会触发风控。我的做法是每个 IP 分配一个可使用次数配额配额用完了就换。每次请求之间加入随机延时延时范围根据目标页面种类调整。同一个 IP 在同一时间段内只负责一个抓取任务避免交叉使用导致频率叠加。对于疑似被标记的 IP立即切换到另一个 IP并把疑似 IP 放进观察区而不是直接拉黑。因为有些 IP 可能只是暂时被限流过段时间又能用了。import random import time def get_random_delay(base2, variance0.8): 生成一个在base*variance到base*(1variance)之间的随机延时 return random.uniform(base * (1 - variance), base * (1 variance)) # 每次请求之间调用 time.sleep(get_random_delay())很多人直接用time.sleep(2)看起来也没什么问题但如果所有请求之间的间隔完全一致风控的时间序列算法能识别出周期性特征。这一点上随机化和人类化是同一个逻辑。4. 拿得出手的完整代码从初始化到数据落库代码部分我会按照模块拆分每个模块之间通过自定义函数衔接。整个结构是配置模块 → 驱动初始化 → 商品列表页解析 → 详情页抓取与字段清洗 → 断点续抓与去重 → 主流程串联。4.1 配置模块# config.py import os # 目标关键词列表可以按需修改 KEYWORDS [ 机械键盘, 无线鼠标, 显示器, ] # 每个关键词最多抓取的商品数 MAX_PRODUCTS_PER_KEYWORD 50 # 结果保存文件 OUTPUT_FILE jd_products.csv # 代理池相关配置 PROXY_API_URL http://your_proxy_api/ip_list # 换成你自己的代理API地址 PROXY_VALIDATE_TIMEOUT 5 MAX_IP_WAIT_TIME 10 # Chrome配置 CHROME_DRIVER_PATH ./chromedriver USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36 # 重试配置 MAX_RETRY_TIMES 3 RETRY_BACKOFF_BASE 3 # 退避基础时间第一次等待3秒第二次6秒第三次12秒4.2 商品列表页解析京东的搜索结果页结构相对稳定。我使用 XPath 进行定位实测下来比 CSS Selector 在京东这种复杂层级页面上更抗变化。# jd_spider.py from lxml import html def parse_list_page(driver, keyword): 解析搜索结果列表页返回商品ID列表。 url fhttps://search.jd.com/Search?keyword{keyword}encutf-8 driver.get(url) # 等待搜索结果加载完成 WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.XPATH, //ul[classgl-warp clearfix])) ) tree html.fromstring(driver.page_source) product_links tree.xpath(//li[contains(class, gl-item)]//div[contains(class, p-img)]/a/href) product_ids [] for link in product_links: if item.jd.com in link: # 提取商品ID格式: https://item.jd.com/100123456.html pid link.split(/)[-1].replace(item.jd.com, ).replace(.html, ).strip(/) if pid.isdigit(): product_ids.append(pid) return list(set(product_ids))[:MAX_PRODUCTS_PER_KEYWORD]这里我等待的 XPath 是搜索结果的外层容器//ul[classgl-warp clearfix]。京东搜索页的列表容器在多数情况下是这个 class。不同的关键词页面的广告位数量不同搜索结果可能分页但锚点容器是稳定的。4.3 详情页抓取与字段清洗详情页是每个商品单独一个 URL。这一步信息密度高也是反爬检测最严格的地方。我要抓的字段包括商品名称、价格、店铺名称、评价数、品牌、分类等。价格这里有个坑京东详情页的价格部分不是纯静态 HTML而是通过 JS 异步请求接口拿到的。如果直接解析driver.page_source里的价格节点很大概率拿到的是空字符串或者一串 ¥0.00。我的做法是解析页面的静态信息用 lxml然后在 Selenium 上执行一段 JS 去请求价格接口再把结果合并。def parse_detail_page(driver, pid): 解析商品详情页返回结构化字段。 url fhttps://item.jd.com/{pid}.html driver.get(url) WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.XPATH, //div[classitemInfo-wrap])) ) tree html.fromstring(driver.page_source) # 提取商品名称 name_node tree.xpath(//div[contains(class, sku-name)]/text()) name name_node[0].strip() if name_node else None # 提取店铺名称 shop_node tree.xpath(//div[contains(class, J-hove-wrap)]//a[classname]/text()) shop_name shop_node[0].strip() if shop_node else None # 提取评价数 comment_node tree.xpath(//div[idcomment-count]/a/text()) comment_count comment_node[0].strip() if comment_node else 0 # 提取品牌 brand_node tree.xpath(//ul[contains(class, parameter2)]//li[contains(text(), 品牌)]/a/text()) brand brand_node[0].strip() if brand_node else None # 获取商品编号(通过JS) sku_id driver.execute_script(return skuId;) or pid # 价格通过JS接口获取 price get_price_by_js(driver, pid) return { pid: sku_id, name: name, price: price, shop_name: shop_name, comment_count: comment_count, brand: brand, url: url, } def get_price_by_js(driver, pid): 通过执行JS发起内部价格接口请求。 js_code var callback arguments[arguments.length - 1]; fetch(/price/v2/prices/skuId%s, { method: GET, credentials: include }).then(res res.json()).then(data { callback(data); }).catch(err { callback(err.toString()); }); % pid result driver.execute_async_script(js_code) if isinstance(result, dict): # 京东价格接口返回的数据{p: 199.00} return result.get(p) or result.get(op) or 0 return 0这里关于execute_async_script多说一句。Selenium 默认的execute_script执行 JS 时会阻塞等待 JS 同步代码完成但 fetch 是异步的同步方法拿不到结果。所以要用execute_async_script并且 JS 代码里要调用最后一个callback参数来把结果传回。有个有意思的地方京东详情页里的价格节点虽然不渲染但页面源码里已经包含了价格接口的 URL而且这个接口域名和当前页面同域所以用fetch请求不需要额外处理 CORScredentials: include带上当前域名的 Cookie 就能正常返回价格。4.4 断点续抓与去重跑到一半程序崩溃是最烦人的尤其是已经抓了几百个商品数据之后。我在项目里加了断点续抓功能import os import json def load_downloaded_products(): 从CSV中加载已抓取的商品ID集合 if not os.path.exists(OUTPUT_FILE): return set() import pandas as pd try: df pd.read_csv(OUTPUT_FILE, usecols[pid]) return set(df[pid].astype(str)) except Exception: return set() def save_product_data(product): 保存单条商品数据追加写入CSV import pandas as pd df pd.DataFrame([product]) if not os.path.exists(OUTPUT_FILE): df.to_csv(OUTPUT_FILE, indexFalse, encodingutf-8-sig) else: df.to_csv(OUTPUT_FILE, modea, headerFalse, indexFalse, encodingutf-8-sig)去重的核心思路是每次从列表页拿到商品 ID 后先检查是否在已抓取集合中如果在就跳过。这样即使中断了下次重新跑也能从上次未完成的位置继续。4.5 主流程串联主流程的逻辑如下初始化代理池。为每个关键词创建搜索任务。每次请求前从代理池取一个可用 IP。加载页面解析数据保存。遇到验证码或异常时切换代理、重试。# main.py import time from jd_spider import create_driver, parse_list_page, parse_detail_page from proxy_pool import ProxyPool def main(): # 初始化代理池 proxy_pool ProxyPool() proxy_pool.load_proxies() # 加载已抓取的商品ID downloaded load_downloaded_products() results [] for keyword in KEYWORDS: print(f正在抓取关键词: {keyword}) # 获取代理IP proxy proxy_pool.get_available_proxy() if not proxy: print(代理池告警可用代理不足等待补充...) proxy_pool.wait_for_proxies() proxy proxy_pool.get_available_proxy() # 创建带代理的浏览器实例 driver create_driver(proxy) try: product_ids parse_list_page(driver, keyword) print(f关键词 {keyword} 共找到 {len(product_ids)} 个商品) for pid in product_ids: if pid in downloaded: continue # 每次访问前随机延时 time.sleep(get_random_delay()) try: product parse_detail_page(driver, pid) if product[name]: save_product_data(product) downloaded.add(pid) results.append(product) print(f[成功] {pid} {product[name]}) else: print(f[跳过] {pid} 商品信息为空) except Exception as e: print(f[失败] {pid} 错误: {e}) # 出现异常时切换代理 driver.quit() proxy proxy_pool.get_available_proxy() driver create_driver(proxy) finally: driver.quit() print(f抓取完成共获得 {len(results)} 条有效数据) return results if __name__ __main__: main()这个主流程设计里有一个关键决策每个关键词用一个独立的浏览器实例。这样做的一个隐性好处是如果某个关键词触发了风控只会影响当前这个浏览器其他关键词的数据已经安全落库了不至于一锅端。5. 实战中反复踩到的坑验证码、超时和风控信号这一部分如果没有实测你根本不知道有多烦。我把这几个问题单独拎出来讲每一个都是我在实际运行中花过真金白银时间、代理费才搞明白的。5.1 滑块验证码出现后的正确处理姿势前面提到了滑块验证码是京东最常用的验证方式。当 Selenium 遇到滑块验证码时最常见的错误做法是赶紧截图、找滑块位置、用代码模拟拖拽。我告诉你新手阶段我确实这么干过但成功率不到 20%。为什么因为京东的滑块拖拽不仅要从 A 点到 B 点还要检测你的拖拽轨迹。我后来干脆改成一种更省事的方案检测到验证码出现什么都不做直接换 IP重新启动浏览器。在代理池充足的情况下换 IP 比解决滑块更快更省心。怎么检测验证码出现我是在显式等待之外加了一个兜底判断def is_login_required(driver): 检测页面是否跳转到了验证/登录页面 current_url driver.current_url if passport.jd.com in current_url or verify in current_url: return True try: # 验证页面的特征元素 driver.find_element(By.XPATH, //div[classJDJRV-slide-inner]) return True except Exception: return False如果检测到是验证页面说明当前 IP 已经搞脏了换一个 IP 重新访问。5.2 超时重试的退避炸设计网络请求超时在爬虫项目中太常见了。但如果你在超时后立即重试连续三次都超时只能说明当前代理节点本身就不稳定换再多 IP 也没用。我用的退避策略是第一次超时等 3 秒重试第二次等 6 秒第三次等 12 秒。三次都失败就果断换代理。这个策略看起来简单但能有效地把节点不稳定和被目标网站限流两种情况分开——如果是后者退避等待和换 IP 也都是通用解法。def robust_get(driver, url, max_retriesMAX_RETRY_TIMES): 带有退避策略的页面访问 for attempt in range(max_retries): try: driver.get(url) # 验证页面是否加载成功 if not is_login_required(driver): return True except Exception as e: print(f第{attempt1}次访问失败: {e}) if attempt max_retries - 1: wait_time RETRY_BACKOFF_BASE * (2 ** attempt) print(f等待 {wait_time} 秒后重试...) time.sleep(wait_time) return False5.3 什么样的运行节奏最不容易触发风控这里告诉你我总结出的经验数据供你参考单 IP 维度京东对一个 IP 的访问风险在请求次数超过 30 次/5 分钟后显著上升。我控制在每个 IP 最多访问 10~15 个商品详情页后就更换。单浏览器维度同一个浏览器实例访问 30 个页面左右页面加载速度会开始变慢说明运行时 Cookie 里已经有疑似标记。我不会冒险用满 30 个页面大概 20 个就重新启动。整体抓取量整个项目单轮抓取建议在 500 个商品以内超过这个量级需要的 IP 数量和稳定性会指数级上升不如分段多次运行。5.4 数据合规提醒最后说一点不是技术问题、但比技术更重要的事。爬虫抓取的数据如果用于个人学习、技术研究、价格对比展示等场景风险相对可控但如果用于商业用途比如做比价网站、价格监控系统需要你仔细考虑数据的合法使用边界。我的原则是严格遵守网站的 robots 协议控制抓取频率不抓取用户个人信息只抓取商品公开信息并且不把抓取到的数据做大规模转售。养成这个习惯不管拿来练手还是以后做项目都能少很多麻烦。6. 从这套方案延展出去换个目标平台思路依然适用项目里写完这套 Selenium 代理池的方案后你可能会发现这套东西不只能爬京东。我在后续的多个爬虫需求中换汤不换药地复用了这套结构头条的文章列表、懂车帝的二手车信息、得物的商品数据核心逻辑都是一样的——用真实浏览器外壳解决 JS 渲染和设备指纹问题用代理池解决 IP 维度的频率限制用随机延时和去重机制让整个采集过程可控。如果你想把这个项目改造成其他平台唯一需要重写的是解析层XPath/CSS 选择器和接口层哪些字段来自渲染页面、哪些来自异步接口从驱动初始化到代理调度再到数据落库的整个管道基本可以原封不动地平移过去。这也是为什么我花了不少篇幅讲原理而不是只给代码——理解了每一行配置在对抗哪一层反爬机制换一个目标平台的时候你就不是在照抄代码而是在复用方案。最后再分享一个小技巧在项目里加个简单的日志模块每次切换代理、触发验证码、抓取成功或失败都记录一行。跑完一轮之后你会收获一份非常清晰的IP 质量表和页面加载时长表下轮运行之前把表现差的 IP 和慢速的关键词挑出来提前清理掉整个爬虫的运行效率能提升一个台阶。