简介:面向需要采集京东商品数据的爬虫学习者,这份实操型资源演示了如何借助浏览器驱动模拟真实用户操作,解决动态加载、翻页遍历与反爬限制等常见问题,对于电商页面这类交互频繁的场景尤其适用。压缩包共 2 个文件,含一个脚本文件和一个文本结果文件,整体仅 2KB;脚本模块清晰,文本文件为抓取结果样例,便于对照验证。已有 245 人学习下载。读者可从中获取商品名称、价格、评价等元素的定位方式,以及显式等待、分页点击的处理技巧,还能看到通过延时、切换用户标识等手段降低封禁风险的完整思路。基于这份简洁骨架,可直接修改商品链接或选择器复用于其他商品,也可扩展至多品类采集、价格监控或数据清洗任务,适合作为入门爬虫开发的参考资料。
1. 用 Selenium 爬京东商品信息:先把“能不能跑”换成“能跑多久”
拿到“selenium爬取京东商品信息.zip”这套包的人,多半带着一个具体诉求:把搜索页里的商品名、价格、店铺、链接批量落进表格。我前前后后写过不下十版抓京东的采集脚本,结论很直接:真正的门槛不在 selenium 怎么写,而在登录态、DOM 结构和风控这三道坎。这篇按能落地的方式拆开讲,先说明为什么选 selenium 而不是 requests,再给一套最小可复现的脚本,最后把新手最容易翻车的五个点位逐个拆掉。适合刚接触爬取、想把采集脚本做成能长期维护的工程的开发者,也适合只要一份能跑的模板、回头自己改字段的人。
2. 为什么选 Selenium:先看清京东商品页的数据入口与三个选型条件
2.1 京东商品列表的数据入口:DOM、懒加载与可见性
京东搜索列表页的核心数据挂在ul.gl-warp下的li.gl-item里,商品名、价格、店铺、链接都集中在一个 li 内。单看结构,用 Requests 直接解析也不是不行,问题出在懒加载和动态渲染:页面首屏只输出前几个商品,滚动到中段才会请求下一批;价格字段往往由 JS 在页面加载完成后写入,raw HTML 里经常是个空标签。这两个特性决定了 Requests 加正则的方案在京东上很不牢靠。
Selenium 打开的是真实浏览器内核,滚动、点击、等待都有明确结果。你用find_element拿到的元素,已经是浏览器重排之后的 DOM;懒加载触发后追加的商品,也能通过后续查找被检索到。这让 Selenium 成为抓取京东商品信息最容易起步的方案。
代价同样明显:速度慢,内存占用高,每开一个浏览器实例就要几百 MB。但如果你的诉求是“把某几个关键词的搜索结果抓下来”,几十页的量级完全在 Selenium 的舒适区内,没必要一上来就上多线程、分布式那套重型方案。
2.2 选型:Selenium、Requests 还是接口直连
| 方案 | 能拿到什么 | 拿不到什么 | 什么时候选它 |
|---|---|---|---|
| Requests 直接请求链接 | 静态 HTML | JS 渲染后的价格、懒加载商品 | 只用来探测页面结构 |
| 接口直连 | 结构化数据,速度快 | 签名参数难构造,需要逆向 | 数据量几万条以上且有逆向经验 |
| Selenium | 完整渲染后的 DOM,交互可控 | 速度慢,资源占用高 | 大多数个人采集任务的首选 |
怎么判断自己适合哪条路?按三个条件依次问。第一,要不要登录态?只看公开价格和销量其实不用登录;要抓 Plus 专享价或促销字段,就需要。第二,页面数据是不是 JS 渲染的?是,就直奔 Selenium。第三,数据量有没有到几万条以上?到了,才值得研究接口直连。
接口直连不是几行requests.get能搞定的——京东接口链接里的签名参数、时间戳、设备指纹一应俱全,没有逆向经验的人开这个坑,大概率会卡在第一步。我的建议是先把 Selenium 跑顺,产出一份小数据验证业务方向,再决定要不要往接口迁移。
2.3 环境准备:先解压 zip,再装全家桶(含伪加密判断)
拿到压缩包第一步不是写代码,是确认里面东西齐不齐。先解压:
unzip "selenium爬取京东商品信息.zip" -d jd_crawler cd jd_crawler python -m venv .venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate pip install selenium webdriver-managerwebdriver-manager会自动下载与 Chrome 匹配的驱动,省掉手动找版本的一环。如果你在公司内网,装不了这个包,就手动去浏览器驱动站点下载对应版本放到 PATH 里,原理是一样的。
解压时如果提示文件损坏或要密码,先别急着找破解工具,很可能是 zip 伪加密。用zipinfo -v看压缩条目里的 encryption 标志位,如果显示0x01而压缩方法仍是普通 deflate,基本可以确定是伪加密,修改标志位后就能正常解压,具体操作放在后面 4.3 讲。
包里有 README 或 requirements.txt 时,先读它。很多作者会在里面写明 Selenium 版本要求和已知坑;没有也没关系,按上面的命令装完,跑下面的脚本即可。
3. 最小可复现的抓取脚本:从扫码登录到 CSV 落盘
3.1 登录态这一关:先扫码登录,再复用它
京东搜索页匿名也能访问,但价格和促销字段经常和登录态绑定,带登录态去抓也能降低误杀概率。所以我习惯先把登录态做出来,再做抓取。第一次运行下面脚本,手动扫码一次,登录态会落盘到本地目录:
# login_once.py from selenium import webdriver from selenium.webdriver.chrome.options import Options options = Options() # 关键参数:指定用户数据目录,登录信息(cookie/localStorage)会持久化 options.add_argument("--user-data-dir=./jd_login_profile") driver = webdriver.Chrome(options=options) driver.get("https://www.jd.com") input("扫码登录完成后按回车继续……") driver.get("https://www.jd.com") print("登录态已保存到 ./jd_login_profile") driver.quit()这段代码做的事情很简单:启动 Chrome,打开京东首页,扫码后回车,关闭浏览器。关键在于--user-data-dir指向了一个本地目录,chrome 会把这一份登录态的 cookie、localStorage 全部写进该目录。之后的采集脚本只要带上同一个参数启动,就等于带着登录态访问。
参数说明:./jd_login_profile是相对路径,建议改成绝对路径,避免后续脚本从不同目录启动时找不着。这个目录不要用你日常浏览的 chrome 默认配置,否则会把常用浏览器环境搞乱。
3.2 搜索与翻页:核心抓取循环怎么写
登录搞定后,进入主循环。下面的脚本按关键词搜索,逐页抓取商品标题、价格、店铺,然后点击下一页:
# fetch_jd.py import time import random from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC KEYWORD = "机械键盘" TOTAL_PAGES = 10 def fetch_page(driver, page): # 京东搜索列表:商品主体是 ul.gl-warp 下的 li.gl-item,类名随时可能改 items = driver.find_elements(By.CSS_SELECTOR, "li.gl-item") rows = [] for item in items: # 标题在 .p-name em 里,注意部分商品标题会包含换行符 name_el = item.find_element(By.CSS_SELECTOR, ".p-name em") # 价格在 .p-price strong i,活动价可能不在这个位置 price_el = item.find_element(By.CSS_SELECTOR, ".p-price strong i") try: shop_el = item.find_element(By.CSS_SELECTOR, ".p-shop span") shop = shop_el.text.strip() except Exception: shop = "" # 部分商品无店铺信息 rows.append({ "title": name_el.text.strip(), "price": price_el.text.strip(), "shop": shop, }) return rows options = Options() options.add_argument("--user-data-dir=./jd_login_profile") options.add_argument("--window-size=1400,900") driver = webdriver.Chrome(options=options) driver.get(f"https://search.jd.com/Search?keyword={KEYWORD}&enc=utf-8") for page in range(1, TOTAL_PAGES + 1): rows = fetch_page(driver, page) print(f"page {page}: {len(rows)} items") if page < TOTAL_PAGES: # 每次翻页前随机停 2.5~4.5 秒,模拟人手点击节奏 time.sleep(random.uniform(2.5, 4.5)) # 用显式等待等“下一页”按钮可点击,而不是固定 sleep next_btn = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.CSS_SELECTOR, "a.pn-next")) ) next_btn.click() time.sleep(random.uniform(1.0, 2.0)) # 等待列表区渲染 driver.quit()这里有两个容易踩的细节。第一,翻页用的a.pn-next是京东翻页区“下一页”的通用选择器,但页面结构调整时它也会变,找不到时就先打开搜索页,右键检查翻页区域的 HTML 再改选择器,别盲改代码。第二,点击下一页后,新一页的商品列表不会立即渲染完,需要一个随机短等待。这个等待不是越多越好,哪怕只等 3 秒,也比点完立刻抓数据要可靠得多。
参数说明:TOTAL_PAGES建议从 2 开始试,跑通再加页数,一上来跑几十页遇到验证码的概率会高很多。window-size设置为 1400x900,是为了更接近普通用户的视口尺寸,有些页面会按视口宽度决定渲染多少商品。
3.3 字段提取与落盘:CSV 去重和价格清洗
抓到的数据不能直接入库,先做两件事:去重和清洗。京东搜索页翻页时偶尔会把上一页的最后一个商品重复带过来,另外价格字段里可能出现“¥”“暂无报价”这类非数字内容。
# save_rows.py import csv from pathlib import Path def clean_price(raw_price): """清洗价格:去掉货币符号,空值转 0""" if not raw_price: return "0" return raw_price.replace("¥", "").strip() def save_rows(out_path, rows, seen): """rows 里的每条记录写入 CSV,title+price 去重""" fieldnames = ["title", "price", "shop", "url"] file_exists = Path(out_path).exists() with open(out_path, "a", newline="", encoding="utf-8-sig") as f: writer = csv.DictWriter(f, fieldnames=fieldnames) if not file_exists: writer.writeheader() for row in rows: key = row["title"] + str(row["price"]) if key in seen: continue seen.add(key) writer.writerow(row)编码用utf-8-sig而不是utf-8,是给 Excel 用户留的后路——带 BOM 的 CSV 被 Excel 打开时中文不乱码。去重键用“标题+价格”,简单直接;如果你抓了 URL 字段,可以换成 URL,准确度更高。清洗价格时注意“到手价”和“页面价”可能混在一起,这一步只做格式清洗,不做口径统一,口径问题放到第 4 章的 4.4 讲。
字段落盘以后,建议每页结束就调一次save_rows,不要攒到全部跑完再统一写。爬虫随时可能崩,每页落盘相当于给自己买了份后悔药。
3.4 反爬基线:把 python selenium 的采集节奏调成人的样子
很多人以为反爬对抗要靠隐蔽浏览器指纹,其实对中小型采集任务来说,最重要的只有一条:别让请求节奏暴露你是程序。
固定 sleep 1 秒是最容易被识别的行为模式。正常人看一页商品至少要扫几秒,不会每页都是精确的整数间隔。我一般把页面间隔设为 2.5 到 4.5 秒之间的随机值,每翻 5 页再额外停 15 秒左右,当作“看完去喝水”的时间。你不需要把参数调得很玄学,关键是让分布看起来有自然波动。
还有一个常见误用:一上来就开 headless 无头模式。无头模式确实省资源,但它在页面行为上经常漏马脚,验证码弹出来的概率比有头模式高不少。除非你已经有充分的排除项验证,否则尽量保持浏览器窗口可见。
| 参数 | 我的基线值 | 说明 |
|---|---|---|
| 页面间隔 | 2.5~4.5 秒随机 | 低于 1.5 秒必然触发限流 |
| 批次停顿 | 每 5 页停 15 秒 | 模拟阅读长列表后的停顿 |
| 连续失败次数 | 3 次即停机 | 重启脚本前先人工确认页面状态 |
这套参数是基线,不是金标准。有人固定 sleep 1 秒跑两小时没事,有人随机间隔照样被拦,影响因素很多。我的底线是:失败后先减速,而不是先重试。采集是长跑,前 20 分钟跑得再快,第 30 分钟被拦下来,整体收益还是零。
4. 踩坑排查:五个让新手翻车的点与现场处理
4.1 元素明明在页面上,却报 NoSuchElementException
现象:手动打开浏览器能看到商品标题和价格,但find_element一直报NoSuchElementException,重试 n 次无效。
原因一般有三个:页面结构改了、懒加载没触发、元素在 iframe 里。京东搜索列表改版后,.p-name em这个路径经常会失效;另外商品列表是滚动懒加载的,直接查找时元素还没渲染进 DOM。
解决:先确认结构,再触发加载。把driver.page_source前 2000 字符打印出来,看 li 元素还在不在,类名有没有变。确认结构没变后,先滚动再查找:
scroll_script = "arguments[0].scrollIntoView();" driver.execute_script(scroll_script, target_element) time.sleep(0.8)这个操作把目标元素滚入视口,懒加载区域的内容会被触发渲染,然后再执行查找,很多“找不到”就能解决。至于 iframe,京东主流程里少见,一旦遇到,先driver.switch_to.frame(frame_name)再找元素。
4.2 登录态丢失:一翻页就回登录页
现象:扫码登录后第一页抓取正常,翻到第二页或第三页,页面突然跳回登录页,后续抓到的全是空数据。
原因:user-data-dir 没复用,或者浏览器版本不一致导致 session 失效。有些人在每次启动时新建了临时用户目录,等于每次都是新访客,京东自然要求重新登录。
解决:确认每次启动 Chrome 都带同一个--user-data-dir=绝对路径。另外,把登录后的 cookie 导出存成 JSON,一旦发现被踢,可以重新注入 cookie,避免再次扫码:
import json from selenium.webdriver.common.by import By cookies = driver.get_cookies() with open("jd_cookies.json", "w", encoding="utf-8") as f: json.dump(cookies, f, ensure_ascii=False, indent=2)重新注入时用driver.add_cookie()逐个加入,注意先访问一次京东域名再注入,否则浏览器会拒绝写入。
4.3 zip 解压报错:伪加密与压缩方法的双重陷阱
现象:解压“selenium爬取京东商品信息.zip”时报文件损坏,或者提示输入密码,但你根本没设置过密码。
原因:zip 伪加密。它只是把压缩条目的 encryption 标志位从0x00改成0x01,文件本身并没有真正加密。Python 的zipfile模块看到标志位就认为加密,直接拒绝解压。
解决:先用zipinfo -v看具体条目,确认是不是伪加密:
zipinfo -v "selenium爬取京东商品信息.zip"看输出里的encryption字段。如果是0x01(伪加密)且压缩方法仍是deflate,用十六进制编辑器把对应条目的标志位改回0x00就能正常解压,全程不需要密码。如果文件是真正加密的,那只能靠密码,普通数据就别折腾暴力破解了,直接找原始来源更省时间。
4.4 价格对不上账:页面价、Plus 价与到手价
现象:抓到的价格和手机上看到的完全不一样,同一个商品,脚本里是 399,详情页里是 359,领券后是 329。
原因:京东列表页展示的往往是“当前展示价”,不是最终到手价。Plus 专享价、满减、优惠券、白条立减这些逻辑全部在详情页或结算链路里生效,列表页的 DOM 里只有基础价。
解决:把抓到的价格当“参考价”处理。需要精确价格时,对每条商品补充访问详情页,用详情页的价格选择器单独抓一遍,再做一版“详情页核价”。注意详情页价格也有异步加载,访问后要等span.price出现再提取,别用固定 sleep。如果你只是做竞品分析,列表页价格够用了,但要写清楚口径,别混用。
4.5 爬着爬着列表变空白:频率被限制后的刹车
现象:跑到第 30 页左右,商品列表区域变成空的,没有报错,页面顶部导航正常,只有商品区不渲染。
原因:大概率是短时间请求次数过多,被风控临时限流。这是黑匣子,没有明确的错误码,页面上也不会有提示,只会给你一个残缺的空列表。
解决:立即停手,不要原地重试。等 30 分钟以上,再把页面间隔从 3 秒调到 6 秒起步。同时给抓取循环加一个“页面商品数校验”:如果一页抓到的商品数少于正常值的一半,自动中断脚本并留下日志,避免后续几百页全是空数据。这一步是整套脚本里最值得加的防护逻辑,哪怕多写几行代码也值。
5. 验证采集结果:抽检比例、字段完整率与断点续爬
5.1 用三条抽样规则判断数据能不能入库
采集完成先别急着分析,用三条规则快速体检。第一条,分层抽样:从每一页里随机抽 1 条,共抽 3% 左右人工核对标题、价格、店铺、链接四要素。第二条,统计空值:价格为空或为 0 的条目占比超过 1%,说明价格选择器已经失效,整批数据需要重新抓取。第三条,重复率:按“标题+价格”去重后计算重复比例,超过 2%,大概率是翻页逻辑出了问题,导致同一页被抓了多次。三条都通过,这份数据才算具备入库条件。
5.2 断点续爬:把进度写进文件,停电也不从头开始
长任务最怕跑到一半中断。我习惯在每页结束时把当前进度写进 JSON,重启脚本时先读进度,跳过往过的页:
import json from pathlib import Path PROGRESS_FILE = Path("progress.json") def load_progress(): if PROGRESS_FILE.exists(): return json.loads(PROGRESS_FILE.read_text(encoding="utf-8")) return {"page": 0} def save_progress(progress): PROGRESS_FILE.write_text( json.dumps(progress, ensure_ascii=False, indent=2), encoding="utf-8" )主循环里每处理完一页调用一次save_progress,重启时用start_page = load_progress()["page"] + 1继续。这套机制比 Excel 断点记录牢靠得多,也方便你随时停掉脚本去处理验证码,处理完再续跑。
有一次我同一天跑了两轮采集,第一轮缺了价格字段,第二轮补抓时没做进度记录,结果多抓了一万多条重复数据,最后靠去重才发现时间白花了。从那以后我把“能跑多久”看得比“能跑多快”更重。希望这些实战细节帮到你,至少让你第一次跑采集时少走几段弯路。
本文还有配套的精品资源,点击获取