
简介基于Java语言的比价网爬虫开源项目面向有Java基础、想学习网络爬虫的开发者以Java结合JavaScript、CSS等技术搭建比价网站数据抓取与分析框架。压缩包共2000个文件约122.75MB含698个JavaScript、430个HTML、303个Java源码、199个CSS及135个Markdown文档等Java源码负责核心爬取逻辑JS用于页面解析JSON/XML承载数据配置目录结构清晰。资源内含完整工程目录、Maven配置与数据库脚本可导入工程运行也便于扩展新站点项目覆盖多线程请求、页面解析、数据存储等爬虫设计要点工程参考价值高目前已有83人学习下载适合入门参考与二次开发。1. 比价网Spider开源项目一个能直接改来用的抓取工程做比价网站最折腾人的不是前端页面而是每天怎么把几十个竞品站点的价格稳定抓下来。手动复制粘贴撑不了几天写一次性脚本又扛不住页面改版。这个基于Java的比价网Spider开源源码就是一个完整的抓取工程——它不只是一堆爬虫脚本而是把请求、解析、存储、调度都串起来了包含6000多个文件其中Java源码303个光JavaScript就有1640个说明它对动态页面处理花了不少功夫。适合正在做比价系统、电商数据采集或者想研究Java爬虫项目整体架构的开发者。源码里附带readme和Maven的pom.xml理论上拉下来构建就能跑实际要改配置的地方我在后面会逐个说。2. 看懂项目骨架6073个文件与Maven依赖的架构信号2.1 文件统计背后的工程信号拿到源码第一件事我习惯先数文件。这个项目的文件构成见下表很能说明问题文件类型数量说明JavaScript1640前端动态逻辑和部分抓取辅助脚本HTML796模板页面也代表爬虫要应对的页面结构样本PNG754页面截图资源或静态图片GIF672动态图片资源抓取策略要单独考虑JSON408数据交换格式爬虫抓取结果通常以JSON存储LESS356样式预编译文件CSS306已编译样式文件Java303核心爬虫逻辑与业务控制Markdown165文档与说明JPG140内容图片从比例上看Java文件只占5%却是整个项目的灵魂因为真正的数据抓取、解析、存储调度全在这303个文件里。前端资源占比高也侧面说明项目不止做抓取还带了一套可展示的前端界面方便直接看抓来的数据。这个形态很适合当二次开发的底子——抓下来的数据不用你另外写可视化项目里已经有一套页面可以对接。2.2 Maven pom.xml的依赖线索项目支持Maven构建核心证据就是根目录的pom.xml。先看这个依赖清单能判断项目的技术路线。常见的Java爬虫比价项目pom.xml里通常包含这样一组依赖dependencies dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpclient/artifactId version4.5.13/version /dependency dependency groupIdorg.jsoup/groupId artifactIdjsoup/artifactId version1.14.3/version /dependency dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.13.0/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdorg.quartz-scheduler/groupId artifactIdquartz/artifactId version2.3.2/version /dependency /dependenciesHttpClient负责发请求Jsoup负责解析HTMLJackson负责处理JSON数据MySQL驱动负责持久化Quartz负责定时任务——这五件套基本就是Java比价爬虫的标配组合。在实际项目里改pom.xml时最需要注意的是版本冲突比如HttpClient 4.x只能配合对应版本的commons-codec盲目升到5.x会把整个抓取流程搞崩。建议先按项目原本的依赖跑通确认没问题后再考虑升级。2.3 src与db目录的标准分工src/ ├── main/ │ ├── java/ # 核心代码controller、service、spider、db │ └── resources/ # 配置文件log4j.properties、jdbc.properties └── test/ # 单元测试 db/ ├── schema.sql # 建表脚本 └── init.sql # 初始化数据这个布局是Java Web项目的通用形态。src目录下通常按三层拆分controller层负责对外提供接口service层做业务处理spider层是最关键的抓取逻辑db层封装数据库访问。db目录里的schema.sql定义了商品表、价格记录表等核心表结构比价网站的核心数据就落在这些表里。你在复现的时候先看db目录的建表脚本再看spider层的代码基本就知道数据是怎么流动的了。2.4 readme.txt与配置文件是第一个入口项目里专门提到readme.txt包含介绍、安装和运行说明这个文件是你复现时的第一入口。我一般会在跑代码之前先做三件事打开readme看启动步骤打开resources目录看jdbc.properties里的数据库连接配置打开log4j.properties看日志输出路径。很多看起来“跑不起来”的问题其实都出在数据库密码没改、日志路径不存在这类小事上。2.5 多语言共存的合理性项目同时使用了Java、Ruby和PHP这不是乱来。常见做法是Java做主流程前端用JavaScript处理动态渲染个别辅助脚本用Ruby或PHP写——比如某些站点的商品变更探测用Ruby脚本更顺手PHP则可能承担一部分旧版的接口服务。你在复现时不用急着删掉非Java文件——它们很可能是某个环节的补充工具。我一般会先跑一遍全流程观察哪些脚本被实际调用再决定保留还是清理避免误删功能模块。3. 爬虫核心链路请求构建、Jsoup解析与JSON字段提取3.1 请求层构建可配置的抓取入口爬虫的第一步是稳定地拿到页面。项目里用HttpClient的场景通常是这样的——把连接池、超时和重试统一封装成一个客户端抓取其他页面时直接复用public class HttpRequestClient { private CloseableHttpClient httpClient; public HttpRequestClient() { // 连接池配置控制最大连接数避免高并发下把目标站点和自己一起压垮 PoolingHttpClientConnectionManager pool new PoolingHttpClientConnectionManager(); pool.setMaxTotal(50); pool.setDefaultMaxPerRoute(10); RequestConfig config RequestConfig.custom() .setConnectTimeout(5000) .setConnectionRequestTimeout(3000) .setSocketTimeout(8000) .build(); httpClient HttpClients.custom() .setConnectionManager(pool) .setDefaultRequestConfig(config) .setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)) .build(); } public String getPage(String url) throws IOException { HttpGet httpGet new HttpGet(url); // 伪装User-Agent避免被目标站点直接拒绝 httpGet.setHeader(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64)); httpGet.setHeader(Accept, text/html,application/xhtmlxml); try (CloseableHttpResponse response httpClient.execute(httpGet)) { int status response.getStatusLine().getStatusCode(); if (status ! 200) { throw new IOException(HTTP error: status); } return EntityUtils.toString(response.getEntity(), UTF-8); } } }这里几个参数值得注意连接超时5秒读取超时8秒最大重试3次。连接池上限50单路由上限10——这不是拍脑袋定的而是根据比价场景里常见的目标站点频率限制算出来的并发太高容易被封并发太低抓几十万商品实在太慢。HTTP状态码检查非常关键很多站点在反爬时会返回403或302你要在拿到页面之前先拦下来别把错误页面存进数据库。3.2 页面解析Jsoup选择器的落地写法解析HTML是另一个大头。项目里有796个HTML文件说明抓取对象主要是商品详情页和列表页。用Jsoup解析价格字段常见的写法是Document doc Jsoup.parse(html); Elements priceItems doc.select(span.price, div.sale-price); for (Element el : priceItems) { String priceText el.text(); // 去掉货币符号和空格转为BigDecimal if (priceText.contains(¥)) { priceText priceText.replace(¥, ).trim(); } if (priceText.contains()) { priceText priceText.replace(, ).trim(); } try { BigDecimal price new BigDecimal(priceText); System.out.println(解析到价格: price); } catch (NumberFormatException e) { // 记录日志跳过这条数据不中断整个任务 logger.warn(价格解析失败, 原始文本: {}, priceText); } }选择器“span.price, div.sale-price”是通用写法实际项目里要按目标页面的CSS类名调整。我一般会在开发阶段先把页面保存成HTML文件然后用Jsoup反复测试选择器确认能稳定提取后再上正式任务。这里有个习惯值得养成解析失败时不要直接抛异常记录下来继续跑比价爬虫的数据量大单条失败不影响大局但要留证据。3.3 JSON接口解析字段缺失不能炸项目里有408个JSON文件说明很多数据不是从HTML里解析的而是直接从接口拿JSON。用Jackson解析的典型写法ObjectMapper mapper new ObjectMapper(); JsonNode root mapper.readTree(jsonString); JsonNode data root.path(data); if (data.isArray()) { for (JsonNode item : data) { String title item.path(title).asText(); BigDecimal price item.path(price).decimalValue(); // 字段是否存在先判断再取值避免解析异常 if (item.has(originPrice)) { BigDecimal originPrice item.path(originPrice).decimalValue(); // 处理原价和现价 } } }这个写法的好处是字段缺失时不会直接抛异常——path()方法会返回一个“空节点”asText()返回空字符串。这个习惯在比价爬虫里很重要因为电商接口改字段是常态少一个字段就崩掉整个任务是不可接受的。另外需要注意decimalValue()在字段值为null时会抛异常稳妥的做法是先判断isNumber()再取值。项目里的JSON文件数量这么多说明数据交换的核心格式就是JSON这里值得多花点时间把容错做扎实。3.4 动态页面的处理不止是解析静态网页项目里1640个JavaScript文件提醒我们很多目标站点的数据是动态渲染的。常见做法有两种第一种是从JSON接口直接取数据绕开渲染过程第二种是用无头浏览器渲染后再解析。如果项目里既有大量JS又有大量JSON通常意味着抓取链路里结合了这两种方案。我一般优先走接口实在找不到接口才上无头浏览器因为无头浏览器的资源消耗是普通请求的几十倍。4. 数据持久化价格落库、批量写入与定时调度4.1 表结构设计商品表和价格历史表分开比价系统的核心是两张表商品表和价格历史表。商品表存SKU基础信息价格历史表存每次抓取的价格快照这样才能做价格走势分析。常见建表结构CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku VARCHAR(64) NOT NULL, title VARCHAR(255), category VARCHAR(128), brand VARCHAR(64), UNIQUE KEY uk_sku (sku) ); CREATE TABLE price_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, price DECIMAL(10,2) NOT NULL, capture_time DATETIME NOT NULL, source VARCHAR(32), INDEX idx_product_time (product_id, capture_time) );这里有两个容易出错的点第一个是sku要做唯一索引防止同一个商品被重复插入第二个是价格字段必须用DECIMAL不能用FLOAT或DOUBLE——浮点数的精度问题在价格场景下会引发非常隐蔽的计算错误。比如某商品价格是19.99元用FLOAT存一遍再读出来可能变成19.989999比价结果就会有莫名其妙的几分钱差异。项目里的db目录如果已经有schema.sql基本也是类似结构你可以对照着调整字段名字。4.2 批量写入别一条条insert抓取是高频操作一条条insert太慢。批量写入的常规走法是用JDBC的batchString sql INSERT INTO price_record (product_id, price, capture_time, source) VALUES (?, ?, ?, ?); try (PreparedStatement ps conn.prepareStatement(sql)) { for (PriceRecord record : records) { ps.setLong(1, record.getProductId()); ps.setBigDecimal(2, record.getPrice()); ps.setTimestamp(3, new Timestamp(record.getCaptureTime().getTime())); ps.setString(4, record.getSource()); ps.addBatch(); } ps.executeBatch(); }注意executeBatch()要配合事务使用否则半截失败时数据会出现一半入库一半丢失。另一个方案是INSERT ... ON DUPLICATE KEY UPDATE利用唯一索引实现幂等写入适合增量更新场景。批量大小也有讲究我一般每批500到1000条太大反而会因为SQL语句过长或锁竞争导致性能下降。4.3 定时任务调度Quartz的典型用法比价爬虫的灵魂是定时跑。项目采用Quartz做调度是常见做法public class PriceCrawlJob implements Job { Override public void execute(JobExecutionContext context) throws JobExecutionException { ListString urls getTargetUrls(); for (String url : urls) { try { String html httpClient.getPage(url); ListPriceRecord records parseAndExtract(html); saveBatch(records); } catch (Exception e) { // 单条失败不影响整体任务记录日志继续下一站 logger.error(任务执行失败, url{}, url, e); } } } }调度间隔一般根据目标网站更新频率来定价格敏感型商品建议1小时一次普通商品一天一次就够。频率太高的结果不是数据更准而是更快被反爬封掉。Quartz的cron表达式里比价爬虫最常用的就是“0 0 * * * ?”这种整点执行的配置或者错峰执行“0 0/30 * * * ?”每半小时跑一次。注意把任务执行时间点分散开避免多个站点在同一秒发起请求。4.4 增量更新用批次号做幂等去重定时任务跑久了总会有重跑的情况——网络抖动导致某一次数据缺失你手动补跑。如果直接重复insert就会出现重复数据。比较稳的做法是引入批次号CREATE TABLE capture_batch ( id BIGINT AUTO_INCREMENT PRIMARY KEY, batch_no VARCHAR(32) NOT NULL, source VARCHAR(32), start_time DATETIME, end_time DATETIME, status TINYINT, UNIQUE KEY uk_batch_no (batch_no) );price_record表里也加一个batch_no字段。每次定时任务启动时生成一个batch_no重跑时沿用同一个batch_no配合唯一索引就能保证同一批任务不会重复入库。这个设计在比价爬虫里特别实用——你想手动补抓某一小时的数据时不用担心和已有数据冲突。4.5 数据库连接池别手动管理Connection比价爬虫的数据库操作频繁直接用DriverManager.getConnection()每次新建连接性能很差。项目如果没配连接池我建议加上HikariCP——它是目前Java生态里比较稳的选择HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/price_db); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(20); config.setMinimumIdle(5); HikariDataSource dataSource new HikariDataSource(config);把连接池的最大连接数控制在20左右就够了。比价爬虫的核心瓶颈通常不在数据库而在目标网站的反爬限制连接池开再大也没用。把连接池配上之后记得用完的Connection要能正常归还到池里否则连接耗尽后整个采集任务会停摆。5. 避坑指南比价爬虫实战中的五个高频问题爬虫项目跑起来容易稳定跑三个月很难。下面这几条都是比价爬虫场景里比较高频的坑按“现象→原因→解决”给你排一遍。5.1 请求头太单薄所有请求突然变403现象代码跑起来第一轮还能抓到数据跑了十几分钟之后所有请求都返回403。原因目标站点检测到多个请求都是同一个User-Agent而且没有Referer字段直接把IP段拉黑了。尤其是比价网站的目标站点本身就配有反爬系统对请求频率非常敏感。解决把User-Agent抽成数组每次请求随机选一个常见的浏览器版本private static final String[] USER_AGENTS { Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36, Mozilla/5.0 (Linux; Android 12; SM-G991B) AppleWebKit/537.36 }; private String randomUserAgent() { return USER_AGENTS[new Random().nextInt(USER_AGENTS.length)]; }同时把Referer设置为目标站点的首页地址并且每次请求之间加随机延迟比如300到800毫秒。这个延迟不能太固定固定间隔反而更容易被识别成机器访问。5.2 页面编码识别错误解析出来全是乱码现象某目标页面返回的HTML里明明写了UTF-8但Jsoup解析后中文全部变成问号。原因EntityUtils.toString(response.getEntity(), UTF-8)是写死的而部分页面实际使用GBK编码。HTTP头里的charset和HTML的meta标签经常不一致写死编码就会踩坑。解决先用响应头或HTML的meta标签探测字符集再决定转码方式public static Charset detectCharset(byte[] bodyBytes) { String head new String(bodyBytes, StandardCharsets.ISO_8859_1); Pattern p Pattern.compile(charset[\]?([a-zA-Z0-9-])); Matcher m p.matcher(head); if (m.find()) { String charset m.group(1); if (!charset.equalsIgnoreCase(utf-8) !charset.equalsIgnoreCase(gbk)) { return StandardCharsets.UTF_8; } return Charset.forName(charset); } return StandardCharsets.UTF_8; }把编码探测做成公共工具类所有页面解析前先过一遍能省掉大部分乱码问题。特别是国内电商页面GBK和UTF-8混合出现的情况很常见。5.3 价格字段转换异常任务大量报错现象日志里频繁出现NumberFormatException程序动不动就卡在一个JSON解析节点上。原因页面价格变成“促销中”“到价提醒”这类非数字文本或带有空格和全角字符BigDecimal直接解析失败。另一个常见情况是价格里有千分位逗号比如“1,299.00”new BigDecimal()会直接抛异常。解决解析前先做文本清理用正则把非数字字符过滤掉String cleaned priceText.replaceAll([^0-9.], ); BigDecimal price new BigDecimal(cleaned);同时把解析失败的情况单独捕获记录当前URL和原始文本继续抓下一个。这里最重要的是不中断整个任务。我在实际项目里专门做了一个“解析失败原因统计”跑完一批任务后看失败原因分布是普遍性问题还是封禁问题一目了然。5.4 定时任务内存持续增长一周后OOM现象项目部署后第一天正常一周后内存占用稳定爬升最终OOM。原因HttpClient连接没有释放每次定时任务新开的线程又累积了未关闭的响应流。EntityUtils.toString()虽然关了流但如果连接池配置不当连接会一直占着。另一个隐性问题Jsoup解析后的Document对象引用被静态变量持有也容易堆内存泄漏。解决用try-with-resources确保close给HttpClient配置连接池最大值并设置空闲回收时间。这次请求用完的response尽快释放具体可以加httpClient HttpClients.custom() .setConnectionManagerShared(pool) .evictIdleConnections(60, TimeUnit.SECONDS) .build();每次定时任务跑完主动调用一下连接回收逻辑别等着GC替你干活。踩过这个坑之后我养成了一个习惯凡是跑长周期的定时任务一定在任务前和任务后各看一眼堆内存确认没有明显增长再部署。5.5 目标页面改版选择器全部失效现象连续一段时间采集正常某天之后价格字段全部抓不到。原因目标电商页面改版class名从price改为salePriceCSS选择器失效。更隐蔽的是页面改版后数据从HTML里移到了某个JS变量里解析逻辑完全对不上。解决引入页面解析失败的告警机制——当某个URL连续3次解析结果为空时自动通知维护人员。我在实际项目中加了一个简单规则解析不到数据时先把原始HTML存成本地文件方便对比if (priceItems.size() 0) { logger.warn(解析结果为空, 保存原始页面: {}, url); FileUtils.writeStringToFile(new File(./debug/ System.currentTimeMillis() .html), html, UTF-8); }这样页面一改版你马上能在debug目录里看到最新结构对比一下就能改选择器修复时间从半天缩到半小时。6. 用日志和断言给爬虫上保险验证的最终姿势想确认爬虫是不是真的正常工作不能只靠肉眼看数据库要有可量化的手段。第一个手段是日志埋点。我在关键节点会这样记logger.info(抓取完成: url{}, 耗时{}ms, 解析商品数{}, url, costMs, parsedCount); logger.warn(抓取异常: url{}, status{}, 响应片段{}, url, status, snippet);记什么很重要——URL、状态码、耗时、解析数量。当数据出现异常波动时回头翻日志能快速定位是请求层挂了还是解析规则失效。第二个手段是数据断言。每次抓完跑一个校验if (totalRecords 0) { logger.error(本次任务解析结果为空触发告警); } if (avgPrice 1 || avgPrice 100000) { logger.error(价格均值超出正常区间疑似解析错位); }还有一个比价场景特有的校验同一款商品在不同站点之间的价格差不能超过合理范围比如同一型号的数码产品两个站点报价差30%以上大概率是某个站点的解析规则出了问题需要人工复核。这个校验可以直接放在入库后的SQL查询里也可以写成独立的Java校验任务。做爬虫最怕的就是“数据在跑但跑的都是错的”。从那以后我每次上线新的解析规则都会强制走一遍这个流程先干跑一批数据看总量和均值再看总量有没有掉到0最后抽查三条原始记录和数据库里的值是不是对得上。最后再用一份一个月的数据做回头校验——把抓到的价格走势和目标站点抽样的价格对比偏差超过5%就要查原因。这套动作看着简单但真的能拦下不少翻车事故。希望帮到你。本文还有配套的精品资源点击获取