拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Selenium+代理IP绕过京东反爬:商品数据采集实战方案

Selenium+代理IP绕过京东反爬:商品数据采集实战方案

相信不少做过电商数据采集的朋友都遇到过这种情况:昨天还能正常跑的爬虫脚本,今天突然返回一堆乱码或者直接弹出滑块验证。京东在这方面算是国内电商平台里反爬升级比较勤快的了,尤其是针对Selenium这类自动化工具的检测,从早期的简单UA校验,到后来的WebDriver特征识别,再到现在的行为轨迹分析和TLS指纹校验,基本一年一个大版本。

我前阵子刚完成一轮京东商品数据的采集脚本重构,整个过程踩了不少坑,也总结出一套目前还算稳定的方案。这篇文章就把完整思路和代码分享出来,重点讲怎么用Selenium配合代理IP绕过京东最新的反爬机制,希望能给正在做同类项目的朋友一些参考。

先说清楚,这套方案主要用于京东商品价格、标题、评论数等公开数据的采集,采集频率控制在较低水平,仅用于个人学习和研究。高频采集、商业用途不在讨论范围内,也容易触发更严厉的反制措施。

1. 京东反爬机制的核心逻辑与破解思路

1.1 京东目前的反爬体系到底在检测什么

很多人以为京东的反爬就是看访问频率,IP访问太快就封,其实远没那么简单。我通过反复测试和抓包分析,京东现在的反爬体系至少在四个维度同时做校验:

第一层是基础请求特征检测。包括User-Agent、Accept-Language、Connection头、Cookie完整性等。这部分通过requests库模拟就能过,但它是第一道门槛,过不了后面全白搭。京东对UA的校验比较严格,缺失UA或者UA里带着无头浏览器特征的请求,会直接在网关层被拦掉。

第二层是浏览器环境指纹检测。这是针对Selenium等自动化工具的核心检测层。京东前端会注入一段JS脚本,检测window.navigator.webdriver属性是否为true,检测window.chrome对象是否存在,检测navigator.plugins数组长度,甚至通过Canvas指纹、WebGL渲染信息来判断当前浏览器是否真实。Selenium原生启动的Chrome浏览器,这些特征都和有头用户操作的真实浏览器有明显差异。

第三层是行为轨迹分析。检测鼠标移动轨迹、滚动行为、点击间隔、页面停留时间是否符合人类操作习惯。机器模拟的鼠标轨迹通常是从A点直线移动到B点,速度恒定,没有加速减速过程,而且点击间隔高度规律。京东会把行为数据打包上传到风控服务器做离线分析,所以有时候当时没事,过几天才被限制。

第四层是IP与账号信誉度评估。数据中心IP、机房IP、共享代理IP的IP段评分通常比较低,一旦同一IP段在短时间内发起大量请求,会触发频率限制和滑块验证。这个维度没法完全规避,只能靠代理IP轮换来降低单IP的压力。

1.2 为什么纯requests方案越来越难走通

过去采集京东数据,用requests模拟HTTP请求,带上Cookie和UA基本就能拿到数据。但现在京东的很多商品详情页数据是通过异步接口动态加载的,关键数据接口加了签名参数(_pdr、_pvid、fingerprint等),这些参数由前端JS动态生成,依赖浏览器环境计算,反向分析成本极高。

另外,京东H5端的接口返回数据经常带上脱敏后的价格,真实价格需要登录后才可见。手机端的部分接口还需要配合设备的唯一标识符。这些限制让纯HTTP方案的开发和维护成本变得非常高,而Selenium这种模拟真实浏览器操作的方式,至少在环境指纹这一层能过掉大部分检测。

1.3 我的整体技术方案选型

经过对比测试,我最终选定的方案是:Selenium 4.x + Chrome浏览器的有头模式(Headless在京东的场景下几乎必被识别)+ 代理IP池 + 行为轨迹模拟 + 合理限速。

