本文目录导读:

- 项目结构
- 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封锁等情况
这个简单的爬虫案例可以帮助您理解爬虫的基本原理和实现方式,在实际使用中,请务必遵守相关法律法规和网站的条款。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播