Java实现简易爬虫案例

wen java案例 4

本文目录导读:

Java实现简易爬虫案例

  1. 项目结构
  2. Maven依赖配置 (pom.xml)
  3. 页面数据模型 (Page.java)
  4. HTML解析器 (HtmlParser.java)
  5. 简易爬虫实现 (SimpleCrawler.java)
  6. 主程序入口 (CrawlerMain.java)
  7. 使用说明
  8. 扩展建议

我来为您提供一个完整的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规范

法律和道德注意事项

  1. 遵守robots.txt: 请求前检查网站的robots.txt文件
  2. 设置请求间隔: 添加延时,避免对目标网站造成压力
  3. 使用合适的User-Agent: 表明爬虫身份
  4. 只爬取允许的网站: 不要爬取有版权保护的网站
  5. 尊重网站服务条款: 只爬取允许的数据
  6. 控制爬取速率: 避免对服务器造成负担

扩展建议

  1. 分布式爬虫: 使用Redis等分布式队列
  2. 多线程优化: 使用线程池提高并发能力
  3. 存储优化: 支持MySQL、MongoDB等数据库存储
  4. 高级解析: 处理JavaScript渲染的页面(Selon/Playwright)
  5. 去重更高效: 使用布隆过滤器
  6. 监控和日志: 集成更完善的监控系统
  7. 代理支持: 添加代理IP池
  8. 反爬虫处理: 处理验证码、IP封锁等情况

这个简单的爬虫案例可以帮助您理解爬虫的基本原理和实现方式,在实际使用中,请务必遵守相关法律法规和网站的条款。

抱歉,评论功能暂时关闭!