这里特意说明一下为什么不用Headless模式。Selenium的Headless模式在Chrome 112版本之后虽然和正常浏览器共用同一个二进制,但navigator.webdriver检测、window.chrome检测这些特征依然和真实浏览器有差异。实测下来,京东只要发现WebDriver特征,会直接拒绝加载商品详情数据,返回一个空壳页面。用有头模式加最小化窗口,配合代理IP,是目前性价比最高的方案。

2. 环境准备与基础配置

2.1 开发环境与依赖库安装

我的环境配置如下,大家可以根据自己的系统做调整:

  • Python 3.9及以上版本
  • Chrome浏览器(保持最新版,我用的是122.x)
  • ChromeDriver(版本必须和Chrome匹配)
  • Selenium 4.x(不建议用3.x,API差别较大)
  • requests库(用于代理API调用)

安装Selenium:

pip install selenium requests

ChromeDriver的下载和安装这里不多说,重点提醒一个坑:Chrome浏览器的自动更新机制可能导致Driver版本不匹配。我建议在代码里做版本检查,或者直接用Selenium Manager(Selenium 4.6以上版本内置),它会自动匹配并下载对应版本的ChromeDriver,省去手动维护的麻烦。

2.2 代理IP的选择与接口对接

代理IP这块是重头戏,也是决定采集成功率的关键因素。我测试了几种类型的代理:

数据中心代理(IDC代理):速度快,价格便宜,但IP段在风控系统中的评分普遍偏低,容易被识别。京东对机房IP的检测比较敏感,经常出现请求能发出去、页面也返回了,但敏感数据被替换成加密串的情况。建议是作为备选方案,不要作为主力。

住宅代理(Residential Proxy):带宽真实、IP段干净,检测难度高,但价格也高。如果一个项目需要长期跑数据,且对成功率要求很高,建议用住宅代理。网络规模主流的有国外代理ip服务商如Luminati、Bright Data等,也有国内的一些厂商,稳定性差别不算太大,主要看IP池规模和响应速度。

动态短效代理(动态IP代理):每次请求分配一个新的IP,IP只有几分钟甚至几十秒的有效期。这类代理做数据采集最友好,不用自己做IP池管理,缺点是访问速度不稳定,需要做重试机制来兜底。

我最终用的是动态短效代理,通过一个HTTP API拉取IP,间隔30秒拉一次,每次拿到5个IP轮换使用。这样既保证了IP的多样性,又降低了单IP的请求压力。

核心的代理获取和切换逻辑:

import requests import itertools class ProxyManager: def __init__(self, api_url, interval=30): self.api_url = api_url self.interval = interval self.proxy_list = [] self.current_index = 0 def fetch_proxies(self): resp = requests.get(self.api_url, timeout=5) if resp.status_code == 200: data = resp.json() self.proxy_list = data.get("data", []) self.current_index = 0 def get_next_proxy(self): if not self.proxy_list: self.fetch_proxies() if not self.proxy_list: return None proxy = self.proxy_list[self.current_index % len(self.proxy_list)] self.current_index += 1 return proxy

3. 核心代码实现与反检测配置

3.1 Selenium启动参数的关键配置

Selenium启动浏览器的参数设置,直接决定了能否通过京东的环境指纹检测。我总结了几个关键参数的配置逻辑:

from selenium import webdriver from selenium.webdriver.chrome.options import Options def create_driver(proxy): options = Options() # 基本配置 options.add_argument('--window-size=1920,1080') options.add_argument('--disable-blink-features=AutomationControlled') options.add_argument('--disable-infobars') options.add_argument('--lang=zh-CN') # 代理配置 if proxy: options.add_argument(f'--proxy-server={proxy}') # 关闭GPU加速,减少资源占用 options.add_argument('--disable-gpu') # 使用真实浏览器User-Agent options.add_argument('--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36') # 排除自动化控制标志 options.add_experimental_option('excludeSwitches', ['enable-automation']) # 禁用自动化扩展 options.add_experimental_option('useAutomationExtension', False) # 设置偏好参数 prefs = { 'profile.default_content_setting_values.notifications': 2, 'credentials_enable_service': False, 'profile.password_manager_enabled': False } options.add_experimental_option('prefs', prefs) driver = webdriver.Chrome(options=options) # 隐藏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'] }); ''' }) return driver

