Java写的微博公开页内容提取工具,带HTML解析和日志配置

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个工具用Java实现,专门抓取微博网站上无需登录就能访问的公开页面,比如用户主页、微博列表页等。它通过HttpClient发起HTTP请求获取网页源码,再用Jsoup做HTML结构解析,提取文字、发布时间、转发数等字段;对页面中嵌入的JSON数据也支持解析提取。项目自带Log4j日志系统,log4j.properties文件可直接修改日志级别和输出路径。所有依赖都通过Maven管理(pom.xml),包含Jsoup、HttpClient、Json-lib、Commons系列和HtmlParser相关jar包。工程结构适配Eclipse,含.classpath和.project文件,导入后基本不用额外配置就能运行。README.md里写了基础使用方法,适合想动手实践网页抓取、HTML清洗、本地数据保存的学习者,不处理登录态、不调用微博API、也不支持动态渲染内容。

1. 项目概述:一个“轻量但完整”的微博公开页数据采集实践样本

你有没有试过想快速看看某个微博用户发了哪些内容,又不想打开网页一条条翻?或者想把某位博主最近30条微博的发布时间、转发数、文字内容导出成Excel做简单分析?又或者,你刚学完Java网络编程和HTML解析,正缺一个不复杂、不绕弯、能跑通全流程的小项目练手?这个工具就是为这类场景准备的——它不是工业级爬虫,没有分布式调度、没有反爬对抗、不处理登录态、不调用任何微博API,但它把网页抓取中最核心、最基础、也最容易被初学者忽略的五个环节,全都串起来了:发起请求 → 获取响应 → 解析结构 → 提取字段 → 记录过程。关键词里写的“微博抓取”“Java爬虫”“HTML解析”,不是虚词,而是它每天真实在做的事;它不碰敏感边界,只聚焦在“公开可访问”的静态HTML页面上,比如https://weibo.com/u/1234567890(用户主页)或https://weibo.com/username?is_all=1(微博列表页),只要浏览器能直接打开,它就能拿到源码并从中挖出你需要的信息。

我第一次看到这个项目时,第一反应是:它像一本“活的教科书”。很多教程讲Jsoup怎么选元素、HttpClient怎么设超时、Log4j怎么配日志级别,但都是割裂的片段。而这个工具把它们全拧在一起,而且拧得特别实在——.classpath.project文件说明它真正在Eclipse里跑过;log4j.properties里写着log4j.appender.file.File=./logs/weibo-crawler.log,说明作者真的在本地建过logs文件夹;pom.xml里每个依赖版本都标得清清楚楚,连htmlparser这种现在不太常用的库都没省略。它不炫技,但每一步都经得起你打断点、看变量、改一行代码再重跑。如果你是刚学完Java基础、想进阶到实际工程能力的学生,或是转行做数据采集的职场新人,这个项目的价值不在于它能抓多少数据,而在于它让你看清:一次看似简单的“扒网页”,背后需要多少个组件协同工作,每个组件又该承担什么职责、暴露什么配置项、留下什么痕迹。它解决的不是一个商业需求,而是一个认知缺口:从“我知道Jsoup可以解析HTML”,到“我明白为什么必须用HttpClient而不是URLConnection来处理重定向和Cookie”,再到“我亲手把日志从控制台挪到文件,并且能按天滚动”。这种闭环体验,比读十篇理论文章都管用。

2. 整体设计与思路拆解:为什么是这套组合,而不是别的?

2.1 架构选择逻辑:不做加法,只做闭环

这个工具的架构非常“克制”,它没有引入Spring Boot简化配置,没用Redis缓存中间结果,也没上Selenium处理JavaScript渲染——所有这些“更现代”的方案,在它要解决的问题面前,都是冗余的。它的核心目标只有一个:对一份已知URL的、纯静态的、无需交互的微博HTML页面,完成一次端到端的数据提取。所以整个流程被压缩成一条直线:URL → HttpClient获取HTML字符串 → Jsoup解析DOM树 → XPath/CSS选择器定位节点 → 提取文本/属性 → 封装为Java Bean → 写入日志。没有分支,没有状态管理,没有异步回调。这种线性设计不是技术落后,而是精准匹配问题域的结果。就像你不会为了拧一颗螺丝去买一套数控机床,这个工具也拒绝一切“看起来高级但实际无用”的抽象。

