☰
Java爬虫实战:京东商品详情与价格数据采集解析
2026/10/14 6:26:45 网站建设 项目流程

先从结论说起:京东的商品详情页本身并不适合作为唯一数据源,直接解析HTML看起来简单,但对于价格这类动态字段,往往会踩坑。我做过几次类似的需求之后,更推荐的做法是先用Java请求商品详情接口拿到基础数据,再按业务需要补充价格、促销、库存等信息。这个思路适用于商品比价、价格监控、选品分析,甚至是竞品调研。无论你是刚接触Java爬虫,还是已有一定经验,这篇内容都能帮你少走弯路。

1. 需求拆分与整体方案设计

1.1 标题背后到底要解决什么问题

“爬取京东商品详细信息 价格”这个需求,拆开来看其实包含三个独立的子问题:商品基本信息怎么拿、价格数据从哪个字段取、以及如何保证数据在时效性和准确性上都过关。很多人一开始只盯着“写代码抓网页”,但真正做起来才发现,难点不在发送请求,而在数据解析和防封策略。

我倾向于先把需求画成一条链路:商品ID来源、详情数据获取、价格解析、数据落库、异常补偿。五个环节缺一不可。尤其是商品ID,如果连商品ID都没有,后续所有工作都无法开展。常见的做法是通过站内搜索接口拿到候选列表,或者直接维护一份已知的商品ID清单。人工复制商品链接再解析ID也是一种办法,但量大了之后就不现实了。

1.2 为什么优先选择接口而非页面解析

京东的PC端页面结构比较庞大,整个HTML里塞了大量脚本和样式代码,真正的商品核心信息往往混杂在JavaScript变量中。直接解析HTML有两个问题:一是定位成本高,二是页面改版后解析逻辑就要跟着改。相比之下,接口返回的JSON数据结构稳定、字段明确,维护成本低很多。

我实测下来,商品详情接口能拿到包含名称、品牌、图片、属性、价格信息在内的结构化数据,部分字段缺失时也能快速定位是接口参数的问题还是商品本身的信息不完整。另外,接口有按商品ID记忆化的特性,同一个商品ID在短时间内的重复请求结果基本一致,适合做缓存。

1.3 技术栈选型:Java生态里的几个稳妥方案

Java做爬虫有几种框架可选,我个人的经验是优先考虑HttpClient加JSON解析库的组合,灵活度高,不依赖重型框架,适合中轻量级任务。常见的选型如下:

  • HTTP客户端:Apache HttpClient、OkHttp、Java 11+原生的java.net.http.HttpClient。
  • JSON解析:Jackson、Gson、Fastjson、Hutool的JSONUtil。
  • 页面解析(备用方案):Jsoup,适合解析部分需要从HTML中提取的信息。
  • 数据存储:直接使用MySQL或PostgreSQL,字段简单时也可以用SQLite。

提示:如果采集量不大,建议用OkHttp。它在连接复用、超时控制、拦截器扩展方面都很顺手,代码写起来也简洁。我之前用Apache HttpClient遇到过一次代理切换不生效的问题,换成OkHttp后通过自定义Interceptor就解决了。

2. 核心细节:商品详情与价格数据处理

2.1 商品ID的来源与标准化

拿到商品ID是第一步。京东商品链接的标准格式是:https://item.jd.com/100012043984.html,中间那一串数字就是商品的SKU标识。如果从搜索页面拿数据,接口返回的列表里也会包含skuId字段。需要注意的是,不同入口返回的ID可能是字符串也可能是长整型,统一转成字符串再处理最稳妥,防止后续拼接时出现精度损失。

2.2 详情接口的请求结构与关键参数

商品详情接口一般返回JSON格式数据,里面包含两个大部分:基本信息区,如商品标题、图片列表、品牌、产地;以及销售信息区,如价格、促销标签、库存状态。

