
简介以Selenium抓取京东商品信息为完整实战案例这份资源面向需要用动态页面爬虫完成数据采集的Python开发者尤其适合刚入门Web自动化、需要处理异步加载或模拟点击场景的学习者。压缩包内共含2个文件1个py爬虫脚本负责核心逻辑1个txt文件用于存放抓取结果整体仅2KB结构一目了然方便直接阅读和调试。脚本从ChromeDriver驱动配置开始逐步展示通过CSS选择器提取商品名称与价格、借助WebDriverWait等待动态元素出现再到分页遍历和访问延时设置等反爬细节覆盖了动态电商页面抓取过程中的主要技术环节。已有245人学习下载既可作为Selenium教程的配套练习也能为分析京东商品价格、评价等数据提供基础脚本使用时需留意目标站点的访问频率要求并注意遵守相关网站的抓取规则。1. 从商品详情页到结构化数据selenium爬取京东商品信息这个资源能解决什么拿到这份「selenium爬取京东商品信息.zip」时我第一反应是又一份贴代码的练习作业。解压之后发现里面除了 Python 脚本还带了一个 Java 版本参考外加一份已经抓好的 a.txt 数据文件。它的定位很清楚给做商品比价、竞品监控、电商数据分析的人提供一套能直接改改就能用的京东商品信息采集方案。京东的反爬严格程度在电商里是出了名的能用 Selenium 模拟真实浏览器硬啃下来说明作者踩过的坑基本都写在脚本里了。适合谁适合刚接触动态网页爬虫、不想从零写定位逻辑的从业者也适合需要快速验证「京东商品数据能不能稳定抓到」的选型阶段。Selenium 的笨重和稳定在这里反而成了优势——它不跟你玩接口签名按用户视角把页面渲染完整再取数。2. Selenium 浏览器驱动三件套先把 WebDriver 跑通再谈抓数据2.1 WebDriver 接口和真实浏览器之间发生了什么Selenium 爬虫和 requests 爬虫的最大差别在于requests 拿到的是 HTML 字符串而 Selenium 驱动的是一个真实浏览器进程。京东的商品详情页大量使用 JavaScript 异步渲染商品名称、价格、促销信息很多是在页面加载完成后由脚本填充的。你用 requests 拿到的源码里价格位置往往是个空标签。WebDriver 的做法是启动 Chrome 或 Firefox让页面完整走完渲染流程再像人一样去读取 DOM。这里要理解一个关键点WebDriver 本身不是爬虫框架它是浏览器自动化协议W3C WebDriver 规范的 Python 实现。你的脚本通过 HTTP 协议把指令发给浏览器驱动如 chromedriver驱动再把指令翻译成浏览器内核能执行的动作。这条链路里任何一个环节版本不匹配脚本就是废的。项目里 Python 版用的就是这条标准链路Java 版走的是同一套 WebDriver 协议只是语言绑定不同。2.2 chromedriver 版本不匹配是第一个坑最常见的翻车现场是这行代码driver webdriver.Chrome(path_to_chromedriver)报错信息通常是SessionNotCreatedException: This version of ChromeDriver only supports Chrome version xxx。这不是项目代码的问题是 chromedriver 和本机 Chrome 主版本号对不上。主版本号必须一致比如 Chrome 125 就必须配 chromedriver 125.x。下载驱动的地方是 Chrome for Testing 的官方镜像站按你的 Chrome 版本号选对应目录即可。下载后放在一个固定目录比如/usr/local/bin/或 Windows 下的D:\tools\然后在代码里写绝对路径不要依赖 PATH 环境变量——PATH 里可能有多个 chromedriver被旧版本抢先就莫名报错。2.3 最小验证脚本确认驱动链路可用再动页面先别急着写京东逻辑用一段最小脚本验证整条链路from selenium import webdriver from selenium.webdriver.chrome.options import Options # 配置浏览器选项无头模式适合服务器环境关闭 GPU 加速避免部分 Linux 环境报错 options Options() options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--disable-gpu) # 指向 chromedriver 的绝对路径版本必须与本机 Chrome 一致 driver webdriver.Chrome(executable_path/usr/local/bin/chromedriver, optionsoptions) driver.get(https://www.jd.com) print(driver.title) # 输出版本号里带京东字样说明链路通了 driver.quit()这段代码里的--headlessnew是无头模式的新版本参数比老写法--headless更接近真实浏览器指纹京东反爬对无头模式有检测但老版本参数暴露特征更明显。--no-sandbox是 Linux 服务器上以 root 跑时必加的Windows 下可以去掉。如果验证脚本能正常输出页面标题说明 WebDriver 链路已经通了后面的元素定位才有意义。这里卡住的话排查顺序是Chrome 版本 → chromedriver 版本 → 绝对路径 → 浏览器权限。3. 定位京东商品信息CSS 选择器与 XPath 的取舍和稳定写法3.1 京东商品详情页的 DOM 结构特征京东的商品页结构比一般网站复杂同一信息在页面里可能存在多个节点比如商品名称在左侧主图和左侧图集里都有副本。我拆过这个项目里的定位逻辑作者直接用.sku-name这个类名去取商品标题这是京东 PC 端详情页最稳定的锚点之一。价格信息则分布在多个位置详情页核心价格在.p-price下的span classprice而促销信息里的价格在另一个独立的p-price容器里取数时必须区分你取的是当前价还是促销价。评价数在.comment-count这类节点下但要注意京东的评价数有「只看当前商品」和「包含相似商品」两种口径DOM 类名各不相同。这里暴露了爬虫开发的一个核心习惯不要背选择器要每次运行前打开开发者工具按CtrlShiftC重新审视目标节点的父级结构。页面改版是电商常态京东大概每两个月会调一次详情页布局写死选择器的脚本生命周期最长不超过一个季度。3.2 名称、价格、评论数的定位实现from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # 等待商品名称节点出现最多等 10 秒sku-name 是京东商品标题的稳定类名 name_elem WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, .sku-name)) ) product_name name_elem.text.strip() # 价格节点存在两个候选.p-price .price 是主价格.p-price .promo-price 是促销价 # 优先取促销价不存在再回退主价格 try: price_elem driver.find_element(By.CSS_SELECTOR, .p-price .promo-price) except Exception: price_elem driver.find_element(By.CSS_SELECTOR, .p-price .price) product_price price_elem.text.strip() # 评论数节点在详情页头部区域.comment-count 里的数字是总评价数 comment_elem driver.find_element(By.CSS_SELECTOR, .comment-count a) comment_count comment_elem.text.strip()这段代码的关键点有两个。第一WebDriverWait配合presence_of_element_located处理动态渲染延迟商品名称是首屏异步加载的如果直接find_element大概率在脚本启动瞬间拿不到节点。第二价格字段用了 try 回退逻辑——促销价节点不一定存在比如无促销的直降商品只有主价格写死一个选择器就会丢失数据。评论数那个a是京东把评论数和「条评价」文字放在同一个链接里取text后需要自己用正则把数字提出来这是原始脚本里没做干净的部分你在复用时可以补上。3.3 选择器优先顺序先 CSS 后 XPath先 ID 后类名做京东爬虫我的经验是能用 CSS 不用 XPath。CSS 选择器短、浏览器原生支持、在document.querySelector里也能验证。XPath 适合两种场景一种是根据文本内容反查元素比如「下一页」按钮没有稳定类名时用//a[contains(text(),下一页)]另一种是跨层级的复杂兄弟节点筛选比如某个价格和某个促销标签在同一父容器内。京东的页面结构里类名有大量语义化命名.sku-name、.p-price、.comment-countCSS 能覆盖绝大多数场景。在定位粒度上尽量选择离文本最近的节点不要取容器再往下钻。比如评论数.comment-count容器里还有a和span两层直接取容器 text 会混入「条」「人」这类杂字。优先选择最内层的文本节点再在上层做兜底。还有一点类名里有空格的如classp-price J-p-price在 CSS 里要写成.p-price.J-p-price两个类名之间不要加空格加了空格就变成后代选择器了这是新手最容易写错的地方。4. 翻页与动态加载WebDriverWait 的等待策略和分页循环的两种写法4.1 显式等待机制固定 sleep 是玄学条件等待才是工程做法每个用过 Selenium 的人都写过time.sleep(3)但这属于玄学调参。页面快的时候 3 秒浪费慢的时候 3 秒不够就崩。项目里用的是WebDriverWait这是显式条件等待每隔 500ms 检查一次指定条件超时才抛异常。对比两种写法# 反面案例固定休眠网络波动直接翻车 import time time.sleep(5) name driver.find_element(By.CSS_SELECTOR, .sku-name).text # 正面案例条件等待节点出现即继续执行 from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC wait WebDriverWait(driver, 15, poll_frequency0.5) name_elem wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, .sku-name)))WebDriverWait构造参数里第一个是 driver 实例第二个是总超时秒数这里给了 15 秒第三个是轮询间隔。presence_of_element_located是最低限度的等待条件它只检查 DOM 里有没有这个节点不保证节点可见、可点击。如果要点击翻页按钮就要用element_to_be_clickable它额外检查元素是否在视口内且没有被遮挡。京东的「下一页」按钮在页面底部需要先滚动到底部才可点击这也是实际运行中常遇到的现象。4.2 分页循环点击翻页按钮与 URL 参数拼接两种路径京东搜索列表页和分类页有分页参数但商品详情页没有分页概念。你要抓的是「搜索结果中某类商品的多个详情页」那么循环的单位是商品卡片而不是详情页内部。两种常见做法# 方式一在搜索结果页逐一点击商品卡片进入详情页抓取后返回 # 适合商品数量少50 个以内保留浏览轨迹更接近真人 results driver.find_elements(By.CSS_SELECTOR, .gl-item .p-name a) urls [a.get_attribute(href) for a in results] for url in urls: driver.get(url) # 这里执行商品详情页的抓取逻辑参考上一章代码 # 抓完回到列表页继续 driver.back() time.sleep(random.uniform(1, 3)) # 方式二直接构造商品 URL 列表逐个访问不依赖列表页 DOM # 适合从数据库或前一轮采集结果里拿到商品 ID 的场景 sku_ids [100012043978, 100016034372] for sku_id in sku_ids: url fhttps://item.jd.com/{sku_id}.html driver.get(url) # 抓取逻辑同上方式一的问题在于driver.back()回到列表页后页面可能执行了重新渲染之前的 DOM 引用全部失效必须重新定位商品卡片元素。方式二更稳直接从商品 ID 构造 URL跳过了列表页的翻页交互。来源可以是京东站内搜索的接口返回也可以是你自己维护的商品库。判定标准很简单如果你要批量抓取的商品是已知 ID永远选择方式二如果必须从搜索结果里发现新商品方式一加WebDriverWait才能扛住列表页重渲染。分页的另一种形态是搜索结果页的「加载更多」或页码跳转。京东搜索页是标准分页page参数可以直接改 URL 参数跳页不需要模拟点击for page in range(1, 6): search_url fhttps://search.jd.com/Search?keyword笔记本电脑page{page} driver.get(search_url) wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, .gl-item))) # 解析当前页所有商品卡片收集链接或直接抓取 cards driver.find_elements(By.CSS_SELECTOR, .gl-item) print(f第 {page} 页捕获 {len(cards)} 个商品)这种拼接 URL 的方式跳过了点击交互但京东对直接改page参数访问的请求检测更敏感表现为访问到第 5、6 页时返回验证码页面。这时候需要回到点击翻页的交互方式或者在每页之间加更长的随机休眠58 秒。两种方式的取舍就是效率和稳定性的平衡我的习惯是前 3 页用 URL 拼接提速第 4 页起切到点击翻页被验证的风险低很多。5. 反爬边界和数据落地延时节奏、User-Agent 伪装与 CSV 存储5.1 触发京东反爬的典型特征不是封 IP是弹验证码和降级页面京东对 Selenium 的检测不是直接封 IP而是渐进式降级。最先出现的是商品价格字段变成「¥」或者整个p-price节点消失接着是跳转到passport.jd.com的登录页做验证最后是返回一个只含 JS 的空壳页面。前两种特征很迷惑人你会误以为是自己的选择器写错了实际上是对端已经识别出你是非真人浏览器。识别 WebDriver 特征的主要途径是navigator.webdriver属性正常的 Chrome 里这个值是undefinedSelenium 控制的浏览器里是true。虽然可以通过 ChromeOptions 的excludeSwitches参数去掉这个特征但更实用的策略是降低请求频率让行为曲线接近真人。这个项目里没有做太深的反检测它给的方案就是老老实实加延时和随机 User-Agent。5.2 限速策略统一延时与随机延时的搭配import random import time # 每个商品详情页抓取完成后随机休眠 2-4 秒 # 不要用固定值固定间隔的请求模式在统计上更容易被识别 time.sleep(random.uniform(2, 4)) # 连续抓取 20 个商品后强制进入长休模拟人离开电脑的状态 if current_index % 20 0: time.sleep(random.uniform(20, 30)) # 更换 User-Agent用 fake_useragent 库从常用浏览器的真实 UA 池里随机取 from fake_useragent import UserAgent ua UserAgent() options.add_argument(fuser-agent{ua.chrome})random.uniform(2, 4)生成 2 到 4 秒之间的浮点数比random.randint(2, 4)更平滑。每 20 个商品的长休是为了应对京东的滑动窗口计数——它统计的是短时间内的请求密度密度的绝对值比总请求量更关键。fake_useragent这个库会从维护的列表里随机返回一个当前流行的浏览器 UA注意这个库偶尔会触发网络请求更新自身数据源离线环境会抛错稳妥做法是预先调用一次把 UA 列表缓存到本地。这里必须强调延时策略只是降低碰撞概率不保证不触发验证。真遇到验证码页面Selenium 能做的有限——人工介入输一次验证码之后 Cookie 生效还能继续跑。所以脚本里要做验证码检测# 检测页面是否跳转到验证码或登录页 current_url driver.current_url if passport.jd.com in current_url or verify in current_url: print(触发了验证需要人工处理程序暂停 60 秒等待手输) time.sleep(60)人工处理的方式是把浏览器窗口停在验证码页人看到后手动滑一下或点一下然后脚本靠同一个 driver 会话继续执行。这就是 Selenium 模式相对纯接口爬虫的优势——会话状态全程保留不需要重新登录。5.3 CSV 落地与编码问题别用默认编码会读到乱码import csv # 数据格式商品ID、名称、价格、评论数、抓取时间 csv_file open(jd_products.csv, a, newline, encodingutf-8-sig) writer csv.writer(csv_file) for product in products: writer.writerow([ product[id], product[name], product[price], product[comment], time.strftime(%Y-%m-%d %H:%M:%S) ]) csv_file.close()utf-8-sig这个编码有两个作用一是写入时带 BOM 头Excel 打开不乱码二是避免 Windows 默认的gbk导致中文字符截断。项目里那个 a.txt 文件应该是早期版本用普通文本写入的商品信息如果你要拿数据做分析强烈建议改成 CSV 并加上抓取时间戳字段。这样可以追查某条数据是哪一轮采集得到的数据出问题时能定位到批次。还有一个容易忽略的点CSV 里商品名称如果包含逗号或换行符csv 模块会自动加引号转义用普通文本写入就会把字段搞乱这也是 a.txt 方案不如 CSV 的另一个原因。6. 避坑与排查五个高频翻车现场和修复方法6.1 元素明明存在却定位不到现象手动在浏览器开发者工具里能看到.sku-name节点甚至用document.querySelector(.sku-name)也能取到但脚本find_element抛NoSuchElementException。原因最常见的是 Selenium 打开的浏览器和你的手动浏览器不在同一个页面状态。京东首页会弹出一个地区选择浮层或优惠券弹窗这个浮层把你的目标节点遮住但 Selenium 的presence_of_element_located只检查节点存在性不管可见性如果用了visibility_of_element_located则会因为节点被遮挡而一直等待超时。另一个原因是 chromedriver 是旧版对新版 Chrome 的 DOM 渲染方式有兼容偏差。解决先把弹窗关掉再定位。京东的浮层通常有右上角的关闭按钮类名是.close用find_element(By.CLASS_NAME, close).click()在定位商品名称之前执行。如果这一步本身报错说明弹窗这次没出现要包 try 跳过。同时把等待条件从presence_of_element_located换成visibility_of_element_located确保节点真的显示在页面上。6.2 价格字段抓到的是乱码或空字符串现象脚本跑完CSV 文件里商品名称正常价格列全空或者出现¥符号后面无数字。原因京东的价格节点有多个1.0版本的页面用p-price部分新页面改成了J-p-price。另一个原因是页面只在鼠标悬停时才渲染价格文本Selenium 模拟的浏览器没有触发 hover 事件节点存在但 text 为空。还有一种是 webdriver 被识别后京东直接返回一个脱敏价格节点text属性里没有数字。解决用 XPath 同时匹配多个价格节点取第一个非空值。用ActionChains模拟鼠标悬停在价格区域强制触发渲染from selenium.webdriver.common.action_chains import ActionChains price_nodes driver.find_elements(By.XPATH, //*[contains(class,price)]) for node in price_nodes: text node.text.strip() if text and text not in (¥, ): price text break else: # 全部为空时模拟鼠标悬停触发渲染 ActionChains(driver).move_to_element(price_nodes[0]).perform() time.sleep(1) price price_nodes[0].text.strip()这个逻辑处理了「多个候选节点」和「悬停渲染」两个场景。实际项目中京东有大概 20% 的商品需要悬停才能拿到价格写死一个节点就会丢这部分数据。6.3 登录弹窗挡住商品卡片点击现象分页循环跑到某个商品时element.click()报了ElementClickInterceptedException页面顶部滑出一个登录推荐弹窗。原因京东在用户浏览了几页之后会随机弹「登录查看最新价格」的提醒这个弹窗有半透明遮罩直接挡住了商品链接。弹窗的出现时机不固定不是每次跑都触发所以不好预判。解决点击前统一处理弹窗。在进入每一页时先检测是否存在.login-dialog或.ui-dialog节点有就执行关闭脚本# 强制移除遮挡元素不依赖关闭按钮有些弹窗的关闭按钮在视口外 driver.execute_script( document.querySelectorAll(.ui-dialog, .login-dialog).forEach(el el.remove()) )用 JavaScript 直接移除节点比找关闭按钮更稳因为弹窗有时不在当前视口内Selenium 的点击会先滚动页面滚动本身又触发新的弹窗。这个脚本在每页加载完、抓取商品卡片之前执行一次成本极低收益很高。6.4 循环翻到第 8 页时下一页按钮消失了现象搜索列表页从第 1 翻到第 7 页都正常第 8 页开始page-item定位不到页面显示的是一个空白骨架。原因这是典型的频率触发。京东对单位时间内搜索页访问量有阈值超过之后不再给你渲染分页按钮返回一个占位页面。和验证码不同它不弹提示就是安静地给你一个空页面你的脚本会在这个页面反复找按钮直到超时。解决检测当前页是否有商品卡片节点.gl-item没有就说明被限流了。先停 30 到 60 秒然后刷新当前页再试一次如果连续三次刷新还是空页面就终止本次任务把当前页码记到日志里下次运行时从断点继续。if not driver.find_elements(By.CSS_SELECTOR, .gl-item): retry_count 1 if retry_count 3: breakpoint page # 记录断点到配置文件 break time.sleep(60) driver.refresh()这个断点续跑逻辑在长任务里是必需品。你永远不知道京东什么时候会对你限流一次性跑完 1000 个商品基本不现实分段跑加断点是性价比最高的方案。6.5 CSV 写入的数据和商品对不上现象抓了 200 个商品检查 CSV 时发现有一些行的价格和商品名称明显不匹配像是错位了。原因脚本里如果用了全局变量收集当前页所有商品然后循环详情页时没有清空列表上一轮的数据就残留在新一轮里导致读取时错位。更隐蔽的原因是京东详情页有「已选」区域这个区域会在你返回列表页后保留上一次的选择状态如果逻辑里复用了之前的选择器变量就会读到旧数据。解决强制每个商品循环开始时重建数据字典不用全局列表累积# 每次进入详情页就新建一个 dict避免循环内数据残留 product {} product[id] sku_id product[name] get_name(driver) product[price] get_price(driver) # 每一轮循环结束把 product 深拷贝进总列表 products.append(product.copy())product.copy()这一步是防坑关键dict是可变对象如果你直接把product赋值进列表下次循环修改product时列表里已有的数据也会跟着变。这个 bug 隐蔽在「看着像是抓到了其实全是最后一个商品的数据」。7. 把脚本调成可运维的小工具断点续跑、结果校验和日志留痕7.1 基于采集时间戳做文件名轮转避免单文件越写越大项目给的 a.txt 是单文件累积模式跑久了文件体积膨胀中途坏了整份数据全丢。我的习惯是按天切分文件文件名带日期参数import time # 按天生成文件名保证每天一个独立文件便于按批次回溯数据 file_date time.strftime(%Y%m%d) csv_file open(fjd_products_{file_date}.csv, a, newline, encodingutf-8-sig)用日期切分后即使某一天的数据因为反爬被封禁导致抓取质量差也只需要回滚那一天的文件不影响历史数据。配合前面的断点续跑逻辑断点信息也按日期记录避免跨天恢复时把昨天的页码用到今天。7.2 抓完一轮后做字段级校验缺价钱的商品重跑一次数据抓完不能直接拿去用先做一次抽样校验。写一个独立的小脚本读取当天 CSV统计空字段的占比重点看价格列和名称列import csv with open(jd_products_20240101.csv, encodingutf-8-sig) as fp: rows list(csv.DictReader(fp)) total len(rows) missing_price [r for r in rows if not r[price]] print(f总数 {total}缺价格 {len(missing_price)}占比 {len(missing_price)/total:.1%})价格缺失率超过 5% 的那一批把对应商品 ID 捞出来单独放进重跑列表。重跑时把延时放长到 46 秒通常能补齐大半。这个校验逻辑不需要写进主脚本它是独立的数据质量门禁跑在主抓完一轮之后。7.3 日志记录被忽略后面排查全是猜脚本跑挂了看一眼异常信息就改代码这是新手做法。每个循环里加一行日志把当前商品 ID、页码、抓到的关键字段值打到文件里排查效率翻倍import logging logging.basicConfig( filenamejd_scraper.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s ) logging.info(fsku {sku_id} 第 {page} 页 价格采集完成: {product[price]})日志里出现的记录能帮你还原现场的完整轨迹是第几个商品开始空的当前浏览器页面 URL 是什么停在了哪一步。从那以后我每次跑抓取任务前都会强制把这三件事做一遍按日期切分输出文件、跑完执行字段校验、全程开着详细日志。这套习惯救过我好几次——有一次凌晨批量任务挂在第 300 个商品上就是靠日志里最后一条 sku ID 锁定了触发限流的页面特征而不是从头看代码猜。这个项目的脚本能帮你跑通 80% 的流程剩下的 20% 稳定性就靠这些运维细节补上。希望帮到你。本文还有配套的精品资源点击获取