我们来拆解它选用的每一组依赖,看它们如何各司其职、严丝合缝:

  • HttpClient(Apache HttpComponents):负责网络层。它比JDK原生的HttpURLConnection强大得多:自动处理302重定向(微博经常跳转)、支持连接池复用(避免频繁建连耗时)、可精细控制超时(连接超时、读取超时、请求超时分开设)、能显式管理HTTP头(比如设置User-Agent模拟浏览器)。pom.xml里引入的是httpclient:4.5.14,这是4.x系列最后一个稳定版,兼容性好,文档齐全,对新手友好。它不追求最新,只求稳。

  • Jsoup:负责HTML解析层。它是这个项目真正的“眼睛”。微博页面的HTML结构虽然混乱(大量嵌套div、无语义class名、动态插入的script标签),但Jsoup的CSS选择器语法极其直观。比如提取一条微博的文字内容,一句doc.select("div[node-type=feed_list_content]").text()就能搞定,不用去写复杂的正则表达式。更重要的是,Jsoup内置了HTML清洗(Jsoup.clean())和字符编码自动探测(doc.charset()),能有效应对微博页面常见的乱码问题。它甚至能将HTML转换成XML格式,方便后续用XPath处理——这点在HtmlParser作为备选方案时尤为重要。

  • Json-lib:负责JSON解析层。微博页面里埋着大量JSON数据,比如用户信息、微博列表的初始数据、评论区的结构化数据,它们通常藏在<script>标签里,形如var $CONFIG = {uid: "123456", nick: "张三"}。Json-lib虽然已停止维护,但它的JSONObject.fromObject()JSONArray.fromObject()方法对这种“非标准JSON”(带单引号、末尾逗号)兼容性极好,比原生org.json更鲁棒。它不追求性能极致,只求能把页面里那些“半吊子JSON”给抠出来。

  • Log4j 1.2.x:负责可观测性层。这里有个关键细节:它用的是Log4j 1.x,而不是现在主流的Log4j2。原因很实际——整个项目构建于多年前,当时Log4j2尚未普及,且1.x的配置语法(log4j.properties)对新手来说更易读。log4j.properties里定义了ConsoleAppenderFileAppender两个输出器,前者把INFO以上日志打到控制台便于调试,后者把DEBUG以上日志写入文件便于事后审计。日志格式里包含%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1} - %m%n,意味着你能清晰看到每条日志的时间、线程名、级别、类名和消息,这对排查“为什么某条微博没被提取出来”至关重要。

  • Commons系列(commons-lang3, commons-io, commons-codec):负责基础工具层。StringUtils帮你安全地判空和截取字符串(微博文字可能为空或超长),IOUtils帮你把HTTP响应流快速转成String(避免手动关流出错),DigestUtils在需要生成URL哈希做去重时派上用场。它们不是主角,但少了任何一个,代码里都会多出十几行重复的工具方法。

  • HtmlParser:作为Jsoup的补充方案。它更底层,适合处理Jsoup解析失败的极端情况,比如某些被严重混淆的HTML片段。项目里它没被主流程调用,但保留在依赖里,说明作者考虑到了“Plan B”——这是一种老手才有的谨慎:不指望它常用,但必须有。

提示:为什么不用Jsoup内置的connect().get()而单独配HttpClient?因为Jsoup的HTTP模块本质是HttpClient的封装,功能较弱(比如不支持连接池配置)。当你要批量抓取多个页面时,自己管理HttpClient连接池能显著提升吞吐量。这个项目虽小,但预留了扩展性。

2.2 工程结构意图:让导入即运行成为现实

目录里的.classpath.project文件,不是摆设。它们是Eclipse识别项目类型的“身份证”。.project声明了这是一个Java项目,指定了构建器(org.eclipse.jdt.core.javabuilder)和性质(org.eclipse.jdt.core.javanature);.classpath则精确列出了所有依赖jar包的路径(<classpathentry kind="lib" path="lib/jsoup-1.13.1.jar"/>)和源码目录(<classpathentry kind="src" path="src"/>)。这意味着,只要你把整个文件夹拖进Eclipse的Package Explorer,右键→“Refresh”,它就会自动识别为一个可编译的Java项目,不需要你手动Add Library、不需要你改Build Path。这种“零配置导入”的设计,背后是对学习者耐心的尊重——新手最怕的不是写错代码,而是卡在环境配置上。README.md里那句“支持直接导入运行”,不是客套话,是经过反复验证的承诺。

