看到这个标题你可能觉得我在开玩笑:都什么年代了,还用Selenium去抓电商数据?现在的技术圈,随便一搜都是requests加逆向签名,又快又省资源。但我不反驳,因为我自己在大多数情况下也是这种组合。可Selenium这种浏览器自动化方案,在"临时性、快速验证、不想碰逆向"的场景里,依然是最稳的保底选项。京东商品数据抓取这个需求,几乎是我被问得最多的爬虫问题之一,竞品比价、市场调研、库存监控,甚至有人单纯想看看某个关键词下大家都在卖什么价格。这篇文章我尽量把从环境配置到完整代码,再到踩过的坑一次性讲清楚,让你照着就能跑起来,而不是只看个热闹。
1. 抓京东商品数据,为什么绕不开Selenium
1.1 京东的页面不是"请求一下"就能拿到的
如果你只用requests去请求京东搜索页,拿到手的HTML里通常根本没有商品列表。京东的前端页面大量依赖JavaScript异步渲染:你看到的商品标题、价格、店铺信息,很多是页面加载后由脚本再发接口动态填充的。也就是说,你拿到的是骨架,肉都在后面的XHR请求里。
有的朋友会说:那就抓它的异步接口呗。对,理论上可以,但京东的接口签名逻辑一直在变,你可能今天把参数逆向通了,明天前端升级又失灵。对于只想要几个关键词搜索结果的普通用户来说,这个学习成本太高了。Selenium的思路不同:我直接驱动一个真浏览器,让页面像普通用户一样加载完,所有异步渲染的逻辑都由网站自己执行,我只要等内容出现在DOM里再把它拿下来就行。
所以Selenium解决的核心问题是:把"页面最终长什么样"这件事交给网站自己搞定,我只负责观察和提取。这在你只需要抓个一两百条数据做分析的时候,比逆向接口靠谱得多。
1.2 "5分钟"的前提条件,先把期望管理做好
标题说5分钟搞定,我不骗你,但得把丑话说在前面。如果你已经安装了Python、Chrome浏览器,并且能把Selenium的浏览器驱动配好,登录态也准备好了,那从启动浏览器到跑出第一份CSV,确实能控制在几分钟内。真正费时间的其实是第一次配置驱动、处理登录验证、摸清页面结构这三个环节。
京东现在有比较强的反爬意识,直接无头浏览器访问搜索页很容易被识别,需要做一点浏览器指纹上的伪装;搜索关键词如果比较热门,还可能触发滑块验证码。这些我都放在后面的章节里处理,但你要心里有数:5分钟跑通指的是"启动代码到拿到数据",不是"从零安装环境到彻底稳定"。这篇文章的核心价值,就是帮你把那些最容易被卡住的前置坑提前填平。
2. 环境准备:把地基打牢,后面才不慌
2.1 安装Selenium与浏览器选择
环境上我默认你用的是Python 3.8以上版本。命令行里一条命令就能装好Selenium库:
pip install selenium浏览器这块,我建议用Chrome,因为它的驱动兼容生态最好,遇到问题网上资料也最多。微软的Edge也可以,因为新版Edge本质是Chromium内核,驱动获取方式和Chrome几乎一样。Firefox也能用,但某些伪装参数的写法要改,没必要一开始就跟自己过不去。
Selenium从4.6版本开始内置了Selenium Manager,理论上它会根据你本机的Chrome版本自动下载对应驱动,省去了手动下载的麻烦。但在某些网络环境里,自动下载也可能失败,到时候还是得回到手动方案。手动方案的逻辑很简单:看浏览器版本号,去驱动网站下载对应版本的驱动,把可执行文件放到PATH路径下,或者直接在初始化代码里用executable_path参数指定。版本号必须在主版本上对齐,比如Chrome 126就尽量找126开头的驱动,差的太远会被拒。
2.2 我用到的Python依赖清单
除了Selenium本身,我还建议顺手装上下面几个库,它们不是必选的,但能让你省很多事:
pandas:数据最终要存成Excel或做去重分析,pandas处理起来最顺手。lxml:Selenium拿到的页面源码如果需要用XPath二次解析,lxml效率高。retrying或tenacity:做请求重试,遇到超时或临时反爬的时候自动等几秒再试。
这些库不影响主流程,但建议一起装上:
pip install pandas lxml tenacity装完之后,先写一个最简单的脚本验证环境是否正常:打开浏览器,访问一个空白页面,打印当前URL。这一步能排除90%的"驱动不匹配"问题。环境这东西,看起来琐碎,但如果你连浏览器都启动不了,后面代码再正确也跑不起来。
3. 完整代码与关键拆解:手把手跑起来
3.1 初始化浏览器:先做基础伪装
京东的反爬虽然不像某些网站那么丧心病狂,但Selenium的自动化特征在它面前很容易暴露。最典型的特征是navigator.webdriver这个属性会变成true,网站脚本一判断就能把你识别成机器人。所以我们要在浏览器启动后,第一时间通过Chrome DevTools协议注入一段脚本,把这个属性改掉。
另外两个可以顺手做的优化:一是关掉图片加载,商品搜索结果里图片占了很大带宽,关掉之后页面加载速度明显提升,反正我们也不抓图片;二是把enable-automation开关去掉,避免浏览器顶部出现"正在受自动测试软件控制"的提示条,这个提示条本身就是一个明显的机器人信号。
import time 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 def init_driver(headless=False): options = Options() # 关闭自动化控制提示条 options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option("useAutomationExtension", False) # 禁用图片,加速页面加载 prefs = {"profile.managed_default_content_settings.images": 2} options.add_experimental_option("prefs", prefs) # 隐藏自动化特征 options.add_argument("--disable-blink-features=AutomationControlled") if headless: options.add_argument("--headless=new") # 指定窗口大小,避免某些页面在无头模式下布局异常 options.add_argument("--window-size=1920,1080") driver = webdriver.Chrome(options=options) # 覆盖 navigator.webdriver 属性 driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); """ }) return driver driver = init_driver(headless=False)这段代码里最值得关注的是Page.addScriptToEvaluateOnNewDocument这个API。它在浏览器加载任何新页面之前提前执行脚本,从而把webdriver属性覆盖掉。如果只在新页面加载之后再执行,那就来不及了,因为网站的检测脚本可能先跑完了。这个顺序细节很多人会忽略,实测下来影响挺大的。
3.2 登录态处理:扫码和Cookie两条路
京东搜索页不加登录态也能访问,但很容易被重定向到登录页,或者商品价格显示不全。更稳妥的做法是先登录一次,拿到登录凭证,后续请求都带着这个凭证。
方案一是手动扫码登录。这个适合你本地临时跑一次,代码里打个断点,浏览器弹出登录二维码,你拿手机扫完,再让脚本继续往下走。好处是几乎不会触发异常风控,坏处是需要人工介入,没法全自动。
方案二是使用Cookie。你先在正常浏览器里登录京东,打开开发者工具的Application面板,找到Cookie列表,把pt_key和pt_token这两个字段的值复制出来。这两个是京东登录态的核心票据。然后我们在代码里把它们拼成一个Cookie字符串,注入到Selenium的浏览器里,这样就能模拟已登录状态。
COOKIE_STR = "pt_key=xxx; pt_token=yyy" def inject_cookie(driver, cookie_str): # 先打开一个京东页面才能设置 Cookie driver.get("https://www.jd.com") driver.delete_all_cookies() for item in cookie_str.split("; "): key, value = item.split("=", 1) cookie = { "name": key, "value": value, "domain": ".jd.com" } driver.add_cookie(cookie) driver.refresh() cookies = COOKIE_STR inject_cookie(driver, cookies)注意,pt_key和pt_token这两个Cookie的有效期不短,但也用不了一辈子。如果哪天脚本突然跳到登录页或者需要验证,第一时间去原浏览器里看看Cookie是否过期,重新复制一份就好。千万不要在代码里硬编码一个已经失效的Cookie,然后怀疑自己的人生。
3.3 搜索、解析与翻页:核心主流程
京东搜索页的URL参数比想象中简单,keyword是关键词,enc是编码。如果想指定排序方式,还可以加psort参数,比如psort=3是按销量排序,psort=1是按综合排序。
商品列表的DOM结构,在正常情况下是这样的:外层容器是id为J_goodsList的div,里面是一个ul列表,每个商品是li标签,CSS类名是gl-item。商品标题在.p-name a里面,价格在.p-price i里面,店铺名在.p-shop a里面。
但京东的页面结构并不是永远不变,前端改版是常态。好在思路是通用的:只要列表容器还是#J_goodsList,定位逻辑基本不用推翻重写。下面我给出完整主流程:
import csv import random from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By def parse_page(driver, writer, page_no): # 等待商品列表加载完成 WebDriverWait(driver, 20).until( EC.presence_of_element_located((By.CSS_SELECTOR, "#J_goodsList li.gl-item")) ) items = driver.find_elements(By.CSS_SELECTOR, "#J_goodsList li.gl-item") for item in items: try: title_el = item.find_element(By.CSS_SELECTOR, ".p-name a") title = title_el.text.strip().replace("\n", " ") link = title_el.get_attribute("href") except Exception: title, link = "", "" try: price = item.find_element(By.CSS_SELECTOR, ".p-price i").text.strip() except Exception: price = "" try: shop = item.find_element(By.CSS_SELECTOR, ".p-shop a").text.strip() except Exception: shop = "" writer.writerow([page_no, title, price, shop, link]) def scroll_to_bottom(driver): """模拟用户缓慢向下滚动,触发懒加载""" driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(1.5) def main(): keyword = "机械键盘" max_pages = 3 driver = init_driver(headless=False) inject_cookie(driver, COOKIE_STR) f = open(f"jd_{keyword}.csv", "w", newline="", encoding="utf-8-sig") writer = csv.writer(f) writer.writerow(["页码", "标题", "价格", "店铺", "链接"]) for page in range(1, max_pages + 1): url = f"https://search.jd.com/Search?keyword={keyword}&enc=utf-8&page={page}" driver.get(url) time.sleep(random.uniform(2, 4)) scroll_to_bottom(driver) parse_page(driver, writer, page) print(f"第{page}页完成,已抓取商品") f.close() driver.quit() if __name__ == "__main__": main()这里有两个细节值得展开。第一,为什么翻页用URL里的page参数而不是点击页面上的"下一页"按钮?因为直接用URL跳转更稳定,点击按钮有时候会因为页面异步加载缓慢而失效。第二,为什么每页之间加随机等待?不是为了装模作样,而是为了降低被风控系统识别的概率。脚本行为如果太规律,比如每3秒固定翻一页,从服务端日志看就是典型的机器人特征。
3.4 数据落盘:CSV编码和去重
CSV看起来简单,但坑不少。我建议写入时指定encoding="utf-8-sig",因为如果用纯utf-8编码,Excel打开中文会乱码。utf-8-sig会在文件开头加一个BOM标记,Excel能正确识别。如果你打算用pandas分析,那utf-8-sig同样没问题。
去重方面,京东搜索页每个关键词下可能会有少量重复商品,特别是翻页抓取的时候,偶尔会遇到"猜你喜欢"推荐位插入的商品列表。可以给CSV加一个简单去重逻辑:把商品链接当作唯一键,写之前判断一下这个链接是否已经在已抓取的集合里。
seen = set() with open("jd_goods.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f) writer.writerow(["页码", "标题", "价格", "店铺", "链接"]) for row in fetch_rows(): url = row[4] if url in seen: continue seen.add(url) writer.writerow(row)如果你要抓的数据量不大,内存里放一个set完全没压力。如果跑到几万条,那就可以考虑改成SQLite存储了。这篇文章以Demo为主,CSV就够用。
4. 实操中常见的几个坑:我替你踩过的
4.1 元素定位失败:页面还在加载,你就急着拿数据
这是Selenium新手最常遇到的问题。find_elements是即时操作,如果页面还没加载出商品列表,返回的就是空列表,你以为抓到了零条数据,其实只是太快了。
解决方案就是显式等待。我在主流程里用了WebDriverWait(driver, 20).until(...),意思是最多等20秒,只要商品列表的li元素出现了,就继续往下走。如果20秒还没出现,就抛出异常。这里的等待时间和网络环境有关,如果你网速慢或访问频繁被限速,可以调到30秒甚至更长。
定位元素这一块,我的建议是优先用CSS选择器,少用XPath里那种冗长的绝对路径。CSS选择器简洁、解析快,而且京东页面的结构调整时,相对结构比绝对路径稳定得多。只要记住"容器固定、item固定、关键属性固定"这三个原则,页面改版后通常只需要微调选择器就能继续用。
4.2 验证码和风控拦截:别硬刚,想办法绕
京东的搜索页偶尔会弹出滑块验证码。实测经验是:搜索关键词越热门、翻页越多、请求频率越高,越容易触发。一旦弹了滑块,Selenium硬拖动也能拖,但成功率不稳定,而且会让风控系统对你更加关注。
我的做法是三层规避。第一,控制抓取规模,单个关键词只抓前几页,拿到有代表性的数据就够了,别贪心。第二,每次抓取之前随机等待3到5秒,模拟人的浏览节奏。第三,保留一个手动兜底:如果验证码出现,代码暂停并提示你人工处理,你手动把滑块拖过去,然后继续跑。这个兜底逻辑很简单:
if "验证" in driver.page_source: input("出现验证码,请手动处理,处理完按回车继续...")有些朋友会纠结如何完全自动过滑块,从我的经验看,这种投入产出比不高。你是在抓公开数据做分析,不是在攻击某个高价值目标,没必要把自己锻炼成验证码对抗专家。人工点一下滑块,成本其实很低。
4.3 价格、店铺抓取为空:异步加载和结构差异
价格为空的情况特别常见,原因是京东的价格是在商品列表加载后,再通过异步接口单独填充的。li元素已经渲染出来了,但价格位置还是空的。所以你不能只等li出现,还要等价格文本不是空字符串。
更好的做法是针对价格字段再做一次显式等待,或者干脆加入一个简单的重试逻辑:如果.p-price i取出来为空,等2秒再取一次。店铺为空则是另一个原因:搜索结果里部分商品是京东自营的,店铺区域结构和其他第三方店铺不一样,选择器抓不到。这种情况就判断一下DOM里有没有.p-shop,没有就记为"自营"。
另外要提醒一点:京东展示给普通用户的价格,通常是经过促销计算后的"到手价"或者"PLUS会员价",和真实结算时你可能享受的价格并不完全一致。所以价格字段适合做趋势分析,别把它当作精确报价来用。
5. 合规意识与工程化改造建议
5.1 爬虫的技术边界
京东在其用户协议里对爬虫采集行为有明确限制,所以这类脚本只适合个人学习、接口研究或者小规模数据分析,不宜用于商业牟利,更不要高频、大规模地去采集。技术上你当然可以控制请求频率,但合规与道德的底线得自己掌握。我的建议很简单:每次抓取量控制在一个合理范围,采集频率远低于正常用户浏览频率,抓到的数据不要二次公开传播。
这一点很多技术教程不会提,但我觉得有必要说透。我有一次帮一个朋友做商品价格调研,只是跑了不到100条数据,依然在一个小时之后触发了风控拦截,整个IP段被暂时限制。不要觉得"我就抓几十条没事",电商平台的反爬监控是全天候的,配合随机延迟能降低风险,但无法保证万无一失。
5.2 从"跑通Demo"到"可用的工具"
如果这篇代码你跑通了,恭喜你,你已经掌握了一个非常实用的技能。但如果想让这个脚本长期用下去,我强烈建议你做几个方向性的改造。
第一,把关键词和页数配置到外部文件里。一个需求列表,用JSON或者YAML管理,脚本启动时读取配置,而不是改代码里的变量。第二,加上异常通知。抓取过程中经常会出现各种未知异常,抛一个try...except,把异常信息通过企业微信机器人或者钉钉机器人发到手机上,这样脚本挂了你第一时间知道。第三,数据入库。量小用CSV,量大了换SQLite,再大了上MySQL或PostgreSQL。入库之后你可以直接用SQL做聚合分析,比反复读CSV高效得多。
{ "keywords": ["机械键盘", "无线鼠标", "显示器支架"], "pages": 2, "sort": "3", "delay_range": [3, 6] }配置化之后,你换关键词不用改代码,维护成本会明显下降。这个脚本我已经在几个模拟项目中翻来覆去用过,稳定性完全可以接受。
最后再分享一个我自己常用的技巧
我平时抓完数据以后,不会直接拿CSV去Excel里看,而是用pandas做一次简单清洗:把价格列转成数值型,去掉明显异常的空值,再做一次按价格区间的分组统计。这一步能让你在30秒内看到这个关键词下不同价格档位的商品分布,比肉眼一行行扫Excel高效很多。
import pandas as pd df = pd.read_csv("jd_机械键盘.csv") df["价格"] = pd.to_numeric(df["价格"], errors="coerce") print(df["价格"].describe()) print(df["价格"].groupby(pd.cut(df["价格"], bins=[0, 100, 300, 500, 1000, 5000])).size())按理说爬虫教程到代码部分就可以结束了,但我觉得多写两句工具链终归没坏处。毕竟抓数据只是第一步,把数据变成能用的信息,才是你做这件事的最终目标。这套流程看起来简单,真能从头到尾跑通的人,已经比大部分只会对着教程敲代码的读者强出一截了。