这段代码里最核心的有两个部分。

第一个是options.add_argument('--disable-blink-features=AutomationControlled'),这个参数能去掉Chrome的自动化控制标志,让navigator.webdriver属性不再是默认的true。当然,现在京东的检测脚本没那么傻,光靠这一个参数已经不够了,还需要配合下面这段CDP命令。

第二个是driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', ...),这段会在每次页面加载之前注入JS脚本,从底层navigator对象里覆盖掉webdriver、plugins、languages这些自动化痕迹。实测下来,这套组合能让Selenium模拟的浏览器在环境指纹上和真实Chrome几乎一致。

3.2 绕过滑块验证与行为模拟

即使环境指纹做得再干净,如果操作行为像机器,一样会被拦。京东的滑块验证出现过几次版本迭代,目前我遇到的主要是两种:一种是拖动式滑块,需要将滑块拖到指定位置;一种是点选式验证,需要在图中找出目标物体并点击。点选式验证通过Selenium自动处理的难度极高,目前没有稳定方案,所以尽量在行为模拟上做足功夫,避免触发验证。

对于拖动式滑块,我试过纯坐标拖动,也试过模拟人类拖拽轨迹的算法,分享一个相对有效的方案:

import random import time def human_like_drag(driver, slider_element, target_x): """模拟人类拖拽滑块""" from selenium.webdriver.common.action_chains import ActionChains # 生成带随机抖动的移动轨迹 steps = random.randint(15, 25) current_x = 0 action = ActionChains(driver) action.click_and_hold(slider_element) for i in range(steps): # 每步移动距离不固定,带随机加速度 if i < steps * 0.3: # 先快速接近目标位置 step_x = random.uniform(target_x / steps * 1.5, target_x / steps * 2) elif i < steps * 0.7: # 中段放慢 step_x = random.uniform(target_x / steps * 0.5, target_x / steps) else: # 末段微调 step_x = random.uniform(target_x / steps * 0.1, target_x / steps * 0.5) # 加上随机的上下偏移 step_y = random.uniform(-2, 2) action.move_by_offset(min(step_x, target_x - current_x), step_y) current_x += step_x action.pause(random.uniform(0.01, 0.05)) # 在目标位置略微停顿后释放 action.pause(random.uniform(0.1, 0.3)) action.release() action.perform()

这个轨迹生成的核心思路是:先快速靠近目标,再放慢进入,最后微调,整体曲线符合人类肌肉控制的特点。纯粹的匀速直线拖动会被行为分析模型一眼看穿。

不过我这里有个实际经验:滑块验证的完整破解周期很长,不同账号、不同IP、不同时间段的滑块形态都有差异。如果你只需要采集商品标题、图片、价格等基础数据,其实可以考虑绕开滑块,用不触发验证的方式获取数据。比如通过手机端H5页面,或者直接调用无登录态的数据接口,验证概率会低很多。

3.3 商品详情页数据提取实现

拿到页面之后,提取数据的逻辑相对简单,但需要注意页面结构的兼容性。京东的商品页前几年改版过几次,字段位置有所变动,建议用多选择器兜底的方式提取:

from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC def parse_product_info(driver, url): driver.get(url) # 等待页面核心元素加载完成 WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, "div.itemInfo-wrap")) ) # 随机延迟,模拟人类阅读时间 time.sleep(random.uniform(2, 5)) product = {} # 标题提取 try: title_el = driver.find_element(By.CSS_SELECTOR, "div.itemInfo-wrap .sku-name") product["title"] = title_el.text.strip() except Exception: product["title"] = "" # 商品编号 try: sku_id = url.split("/")[-1].replace(".html", "") product["sku_id"] = sku_id except Exception: product["sku_id"] = "" # 价格 - 最多重试3次 price = None for _ in range(3): try: price_el = driver.find_element(By.CSS_SELECTOR, "span.priceJd") price = price_el.get_attribute("data-price") if not price: price = price_el.text.strip().replace("¥", "") if price: break except Exception: time.sleep(random.uniform(1, 2)) product["price"] = price return product