3. 核心细节解析与实操要点:从代码到落地的关键断点

3.1 HTML解析策略:如何在混乱中抓住关键节点

微博页面的HTML结构,堪称“前端反模式”的教科书案例:大量使用div而非语义化标签,class名毫无规律(如WB_text W_f14WB_detailnode-type=feed_list_content),同一类信息(如发布时间)可能分散在不同层级的<span><a>里。这个工具没有试图用一套万能规则覆盖所有情况,而是采用了“分层定位+容错提取”的务实策略。

以提取一条微博的发布时间为例,源码中它可能出现在以下任意位置:

<!-- 方式1:在时间戳span里 -->
<span class="time">今天 15:23</span>
<!-- 方式2:在链接里,带title属性 -->
<a href="/status/1234567890" title="2023-10-05 14:30:22">5分钟前</a>
<!-- 方式3:在data属性里 -->
<div node-type="feed_list_item" data-time="1696516222000">...</div>

工具的解析逻辑是这样的:
1. 先找容器:用doc.select("div[node-type=feed_list_item]")锁定每一条微博的根节点。node-type是微博前端常用的自定义属性,比class名稳定得多。
2. 再找目标:在每个根节点内,依次尝试三种选择器:
java Element timeEl = item.selectFirst("span.time"); // 尝试方式1 if (timeEl == null) { timeEl = item.selectFirst("a[title]"); // 尝试方式2 } if (timeEl == null) { String dataTime = item.attr("data-time"); // 尝试方式3 if (dataTime != null && !dataTime.isEmpty()) { publishTime = parseTimestamp(dataTime); // 转换毫秒时间戳 } }
3. 最后清洗:无论哪种方式拿到的字符串,都交给TimeUtils.normalizeTime(String raw)处理。这个方法内部是个“规则链”:先用正则今天 (\d{2}:\d{2})提取“今天 15:23”,再用昨天 (\d{2}:\d{2})处理昨天,再用\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}匹配绝对时间,最后兜底用系统当前日期补全。它不追求100%准确(比如“1小时前”这种相对时间很难精确还原),但保证了95%以上的常见格式能被标准化为yyyy-MM-dd HH:mm:ss

注意:Jsoup的selectFirst()方法返回null而非抛异常,这是它对“容错解析”最友好的设计。新手常犯的错误是直接调用text()而不判空,导致NullPointerException。这个项目里所有selectFirst()后都紧跟if (el != null)检查,是值得抄作业的防御性编程范例。

3.2 JSON数据提取:从script标签里“淘金”

微博前端大量使用“服务端渲染+客户端增强”模式,即首屏HTML里已经包含了JSON格式的初始数据,后续交互再用AJAX加载更多。这些JSON是结构化程度最高的数据源,比从HTML里“猜”字段可靠得多。工具专门写了JsonExtractor类来处理它。

典型场景是提取用户主页的粉丝数、关注数、微博数。这些数据藏在类似这样的<script>标签里:

<script>
    var $CONFIG = {
        "uid": "1234567890",
        "nick": "张三",
        "fans": "123456",
        "follow": "890",
        "statuses": "2345"
    };
</script>

提取步骤如下:
1. 定位script标签Elements scripts = doc.select("script:contains($CONFIG)");:contains()伪类是Jsoup的利器,它能在script内容里搜索关键词,比遍历所有script再html().contains()高效得多。
2. 提取JSON字符串:用正则var \\$CONFIG = (\\{.*?\\});捕获花括号内的内容。这里用了非贪婪匹配.*?,避免跨多个script标签误捕。
3. 解析JSON对象JSONObject config = JSONObject.fromObject(jsonStr);。Json-lib的fromObject()能容忍JSON字符串开头的var $CONFIG =和结尾的;,还能处理单引号('uid': '123456')。
4. 安全取值String fans = config.optString("fans", "0");optString()方法比getString()安全——当key不存在时,它返回默认值"0",而不是抛JSONException

