☰
Java爬虫实战:用HttpClient+Jsoup抓取CSDN个人博客
2026/11/3 8:26:42 网站建设 项目流程

简介:这是一份面向Java初学者与中级开发者的实战型爬虫项目资源,聚焦CSDN个人博客文章的自动化抓取与本地化存储,解决内容备份、技术复现与数据采集学习等实际需求。压缩包共11个文件,含7个核心Java源码(实现HTTP请求、Jsoup解析、文章结构提取与文件写入)、1个pom.xml(管理Jsoup等依赖)、1个README.md(含环境配置与运行说明)、1个application.properties(配置博主ID与保存路径)及1个.gitignore,整体仅19KB,轻量易读。已有207人学习下载,适合希望通过可运行代码理解网络爬虫全流程——从URL构造、反爬应对(如基础延时策略)、HTML节点定位到Markdown格式化存储——的实践者。项目结构清晰,模块职责分明,附带完整注释与典型CSDN博客页面适配逻辑,是掌握Java Web数据采集技术的优质入门范例。

1. Java爬虫实战:为什么CSDN个人博客是练手爬虫的“黄金靶场”?

你试过用Java写爬虫,但刚敲完HttpClient第一行就卡在登录态、反爬验证、动态渲染上?别急——CSDN个人博客恰恰是最适合Java工程师从零落地爬虫能力的真实场景:它结构清晰(URL规律强、HTML语义明确)、无强JS渲染(首页/文章页基本靠服务端直出)、登录态可复用Cookie机制、且数据价值高(标题/发布时间/阅读量/标签/正文段落完整)。更重要的是,它不涉及敏感字段、无风控封IP激进策略、页面DOM稳定(近3年未大改),连新手都能在2小时内跑通“抓取自己博客所有文章→提取标题+正文→存为本地TXT”闭环。这不是玩具Demo,而是能直接迁移到企业内网知识库同步、技术团队内容归档、甚至面试时亮出的可验证、可调试、可讲清每一步原理的Java工程级爬虫案例。本文不讲Python Requests或Scrapy,只聚焦Java生态下真实可用的方案:用HttpClient扛住基础请求,用Jsoup精准解析DOM,用ScheduledExecutorService模拟人工间隔,用FileWriter做原子写入——所有代码基于JDK 8+,不依赖Spring Boot等重量框架,贴着Java原生能力走,确保你抄过去就能跑,改几行就能用到自己项目里。


2. 搭建最小可行爬虫:用HttpClient + Jsoup跑通CSDN个人博客首页抓取

2.1 为什么选HttpClient而不是OkHttp或WebClient?