这里需要补充说明的是,京东商品价格在不同状态下的展示逻辑有差异。未登录状态下,部分商品价格会被加密或显示为促销区间;登录状态下,不同账号可能看到不同的促销价格。如果你采集的价格需要精确到某个级别,尽量在登录状态下操作,或者结合多个价格源交叉验证。

3.4 完整采集主流程

把上面的模块串起来,完整的采集流程如下:

import random import time import pandas as pd def crawl_jd_product(keyword, max_pages=3): proxy_manager = ProxyManager("http://你的代理API地址") proxy_manager.fetch_proxies() # 构建搜索URL base_url = f"https://search.jd.com/Search?keyword={keyword}&enc=utf-8" results = [] for page in range(1, max_pages + 1): # 每隔一段时间换代理 proxy = proxy_manager.get_next_proxy() if not proxy: print("获取代理失败,等待重试") time.sleep(5) continue driver = None try: driver = create_driver(proxy['http']) if page == 1: url = base_url else: url = f"{base_url}&page={page}" driver.get(url) time.sleep(random.uniform(3, 6)) # 滚动页面,模拟人类浏览行为 for _ in range(random.randint(3, 6)): driver.execute_script(f"window.scrollBy(0, {random.randint(300, 800)});") time.sleep(random.uniform(0.5, 1.5)) # 提取商品列表 items = driver.find_elements(By.CSS_SELECTOR, "li.gl-item") for item in items: try: link = item.find_element(By.CSS_SELECTOR, "div.p-name a") product_url = link.get_attribute("href") title = link.text.strip() price_el = item.find_element(By.CSS_SELECTOR, "div.p-price i") price = price_el.text.strip() results.append({ "title": title, "price": price, "url": product_url }) except Exception as e: continue print(f"第{page}页采集完成,获取{len(items)}条商品,累计{len(results)}条数据") except Exception as e: print(f"第{page}页采集失败: {e}") finally: if driver: driver.quit() # 页面间随机延时,控制请求频率 time.sleep(random.uniform(8, 15)) return pd.DataFrame(results) if __name__ == "__main__": df = crawl_jd_product("机械键盘", max_pages=3) df.to_csv("jd_keyboard_products.csv", index=False, encoding="utf-8-sig") print(f"采集完成,共保存{len(df)}条数据")

搜索列表页的采集逻辑相对稳定,核心的风险点在于翻页之后的IP频率限制。我每隔8到15秒才翻一次页,每个IP最多跑两到三页,两天实测下来,基本不会触发滑块验证。

4. 常见问题与排查技巧实录

4.1 页面返回空壳数据,商品信息无法提取

这是一个高频问题。表现是页面能加载,浏览器的地址栏能看到商品标题,但程序里提取不到任何数据,页面上大量关键DOM节点被替换成空节点。

排查步骤:

第一,确认是不是被京东风控识别了。打开页面前先手动用同样的IP和浏览器访问一次,看看能否正常显示数据。如果手动访问也异常,说明IP或者浏览器指纹已经被标记,换个代理即可。

第二,检查Cookie状态。部分商品信息在无登录态下会降级展示,尤其是价格数据。此时可以清空Cookie后重新访问,或者优化登录流程。

第三,检查页面是否有验证码。如果返回的HTML中包含nc_wrapper相关的DOM节点,说明触发了滑块验证或验证码弹窗。

4.2 Selenium启动后WebDriver属性无法隐藏

这个问题的原因是ChromeDriver版本和注入脚本时机不一致。Page.addScriptToEvaluateOnNewDocument要求在页面加载前注入,如果注入时机晚于页面文档解析,navigator.webdriver就会被读取到原始值。

解决方式有两种:

第一种,把注入代码放在create_driver函数中,确保在driver.get()任何URL之前执行。第二种,升级到Selenium 4.6以上版本,配合最新ChromeDriver,稳定性会好很多。

4.3 代理IP频繁失效导致请求超时

