
简介这是一份用Java语言实现的比价网站爬虫开源项目面向需要构建比价数据采集与分析系统的开发者适合爬虫技术学习、二次开发和项目实战。压缩包共包含2000个文件大小约122.75MB文件类型以JavaScript、HTML、Java为主并涵盖CSS、Markdown、JSP、XML、JSON、Properties等分别承担页面交互、结构解析、爬虫逻辑、样式设计、文档说明、动态页面处理及配置管理等工作整体目录结构清晰便于快速定位核心模块。目前已有81人加入学习作为开源项目附带了项目说明、Maven配置和数据库相关资源能够帮助使用者理解从数据抓取、解析清洗到存储入库的完整链路尤其适合希望深入了解网络爬虫架构设计的开发者实战参考。1. 从比价场景看Java爬虫的工程化边角拿到“基于Java语言的比价网Spider开源设计源码”这个标题时我第一反应不是爬虫怎么写而是比价网真正的难点在哪。一个能上线的比价系统它的Spider每天要处理几十万张商品页从不同电商站点抽取同款商品的价格、规格、促销信息再做去重、归一化、持久化最后通过接口供前端查询。真正的工作量不在“抓”而在“理”同一个SKU在不同平台可能型号写法不同价格快照怎么存、怎么判断涨跌、怎么避免重复抓取这些才是比价网Spider源码里最有价值的部分。本文以Java技术栈为主线从开源设计常见的模块拆分讲起落到可执行的代码和参数适合已经能写单机爬虫、想把系统做成工程化服务的开发者。新手也能跟着步骤跑通一个最小闭环。2. 比价网Spider的模块划分与数据模型设计2.1 为什么用Java而不是Python做比价爬虫很多初创团队用Python做爬虫原型因为requests加BeautifulSoup写起来快。但比价网到了生产阶段Java的优势会逐渐显出来一是并发体系成熟线程池、阻塞队列、Future配合Spring的调度能精确控制抓取速率二是类型安全商品和价格的强类型模型在字段膨胀时不容易出错三是部署生态WAR或Jar包丢到传统中间件直接跑运营人员用JMX就能监控。比价网Spider的开源设计里通常把采集和解析分离成两个独立模块中间用消息队列或内存阻塞队列解耦。采集模块负责网络IO解析模块只处理HTML字符串。这样当某个电商改版页面时只动解析规则抓取线程不受影响。模块之间通过一个SpiderContext传递上下文包括请求头、当前URL、深度和自定义属性。2.2 从源码看Spider核心模块组成一个典型的Java比价网Spider源码会划分为以下模块每个模块对应一个Maven子工程或者一个包。我见过的最小可运行版本包含五个部分模块职责关键类collector负责网络请求与响应下载HttpCollectorparser解析HTML抽取商品字段ProductParsernormalizer字段清洗、单位换算、同品归一Normalizerstorage写入MySQL、Redis或ESProductStorescheduler任务队列、去重、调度策略SpiderScheduler这个划分的核心思想是“规则与流程分离”。采集线程不关心页面里是什么商品只负责拿到HTML解析器不关心网络状态只负责从Document里提取数据。源码级别的设计通常还会定义一个SpiderTask对象包含站点ID、模板ID、目标URL和回调解析器。这样新增一个电商平台不需要改主流程只新增一套SiteTemplate配置。2.3 商品与价格的数据模型定义2.3.1 SKU与价格快照的表结构比价网不能只存一个“最新价格”因为用户要看历史价格曲线也要判断“降价了没有”。所以数据模型必须分为两张表一张商品表存放静态属性一张价格快照表每次抓取后插入一条记录。CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sku_code VARCHAR(64) NOT NULL, site_code VARCHAR(32) NOT NULL, product_name VARCHAR(255) NOT NULL, brand VARCHAR(64), specs_json VARCHAR(1024), product_url VARCHAR(512), first_seen_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_site_sku (site_code, sku_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE price_snapshot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, price DECIMAL(10,2) NOT NULL, original_price DECIMAL(10,2), promotion_type VARCHAR(32), snapshot_time DATETIME NOT NULL, crawl_batch_id VARCHAR(64), KEY idx_product_time (product_id, snapshot_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这两条建表语句覆盖了比价网最核心的存储需求。product表用site_code sku_code做唯一键避免同一平台同一商品重复录入price_snapshot表以product_id和时间维度建立索引支撑“某商品最近30天价格趋势”这类查询。crawl_batch_id用来追溯某一次全量抓取或定向补抓的数据来源排查脏数据时非常有用。2.3.2 归一化字段如何避免“同品不同价”同一个商品在不同平台可能叫“iPhone 15 128GB 黑色”和“苹果 iPhone15 128G 黑色”如果不做归一化会出现两个product记录比价就没有意义。常见做法是给product表增加一个normalized_key字段用品牌系列容量颜色拼成一个指纹串再做MD5或直接存储。public class Normalizer { public static String generateSkuKey(String siteCode, String productName, String brand, String specsJson) { // 先做基础清洗转小写、去掉空格和括号内的修饰词 String cleanedName productName.toLowerCase() .replaceAll(\\s, ) .replaceAll([(].*?[)], ); // 品牌和规格用固定分隔符拼接 return siteCode : brand.toLowerCase().trim() : cleanedName : specsJson.trim(); } }上述逻辑说明normalized_key不直接用于展示只做等值匹配。拼接时要注意顺序品牌、型号、规格必须固定否则同一商品生成两个key。实际项目中这个key会对应平台商品的“标准品ID”通常由一个离线加工任务维护。Spider解析到新商品时先通过该key查标准品库命中则更新价格快照未命中则先建标准品再建快照。3. 基于Java的Spider抓取实现与参数调优3.1 抓取框架选型HttpClient与Jsoup的配合比价网Spider里最常见的组合是Apache HttpClient 4.x或5.x负责网络传输Jsoup负责解析HTML。网络层只解决“拿到字节”解析层解决“从字节变成对象”。有些源码会用WebMagic它本身封装了多线程和URL管理但如果你要对接Spring事务和Redis还是HttpClient加Jsoup的裸组合更容易掌控。下面是一个最简的抓取解析流程使用Java 11的HttpClient替代传统依赖适合演示import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class SimpleFetcher { private final HttpClient httpClient; public SimpleFetcher() { this.httpClient HttpClient.newBuilder() .connectTimeout(Duration.ofMillis(8000)) .followRedirects(HttpClient.Redirect.NORMAL) .build(); } public String fetch(String url) throws Exception { HttpRequest request HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(10)) .header(User-Agent, Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36) .header(Accept-Language, zh-CN,zh;q0.9) .GET() .build(); HttpResponseString response httpClient.send( request, HttpResponse.BodyHandlers.ofString()); return response.body(); } }这段代码不是展示API调用而是说明比价爬虫抓取时的三个基本参数connectTimeout控制建连timeout控制整体请求followRedirects必须开启因为很多电商平台对商品页有302跳转。如果connectTimeout设太短弱网环境误杀多设太长线程会卡住不释放整体吞吐下降。经验值在4到10秒之间。3.2 解析商品列表页的XPath与选择器策略拿到HTML后用Jsoup选择器解析商品名、价格、规格。这里有个容易被忽略的点同一页面里价格可能出现在多个节点比如页面JSON-LD里的price字段和页面渲染出的span.price。源码设计里需要定义选择器优先级避免抓到划线价或隐藏价。import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; public class ProductParser { public ParsedProduct parse(String html, String pageUrl) { Document doc Jsoup.parse(html); ParsedProduct product new ParsedProduct(); product.setProductName(extractText(doc, h1.product-title,p[itempropname], 京东商品标题)); product.setPrice(extractPrice(doc, span[itempropprice], meta[itempropprice], 价格)); product.setSpecs(extractSpecs(doc)); return product; } private String extractText(Document doc, String cssQuery, String fieldName) { Element el doc.selectFirst(cssQuery); if (el null) return ; return el.text().trim(); } }这里的关键是选择器用逗号分隔多个候选项表示“先找第一个找不到找第二个”。比价网源码里这类查询通常不会硬编码而是抽象成SiteTemplate表中的selector字段由运营人员在后台配置。解析失败时不要直接抛异常要把失败原因写到日志和任务表例如“选择器未命中h1.product-title”这样调试时能迅速判断是页面改版还是反爬返回了验证页。3.3 抓取频率、重试与代理池的3个必调参数3.3.1 线程数与超时设置比价网抓取是IO密集型线程数不是越大越好。Java线程池里设置核心线程数时会遵守一个公式目标每秒请求数乘以平均响应时间。比如你想保持每秒10个请求平均响应1秒那么需要的并发线程约10个。源码里常见的错误是直接把线程数设为CPU核数结果在等待IO时白白浪费CPU。线程数 单线程QPS × 平均响应时间假设一台机器单线程每秒能发出1.5个请求平均响应时间800ms那么支撑5个并发请求需要约3条线程。实际预留30%余量配置4到5条即可。线程池队列尽量不要用无界队列比价网的URL列表是非稳态流量峰值可能瞬间增高无界队列会导致内存飙升。用ArrayBlockingQueue配合CallerRunsPolicy是稳妥选择。3.3.2 重试策略与背压抓取失败必须区分“概率性失败”和“永久性失败”。超时、连接重置、远端5xx属于概率性失败可以重试404、410以及被反爬返回的登录页属于永久性失败重试只会浪费资源。spider: fetch: retry-count: 3 retry-interval-ms: 1000 retry-multiplier: 2 backoff-base-time-ms: 500 max-backoff-time-ms: 15000重试指数退避的配置含义第一次失败后等待500ms之后每次翻倍最大到15秒。retry-count建议不超过4次。很多开源源码里重试逻辑和业务异常混在一起正确的做法是单独定义一个TransientException只有它触发重试。另外当连续失败率超过20%时应当暂停整个站点的抓取任务等待一段时间后再恢复而不是继续压榨代理池。3.3.3 代理池接入面向电商平台的比价Spider没有代理池基本扛不住高频访问。开源设计里通常会抽象出ProxyProvider接口提供getProxy()方法。调度器在请求前向代理池借一个代理请求完成或失败后归还。public interface ProxyProvider { Proxy getProxy(); void returnProxy(Proxy proxy, boolean invalid); } public class Proxy { private String ip; private int port; private long expireTime; }代理池不要只做“分配IP”要记录每个代理的成功率。如果某个代理连续三次请求都超时应当标记为失效并延迟到下一轮再验证。同时代理池里的IP需要定期健康检查。源码里常见的错误是代理用过一次就丢导致每次新建TCP连接延迟反而比直连更高。正确做法是每个代理维持一个连接池或者至少保持长连接状态。4. 开源源码中的去重、更新与反爬应对4.1 基于Redis的URL与价格指纹去重比价网Spider的调度器需要判断一条URL是否已经抓取过。这个用数据库查当然可以但高并发下性能太差。开源实现里几乎都用Redis的Set或HyperLogLog来做布隆式判断。这里有一个关键设计URL去重和商品去重应该是两回事。URL去重防止重复请求商品去重防止重复入库。# 添加待抓取URL并防止重复进入任务队列 SADD spider:url:todo https://example.com/product/123 # 判断是否已抓取过 SISMEMBER spider:url:done https://example.com/product/123 # 商品指纹去重fingerprint由Normalizer生成 SADD spider:product:fingerprint site:brand:iphone123128gblack这套命令的问题在于去重集合会无限膨胀。一个运营超过半年的比价网URL集合可能破亿。所以开源设计里更推荐对URL做取模分片比如按站点拆成多个Set并且定期对长时间无更新的URL做冷数据迁移。商品指纹去重后还要把指纹与标准品ID映射关系存起来格式为product:fingerprint:{fp}映射到productId。4.2 价格变动监测增量抓取与版本对比比价网最有价值的能力是“发现价格变动”。全量刷新页面不现实所以需要增量策略。常见做法是维护一张crawl_plan表记录每个站点每个商品下次抓取时间。当某个商品价格频繁变动时提高抓取优先级长期不变则降低频次。价格对比可以用简单的版本号判断public class PriceComparator { public boolean isChanged(PriceSnapshot newSnapshot, PriceSnapshot oldSnapshot) { if (oldSnapshot null) return true; // 价格差异小于0.01元视为未变化 return Math.abs(newSnapshot.getPrice().subtract(oldSnapshot.getPrice())) .compareTo(new BigDecimal(0.01)) 0; } }这里的逻辑说明比较价格时不能直接用equals因为BigDecimal的精度和scale会影响结果。使用subtract后取绝对值与阈值比较同时考虑促销类型。比如同一个商品从“无促销”变成“满300减50”实际到手价变了但商品标价没变。开源源码里这种“到手价”的计算往往被忽略导致比价结果不准。实现时需要在快照表里增加effective_price字段由业务规则统一计算。4.3 常见反爬的破解与边界4.3.1 请求头与Cookie模拟比价网请求它站页面时至少要带上完整的浏览器请求头User-Agent、Accept、Accept-Language、Referer。很多开源源码只写一个UA忽略Accept-Language会触发一些站点的风控策略。Cookie的处理分两类一类是首次访问不需要登录的公开页面只需携带站点种下的基础Cookie另一类需要模拟登录后获得访问权限此时要把登录态缓存起来定时刷新。不要把所有站点请求都放在同一个HttpClient实例里因为Cookie会串味。开源源码里常见做法是为每个站点单独维护一个HttpClientContext。4.3.2 验证码与登录态处理如果目标站出现验证码Spider源码里最合理的方案是“感知并跳过”而不是自动破解。通过判断HTML中是否存在验证码图片特征或captcha关键字将该URL标记为受限等待后台人工处理。自动化打码平台虽然可用但成本与合规风险都不低。开源的比价爬虫设计里遇到验证码的正确动作是把页面特征截图或保存HTML片段并进入一个“受控队列”。这个队列的抓取频率被刻意降低比如每次间隔10秒以上以减少触发验证码的频率。4.3.3 限流与风控的规避策略比价网做的是公开商品信息采集但也要遵守目标站点对访问频率的容忍度。源码里要有“站点级限流器”使用令牌桶算法控制单一站点每秒最大请求数。public class TokenBucketRateLimiter { private final int capacity; private final double refillRatePerSecond; private double tokens; private long lastRefillTime System.currentTimeMillis(); public synchronized boolean tryAcquire() { long now System.currentTimeMillis(); tokens Math.min(capacity, tokens (now - lastRefillTime) / 1000.0 * refillRatePerSecond); lastRefillTime now; if (tokens 1) { tokens - 1; return true; } return false; } }建议按站点建一个限流器实例比如京东每秒2请求淘宝每秒1请求。不要设为全局统一限流因为不同站点承受能力差异很大。refillRatePerSecond是令牌桶每秒补充的令牌数决定长期平均速率。调大这个值只影响平滑程度不影响最大突发速率突发速率由capacity控制。实际运营中突发请求最容易触发风控所以capacity要比平均值高一点但不要超过3倍。5. 把Spider源码改造成服务的快速路径5.1 用Spring Boot封装采集任务拿到一份比价网Spider开源代码后最直接的落地方式是把它嵌进Spring Boot工程。把采集、解析、存储分别做成Service用ApplicationRunner监听启动事件按配置决定是否立即执行任务。Component public class SpiderRunner implements ApplicationRunner { Autowired private SpiderScheduler scheduler; Override public void run(ApplicationArguments args) { if (args.containsOption(run-spider)) { scheduler.start(); } } }这样设计的目的是让服务启动时不立刻抓取而是等运维显式传参。生产环境中建议用独立的采集实例和API实例分离部署避免抓取占用的CPU和带宽影响对外服务。5.2 定时调度与任务拆分比价网的抓取任务不能只靠Scheduled(cron 0 0 2 * * ?)这种死板定时因为不同站点有不同的低峰时段。更合理的做法是将任务存库用Quartz或ElasticJob做分布式调度。开源源码里如果只依赖Spring内置调度要特别注意单点问题。任务表结构至少包含site_code、cron_expression、last_run_time、task_status。5.3 一次验证抓取质量的最低成本方案改造完源码后先用一个站点、几条URL做端到端验证。启动服务后观察三件事第一条URL是否成功入库对应价格快照是否正确生成再次启动相同任务时URL去重是否命中。# 检查商品数据 mysql -uroot -p -e select count(*) from product; # 检查价格快照是否随时间增加 mysql -uroot -p -e select snapshot_time, price from price_snapshot order by id desc limit 5; # 查看Redis里去重集合大小 redis-cli scard spider:url:done这三条命令能快速验证数据链路是否通畅。如果spider:url:done集合在第二次运行后没有变大说明去重生效。如果价格快照每轮都在增加但商品表没新增说明解析与归一化正常。这份验证方案不需要复杂的监控面板适合刚接手开源源码的团队快速确认改动没有破坏核心流程。比价网Spider的价值最终体现在数据准确性和更新时效上与其追求更大范围的抓取不如先把单站点的解析规则和价格对比逻辑打磨到几乎没有误报。本文还有配套的精品资源点击获取