简介:基于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 文件统计背后的工程信号
拿到源码第一件事,我习惯先数文件。这个项目的文件构成(见下表)很能说明问题:
| 文件类型 | 数量 | 说明 |
|---|---|---|
| JavaScript | 1640 | 前端动态逻辑和部分抓取辅助脚本 |
| HTML | 796 | 模板页面,也代表爬虫要应对的页面结构样本 |
| PNG | 754 | 页面截图资源或静态图片 |
| GIF | 672 | 动态图片资源,抓取策略要单独考虑 |
| JSON | 408 | 数据交换格式,爬虫抓取结果通常以JSON存储 |
| LESS | 356 | 样式预编译文件 |
| CSS | 306 | 已编译样式文件 |
| Java | 303 | 核心爬虫逻辑与业务控制 |
| Markdown | 165 | 文档与说明 |
| JPG | 140 | 内容图片 |
从比例上看,Java文件只占5%,却是整个项目的灵魂,因为真正的数据抓取、解析、存储调度全在这303个文件里。前端资源占比高,也侧面说明项目不止做抓取,还带了一套可展示的前端界面,方便直接看抓来的数据。这个形态很适合当二次开发的底子——抓下来的数据不用你另外写可视化,项目里已经有一套页面可以对接。
2.2 Maven pom.xml的依赖线索
项目支持Maven构建,核心证据就是根目录的pom.xml。先看这个依赖清单,能判断项目的技术路线。常见的Java爬虫比价项目,pom.xml里通常包含这样一组依赖:
<dependencies> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency> <dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.14.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> <dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>2.3.2</version> </dependency> </dependencies>HttpClient负责发请求,Jsoup负责解析HTML,Jackson负责处理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/xhtml+xml"); 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的batch:
String 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 { List<String> urls = getTargetUrls(); for (String url : urls) { try { String html = httpClient.getPage(url); List<PriceRecord> 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改为salePrice,CSS选择器失效。更隐蔽的是页面改版后数据从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%就要查原因。这套动作看着简单,但真的能拦下不少翻车事故。希望帮到你。
本文还有配套的精品资源,点击获取