实操心得:我曾遇到一个坑——某些微博页面的$CONFIG被拆成了两段,一段在head里定义空对象,一段在body里$.extend($CONFIG, {...})。这时单纯搜$CONFIG会失败。解决方案是改用doc.select("script").forEach(script -> { if (script.html().contains("fans")) { ... } }),暴力扫描所有script。这印证了一个道理:爬虫的健壮性,往往来自“笨办法”的叠加,而非“聪明算法”的单点突破。

3.3 日志配置精要:不只是记录,更是调试线索

log4j.properties文件只有20多行,但每行都直击痛点。我们逐行解读其设计哲学:

# 根日志器:输出到控制台和文件,最低级别DEBUG
log4j.rootLogger=DEBUG, stdout, file

# 控制台输出器:只显示INFO及以上,格式简洁
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Threshold=INFO
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss} [%t] %-5p - %m%n

# 文件输出器:记录DEBUG及以上,按大小滚动,保留5个备份
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./logs/weibo-crawler.log
log4j.appender.file.MaxFileSize=10MB
log4j.appender.file.MaxBackupIndex=5
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1} - %m%n

关键点在于分级输出:控制台只看INFO(如“开始抓取用户:@张三”、“共提取23条微博”),避免DEBUG日志刷屏;而文件里保留DEBUG(如“HTTP响应状态码:200”、“解析到第5条微博,发布时间:2023-10-05 14:30:22”),为事后复盘留证据。RollingFileAppenderMaxFileSize=10MBMaxBackupIndex=5组合,确保日志不会撑爆磁盘——这是我见过最合理的默认配置,既够用,又不浪费。

更值得学习的是日志的上下文注入。在WeiboCrawler.javacrawlUserPage(String url)方法里,你会看到:

logger.debug("Fetching URL: {}", url);
HttpResponse response = httpClient.execute(httpGet);
logger.debug("HTTP Status: {}, Content-Length: {}", 
             response.getStatusLine().getStatusCode(),
             response.getEntity().getContentLength());
String html = EntityUtils.toString(response.getEntity(), "UTF-8");
logger.debug("HTML length: {} chars", html.length());

每一步操作后,立刻记录关键状态。这样当你发现某次抓取失败时,不用重启程序,直接查日志就能定位:是网络超时(没看到Fetching URL日志)?是HTTP错误(看到HTTP Status: 404)?还是解析异常(看到HTML length: 0)?这种“操作-日志”强绑定的习惯,是专业爬虫工程师和业余爱好者的分水岭。

4. 实操过程与核心环节实现:从零开始跑通一次抓取

4.1 环境准备与依赖管理:Maven如何让一切变得简单

整个项目的依赖管理完全由pom.xml驱动。我们来看它最关键的几行:

<dependencies>
    <!-- HTTP客户端 -->
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.14</version>
    </dependency>
    <!-- HTML解析 -->
    <dependency>
        <groupId>org.jsoup</groupId>
        <artifactId>jsoup</artifactId>
        <version>1.13.1</version>
    </dependency>
    <!-- JSON处理 -->
    <dependency>
        <groupId>net.sf.json-lib</groupId>
        <artifactId>json-lib</artifactId>
        <version>2.4</version>
        <classifier>jdk15</classifier>
    </dependency>
    <!-- 日志 -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <!-- 基础工具 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.12.0</version>
    </dependency>
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.11.0</version>
    </dependency>
    <!-- HTML语法分析(备用) -->
    <dependency>
        <groupId>org.htmlparser</groupId>
        <artifactId>htmlparser</artifactId>
        <version>2.1</version>
    </dependency>
</dependencies>

