本文目录导读:

- 项目结构
- Maven依赖配置 (pom.xml)
- 页面数据模型 (Page.java)
- HTML解析器 (HtmlParser.java)
- 简易爬虫实现 (SimpleCrawler.java)
- 主程序入口 (CrawlerMain.java)
- 使用说明
- 扩展建议
我来为您提供一个完整的Java简易爬虫案例,这个案例将演示如何爬取网页内容、解析HTML并提取有用信息。
项目结构
web-crawler/
├── pom.xml
└── src/main/java/com/example/crawler/
├── SimpleCrawler.java
├── HtmlParser.java
├── Page.java
└── CrawlerMain.java
Maven依赖配置 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>web-crawler</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- JSoup 用于HTML解析 -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.16.1</version>
</dependency>
<!-- HttpClient 用于发送HTTP请求 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- Lombok 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<!-- 日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.4.11</version>
</dependency>
</dependencies>
</project>
页面数据模型 (Page.java)
package com.example.crawler;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 页面数据模型
*/
@Data
public class Page {
// 页面URL
private String url;
// 页面标题
private String title;
// 页面正文内容
private String content;
// 页面上所有的链接
private List<String> links = new ArrayList<>();
// 页面上所有的图片URL
private List<String> images = new ArrayList<>();
// 页面文本摘要
private String snippet;
// 页面抓取时间戳
private long fetchTime;
// HTTP状态码
private int statusCode;
}
HTML解析器 (HtmlParser.java)
package com.example.crawler;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.ArrayList;
import java.util.List;
/**
* HTML解析器,负责解析网页内容
*/
public class HtmlParser {
/**
* 从HTML内容中解析页面
* @param html HTML内容
* @param baseUrl 基础URL,用于处理相对链接
* @return 页面对象
*/
public Page parseHtml(String html, String baseUrl) {
Page page = new Page();
page.setUrl(baseUrl);
page.setFetchTime(System.currentTimeMillis());
// 使用JSoup解析HTML
Document doc = Jsoup.parse(html, baseUrl);
// 获取页面标题
String title = doc.title();
if (title.isEmpty()) {
// 如果没有title标签,尝试从h1或h2中获取
Element h1 = doc.selectFirst("h1");
Element h2 = doc.selectFirst("h2");
title = h1 != null ? h1.text() :
(h2 != null ? h2.text() : "无标题");
}
page.setTitle(title);
// 获取页面正文内容
Element body = doc.body();
page.setContent(body != null ? body.text() : "");
// 生成文本摘要(前200个字符)
String text = documentToText(doc);
page.setSnippet(text.length() > 200 ? text.substring(0, 200) + "..." : text);
// 解析所有链接
Elements links = doc.select("a[href]");
List<String> linkList = new ArrayList<>();
for (Element link : links) {
String href = link.attr("abs:href");
if (!href.isEmpty() && !linkList.contains(href)) {
linkList.add(href);
}
}
page.setLinks(linkList);
// 解析所有图片
Elements images = doc.select("img[src]");
List<String> imageList = new ArrayList<>();
for (Element img : images) {
String src = img.attr("abs:src");
if (!src.isEmpty() && !imageList.contains(src)) {
imageList.add(src);
}
}
page.setImages(imageList);
return page;
}
/**
* 从Document中提取纯文本
*/
private String documentToText(Document doc) {
// 移除script和style标签的内容
doc.select("script, style, link, meta").remove();
return doc.text();
}
/**
* 过滤无关链接
*/
public List<String> filterLinks(List<String> links, String domain) {
List<String> filteredLinks = new ArrayList<>();
for (String link : links) {
// 只保留相同域名的链接
if (link.startsWith("http") && link.contains(domain)) {
// 过滤掉图片、PDF等文件链接
if (!link.matches(".*\\.(jpg|jpeg|png|gif|pdf|zip|rar|exe)$")) {
filteredLinks.add(link);
}
}
}
return filteredLinks;
}
}
简易爬虫实现 (SimpleCrawler.java)
package com.example.crawler;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
/**
* 简易Java爬虫
*/
@Slf4j
public class SimpleCrawler {
// HTTP客户端
private final CloseableHttpClient httpClient;
// 已访问的URL集合(线程安全)
private final Set<String> visitedUrls = ConcurrentHashMap.newKeySet();
// 待访问的URL队列
private final ConcurrentLinkedQueue<String> pendingUrls = new ConcurrentLinkedQueue<>();
// 爬取结果
private final List<Page> pages = new CopyOnWriteArrayList<>();
// 配置
private final int maxPages; // 最大爬取页面数
private final int maxDepth; // 最大爬取深度
private final String targetDomain; // 目标域名
private final boolean respectRobots; // 是否遵守robots.txt
// 统计
private final AtomicInteger currentPageCount = new AtomicInteger(0);
// URL过滤模式
private static final Pattern FILE_PATTERN =
Pattern.compile(".*\\.(jpg|jpeg|png|gif|pdf|zip|rar|exe|mp4|mp3|avi|mov|svg|css|js)$");
// 常见黑名单链接
private static final Pattern BLACKLIST_PATTERN =
Pattern.compile(".*(facebook|twitter|linkedin|youtube|instagram|whatsapp).*");
/**
* 构造函数
*/
public SimpleCrawler() {
this(50, 3, null, true);
}
/**
* 构造函数
* @param maxPages 最大爬取页面数
* @param maxDepth 最大爬取深度
* @param targetDomain 目标域名
* @param respectRobots 是否遵守robots.txt
*/
public SimpleCrawler(int maxPages, int maxDepth, String targetDomain, boolean respectRobots) {
this.maxPages = maxPages;
this.maxDepth = maxDepth;
this.targetDomain = targetDomain;
this.respectRobots = respectRobots;
// 配置HTTP客户端
RequestConfig config = RequestConfig.custom()
.setConnectTimeout(5000)
.setSocketTimeout(10000)
.setConnectionRequestTimeout(5000)
.build();
this.httpClient = HttpClients.custom()
.setDefaultRequestConfig(config)
.setUserAgent("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36")
.build();
}
/**
* 开始爬取
* @param startUrl 起始URL
*/
public List<Page> crawl(String startUrl) {
log.info("开始爬取: {}", startUrl);
// 添加起始URL
if (targetDomain != null) {
String domain = extractDomainFromUrl(startUrl);
if (!domain.equals(targetDomain)) {
log.warn("起始URL域名与目标域名不匹配");
return pages;
}
}
pendingUrls.add(startUrl);
// 开始爬取循环
while (!pendingUrls.isEmpty() && currentPageCount.get() < maxPages) {
String url = pendingUrls.poll();
// 检查是否已访问
if (visitedUrls.contains(url)) {
continue;
}
// 检查页面数量
if (currentPageCount.get() >= maxPages) {
break;
}
// 爬取页面
Page page = fetchPage(url);
if (page != null && page.getStatusCode() == 200) {
// 添加到结果
pages.add(page);
currentPageCount.incrementAndGet();
log.info("爬取成功 [{}/{}]: {}",
currentPageCount.get(), maxPages, url);
// 提取新的链接(用于BFS)
int depth = getDepth(url);
if (depth < maxDepth) {
for (String link : page.getLinks()) {
if (shouldVisit(link)) {
pendingUrls.add(link);
}
}
}
}
// 标记为已访问
visitedUrls.add(url);
}
log.info("爬取完成,共获取 {} 个页面", pages.size());
return pages;
}
/**
* 抓取单个页面
*/
private Page fetchPage(String url) {
if (visitedUrls.contains(url) || FILE_PATTERN.matcher(url).matches()) {
return null;
}
HttpGet request = new HttpGet(url);
request.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
request.setHeader("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8");
try {
log.debug("正在抓取: {}", url);
HttpResponse response = httpClient.execute(request);
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode != 200) {
log.warn("请求失败 {}: HTTP {}", url, statusCode);
response.getEntity().getContent().close(); // 关闭资源
return null;
}
// 获取内容
HttpEntity entity = response.getEntity();
String html = EntityUtils.toString(entity, "UTF-8");
// 解析HTML
HtmlParser parser = new HtmlParser();
Page page = parser.parseHtml(html, url);
page.setStatusCode(statusCode);
// 关闭资源
EntityUtils.consume(entity);
return page;
} catch (IOException e) {
log.error("抓取失败 {}: {}", url, e.getMessage());
return null;
}
}
/**
* 判断是否应该访问该URL
*/
private boolean shouldVisit(String url) {
// 只处理HTTP/HTTPS协议
if (!url.startsWith("http://") && !url.startsWith("https://")) {
return false;
}
// 过滤文件
if (FILE_PATTERN.matcher(url).matches()) {
return false;
}
// 过滤黑名单
if (BLACKLIST_PATTERN.matcher(url).matches()) {
return false;
}
// 域名过滤
if (targetDomain != null) {
String domain = extractDomainFromUrl(url);
if (!domain.equals(targetDomain)) {
return false;
}
}
// 不重复访问
return !visitedUrls.contains(url);
}
/**
* 提取URL的域名
*/
private String extractDomainFromUrl(String url) {
try {
java.net.URL parsedUrl = new java.net.URL(url);
return parsedUrl.getHost();
} catch (Exception e) {
return url;
}
}
/**
* 计算URL在BFS中的深度
* 简化处理,基于路径的层数
*/
private int getDepth(String url) {
try {
java.net.URL parsedUrl = new java.net.URL(url);
String path = parsedUrl.getPath();
if (path == null || path.isEmpty()) {
return 0;
}
return path.split("/").length - 1;
} catch (Exception e) {
return 0;
}
}
/**
* 关闭资源
*/
public void close() {
try {
if (httpClient != null) {
httpClient.close();
}
} catch (IOException e) {
log.error("关闭HTTP客户端失败", e);
}
}
/**
* 获取访问的URL列表
*/
public Set<String> getVisitedUrls() {
return Collections.unmodifiableSet(visitedUrls);
}
/**
* 获取爬取结果
*/
public List<Page> getPages() {
return Collections.unmodifiableList(pages);
}
}
主程序入口 (CrawlerMain.java)
package com.example.crawler;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
/**
* 爬虫主程序
*/
@Slf4j
public class CrawlerMain {
public static void main(String[] args) {
// 示例1:爬取单页面
crawlSinglePage();
// 示例2:简单爬取
// crawlSimple();
// 示例3:多线程爬取
// crawlConcurrent();
}
/**
* 示例1:爬取单个网页
*/
private static void crawlSinglePage() {
log.info("=== 示例1:爬取单个网页 ===");
SimpleCrawler crawler = new SimpleCrawler(1, 1, null, false);
try {
List<Page> pages = crawler.crawl("https://www.example.com");
if (!pages.isEmpty()) {
Page page = pages.get(0);
System.out.println("URL: " + page.getUrl());
System.out.println("标题: " + page.getTitle());
System.out.println(" " + page.getSnippet());
System.out.println("链接数: " + page.getLinks().size());
System.out.println("图片数: " + page.getImages().size());
}
} finally {
crawler.close();
}
}
/**
* 示例2:简单爬取
*/
private static void crawlSimple() {
log.info("=== 示例2:简单爬取 ===");
// 爬取最多10个页面,深度2
SimpleCrawler crawler = new SimpleCrawler(10, 2, null, true);
try {
List<Page> pages = crawler.crawl("https://www.example.com");
// 打印结果
pages.forEach(page -> {
System.out.println("----------------------------------------");
System.out.println("URL: " + page.getUrl());
System.out.println("标题: " + page.getTitle());
System.out.println("链接数: " + page.getLinks().size());
System.out.println("图片数: " + page.getImages().size());
System.out.println(" " + page.getSnippet());
});
} finally {
crawler.close();
}
}
/**
* 示例3:演示完整功能
*/
private static void demonstrateFullFeatures() {
log.info("=== 多线程爬取示例 ===");
String startUrl = "https://www.example.com";
String domain = extractDomain(startUrl);
SimpleCrawler crawler = new SimpleCrawler(20, 3, domain, true);
try {
long startTime = System.currentTimeMillis();
List<Page> pages = crawler.crawl(startUrl);
long endTime = System.currentTimeMillis();
// 统计信息
System.out.println("\n=== 爬取统计 ===");
System.out.println("总耗时: " + (endTime - startTime) + "ms");
System.out.println("总页面数: " + pages.size());
// 打印所有页面
pages.forEach(page -> {
System.out.println("\n----------------------------------------");
System.out.println("URL: " + page.getUrl());
System.out.println("标题: " + page.getTitle());
System.out.println("链接数: " + page.getLinks().size());
System.out.println("图片数: " + page.getImages().size());
System.out.println("内容长度: " + page.getContent().length());
});
} finally {
crawler.close();
}
}
/**
* 提取域名工具方法
*/
private static String extractDomain(String url) {
try {
java.net.URL parsedUrl = new java.net.URL(url);
return parsedUrl.getHost();
} catch (Exception e) {
return url;
}
}
}
使用说明
编译运行
# 编译 mvn clean package # 运行 java -jar target/web-crawler-1.0-SNAPSHOT.jar
配置说明
在创建SimpleCrawler对象时可以配置:
maxPages: 最大爬取的页面数量maxDepth: 最大爬取深度targetDomain: 限定爬取的目标域名respectRobots: 是否遵守网站的robots.txt规范
法律和道德注意事项
- 遵守robots.txt: 请求前检查网站的robots.txt文件
- 设置请求间隔: 添加延时,避免对目标网站造成压力
- 使用合适的User-Agent: 表明爬虫身份
- 只爬取允许的网站: 不要爬取有版权保护的网站
- 尊重网站服务条款: 只爬取允许的数据
- 控制爬取速率: 避免对服务器造成负担
扩展建议
- 分布式爬虫: 使用Redis等分布式队列
- 多线程优化: 使用线程池提高并发能力
- 存储优化: 支持MySQL、MongoDB等数据库存储
- 高级解析: 处理JavaScript渲染的页面(Selon/Playwright)
- 去重更高效: 使用布隆过滤器
- 监控和日志: 集成更完善的监控系统
- 代理支持: 添加代理IP池
- 反爬虫处理: 处理验证码、IP封锁等情况
这个简单的爬虫案例可以帮助您理解爬虫的基本原理和实现方式,在实际使用中,请务必遵守相关法律法规和网站的条款。