动态短效代理的稳定性取决于服务商。我遇到过代理列表中部分IP端口无法连接的情况,需要增加代理验证和自动重试机制:

def get_valid_proxy(proxy_manager, timeout=10): """获取一个可用代理,失败自动换下一个""" for _ in range(5): proxy = proxy_manager.get_next_proxy() if not proxy: return None proxy_url = f"{proxy['http']}" try: test_resp = requests.get("https://www.jd.com", proxies={"http": proxy_url, "https": proxy_url}, timeout=timeout) if test_resp.status_code == 200: return proxy except Exception: continue return None

这层验证逻辑会增加一点延时,但能显著减少因为代理无效导致的浏览器启动又退出。

4.4 采集一段时间后触发滑块验证

如果采集过程中突然出现滑块验证,处理方式有两种:

第一种,立即暂停程序,等待滑块验证失效(一般5到15分钟会自动消失)。第二种,切换全新IP和全新浏览器实例重新启动。

我这里要特别说一句:即使加了代理和指纹隐藏,京东对长时间连续性采集行为的识别依然很精准。最佳策略还是控制采集频率,把每小时请求量压在一个相对安全的水平。毕竟做数据采集核心是细水长流,不是一次性全量拉取。

5. 优化与进阶方向

5.1 登录态的引入

登录后采集能获取到更准确的促销价格、优惠券信息,也能有效降低风控等级。但登录操作本身会引入更多变量,比如登录验证码、短信验证、设备绑定等。我的建议是,如果对价格精度要求不高,优先不登录;如果一定要登录,用独立的账号体系,并且单账号的并发请求量一定要控制住。

5.2 数据接口直连方案

Selenium方案虽然在指纹模拟上更彻底,但性能有限,每个页面的加载和解析至少需要5到10秒。如果采集规模更大,可以考虑中间人代理方案:用Selenium只做浏览器环境搭建,真实的数据请求通过requests库直连京东的异步接口完成。

具体做法是:用Selenium访问页面拿到完整Cookie,然后退出Selenium,用requests配合这些Cookie请求商品详情接口。这样能大幅提升采集速度,同时保留Cookie中的真实性验证信息。

5.3 分布式采集的架构思路

如果采集规模到了每天几十万条级别,单机Selenium方案就不太够用了。可以考虑用Redis做一个分布式任务队列,多台机器各自运行采集节点,任务队列里放商品链接,采集结果统一写入数据库。代理IP这块可以单独做一个代理池服务,统一管理IP的获取、验证、释放,这样多个节点之间的IP不会交叉使用,降低了同一个IP段被集中标记的风险。

这个架构的复杂度比我上面分享的代码高不少,但大的采集项目早晚会走到这一步,算是一个方向参考。

5.4 爬虫框架的替代思考

现在也有不少人转向使用Scrapling这类新一代爬虫框架,或者把Selenium仅用于获取Cookie和Token,页面解析交给lxml、parsel这些高效的解析库。Selenium在这个链条里的定位越来越像一个“环境模拟器”,而不是一个“数据抓取器”。这个思路和我的方案并不冲突,实际项目里甚至可以结合着用。先把采集链路跑通,后续再根据需求做性能优化。

5.5 成本与稳定性的平衡建议

最后聊一下长期运营的成本控制。代理IP是整个方案里每时每刻都在消耗的资源,选择代理时要重点关注几个指标:IP池的可用率(能用与提取比例)、切换速度、并发支持数。先买小额套餐测试1到2天,观察采集成功率和代理用量,再决定是否扩容。

另外,不要把鸡蛋放在一个篮子里。建议至少接入两家代理服务商的API做双保险,一家挂了可以自动切换到另一家,避免采集中断。

回到我个人的实际操作体会,京东的反爬体系一直在迭代,今天能用的隐蔽手段,过几个月可能就失效了。保持对浏览器环境和反爬策略的持续关注,比任何一套固定代码都重要。如果你正在规划这个方向的采集项目,建议先小批量跑通流程,再逐步放量,这样遇到风控升级时,损失和调整成本都是可控的。

返回列表