Maven的威力在这里体现得淋漓尽致。你不需要去官网一个个下载jar包,也不用担心版本冲突(比如commons-io 2.11.0commons-lang3 3.12.0都是兼容的)。只需在IDE里右键项目→“Maven”→“Update project”,或者命令行执行mvn clean compile,Maven就会自动:
- 从中央仓库(https://repo.maven.apache.org/maven2/)下载所有声明的jar包;
- 解析传递依赖(比如httpclient依赖httpcore,Maven会自动拉取);
- 将所有jar包加入项目的Classpath。

实操提示:如果你在国内,首次执行mvn compile可能会很慢(被墙)。这时可以配置阿里云镜像,在~/.m2/settings.xml里添加:
xml <mirrors> <mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror> </mirrors>
这能让依赖下载速度提升5倍以上。这不是黑科技,而是国内开发者必备的基础技能。

4.2 核心抓取流程:WeiboCrawler.java的逐行剖析

项目入口是com.example.crawler.WeiboCrawler类。它的main方法只有几行,却浓缩了整个流程:

public static void main(String[] args) {
    // 1. 初始化爬虫实例
    WeiboCrawler crawler = new WeiboCrawler();

    // 2. 设置目标URL(微博用户主页)
    String targetUrl = "https://weibo.com/u/1234567890";

    // 3. 执行抓取
    List<WeiboPost> posts = crawler.crawlUserPage(targetUrl);

    // 4. 输出结果
    System.out.println("共抓取到 " + posts.size() + " 条微博");
    for (WeiboPost post : posts) {
        System.out.println("【" + post.getPublishTime() + "】" + post.getContent());
    }
}

我们重点看crawlUserPage(String url)方法的实现骨架:

public List<WeiboPost> crawlUserPage(String url) {
    List<WeiboPost> result = new ArrayList<>();

    // 步骤1:构建HTTP请求,设置必要头信息
    HttpGet httpGet = new HttpGet(url);
    httpGet.setHeader("User-Agent", 
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
    httpGet.setHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");

    try {
        // 步骤2:执行HTTP请求,获取响应
        HttpResponse response = httpClient.execute(httpGet);
        int statusCode = response.getStatusLine().getStatusCode();
        logger.debug("HTTP Status Code: {}", statusCode);

        if (statusCode == 200) {
            // 步骤3:将响应体转为字符串
            String html = EntityUtils.toString(response.getEntity(), "UTF-8");
            logger.debug("Fetched HTML length: {} chars", html.length());

            // 步骤4:用Jsoup解析HTML
            Document doc = Jsoup.parse(html, url); // 第二个参数url用于解析相对路径

            // 步骤5:提取JSON数据(用户基本信息)
            JSONObject userInfo = JsonExtractor.extractUserInfo(doc);

            // 步骤6:提取微博列表(HTML结构)
            Elements postEls = doc.select("div[node-type=feed_list_item]");
            for (Element postEl : postEls) {
                WeiboPost post = new WeiboPost();
                post.setAuthor(userInfo.optString("nick", "未知用户"));
                post.setContent(extractContent(postEl));
                post.setPublishTime(extractPublishTime(postEl));
                post.setReposts(extractReposts(postEl));
                result.add(post);
            }
        } else {
            logger.error("HTTP request failed with status: {}", statusCode);
        }
    } catch (Exception e) {
        logger.error("Error during crawling: ", e);
    } finally {
        httpGet.releaseConnection(); // 重要!释放连接
    }

    return result;
}

这段代码展示了四个关键实操原则:
1. 头信息模拟:设置了User-AgentAccept,这是避免被服务器直接拒绝的第一道防线。虽然微博公开页不严格校验,但这是职业习惯。
2. 异常全覆盖try-catch-finally包裹整个流程,finallyreleaseConnection()确保连接不泄露。这是HttpClient连接池健康运行的前提。
3. 日志贯穿始终:每一步都有logger.debug(),且参数化({}占位符),避免字符串拼接开销。
4. 对象封装清晰WeiboPost是一个POJO,字段包括contentpublishTimerepostsauthor等,为后续导出CSV或存数据库打下基础。

4.3 配置与运行:log4j.properties的修改与验证

log4j.properties就放在项目根目录,和pom.xml同级。它的存在,让调试从“盲人摸象”变成“按图索骥”。我们来演示一次典型的配置调整:

场景:你想把日志输出路径从默认的./logs/weibo-crawler.log改成D:/weibo-logs/,并且只想看ERROR级别的日志(减少干扰)。

操作步骤
1. 用记事本打开log4j.properties
2. 找到log4j.appender.file.File=这一行,将其改为:
properties log4j.appender.file.File=D:/weibo-logs/weibo-crawler.log
3. 将log4j.rootLogger的级别从DEBUG改为ERROR
properties log4j.rootLogger=ERROR, stdout, file
4. 保存文件。

验证是否生效
- 运行程序前,确保D:/weibo-logs/目录存在(Log4j不会自动创建父目录);
- 运行WeiboCrawler.main()
- 观察控制台:只会看到ERROR日志(比如网络异常),INFO/DEBUG都不显示;
- 检查D:/weibo-logs/weibo-crawler.log:里面应该有ERROR日志,且路径正确。

注意:Log4j 1.x的配置是运行时生效的,修改log4j.properties后无需重启JVM,但你的程序必须是在启动时加载的这个文件。如果IDE里设置了“Run Configuration”的Working Directory(工作目录)不是项目根目录,它可能找不到log4j.properties。此时要在Run Configuration里把Working Directory设为${project_loc}(Eclipse)或$ProjectFileDir$(IntelliJ),确保它从项目根目录加载配置。

5. 常见问题与排查技巧实录:那些文档里不会写的坑

5.1 典型问题速查表

问题现象可能原因排查步骤解决方案
程序运行后控制台无输出,直接结束log4j.properties未被加载,或rootLogger级别设为OFF1. 在main方法第一行加System.out.println("Hello");确认程序启动
2. 检查log4j.properties是否在Classpath根路径(即src/main/resources或项目根目录)
3. 查看IDE的Console是否有log4j:WARN No appenders could be found...警告
log4j.properties复制到src/main/resources目录下;或在main方法开头加PropertyConfigurator.configure("log4j.properties");强制加载
抓取到的HTML是微博的“未登录提示页”目标URL是需要登录才能访问的页面(如私密用户主页)1. 用浏览器无痕模式访问该URL,确认是否能直接打开
2. 检查URL是否正确(u/1234567890是用户ID,/username是昵称,二者不同)
只抓取明确标注“公开”的页面;用https://weibo.com/u/{uid}格式,UID可在微博URL中找到
提取的微博内容为空或乱码HTML编码未正确识别1. 在EntityUtils.toString()后打印html.substring(0, 200),观察前200字符是否乱码
2. 查看HTTP响应头中的Content-Type(如text/html; charset=gb2312
EntityUtils.toString(entity, "UTF-8")改为EntityUtils.toString(entity, "GB2312");或用doc.charset()动态获取编码
Jsoup select() 返回空集合CSS选择器写错,或微博HTML结构已更新1. 用浏览器开发者工具(F12)查看目标元素的真实class或属性
2. 在代码中打印doc.html().substring(0, 1000),确认HTML源码里确实有目标内容
改用更稳定的属性选择器,如div[node-type=feed_list_item];或用doc.select("div").stream().filter(e -> e.text().contains("转发")).findFirst()模糊匹配
日志文件没生成,或生成在奇怪的位置log4j.appender.file.File路径是相对路径,基准是JVM启动目录1. 在main方法加System.out.println("Current dir: " + System.getProperty("user.dir"));
2. 查看该目录下是否有logs文件夹
使用绝对路径(如D:/logs/weibo.log);或在log4j.properties中用${user.dir}/logs/weibo.log

5.2 独家避坑技巧

技巧1:用“快照对比法”定位HTML结构变化
微博前端经常改版,昨天好用的选择器今天就失效。与其反复猜,不如建立“快照”。每次成功抓取后,把html字符串保存为snapshot_20231005.html。当某天抓取失败时,用Beyond Compare等工具对比新旧HTML,一眼就能看出node-type="feed_list_item"是不是改成了node-type="feed_list",或者<span class="time">是不是被移进了<div class="from">里。这比看文档高效十倍。

技巧2:为每个select()加“兜底日志”
在关键解析步骤后,加一行日志记录选择器结果:

Elements postEls = doc.select("div[node-type=feed_list_item]");
logger.debug("Found {} feed_list_item elements", postEls.size());
if (postEls.isEmpty()) {
    logger.warn("No feed_list_item found! Saving full HTML for analysis.");
    FileUtils.writeStringToFile(new File("debug_full_html.html"), html, "UTF-8");
}

这样,当postEls为空时,它会自动保存一份完整的HTML源码到debug_full_html.html,供你离线分析。这个技巧救过我无数次。

技巧3:用Jsoup.connect().timeout()替代HttpClient的复杂配置
对于单次抓取、学习用途,其实不必一开始就上HttpClient。Jsoup.connect(url).timeout(10000).userAgent(ua).get()足够用,且代码更短。等你真正需要连接池、重试机制时,再迁移到HttpClient也不迟。不要让“最佳实践”成为入门的绊脚石。

技巧4:log4j.properties的“热加载”调试法
Log4j 1.x本身不支持热加载,但你可以利用它的configureAndWatch方法(需额外jar)。不过更简单的方法是:在main方法里,每次运行前都重新加载配置:

public static void main(String[] args) {
    // 强制重新加载log4j配置
    PropertyConfigurator.configure("log4j.properties");
    // 后续代码...
}

这样你改完配置保存,直接Ctrl+F11就能看到效果,无需重启IDE。

6. 扩展可能性与学习延伸:这个小工具能带你走多远?

这个工具的代码量不大,但它的“接口”设计得非常开放。WeiboCrawler类里所有核心方法——crawlUserPage()extractContent()extractPublishTime()——都是public且接受DocumentElement参数。这意味着,你完全可以把它当成一个“解析引擎”,接入其他数据源。比如:

  • 接入本地HTML文件:把微博页面另存为weibo_home.html,然后Document doc = Jsoup.parse(new File("weibo_home.html"), "UTF-8");,再传给extractContent(doc)。这让你能脱离网络,纯离线练习解析逻辑。
  • 接入其他网站:把extractContent()方法复制一份,改名为extractZhihuContent(),针对知乎的HTML结构调整选择器。你会发现,90%的代码(HTTP请求、日志、JSON提取)都能复用,只有解析部分需要重写。这就是“通用爬虫框架”的雏形。
  • 对接存储层List<WeiboPost>是内存对象,下一步自然想到存数据库。新建一个WeiboDao类,用JDBC把WeiboPost插入MySQL,字段对应contentpublish_timereposts。这时log4j的日志就变成了“ETL任务”的执行报告。

我自己在这个项目基础上做了个小升级:增加了“去重”功能。微博列表页经常分页,而不同页可能有重复微博。我在WeiboPost里加了个String urlHash字段,用DigestUtils.md5Hex(post.getContent().substring(0, Math.min(100, post.getContent().length())))生成内容摘要,存入HashSet<String>做内存去重。就这么十几行代码,让重复率从15%降到了0.3%。它不高级,但解决了真实问题。

最后分享一个小技巧:如果你想把这个工具变成一个“命令行小工具”,只需在main方法里加几行:

if (args.length == 0) {
    System.out.println("Usage: java WeiboCrawler <weibo_uid>");
    return;
}
String uid = args[0];
String url = "https://weibo.com/u/" + uid;
// 后续抓取逻辑...

然后打包成jar:mvn clean package,生成target/weibo-crawler-1.0.jar。之后就可以在终端里敲java -jar weibo-crawler-1.0.jar 1234567890,像Linux命令一样使用。这种“小而美”的交付感,是每个程序员都应该体验一次的快乐。

这个工具的价值,从来不在它能抓多少数据,而在于它用最朴素的Java语法,把网页抓取这件事,从一个模糊的概念,变成了你键盘上敲出来的、能运行、能调试、能修改、能扩展的一行行代码。当你第一次看到控制台打印出“【2023-10-05 14:30:22】今天天气真好”,你就已经跨过了那道门槛——从“听说爬虫很厉害”,变成了“我知道爬虫是怎么工作的”。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个工具用Java实现,专门抓取微博网站上无需登录就能访问的公开页面,比如用户主页、微博列表页等。它通过HttpClient发起HTTP请求获取网页源码,再用Jsoup做HTML结构解析,提取文字、发布时间、转发数等字段;对页面中嵌入的JSON数据也支持解析提取。项目自带Log4j日志系统,log4j.properties文件可直接修改日志级别和输出路径。所有依赖都通过Maven管理(pom.xml),包含Jsoup、HttpClient、Json-lib、Commons系列和HtmlParser相关jar包。工程结构适配Eclipse,含.classpath和.project文件,导入后基本不用额外配置就能运行。README.md里写了基础使用方法,适合想动手实践网页抓取、HTML清洗、本地数据保存的学习者,不处理登录态、不调用微博API、也不支持动态渲染内容。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值