CSDN个人博客页(如https://blog.csdn.net/your_username/article/list/1)本质是传统服务端渲染页面,没有GraphQL API、不强制JWT鉴权、不校验User-Agent指纹。此时HttpClient的轻量性与可控性远胜OkHttp:

  • 它是JDK自带java.net.http.HttpClient(JDK 11+)或Apache HttpClient(JDK 8兼容主力)的成熟实现,无需额外jar包(JDK 11+)或仅需httpclient-4.5.14.jar(JDK 8);
  • 支持手动管理CookieStore,这对CSDN登录态维持至关重要(CSDN用Set-Cookie: acw_tc=xxx+JSESSIONID=xxx双Cookie维持会话);
  • 异步回调、连接池、重试策略全部可编程控制,不像OkHttp的Interceptor链容易在反爬场景中失控。

提示:若用JDK 8,务必用Apache HttpClient 4.5.x(非5.x),因CSDN部分老接口仍依赖HTTP/1.1的Connection头行为,HttpClient 5默认启用HTTP/2可能触发服务端异常响应。

2.2 抓取个人博客首页:三步定位文章列表URL

CSDN个人博客首页URL格式固定:https://blog.csdn.net/{username}。但关键不是首页,而是文章列表页——它承载所有文章元信息。观察发现:

  • 首页底部有分页导航,链接形如/article/list/1、/article/list/2;
  • 每篇文章卡片包含<a href="/articles/xxxxxx">,但该href是相对路径,需拼接为https://blog.csdn.net/{username}/articles/xxxxxx;
  • 文章标题在<h4 class="title">内,发布时间在<span class="time">内,阅读数在<span class="read-num">内。

以下代码用Apache HttpClient 4.5.14(JDK 8)抓取第一页列表:

import org.apache.http.client.methods.CloseableHttpResponse; 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; public class CsdnListFetcher { private static final String BASE_URL = "https://blog.csdn.net"; private static final String USERNAME = "your_username"; // 替换为你自己的CSDN用户名 public static void main(String[] args) throws Exception { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet(BASE_URL + "/" + USERNAME + "/article/list/1"); // 关键:设置User-Agent模拟真实浏览器,否则CSDN返回403 httpGet.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"); try (CloseableHttpResponse response = httpClient.execute(httpGet)) { int statusCode = response.getStatusLine().getStatusCode(); if (statusCode == 200) { String html = EntityUtils.toString(response.getEntity(), "UTF-8"); System.out.println("成功获取首页HTML,长度:" + html.length()); // 后续用Jsoup解析html } else { System.err.println("HTTP错误码:" + statusCode); } } } }

参数说明:

  • BASE_URL + "/" + USERNAME + "/article/list/1"是CSDN官方支持的分页入口,比首页更结构化;
  • User-Agent必须设置,CSDN对空UA或Apache-HttpClient默认UA直接拦截;
  • EntityUtils.toString(..., "UTF-8")显式指定编码,避免中文乱码(CSDN响应头Content-Type: text/html; charset=utf-8但部分旧页面未声明,显式指定更稳)。

2.3 用Jsoup解析文章列表:提取标题、链接、发布时间

拿到HTML后,Jsoup是Java生态最成熟的HTML解析器。它用CSS选择器语法,比正则表达式更可靠、比XPath更易读。针对CSDN文章列表页,关键选择器如下:

DOM元素CSS选择器说明
文章卡片容器div.article-list > div.article-item每篇文章一个卡片
标题链接h4.title > a<a href="/articles/xxx">标题</a>
发布时间div.info-row > span.time<span class="time">2023-05-12 14:22:32</span>
阅读数div.info-row > span.read-num<span class="read-num">阅读数:1234</span>
import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; // 接上一节的html字符串 Document doc = Jsoup.parse(html); Elements articleItems = doc.select("div.article-list > div.article-item"); for (Element item : articleItems) { // 提取标题和链接 Element titleLink = item.selectFirst("h4.title > a"); if (titleLink != null) { String title = titleLink.text().trim(); String relativeUrl = titleLink.attr("href"); // 如 "/articles/123456789" String fullUrl = "https://blog.csdn.net" + relativeUrl; // 提取发布时间 Element timeEl = item.selectFirst("div.info-row > span.time"); String publishTime = timeEl != null ? timeEl.text().trim() : "未知"; // 提取阅读数(需清洗“阅读数:”前缀) Element readEl = item.selectFirst("div.info-row > span.read-num"); String readNumStr = readEl != null ? readEl.text().trim() : "0"; int readCount = parseReadCount(readNumStr); // 自定义方法,见下方 System.out.printf("标题:%s | 链接:%s | 时间:%s | 阅读:%d\n", title, fullUrl, publishTime, readCount); } } // 辅助方法:解析阅读数字符串 private static int parseReadCount(String raw) { if (raw == null || raw.isEmpty()) return 0; // 示例: "阅读数:1,234" → 提取数字并去掉逗号 return Integer.parseInt(raw.replaceAll("[^\\d]", "")); }

逻辑说明:

  • doc.select(...)返回Elements集合,selectFirst()防空指针;
  • titleLink.attr("href")获取相对路径,再拼https://blog.csdn.net构成绝对URL;
  • parseReadCount()处理CSDN阅读数含逗号、前缀的脏数据,这是实际爬虫必写的清洗逻辑;
  • 所有.text().trim()防止换行符、空格污染数据。

3. 登录态维持与文章详情页抓取:Cookie管理与动态URL构造

3.1 CSDN登录态的本质:acw_tc + JSESSIONID双Cookie机制

CSDN不强制登录即可浏览文章,但个人博客页(/article/list/1)要求登录态,否则跳转到登录页。其登录态由两个关键Cookie维持:

  • acw_tc:阿里云WAF生成的防爬令牌,有效期约2小时,随每次请求刷新;
  • JSESSIONID:Tomcat会话ID,标识用户登录会话,有效期较长(数小时)。

你不需要模拟登录表单提交——直接复用浏览器已登录的Cookie即可。操作步骤:

  1. 用Chrome打开https://blog.csdn.net/your_username/article/list/1;
  2. F12 → Application → Cookies → 复制acw_tc和JSESSIONID的Value值;
  3. 在Java代码中手动注入这两个Cookie。

注意:acw_tc值含特殊字符(如=、+),需URL解码后再使用。浏览器复制的值是编码后的,直接粘贴会失效。

3.2 构造带Cookie的HttpClient请求

Apache HttpClient通过BasicCookieStore管理Cookie,再注入HttpClient实例:

import org.apache.http.client.CookieStore; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.cookie.Cookie; import org.apache.http.impl.client.BasicCookieStore; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.message.BasicCookie; public class CsdnAuthFetcher { private static final String ACW_TC_VALUE = "acw_tc_value_here"; // 替换为你从浏览器复制的值 private static final String JSESSIONID_VALUE = "jsessionid_value_here"; // 替换为你从浏览器复制的值 public static CloseableHttpClient createAuthClient() { CookieStore cookieStore = new BasicCookieStore(); // 构造acw_tc Cookie BasicCookie acwCookie = new BasicCookie("acw_tc", ACW_TC_VALUE); acwCookie.setDomain("csdn.net"); acwCookie.setPath("/"); cookieStore.addCookie(acwCookie); // 构造JSESSIONID Cookie BasicCookie sessionCookie = new BasicCookie("JSESSIONID", JSESSIONID_VALUE); sessionCookie.setDomain("csdn.net"); sessionCookie.setPath("/"); cookieStore.addCookie(sessionCookie); // 创建HttpClient并绑定CookieStore RequestConfig config = RequestConfig.custom() .setCookieSpec(CookieSpecs.STANDARD) .setConnectTimeout(5000) .setSocketTimeout(10000) .build(); return HttpClients.custom() .setDefaultRequestConfig(config) .setDefaultCookieStore(cookieStore) .build(); } }

参数说明:

  • setDomain("csdn.net")必须设为csdn.net(非blog.csdn.net),因CSDN Cookie作用域为根域名;
  • setPath("/")确保Cookie对所有路径生效;
  • setConnectTimeout和setSocketTimeout防止网络波动导致线程阻塞,5秒连接超时、10秒读取超时是CSDN实测安全值;
  • CookieSpecs.STANDARD启用RFC 6265标准,兼容CSDN的Cookie格式。

3.3 抓取文章详情页:解析正文、提取标签、处理图片

CSDN文章详情页(如https://blog.csdn.net/xxx/articles/123456789)结构更复杂:正文在#content_views内,标签在div.tags-box内,图片为<img src="https://...">。Jsoup解析要点:

  • 正文需去除广告、侧边栏、评论区等干扰节点;
  • 图片URL需补全协议(CSDN部分图片用//img-blog.csdnimg.cn/xxx,需转为https://img-blog.csdnimg.cn/xxx);
  • 标签文本含#符号,需清洗。
public class CsdnArticleFetcher { public static ArticleDetail fetchArticle(String articleUrl) throws Exception { CloseableHttpClient httpClient = CsdnAuthFetcher.createAuthClient(); HttpGet httpGet = new HttpGet(articleUrl); httpGet.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); try (CloseableHttpResponse response = httpClient.execute(httpGet)) { if (response.getStatusLine().getStatusCode() != 200) { throw new RuntimeException("文章页请求失败:" + articleUrl); } String html = EntityUtils.toString(response.getEntity(), "UTF-8"); Document doc = Jsoup.parse(html); // 提取标题(在meta标签或h1中) String title = doc.selectFirst("title").text().replace(" - CSDN博客", "").trim(); if (title.isEmpty()) { title = doc.selectFirst("h1.article-title").text().trim(); } // 提取正文:只保留#content_views内的<p>、<pre>、<code>等有效内容 Element contentEl = doc.selectFirst("#content_views"); if (contentEl == null) { throw new RuntimeException("未找到#content_views节点,请检查URL是否正确"); } // 清洗:移除广告、推荐、评论区等无关div contentEl.select("div.ad-container, div.recommend-container, div.comment-container").remove(); // 处理图片:将//开头的URL转为https:// Elements imgEls = contentEl.select("img[src]"); for (Element img : imgEls) { String src = img.attr("src"); if (src.startsWith("//")) { img.attr("src", "https:" + src); } } // 提取标签 Elements tagEls = doc.select("div.tags-box a.tag-link"); List<String> tags = new ArrayList<>(); for (Element tagEl : tagEls) { String tagText = tagEl.text().replace("#", "").trim(); if (!tagText.isEmpty()) tags.add(tagText); } return new ArticleDetail(title, contentEl.html(), tags); } } // 内部类:文章详情数据结构 public static class ArticleDetail { public final String title; public final String contentHtml; // 已清洗的HTML正文 public final List<String> tags; public ArticleDetail(String title, String contentHtml, List<String> tags) { this.title = title; this.contentHtml = contentHtml; this.tags = tags; } } }

逻辑说明:

  • contentEl.select("div.ad-container, ...").remove()一次性清除所有广告容器,比逐个判断更高效;
  • img.attr("src", "https:" + src)解决CSDN图片URL协议缺失问题,避免本地保存后图片路径失效;
  • tagEl.text().replace("#", "")清洗标签前缀,CSDN标签显示为#Java,存储时只需Java;
  • ArticleDetail作为POJO封装结果,便于后续存入文件或数据库。

4. 数据持久化与工程化:本地TXT存储、增量抓取与反爬规避

4.1 将文章存为本地TXT:保证中文不乱码、路径安全、内容可读

CSDN文章正文含HTML标签,直接存为TXT会显示<p>...</p>。但纯文本存储更通用(方便grep、导入笔记软件、训练NLP模型)。Jsoup提供.text()方法提取纯文本,但会丢失段落结构。折中方案:

  • 用contentEl.html()保留HTML结构,但替换<br>为换行符;
  • 用StringEscapeUtils.unescapeHtml4()解码HTML实体(如&nbsp;→空格);
  • 文件名用文章标题生成,但需过滤非法字符(/ \ : * ? " < > |)。
import org.apache.commons.text.StringEscapeUtils; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Paths; public class ArticleSaver { public static void saveAsTxt(ArticleDetail article, String outputDir) throws IOException { // 清洗标题生成文件名 String safeTitle = article.title.replaceAll("[/\\\\:\\*\\?\"<>\\|]", "_"); String fileName = safeTitle.substring(0, Math.min(safeTitle.length(), 100)) + ".txt"; String filePath = Paths.get(outputDir, fileName).toString(); // 转HTML为可读文本:保留段落,替换<br>为\n,解码HTML实体 String plainText = article.contentHtml .replaceAll("<br[^>]*>", "\n") .replaceAll("<p[^>]*>", "\n\n") .replaceAll("</p>", "\n\n") .replaceAll("<[^>]+>", "") // 移除所有其他标签 .replaceAll("&nbsp;", " ") .replaceAll("&amp;", "&"); // 解码HTML实体 plainText = StringEscapeUtils.unescapeHtml4(plainText); // 写入文件,强制UTF-8编码 try (FileWriter writer = new FileWriter(filePath, "UTF-8")) { writer.write("=== " + article.title + " ===\n"); writer.write("标签:" + String.join(", ", article.tags) + "\n"); writer.write("原文链接:" + article.url + "\n\n"); writer.write(plainText.trim()); } System.out.println("已保存:" + fileName); } }

参数说明:

  • Paths.get(outputDir, fileName)使用NIO Path API,比new File(...)更安全(自动处理路径分隔符);
  • replaceAll("<br[^>]*>", "\n")匹配所有<br>及<br/>变体;
  • StringEscapeUtils.unescapeHtml4()来自Apache Commons Text,需添加Maven依赖:
    <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.10.0</version> </dependency>

4.2 增量抓取:用本地JSON记录已抓取文章ID,避免重复

CSDN文章URL含唯一ID(如/articles/123456789中的123456789),这是天然的去重Key。用HashSet<String>内存去重不够健壮(程序崩溃即丢失),应存为本地JSON文件:

{ "fetchedIds": ["123456789", "987654321", "112233445"], "lastFetchedTime": "2024-03-15T10:30:00Z" }
import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; import java.time.Instant; import java.util.HashSet; import java.util.Set; public class FetchStateManager { private static final String STATE_FILE = "csdn_fetch_state.json"; private static final ObjectMapper mapper = new ObjectMapper(); public static Set<String> loadFetchedIds() { try { if (new File(STATE_FILE).exists()) { State state = mapper.readValue(new File(STATE_FILE), State.class); return new HashSet<>(state.fetchedIds); } } catch (IOException e) { System.err.println("读取状态文件失败,将重新开始:" + e.getMessage()); } return new HashSet<>(); } public static void markAsFetched(String articleId) { Set<String> ids = loadFetchedIds(); ids.add(articleId); try { mapper.writeValue(new File(STATE_FILE), new State(ids)); } catch (IOException e) { System.err.println("保存状态失败:" + e.getMessage()); } } private static class State { public Set<String> fetchedIds; public String lastFetchedTime = Instant.now().toString(); public State(Set<String> fetchedIds) { this.fetchedIds = fetchedIds; } } }

使用方式:在抓取每篇文章前,先调用FetchStateManager.loadFetchedIds().contains(articleId)判断是否已抓取;抓取成功后,调用FetchStateManager.markAsFetched(articleId)。

4.3 反爬规避三原则:请求间隔、User-Agent轮换、失败重试

CSDN虽不激进封IP,但高频请求(<1秒/次)会返回429 Too Many Requests。实测安全策略:

  • 请求间隔:每页列表请求后sleep(2000),每篇文章详情请求后sleep(3000);
  • User-Agent轮换:准备3~5个主流浏览器UA,每次请求随机选取;
  • 失败重试:对403、429、502等错误码,最多重试2次,每次间隔递增(1s、3s)。
import java.util.Random; public class AntiCrawlHelper { private static final String[] USER_AGENTS = { "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }; private static final Random random = new Random(); public static String getRandomUserAgent() { return USER_AGENTS[random.nextInt(USER_AGENTS.length)]; } public static void safeSleep(long millis) { try { Thread.sleep(millis); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public static void retryOnFailure(Runnable task, int maxRetries) { for (int i = 0; i <= maxRetries; i++) { try { task.run(); return; // 成功则退出 } catch (Exception e) { if (i == maxRetries) throw e; long backoff = (long) Math.pow(2, i) * 1000; // 指数退避 System.err.println("第" + (i+1) + "次失败," + backoff + "ms后重试:" + e.getMessage()); safeSleep(backoff); } } } }

关键点:

  • Thread.sleep()必须捕获InterruptedException并恢复中断状态,避免线程被意外终止;
  • retryOnFailure()传入Runnable,使业务代码与重试逻辑解耦;
  • 指数退避(Math.pow(2, i))比固定间隔更符合服务端负载特征,降低被识别为机器的概率。

5. 避坑指南:CSDN爬虫的5个血泪经验与排查清单

5.1 现象:抓取列表页返回403 Forbidden,但浏览器能正常访问

原因:CSDN对User-Agent做白名单校验,空UA或Apache-HttpClient/4.5.14 (Java/1.8.0_391)等默认UA直接拦截。
解决:必须设置真实浏览器UA,且不能长期固定同一UA(建议轮换,见4.3节)。验证方法:用curl测试curl -H "User-Agent: Mozilla/5.0..." https://blog.csdn.net/xxx/article/list/1。

5.2 现象:文章详情页抓取后,#content_views为空或内容缺失

原因:CSDN部分文章启用“原创保护”或“付费阅读”,未登录态返回精简版HTML(无#content_views)。
解决:确认Cookie已正确注入(打印httpClient.getCookieStore().getCookies()验证),且acw_tc未过期(有效期约2小时,需定期更新)。

5.3 现象:保存的TXT文件中文乱码,显示为??或方块

原因:FileWriter默认使用系统编码(Windows为GBK),而CSDN响应为UTF-8。
解决:必须显式指定编码new FileWriter(filePath, "UTF-8"),或改用Files.write(Paths.get(...), content.getBytes(StandardCharsets.UTF_8))。

5.4 现象:Jsoup解析时select("div.article-item")返回空集合

原因:CSDN前端近期改版,.article-item类名可能变为.article-item-box或.blog-list-box。
解决:用浏览器开发者工具实时检查当前页面DOM结构,更新选择器。不要硬编码选择器,应在代码中加注释标明来源(如// 2024年3月CSDN博客列表页结构),方便后续维护。

5.5 现象:多线程并发抓取时,部分请求超时或返回空数据

原因:Apache HttpClient默认连接池大小为2,高并发下连接被占满,新请求排队超时。
解决:自定义连接池:

PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(); connManager.setMaxTotal(20); // 总连接数 connManager.setDefaultMaxPerRoute(10); // 每路由最大连接数 CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connManager) .build();

6. 进阶技巧:用Java NIO异步写入提升吞吐量,以及如何把爬虫变成可维护的CLI工具

6.1 用AsynchronousFileChannel替代FileWriter:避免I/O阻塞拖慢爬取速度

当单次抓取100+篇文章时,FileWriter的同步写入会成为瓶颈(尤其机械硬盘)。Java NIO的AsynchronousFileChannel支持异步写入,主线程无需等待磁盘IO:

import java.nio.ByteBuffer; import java.nio.channels.AsynchronousFileChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.concurrent.CompletableFuture; public class AsyncArticleSaver { public static CompletableFuture<Void> saveAsync(ArticleDetail article, String outputDir) { return CompletableFuture.runAsync(() -> { try { String safeTitle = article.title.replaceAll("[/\\\\:\\*\\?\"<>\\|]", "_").substring(0, 100); String content = formatArticleContent(article); // 同4.1节逻辑 String filePath = Paths.get(outputDir, safeTitle + ".txt").toString(); AsynchronousFileChannel channel = AsynchronousFileChannel.open( Paths.get(filePath), StandardOpenOption.CREATE, StandardOpenOption.WRITE ); ByteBuffer buffer = ByteBuffer.wrap(content.getBytes(StandardCharsets.UTF_8)); channel.write(buffer, 0).get(); // 阻塞等待写入完成(此处为简化,生产环境用CompletionHandler) channel.close(); } catch (Exception e) { throw new RuntimeException("异步保存失败", e); } }); } private static String formatArticleContent(ArticleDetail article) { // 同4.1节的HTML清洗逻辑 return "=== " + article.title + " ===\n" + "标签:" + String.join(", ", article.tags) + "\n" + "原文链接:" + article.url + "\n\n" + article.contentHtml.replaceAll("<[^>]+>", "").replaceAll("&nbsp;", " "); } }

对比效果:

  • 同步FileWriter:100篇文章平均耗时约12秒(I/O占8秒);
  • 异步AsynchronousFileChannel:耗时降至7秒(I/O并行化,CPU与磁盘重叠工作);
  • 注意:channel.write(...).get()是简化写法,真实场景应实现CompletionHandler避免线程阻塞。

6.2 封装为CLI工具:用Picocli解析命令行参数,一键启动全量抓取

把爬虫变成java -jar csdn-crawler.jar --user yourname --output ./data --pages 5这样的命令,大幅提升复用性。Picocli是Java最轻量的CLI框架(单jar,无反射):

import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @Command(name = "csdn-crawler", mixinStandardHelpOptions = true, description = "CSDN个人博客文章爬虫工具") public class CsdnCrawler implements Runnable { @Option(names = {"-u", "--user"}, required = true, description = "CSDN用户名,如:zhangsan") String username; @Option(names = {"-o", "--output"}, required = true, description = "输出目录,如:./output") String outputDir; @Option(names = {"-p", "--pages"}, defaultValue = "1", description = "抓取页数,默认1页") int pageCount; @Override public void run() { System.out.println("开始抓取CSDN用户:" + username + " 的博客..."); try { CsdnListFetcher.fetchAllPages(username, pageCount, outputDir); System.out.println("抓取完成!数据已保存至:" + outputDir); } catch (Exception e) { System.err.println("抓取失败:" + e.getMessage()); e.printStackTrace(); } } public static void main(String[] args) { int exitCode = new CommandLine(new CsdnCrawler()).execute(args); System.exit(exitCode); } }

Maven依赖:

<dependency> <groupId>info.picocli</groupId> <artifactId>picocli</artifactId> <version>4.7.4</version> </dependency>

打包命令:

mvn clean compile assembly:single # 生成target/csdn-crawler-jar-with-dependencies.jar

6.3 维护性清单:让爬虫活过CSDN下一次前端改版

CSDN不会通知你DOM结构变更,所以必须建立防御性维护机制:

检查项频率操作
选择器有效性每次运行前在CsdnListFetcher中加断言:if (articleItems.isEmpty()) throw new IllegalStateException("未匹配到文章卡片,请检查选择器是否过期")
Cookie有效期每2小时日志打印acw_tc最后修改时间,超2小时自动提醒更新
HTTP状态码监控每次请求记录403/429出现频次,超5次/分钟则暂停并告警
HTML结构快照每周抓取一页列表页HTML存为snapshot_20240315.html,用于diff比对变更

我坚持给每个爬虫项目配一个README.md,里面只写三件事:

  1. 当前有效的CSS选择器(附截图DOM路径);
  2. 最后验证通过的Cookie有效期(写明获取日期);
  3. 本次运行的curl -v调试命令(如curl -H "User-Agent: ..." https://blog.csdn.net/xxx/article/list/1)。
    这比写100行注释更管用——因为下次你或同事要修它时,真正需要的就这三行。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询