拓冰建站拓冰建站
首页 / 资讯中心 / 正文

京东整店图片链接批量采集:Python爬虫从SKU到URL全攻略

做了几年电商代运营和数据整理我最怕听到的一句话就是帮我把这家店铺的商品图全部导出来。第一次接到京东整店图片链接采集的需求时我以为无非就是打开店铺页面几十张图另存为结果一看全店两千多个SKU光主图加详情图就接近两万张手工一张张存显然不现实。后来我把思路从下载图片改成采集图片链接一个小时就全跑完了。这篇文章就想跟你们聊聊京东整店图片链接采集到底是什么思路、URL有哪些规律、怎么用Python把整店图片链接批量抓下来存进数据库以及我在实际采集过程中踩过的一些坑。讲实话这种采集需求在电商圈非常普遍但真正说得清楚的不多。你可能只是想知道怎么拿到高清主图但实际碰到的会是协议相对地址、懒加载图片、店铺接口风控这一大堆问题。这篇文章从需求分析、URL规律、代码实现到防采集应对按我实际操作过的顺序来写适合有Python基础、想自己跑通整店图片采集的运营和技术同学参考。1. 先想清楚采集链接比下载图片到底划算在哪1.1 四个最常见的需求场景先说为什么会有京东整店图片链接采集这种需求。我接到过的需求大概能归成四类。第一类是多平台铺货京东店做起来了要去拼多多、抖音开店商品图需要重新整理上传如果能拿到原图的链接下载或让平台直接拉图会快很多。第二类是店铺数据备份京东商家后台编辑商品时误删主图、被系统判定图片违规下架、或者运营改版导致图片被替换这些情况很常见提前把全店的图片链接备份下来恢复成本极低。第三类是素材库归档美工做活动页需要按SKU找图没有一套完整的图片索引就得去后台一张张翻。第四类是竞品观察不下载图片但定期记录竞品主图链接变化能反推出对方的上新和活动节奏。这四类需求听起来不一样但最后都指向同一个动作把整店所有商品的所有图片链接尽可能完整地、结构化地收集起来。1.2 链接采集和图片下载的代价对比很多人一上来就说把图片下载到本地但我实际测算过这通常是笔亏本买卖。假设一个店铺有3000个SKU平均每个SKU有5张主图加10张详情图图片总数是45000张。京东图片原图单张经常在500KB到2MB之间按平均1MB算全部下载就是45GB左右。这个体量无论是本地存储、云盘同步还是后续批量上传都非常难受。而采集链接就完全不同。一条图片URL短则一两百字符长也不过三四百字符45000条链接撑死20MB文本MySQL里一张表轻松放下。链接是文本天然适合做增量、去重、检索和后续按需拉取。更重要的是京东的图片服务支持通过URL参数直接控制尺寸采集链接之后想生成缩略图、650图、原图改个参数就行不需要本地存储多份文件。我后来做多平台铺货直接给平台填图片URL让它自己拉图比先下载再上传稳定得多。1.3 不碰高危操作采集前的合规边界虽然这篇文章讲采集但有一句话我必须放在前面不要拿这套东西去抓别人的店铺做侵权、仿冒、恶意竞争。我自己的原则是只采集自己有权处理的店铺包括自己经营的店铺、代运营合作方明确授权的店铺、或者拿到书面授权的品牌店铺。抓取节奏要控制在像人一样浏览的范围不开高并发、不爆请求、不绕过登录权限验证。京东本身有开放平台正规需求建议优先研究官方API商家权限够用就直接走官方通道本文这套东西更适合官方API覆盖不到、或者临时需要整理自有数据的场景。数据抓下来只用于内部整理和业务需要不对外贩卖、不发布。2. 京东图片链接的组成规律看懂URL才能改出高清图2.1 京东图片域名和存储路径采集京东图片链接第一步不是写爬虫而是先搞清楚京东的图片URL长什么样。京东图片服务用的是一套独立域名的CDN常见的域名前缀是img10.360buyimg.com到img30.360buyimg.com也有m.360buyimg.com新一些的链接可能是//img*.360buyimg.com/jfs/t1/xxx.jpg这种协议相对形式。URL里的jfs是京东文件存储的标识看到jfs/t1基本可以确定是京东主图体系里的图片。我拿一个实际的链接举例https://img14.360buyimg.com/n1/jfs/t1/123456789_abc.jpg这里img14是CDN节点编号n1是尺寸档位jfs/t1/...是文件在存储中的路径。采集时不需要理解每个节点编号的含义但要记住图片域名可以变但jfs/t1/之后的路径通常就是这张图的唯一标识。后面做去重时我一般以jfs/t1/后面的路径为准域名和尺寸参数都不影响图片本质。2.2 缩略图规格参数从n1到s0京东图片URL里最有价值的就是尺寸档位这一块。不同场景下商品详情页会引用不同档位的图片常见的n0、n1、n2、n5、n7对应的历史经验尺寸如下档位常见用途经验尺寸n0大图800x800及以上n1中图350x350左右n2列表图400x400左右n5缩略图200x200左右n7列表大图450x450左右s0原图标识原尺寸s440x440_自定义尺寸宽高按前缀指定这里要特别说明京东在不同时期调整过图片规格档位对应的实际像素不一定总是上面这个表甚至同一张图同时存在多个档位。我实际验证过把n1改成n0通常能拿到更大的图但也见过部分店铺图只有单一规格。比档位更稳定的取原图方式是看URL里有没有s0比如s0_jfs/...表示原图。遇到s450x450_jfs/...这种自定义尺寸前缀直接去掉s450x450_保留jfs/...部分很多时候也能访问。2.3 去参数、换协议、统一域名采集链接时最常遇到两个问题协议相对地址和URL尾部参数。京东页面源码里非常喜欢用//img14.360buyimg.com/...这种不带协议头的写法浏览器打开没问题但你把这种字符串丢给第三方平台的上传接口、或者用requests直接请求很容易出问题。我的处理方法是统一加https:前缀实在不放心就把http://也转成https://。另外一个坑是图片URL可能会带?结尾的参数或者_m、_s这样的短暂后缀前者可能是日志追踪用的后者往往指向马赛克或缩略变体。你自己写采集程序时可以做一层URL规范化规则包括协议相对地址补全、归档后用jfs/路径去重、如果确认是缩略图变体则尝试还原。需要注意的是不要盲目把所有参数都删掉有些参数可能是图片裁剪指令删了反而拿不到想要的图。我的经验是先保留原始URL入库只在展示或下载时再做规格转换。3. 整店商品ID收集绕开搜索接口的三种可行路径拿到全店的图片链接前提是先拿到全店的商品ID也就是SKU列表。这一步决定了你能覆盖多少商品也是整店采集和单商品采集最核心的区别。京东店铺页面本身就有商品列表但它是异步加载的直接requests请求首页HTML只能拿到第一屏。我整理了自己试过的三条可行路径从易到难排一下。3.1 路径APC店铺页HTML解析最简单直接的方式是请求PC端店铺页URL长这样https://mall.jd.com/index-100012043.html中间那串数字是店铺ID从店铺网址里就能拿到。用requests请求这个页面后页面上方的部分商品会以HTML标签形式渲染出来里面要么是//item.jd.com/123456.html这种商品链接要么是li>import re skus re.findall(r//item\.jd\.com/(\d)\.html, html)第二种是提取>import re skus re.findall(rdata-sku(\d), html)用BeautifulSoup也可以原理一样from bs4 import BeautifulSoup soup BeautifulSoup(html, lxml) items soup.select(li[data-sku]) skus [li.get(data-sku) for li in items if li.get(data-sku)]这里有个经验单靠一种提取方式容易漏数据最好把两种正则和BeautifulSoup的结果合并然后去重。商品ID全是纯数字正则很容易过滤掉其他干扰。拿到SKU列表后先存一份本地或者直接存MySQL方便后面断点续爬。4. 商品详情页的图片链接提取实战SKU列表到手后核心工作就变成了逐个商品页提取图片链接。京东商品详情页的图片分布在三个位置主图区、SKU切换图、详情内容图。这三个位置的提取逻辑不一样我分开说。4.1 主图区spec-list与spec-img京东PC商品详情页的主图区DOM结构大致是#spec-list里放缩略图列表#spec-img里放当前大图。缩略图一般长这样li>from bs4 import BeautifulSoup def extract_main_images(html): soup BeautifulSoup(html, lxml) images [] for li in soup.select(#spec-list li): img li.select_one(img) if not img: continue url img.get(data-url) or img.get(src) if url: images.append(url) return images如果一个商品有多个颜色规格#spec-list里的li可能包含不同SKU对应的图>def extract_detail_images(html): soup BeautifulSoup(html, lxml) images [] container soup.select_one(#J-detail-content) or soup.select_one(#product-detail) if not container: return images for img in container.select(img): url img.get(data-lazyload) or img.get(data-src) or img.get(src) if url and loading not in url.lower() and url ! //img*.360buyimg.com/img/1.gif: images.append(url) return images这里特别提醒如果你发现详情图只抓到3张、5张大概率是选错了属性抓了占位图而不是真实图。不要只信src京东的懒加载属性在不同时期不同版本有差异实际抓的时候可以先打印一个img标签看看属性列表再决定取哪个。4.3 SKU维度的小图如何处理除了主图和详情图很多商品还有SKU维度的图片比如不同款式对应的小图标。这类图往往在#spec-list里和主图缩略图混在一起但尺寸特别小可能只有50x50或100x100。如果你做素材归档小图没什么用如果做SKU级的数据记录小图又必须保留。我的处理方式是在数据库里加一个image_type字段主图为main详情图为detailSKU小图为sku提取时根据DOM位置判断类型写入不同值。这样后面导出Excel时能按类型筛不会把缩略图和主图混在一起。5. 完整脚本从店铺ID到MySQL一张表前面讲的都是零散思路这里给一套能落地的完整脚本。我的设计目标是输入一个店铺ID跑完后MySQL里多一张干净、去重、带类型的图片链接表。为了控制篇幅代码做了简化核心结构和逻辑是完整的。5.1 建表结构与字段设计先建表。我推荐的字段结构如下CREATE TABLE jd_product_images ( id INT AUTO_INCREMENT PRIMARY KEY, sku_id BIGINT NOT NULL COMMENT 商品ID, image_url VARCHAR(500) NOT NULL COMMENT 图片完整链接, image_type ENUM(main,detail,sku) DEFAULT main COMMENT 图片类型, sort_no INT DEFAULT 0 COMMENT 排序号, raw_url VARCHAR(500) DEFAULT COMMENT 原始链接, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_sku_url (sku_id, image_url(200)) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个细节。一是uk_sku_url联合唯一索引重复采集同一商品时不会产生重复记录。二是raw_url字段专门存原始链接image_url存处理后的标准化链接两个都保留方便后续排查问题。5.2 主流程代码下面是一段简化的主流程目录结构就一个Python文件适合临时任务。import re import json import time import random import pymysql import requests from bs4 import BeautifulSoup UA (Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36) HEADERS { User-Agent: UA, Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9, Connection: keep-alive, } def normalize_url(url, referer): if not url: return url url.strip() if url.startswith(//): url https: url elif url.startswith(http://): url https:// url[7:] return url def get_shop_skus(shop_id, max_page10): skus [] headers dict(HEADERS) headers[Referer] fhttps://mall.jd.com/index-{shop_id}.html for page in range(1, max_page 1): url fhttps://mall.jd.com/index-{shop_id}.html?page{page}pageSize30 try: resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 html resp.text except Exception as e: print(f[list error] page{page} err{e}) break page_skus re.findall(r//item\.jd\.com/(\d)\.html, html) page_skus re.findall(rdata-sku(\d), html) page_skus list(dict.fromkeys(page_skus)) if not page_skus: break skus.extend(page_skus) print(f[shop] page{page} got{len(page_skus)} total{len(set(skus))}) time.sleep(random.uniform(1.5, 3.5)) return list(dict.fromkeys(skus)) def get_item_images(sku): headers dict(HEADERS) headers[Referer] fhttps://item.jd.com/{sku}.html url fhttps://item.jd.com/{sku}.html resp requests.get(url, headersheaders, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, lxml) images [] for li in soup.select(#spec-list li): img li.select_one(img) if not img: continue raw img.get(data-url) or img.get(src) if raw: images.append((normalize_url(raw), main, 0, raw)) container soup.select_one(#J-detail-content) or soup.select_one(#product-detail) if container: for i, img in enumerate(container.select(img)): raw img.get(data-lazyload) or img.get(data-src) or img.get(src) if raw and loading not in raw.lower(): images.append((normalize_url(raw), detail, i, raw)) return images def save_images(conn, sku, images): if not images: return 0 sql INSERT INTO jd_product_images (sku_id, image_url, image_type, sort_no, raw_url) VALUES (%s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE sort_no VALUES(sort_no) rows [(sku, url, img_type, sort_no, raw) for url, img_type, sort_no, raw in images] with conn.cursor() as cur: cur.executemany(sql, rows) conn.commit() return len(rows) def main(shop_id): conn pymysql.connect(host127.0.0.1, userroot, password123456, databasejd_spider, charsetutf8mb4) skus get_shop_skus(shop_id) print(f[total] sku count{len(skus)}) for idx, sku in enumerate(skus, 1): try: images get_item_images(sku) saved save_images(conn, sku, images) print(f[{idx}/{len(skus)}] sku{sku} images{saved}) except Exception as e: print(f[item error] sku{sku} err{e}) conn.rollback() time.sleep(random.uniform(2, 5)) if __name__ __main__: main(100012043)这段代码最需要注意的地方是save_images里的ON DUPLICATE KEY UPDATE。如果没有这行重复跑同一个SKU会直接报唯一键冲突加上之后重复采集只会更新排序号不会插入重复链接可以放心做每日增量。5.3 批量插入与增量更新上面代码里用的是executemany这个比一条条execute快很多几千个SKU、几万张图的场景下差别很明显。如果数据量更大你可以改成先攒满几百条再分批commit减少数据库连接压力。断点续爬也是必须考虑的。整店几万个链接一次跑完不太现实中间网络抖动、风控、断电都会中断。我在实际项目里用一个简单方案在本地存一个done.txt每处理完一个SKU就往里追加一行重启时先读取已完成列表跳过这些SKU。比维护数据库状态简单效果也够用。6. 反爬、防采集与请求节奏控制京东整店采集跟单商品采集最大的差异在请求量上。单商品页请求一次就完事整店动辄几千上万次请求非常容易触发风控。很多人在这一步翻车不是代码写错而是请求太快被对面拦了。这一章我专门讲讲把频率控制好之后实际会遇到哪些风控表现。6.1 京东常见的风控表现我不止一次遇到过下面这几种情况按遇到的频率排个序正常返回200但页面内容是登录跳转或验证页商品数据完全不在里面。返回412状态码这在京东系站点很常见属于WAF层面的拦截。商品列表能打开但翻到第3页、第5页突然没有商品数据而且不再恢复。短时间内请求过多后图片链接还能打开但商品详情页开始要求滑块验证。接口返回空JSON或者错误字段页面本身没有任何异常提示。这些表现说明风控不是给你明确弹窗说你太快了而是用一种看起来什么都没发生的方式让你抓到的数据不完整。所以不能只看请求是否成功还要在代码里加校验逻辑如果一页商品ID为零、或详情页提取不到主图就要立即停止而不是继续空跑。6.2 请求头的伪装细节请求头是风控判断的第一层。我在采集时用的UA是完整版Chrome UA不是Python默认的python-requests。Referer也要配套请求店铺列表页时Referer填店铺页链接请求商品详情页时Referer填对应商品页链接。再加上Accept-Language: zh-CN,zh;q0.9尽量让请求看起来像正常浏览器。另外一个细节不要带登录Cookie去抓。很多人习惯抓包时把自己登录后的Cookie全复制过来这是个大坑。登录态Cookie会暴露你的账号身份一旦触发风控可能直接影响账号安全。我建议用一个没有登录任何账号的浏览器冷环境手动打开京东店铺页拿到基础Cookie只带这些基础标识就够了。基础Cookie的作用是让服务端认为这是一个真实浏览器会话而不是为了绕过权限。6.3 遇到412、滑块、验证码的处置顺序真遇到风控了不要慌按下面这个顺序处理立刻停止爬取让IP和cookie都冷静一段时间短则10分钟长则半小时。检查是不是某个页面导致的风控换一个User-Agent或者换一个网络出口再试。降低请求频率把间隔从2秒拉到5秒以上单线程跑。尝试换路径PC端被卡就换移动端店铺页店铺列表被卡就换异步接口。如果某个店铺始终触发滑块就不要再强行抓了等第二天再跑。我自己的体会是整店采集更像是在和风控系统做邻居目标不是突破而是不打扰。用低频、礼貌的方式去请求大多数店铺是能拿完数据的。不要迷信代理池和高并发那些方案对后台数据可能有效但对公开页面来说反而更容易被盯上。7. 踩坑记录我实际跑整店采集时遇到的五个问题最后这部分写几个我踩过的真实问题每一个都是跑了几千个SKU之后才发现的。这些问题不大但很影响数据质量写出来给你们避避坑。7.1 图片链接是协议相对地址导致平台拉图失败第一次跑完采集链接全存进MySQL后我把图片链接导给同事用链接是//img14.360buyimg.com/n1/jfs/xxx.jpg这种。同事填到第三方平台后图片全部加载不出来。原因就是开头少了https:。后来我加了规范化函数把协议相对地址统一补全再导出就没有问题了。这个坑看起来小实际影响面很大特别是数据给不熟悉技术的运营同事用时他们不会自己去补协议头。7.2 URL尾部带参数导致去重失败京东部分图片链接会带时间戳或日志参数同一个图片在不同时间抓可能会得到两条URL实际指向同一个文件只是尾部参数不同。刚开始没注意导致去重失效同一张图在数据库里出现了三四条。我的解决办法是单独存一个raw_url字段保存原始链接然后用jfs/t1/...这一段作为业务去重的辅助判断遇到可确认的参数差异就只保留一条。7.3 详情页懒加载只抓了占位图这个前面提过但我还是想单独说一下。我第一次写详情图提取时用的是img.get(src)结果抓回来的全是loading占位图一张真实的都没有。后来打开实际HTML一看真实图片在>
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门