简介:这是一份演示如何用Selenium自动化抓取京东商品信息的入门级代码包,面向Python爬虫初学者或需要处理动态加载页面的开发者。压缩包体积仅2KB,包含1个Python脚本和1个txt数据文件;脚本主体覆盖WebDriver初始化、商品页面跳转、通过CSS选择器或XPath提取名称与价格、借助WebDriverWait显式等待动态元素出现,以及模拟点击翻页遍历多商品等关键环节。txt文件则用于保存或整理抓取结果,整体结构简洁,便于逐行对照学习。资源虽然精简,但将Selenium最常用操作浓缩在一个示例中,同时附带延时控制、随机User-Agent等反反爬思路,可帮助读者理解浏览器自动化与网页数据采集的实际配合方式。当前已有245人学习下载,适合用来熟悉基础爬虫流程,并可作为后续扩展到多线程、代理IP或数据清洗项目的参考起点。
1. 用 Selenium 抓京东商品,为什么这个方案到现在还有人用
做竞品比价、选品分析或者品牌舆情监控的人,早晚都会遇到同一个问题:拿不到商品数据。京东的反爬不像某些小站点那样形同虚设,它的动态渲染、异步加载和风控策略都在升级,但它又不像有些平台那样完全封死自动化。于是 Selenium 这种笨办法反而成了绕不开的兜底方案——不依赖任何私有 API,不破解任何签名,就是开一个真实浏览器窗口按部就班地操作,拿到的是完全真实的数据流。
这套方案适合两类人:一是短期抓几十页、几百个商品做分析,不值得引入重型分布式爬虫框架的团队;二是被接口反爬逼到墙角,只有 Selenium 还能稳定出数的场景。它的核心价值就一句话:用大体量浏览器的仿真行为,换取可控的反爬代价。下面我把从环境准备到字段清洗的完整路径拆开讲。
2. 环境准备与驱动匹配:这里浪费的时间比写爬虫本身还多
2.1 浏览器版本和 Driver 版本对不上,Selenium 直接给你脸色看
先说一个反直觉的事实:Selenium 本身只是个协议层,真正干活的是浏览器驱动。装 Selenium 只需要一条命令,但驱动和浏览器版本不匹配会让你在启动阶段就翻车,而且报错信息特别有迷惑性,常见的是session not created或者unknown error: DevToolsActivePort file doesn't exist。我见过太多人把时间浪费在重装 Selenium 上,实际上问题出在 Chrome 和 chromedriver 的版本大版本号不一致。
我是这么处理的:先打开 Chrome 的「关于」页面看版本号,再去对应版本的驱动下载页拿同版本的驱动。这里有个小技巧,驱动下载页的文件名里会带版本号,比如chromedriver_mac_arm64这种,别下错了平台。如果你用的是 Linux 服务器,还得注意是 64 位还是 32 位,现在基本全是 64 位了。
# 建议用 webdriver-manager 自动匹配,别再手动下驱动了 from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager options = webdriver.ChromeOptions() options.add_argument("--headless=new") # 新无头模式,反检测能力比旧版强 options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") # root 用户跑 CI 必须加 # 禁用自动化控制标志,降低被识别为 Selenium 的概率 options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=options) driver.get("https://www.jd.com") print(driver.title)这段代码的核心是ChromeDriverManager().install(),它会自动读取你本机 Chrome 的版本号,然后下载对应版本的驱动,从根上消除版本不匹配问题。--headless=new是新版 Chrome 109+ 才有的无头模式,用它不是因为快,而是因为它的指纹特征和真实浏览器更接近,被检测的概率比老无头模式低。excludeSwitches和useAutomationExtension这两个参数是去掉navigator.webdriver标记的第一道防线,后面还会讲到别的。
注意:
--headless=new的兼容性需要 Chrome 109 以上,如果你还在用老版本,去掉这个参数直接用--headless,但要做好被反爬识别的心理准备。
2.2 京东页面加载机制:Selenium 等的是什么,你不等就白等
京东的搜索页不是一打开就有数据的。页面先加载骨架结构,然后通过 JavaScript 异步请求商品数据,最后才渲染到 DOM 上。这里有个关键点:你用driver.get()返回时,页面大概率只加载了一部分,商品列表可能还是空的。如果你立刻去定位元素,会拿到空节点。
所以 Selenium 方案里最重要的一步是显式等待。别用time.sleep(5)这种固定等待,网络慢的时候 5 秒不够,网络快的时候纯浪费 3 秒。正确做法是用WebDriverWait加条件,等到目标元素出现再动手。
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By url = "https://search.jd.com/Search?keyword=机械键盘&enc=utf-8&wq=机械键盘" driver.get(url) # 等待商品列表容器出现,超时 20 秒 wait = WebDriverWait(driver, 20) cards = wait.until( EC.presence_of_all_elements_located((By.CSS_SELECTOR, "#J_goodsList li.gl-item")) ) print(f"第一页商品数量: {len(cards)}")presence_of_all_elements_located等待的是 DOM 里出现了这些节点,但注意它不保证价格已经渲染完成——京东的商品价格经常比标题晚一步出现。所以后面解析字段时,对价格还要再单独等待一次,不能想当然地认为列表出现就万事大吉。这里的wait对象是复用的,后续每一步都用它来等,而不是每次新建,逻辑会更清晰。
3. 解析商品卡片与字段清洗:拿到原始数据只是第一步
3.1 CSS 选择器定位:京东前端改版后的稳定锚点
京东搜索页的商品卡片结构相对稳定,核心字段的锚点是多年没怎么动的:每个商品项在#J_goodsList下的li.gl-item里,标题在.p-name下的<em>标签,店铺名在.p-shop下的<a>标签。这些 selector 我用了一年多,京东前端改过几次版,这些锚点都没变过,算是比较靠谱的抓取目标。
但有几个细节需要注意。第一,li.gl-item里可能混入广告位,这些广告的 DOM 结构和普通商品一样,但会多一个>from selenium.webdriver.common.by import By import re def parse_card(card): """从单个商品卡片中提取标题、价格、店铺、链接""" # 标题在 .p-name em 里,去掉换行和多余空格 title_node = card.find_element(By.CSS_SELECTOR, ".p-name em") title = re.sub(r"\s+", "", title_node.text).strip() # 价格可能在 .p-price strong i 里,也可能是异步填充,给 3 秒缓冲 try: price_node = card.find_element(By.CSS_SELECTOR, ".p-price strong i") price = float(price_node.text) except ValueError: price = None # 价格还没渲染出来,标记为缺失 # 店铺名可能为空,对应京东自营 shop_node = card.find_elements(By.CSS_SELECTOR, ".p-shop a") shop = shop_node[0].text if shop_node else "京东自营" # 商品链接在跳转链接的 href 里,用它反查 sku link_node = card.find_element(By.CSS_SELECTOR, ".p-img a") link = link_node.get_attribute("href") sku_match = re.search(r"/(\d+)\.html", link) sku = sku_match.group(1) if sku_match else None return { "sku": sku, "title": title, "price": price, "shop": shop, "link": link, }
这里有个取舍问题:为什么用 CSS 选择器而不用 XPath?京东的 DOM 层级里 XPath 的路径经常变,CSS 选择器是直接按 class 命中,改动少一个层级就少一次维护成本。这个函数的输入是一张商品卡片元素,输出是结构化字典,后续不管是写 CSV 还是入 MySQL 都方便。把解析写成纯函数还有个好处——你可以在真实页面上拷一段卡片 HTML 下来,本地用 pytest 反复测,不用每次都跑一遍浏览器。
3.2 翻页的四个坑:页码从 2 开始、s 参数、点击还是跳转、请求频率
京东搜索页的翻页逻辑藏了几个坑。第一,URL 里的 page 参数不是从 1 开始的,第 1 页不传 page,第 2 页是page=2,第 3 页是page=3,以此类推。第二,有些页面会带s参数和click参数,这俩是京东用于追踪搜索行为的,直接构造 URL 不要求必须带。第三,你直接通过driver.get()跳转到page=5,页面可能实际渲染的还是第 1 页内容——因为数据是异步加载的,URL 变了但内容还没来得及请求。这时候最稳的办法不是跳转,而是用 Selenium 在页面底部点击「下一页」按钮。
def next_page(driver, wait): """点击翻页按钮,返回 True 表示翻页成功,False 表示没有下一页""" try: # 京东的翻页按钮在 .page-nav 区域,注意总是一大排数字 next_btn = wait.until( EC.element_to_be_clickable( (By.CSS_SELECTOR, ".pn-next") ) ) next_btn.click() # 翻页后强制等待商品列表刷新,价格节点是重新创建的 wait.until( EC.presence_of_all_elements_located( (By.CSS_SELECTOR, "#J_goodsList li.gl-item") ) ) return True except Exception: return False点击翻页有个副作用:京东每翻几页会弹出一个登录框,这个框会打断你的点击流程。所以每次翻完页,我都要顺手检查一下有没有遮罩层,有就关掉。另外,翻页次数多了以后,会出现验证码跳转页面,这时候再点下一页就没反应了,需要单独处理。以后再讲验证码的问题,这里先记住:翻页本身不是问题,翻页触发风控才是问题。我一般在翻到第 5 页以后,在两次请求之间加一个随机等待 3 到 7 秒,打乱请求节奏。
3.3 数据落盘:CSV 和 Excel 的编码坑、字段边界、增量去重
抓到的数据最终要落盘。很多人的第一反应是存 Excel,但 Selenium 抓取通常是一次性跑几千条,用 openpyxl 写 Excel 又慢又容易崩。我的习惯是先用 CSV 做中间存储,后续有需要再转 Excel,这样数据出了任何问题都好排查。
写 CSV 时第一个坑就是编码。Excel 打开 UTF-8 编码的 CSV 文件,中文全是乱码。解决办法是写入时用utf-8-sig,也就是带 BOM 的 UTF-8,Excel 才能正确识别。第二个坑是换行符——商品标题里可能带着\n或者\r,直接写会把一行数据拆成两行。所以在写入前,所有字符串字段都要把\n和\r替换成空格。
import csv from pathlib import Path def save_to_csv(items, filepath): """增量追加写入 CSV,用 sku 做去重判断""" filepath = Path(filepath) exists = filepath.exists() with open(filepath, "a", encoding="utf-8-sig", newline="") as f: writer = csv.DictWriter(f, fieldnames=["sku", "title", "price", "shop", "link"]) if not exists: writer.writeheader() for item in items: # 字段清洗:去掉换行,避免破坏 CSV 结构 item["title"] = item["title"].replace("\n", "").replace("\r", "") writer.writerow(item)newline=""这个参数容易被忽略——不加的话,在 Windows 上写 CSV 会在每行后面多一个空行,读回来时每行都带着\r\r\n的残留。encoding="utf-8-sig"是给 Excel 打开用的,如果后续程序读取,用utf-8打开也没问题。去重逻辑上,增量写入时用sku做唯一键,每次抓完一批,先读已有的 CSV 收集 SKU 集合,新数据里sku已经被抓过就跳过,避免跑批任务时重复数据越堆越多。
4. 风控识别与反检测策略:为什么你的 Selenium 一抓就挂
4.1 京东靠什么认出 Selenium:WebDriver 标记和浏览器指纹
很多初学者不理解:明明我用 Selenium 打开浏览器,京东为什么像长了眼睛一样每次都弹验证码?答案在你的 JavaScript 环境里。Selenium 控制的 Chrome 会往全局挂一个navigator.webdriver属性,它的值是true。正常用户浏览器的这个值要么是undefined要么是false。网站的反爬脚本只要执行一行navigator.webdriver === true,就能判断你是机器人。
但这只是最浅的一层。更狠的检测手段是 CDP,也就是 Chrome DevTools Protocol。网站可以通过--remote-debugging-port参数检测到你开启了调试端口,而 Selenium 必须要这个端口才能工作,这就陷入了一个死循环。更别提一些专业的风控 SDK,比如设备指纹采集,它能从 Canvas 渲染、WebGL 图形、时区、字体列表里提取你的设备指纹。如果你在同一台服务器、同一个 IP 上跑了几百次 Selenium,指纹一致性反而成了最大的破绽。
# 启动前注入 stealth 脚本,掩盖自动化特征 import time from selenium.webdriver.common.by import By options.add_argument("--disable-blink-features=AutomationControlled") driver = webdriver.Chrome(service=service, options=options) # 在页面加载任何脚本前执行 stealth JS stealth_js = """ 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'] }); """ driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {"source": stealth_js}) driver.get("https://search.jd.com/Search?keyword=机械键盘&enc=utf-8")--disable-blink-features=AutomationControlled这个参数能去掉大部分自动化控制标记,Page.addScriptToEvaluateOnNewDocument是在页面打开前注入脚本,这样即使页面之后读取navigator.webdriver,拿到的也是被改写的结果。这套组合拳能应付大部分基于 JS 标记检测的反爬策略,但应付不了行为分析——如果你的请求频率每分钟超过 20 次,IP 地址、设备指纹、行为轨迹三个维度交叉验证,照样会被识别。
4.2 验证码的触发节奏:识别、等待、人工介入
我发现一个规律:京东的验证码不是你在第 1 次请求时就触发的,而是有个积累过程。刚开始几个请求完全正常,第 10 个、第 20 个、第 50 个,触发点没有固定规律。这个设计很聪明,它让你以为风控没生效,等你跑得正爽的时候一下子卡死,之前的所有抓取白费。
应对方式也是分层的。第一层是这个请求失败了,先别急着重试,停下来等 30 到 60 秒,换个 User-Agent,再请求一次。很多时候只是临时限流,不是永久封禁。第二层是如果页面跳到了验证码页面,Selenium 的当前 URL 会变成verify.jd.com这类地址,你需要在代码里做一个 URL 判断。发现跳到验证码页面就立刻停手,而不是傻乎乎地继续点翻页。
def check_verification(driver): """检查页面是否跳转到验证码或登录拦截页""" current = driver.current_url if "verify" in current or "passport" in current: print(f"触发风控,当前 URL: {current}") return True # 检测页面里是否出现滑块验证码的 iframe try: driver.find_element(By.ID, "JDJRV-wrap-login") return True except Exception: return False这里我不推荐自动化去破解滑块验证码,原因有两条。第一,京东的滑块验证码带有行为轨迹分析,程序生成的拖拽轨迹在加速度和抖动模式上和真人差距明显,硬要模拟反而更容易被标记;第二,这是一个军备竞赛的泥潭,京东更新一次验证码策略,你的破解代码就得重写一次。正确做法是把验证码识别当成一个运维事件:触发后停止抓取,发个通知让值班的人手动过一下,或者干脆让请求频率降到底,等风控冷却了再回来。
4.3 请求频率与 IP 池的现实问题:别指望免费方案能跑大规模
讲了这么多隐藏手段,最后必须泼一盆冷水:Selenium 这种方案天生就不是为大规模抓取准备的。每个浏览器进程要吃几百 MB 内存,一台 8G 内存的服务器最多同时跑 5 个实例;每个请求都要等完整页面加载,就算做无头模式,抓一页也要 5 到 10 秒;再加上反爬需要控制频率,一个小时能抓完几千条已经算高效了。
如果你有更高的量级需求,比如每天抓 10 万条,Selenium 就不合适了,应该考虑用 requests 直接调接口拿 JSON 数据。但京东的接口有签名参数,破解签名的成本也不低。所以现实的路径是:小规模、低频、短周期的抓取任务用 Selenium 完全够用;大规模持续抓取就应该老老实实考虑商业数据服务或者找官方合作渠道,而不是在 Selenium 上硬撑。这不是技术能力问题,是投入产出比的问题。
5. 常见问题排查:5 个把新手卡死的坑,我全踩过
5.1 超市里商品定位到空 list:不是等待不够,是页面根本没加载出来
现象:用find_elements(By.CSS_SELECTOR, "#J_goodsList li.gl-item")拿到的列表长度是 0,但你在浏览器开发者工具里明明能看到商品卡片。
原因:这个页面是异步渲染的。driver.get()返回不代表数据加载完成。我见过很多人在地址栏手动打开这个 URL,看到内容马上就写代码,结果代码运行时机比人眼慢一点点,商品列表还没来得及插入 DOM。
解决:改成显式等待,不要用隐式等待。driver.implicitly_wait(10)是给每一次查找操作加了一个兜底超时,但它不会主动等待「商品列表出现」这个条件。用WebDriverWait配合presence_of_all_elements_located才能精确等对时机。另外检查一下你的网络,如果公司网络访问京东本身就慢,把超时时间从 10 秒放宽到 20 秒。
5.2 价格字段是空字符串或 "¥" 开头,解析直接报 ValueError
现象:price_node.text返回的不是数字,而是空字符串,偶尔还有¥1.00这种像占位符的数据。
原因:京东的商品卡片里,标题、店铺、链接是首屏渲染的,价格经常是异步填充的。有些商品无货时显示的不是具体价格,而是「暂无报价」或者一个破折号。如果你用float(price_node.text)强转,遇到非数字字符就会崩。
解决:解析前先做一次文本清洗,把¥、逗号、空格全部去掉,再用正则提取数字部分。提取不到数字就置为None,同时做continue或标记缺失,不要让一个坏数据毁掉整批解析。这一步我这边的经验是:每 100 条里大概有 2 到 3 条价格异常,算是正常范围。
5.3 CSV 文件用 Excel 打开全乱码,换行还错位
现象:encoding="utf-8"写入的 CSV,Excel 打开后中文全是乱码;或者一条商品记录被拆成了三四行。
原因:Excel 默认用 ANSI 编码打开 CSV,UTF-8 的字节流被解释成乱码。换行错位是因为标题里有原始换行符\n,CSV 的引号包裹机制没有生效。
解决:写入编码改成utf-8-sig,同时newline=""防止 Windows 下出现空行。字符串字段在写入前清洗换行和回车。这个坑属于必踩项目,见过一次以后就会刻在脑子里。
5.4 翻页翻到一半就停了:控制台报错 ElementClickInterceptedException
现象:跑了 3 页正常,第 4 页开始点击「下一页」按钮抛异常ElementClickInterceptedException,意思是元素被挡住了。
原因:京东的页面里有悬浮的推广遮罩层、登录弹窗或者客服气泡窗口,它们覆盖在翻页按钮上方,Selenium 的点击被拦截。弹窗出现时机不确定,和你的抓取频率有一定相关性。
解决:点击翻页按钮前,先尝试关闭弹窗。常见的关闭按钮 selector 是.close和#closeTANWindow。关不掉的话,用 JavaScript 直接点击:driver.execute_script("arguments[0].click()", next_btn),绕过遮挡层。但这只能治标,遮罩层的存在本身就是一个信号——你的请求频率太快了,建议把翻页间隔从 2 秒拉大到 5 秒。
5.5 开了无头模式就触发验证码,有头模式反而没事
现象:同一套代码,--headless=new跑 20 页触发验证码,改成有头模式跑 50 页都正常。
原因:无头模式虽然有了新版本渲染引擎,但它的 TLS 指纹、Canvas 渲染结果和真实浏览器还是有差异。京东的风控系统对无头环境的评分比对有头环境高,尤其当你同时用了非浏览器默认的窗口尺寸时,这个差距更明显。
解决:如果目标数据量不大,直接用有头模式跑,挂在服务器上别管它就行,不想弹出的可以把窗口最小化。如果非要无头模式,把窗口尺寸设置成1920,1080这种常见分辨率,加上--lang=zh-CN等参数,把指纹特征拉回正常区间。但坦白说,无头模式想做到和有头模式一样安全,目前还没有 100% 的办法。
6. 进阶:把详情页字段补齐,用会话复用做可控的并发加速
基础列表页能跑了之后,你会发现商品标题、价格、店铺这些信息够做粗粒度分析,但做不了深度竞品分析——缺商品规格、缺评论数、缺促销信息。这些都得进详情页拿。我一般会用列表页的 SKU 拼出详情页 URL,然后复用同一个浏览器会话逐条访问。但详情页的加载更重,多开两个详情页就要小心被风控盯上。
我的习惯是控制并发粒度:不是开 10 个 Selenium 实例,而是 2 到 3 个实例,每个实例通过window.open新开标签页,一个标签页处理详情页,一个标签页保持列表页状态。请求完就立刻关闭标签页,保持整个会话的活跃度像真人浏览。实测 2 个实例、每个实例 1 个详情页标签,抓取速度大约是一小时 300 个详情页,睡眠间隔控制在 3 到 6 秒,连续跑 4 小时不会触发验证码。
一个值得注意的细节是,详情页里的促销信息、优惠券倒计时是动态节点,位置不固定,不要用固定 CSS 锚点去定位,而是用「包含文本」来匹配,比如//div[contains(text(),'促销')]/following-sibling::div这种相对路径。最后,落库前做一次完整性校验:每个商品必须同时存在 SKU、标题、价格三个核心字段,缺失任何一个就打上incomplete标记,让后续的数据分析流程提前知道这批数据质量不够硬。
我的习惯是每次抓完数据后,随机抽 5 条去页面手动核对一遍,确认标题、价格、店铺都没错位。这个习惯救过我很多次——有次京东改版把店铺和广告位互换位置,我的解析代码全跑偏了,要不是人工核对及时发现,整批数据就废了。做爬虫这件事,永远给自己的数据留一条人工核验的后路,不迷信代码输出。希望这些经验能帮你少走弯路。
本文还有配套的精品资源,点击获取