请求参数里比较重要的是商品ID和渠道标识。部分接口还需要带上User-Agent和Referer,特别是Referer,如果缺失或与商品页面来源不符,容易触发风控。我在项目中会在请求头里固定设置一组与正常浏览器一致的浏览器指纹信息,包括User-Agent、Accept、Accept-Language、Accept-Encoding,以及一个稳定的Cookie,Cookie不一定需要登录态,但空Cookie有时会让接口返回异常数据。

2.3 价格的三种形态与解析策略

价格字段在接口中可能是数字,也可能是字符串,还可能是嵌套对象。常见的有以下三种:

  • 单品价:直接返回一个数字,比如1299.00。
  • 促销价:包含活动价、秒杀价,或者需要配合优惠券计算到手价。
  • 价格区间:针对多规格商品,返回一个区间,比如2999到3999。

解析时不能直接把整个价格字段当最终值使用,建议先判断字段类型,再按业务规则提取。我踩过一次坑:某商品的接口返回了两个价格字段,其中一个标注为"historyPrice",我误当成现价展示,后来发现那其实是历史参考价,像划线价一样用于衬托优惠力度的。

2.4 动态加载与不稳定的字段

有些页面上的价格是异步加载的,首屏HTML里只有“暂无报价”这类默认文案,真实价格要等待额外的价格查询接口返回。这种情况下,解析HTML几乎没有意义,必须直接定位接口。一个简单的判断方法:用浏览器开发者工具打开Network面板,刷新商品页,观察哪个请求返回了包含价格数字的JSON,那就是你要找的接口。抓到那个接口后,用Java模拟请求即可,不需要关心页面如何渲染。

如果在接口里也找不到价格,我建议直接搜索该商品在移动端H5页面的对应的数据接口,接口路径可能不同,但返回的数据通常更精简,解析更容易。

2.5 数据入库时的信息保留

价格是实时性很强的数据,入库时至少要保留采集时间、商品ID、价格快照、原始请求状态。建议商品信息放一张表,价格历史单独一张表。价格表主键可以是商品ID加采集时间,这样既能查最新价,也能回溯历史。我在做某跨平台商品比价项目时,还额外加了一个业务日期字段,专门用天级别聚合查询,效果很好。

下面给出一个建表参考:

CREATE TABLE product_info ( sku_id VARCHAR(32) PRIMARY KEY, title VARCHAR(512), brand VARCHAR(128), category_name VARCHAR(128), image_url VARCHAR(512), detail_data JSON, update_time DATETIME ); CREATE TABLE product_price_history ( id BIGINT AUTO_INCREMENT PRIMARY KEY, sku_id VARCHAR(32), price DECIMAL(10,2), promo_price DECIMAL(10,2), price_type TINYINT, crawl_time DATETIME, UNIQUE KEY uk_sku_time (sku_id, crawl_time) );

3. 实操过程:从请求到落库的完整实现

3.1 准备开发环境与依赖

我用的环境是JDK 17,构建工具是Maven。依赖方面,选择OkHttp、Jackson、Hutool、MySQL驱动,以及一个简单的连接池。如果不用连接池,也可以直接用Spring的JdbcTemplate配合数据源。

这里给出Maven依赖的核心部分:

<dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.12.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

3.2 请求头的完整配置

请求头是整个爬取逻辑里最容易出错又最影响成功率的部分。我整理了一份常用模板,你可以直接抄作业:

OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES)) .addInterceptor(new Interceptor() { @Override public Response intercept(Chain chain) throws IOException { Request original = chain.request(); Request request = original.newBuilder() .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") .header("Accept", "application/json, text/plain, */*") .header("Referer", "https://item.jd.com/") .header("Accept-Language", "zh-CN,zh;q=0.9") .header("Connection", "keep-alive") .build(); return chain.proceed(request); } }) .build();

注意:Referer里的商品ID和实际请求的商品ID保持同站即可,不要留空。有些接口要求Referer必须来自商品页域名,否则会返回403或验证页。

3.3 获取商品详情核心代码

这里给出一个简化版的核心方法,思路是传入商品ID,请求详情接口,解析JSON并返回一个商品对象。

public ProductInfo fetchProductDetail(String skuId) throws IOException { String url = "https://api.example.com/item/" + skuId; // 按实际接口替换 Request request = new Request.Builder() .url(url) .get() .build(); try (Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { throw new IOException("请求失败: " + response.code()); } String body = response.body().string(); JsonNode root = objectMapper.readTree(body); ProductInfo info = new ProductInfo(); info.setSkuId(skuId); info.setTitle(root.path("data").path("title").asText()); info.setBrand(root.path("data").path("brand").asText()); JsonNode priceNode = root.path("data").path("price"); if (priceNode.isNumber()) { info.setPrice(priceNode.decimalValue()); } return info; } }

实际开发中,接口地址和字段名需要根据目标接口做调整。先用在线接口调试工具确认返回结构,再写解析代码,能节省大量调试时间。

3.4 批量任务与限速控制

批量采集时,不建议使用线程池无限并发。电商平台对高并发请求很敏感,[举个例子],50个线程同时请求同一个接口,跑不了几分钟就会陆续出现验证或超时。我建议采用固定线程池加信号量限速的方式,将每秒请求数控制在个位数,并在任务队列中增加随机延时。

ExecutorService executor = Executors.newFixedThreadPool(5); for (String skuId : skuIdList) { executor.submit(() -> { try { ProductInfo info = fetchProductDetail(skuId); saveProduct(info); } catch (Exception e) { log.error("处理失败 sku: {}", skuId, e); } try { Thread.sleep(ThreadLocalRandom.current().nextLong(1000, 3000)); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } executor.shutdown();

这种写法虽然简单,但在生产环境已经足够用。如果采集量达到十万级以上,建议将任务列表放到消息队列里,消费者按需拉取,这样更好控制速率和失败重试。

3.5 入库与更新策略

商品基础信息使用REPLACE INTO或INSERT ON DUPLICATE KEY UPDATE方式写入,确保SKU一致时更新内容和更新时间。价格历史表则直接INSERT,每次采集都新增记录。

public void saveProduct(ProductInfo info) { String sql = "INSERT INTO product_info (sku_id, title, brand, category_name, image_url, detail_data, update_time) " + "VALUES (?, ?, ?, ?, ?, ?, ?) " + "ON DUPLICATE KEY UPDATE title=VALUES(title), brand=VALUES(brand), update_time=VALUES(update_time)"; // 执行JDBC操作 }

这里要留意JSON字段的处理。MySQL的JSON类型支持得很好,但前提是写入的JSON字符串必须是合法的。我建议在保存前调用一次序列化器的writeValueAsString,不要手动拼字符串,避免因括号遗漏导致入库失败。

3.6 缓存与去重

有些商品短期内价格不变,重复采集会造成不必要的资源浪费。我习惯在采集前查一次最新记录的时间戳,如果在预设的过期时间内,就跳过采集。比如价格更新不频繁的商品可以设置30分钟过期时间,促销期间可以缩短到5分钟。

public boolean isFresh(String skuId, Duration maxAge) { // 查最新crawl_time LocalDateTime last = priceHistoryDao.getLastCrawlTime(skuId); if (last == null) { return false; } return last.isAfter(LocalDateTime.now().minus(maxAge)); }

4. 常见问题与排查技巧实录

4.1 请求返回异常状态码

出现403或406时,优先检查请求头。403多半是User-Agent或Referer不符合要求,406可能是Accept字段设置不对。有一次我把Accept直接设置成application/json,但目标接口只接受text/html,结果持续返回406,排查了半天。

4.2 返回数据缺少价格字段

这不是代码问题,是接口选择问题。商品详情接口里没有价格时,换一个接口试试,比如搜索接口返回的列表信息里往往就包含价格字段。或者查看移动端页面加载时调用的接口,它们返回的价格数据通常更直接。

4.3 价格字段是null或空字符串

如果接口返回的价格是null,一种情况是该商品已下架或区域缺货,另一种情况是该接口需要额外的区域参数。部分平台的价格接口依赖于用户定位,如果你用的是默认地区,可能拿不到价格。这时可以在请求参数中补充一个常见的省会城市ID,实测能解决大部分缺价问题。

4.4 验证码与滑块

验证码的触发原因通常是请求频率过高,或者请求特征过于类似。不要尝试绕过验证码,而是降低频率并让请求重新变得随机。除了延时,还可以在白天与半夜使用不同的请求频率,打破规律。如果条件允许,引入代理池也能降低风险,但代理质量参差不齐,有时反而会拖慢请求,需要做质量评估。

4.5 JSON解析异常

接口正常返回了JSON,但使用Jackson解析时抛出UnrecognizedPropertyException,这不是项目写错了,而是Jackson默认会忽略未知字段,但你配置了FAIL_ON_UNKNOWN_PROPERTIES=true。建议把该配置设为false,避免因接口增加字段导致整体解析失败。

4.6 数据入库时大文本字段超长

商品标题可能长达几百个字符,图片URL也可能超长。建表时VARCHAR长度给足,或者直接使用TEXT。我曾经因为字段长度限制导致title被截断,相比商品信息不完整,插入失败更麻烦,所以字段类型和长度一定要提前想清楚。

5. 想要抓得稳,还得会观察这些细节

采集工作有七分在准备,三分在代码。准备不仅指技术方案的选型,还包括对目标接口行为的观察。我的习惯是先用Postman一类工具手动发几次请求,记录每次请求返回的响应时间、状态码、数据是否完整。连续多请求几十次后,观察失败率的变化,就能大致摸清平台的容忍度,再据此制定采集节奏。

另一个细节是错误日志。采集任务的失败重试不能盲目重试,每次重试前要判断失败原因。如果是限流或验证码,等一段时间再重试;如果是参数错误,重试多少次都没意义,应当记录并跳过。

提示:在开发调试阶段,不要用真实商品ID做压力测试。更稳妥的办法是先拿5到10个商品ID循环请求,观察返回结果是否一致,确认接口稳定后再扩展到全量商品。

6. 更近一步:把价格数据用起来

单纯把价格抓下来存进数据库只是一半工作,真正有价值的是基于价格数据做分析和决策。我做过比较典型的应用有两个:

第一个是价格监控与告警。针对关注的商品,按小时或天采集价格,如果价格降到设定阈值,就推送提醒。实现时就是在价格历史表上跑一个定时查询,筛选出当前价格低于记录的最近N天最低价的商品,同时去重告警,防止每次采集都重复发送。

第二个是竞品比价分析。把同品类多个竞品的历史价格拉出来对比,看谁在促销、谁在涨价、价格调整频率如何。这类分析用到的主要是MySQL窗口函数,比如用LAG计算相邻两次采集的价格差值,就能发现降价频次。

这两个应用都不需要扩大采集范围,只需要在你已有的单商品采集基础上,增加商品ID数量和采集频率即可。真正考验人的是数据可靠性,毕竟分析结果的准确性取决于原始价格数据的质量。

7. 客观认识采集边界

最后说点实际的。做采集要清楚边界,不能只盯着技术。首先要遵守相关法律法规和平台规则,只采集合法公开的信息,不碰用户隐私,不破坏服务稳定性。其次,个人学习和小规模试验可以,但如果要规模化采集商业数据,务必评估法律风险,必要时咨询专业人士。

技术上也有一些替代方案。不少平台提供开放API或数据服务,如果业务预算允许,优先使用官方渠道,既稳定又省心。爬虫方案更适合技术预研、竞品观察等临时场景。我的建议是,在动手写代码前,先花时间确认数据源的合法性,再评估技术方案的性价比,这样对个人成长和项目交付都更负责。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询