
简介一套利用Java HttpClient与HtmlParser实现的简单搜索引擎爬虫项目源码面向具备基础Java语法、希望入门网络爬虫的开发者也适合作为课程设计或毕业设计的参考模板。压缩包共42个文件其中包含5个Java源文件、8个编译后的class文件、16个常用依赖jar包如httpclient、htmlparser以及4个抓取的HTML页面样本总大小仅3.1MB结构精简便于阅读。项目围绕SearchEngine场景展示从构造GET请求、发送请求、解析HTML文档到提取搜索结果标题与链接的完整流程覆盖初始化客户端、请求页面、解析响应、提取数据、存储结果与异常处理等关键步骤并附带了Eclipse工程配置和第三方库导入后即可运行调试。已有262人学习适合想快速掌握HttpClient与HtmlParser核心用法并动手实践简单爬虫的Java学习者也便于在此基础上扩展多页抓取、数据持久化等功能。 做 Java 后端的人多半会被问到一个问题写爬虫不是 Python 的事吗用 HttpClient、HtmlParser 这些 Java 技术栈是不是绕远了其实我在实际项目里抓过不少公开数据体验是如果目标网站只要求服务端渲染、公开访问、无需复杂登录用 Java 做一条简单爬虫反而方便因为它能直接嵌进现有业务系统登录态、数据落库、定时任务都可以复用同一套体系。这个项目就是一个很典型的例子HttpClient 负责“拿到网页”HtmlParser 负责“抽走数据”两者配合几十行代码就能实现对目标站点的内容采集。这篇文章会从环境准备讲到完整实现再把我踩过的坑一并列出来。适合刚接触爬虫的 Java 同学也适合准备 Java 面试时想拿一两个实操亮点的人参考。毕竟面试官常问“你写过爬虫吗”你如果能把这套流程讲清楚比背八股文有用得多。我自己刚接触的时候也纠结过要不要直接上 Spring Boot 全家桶后来发现一个干净的命令行工程就够验证思路等你真想把它工业化再往数据层和调度层扩展不迟。1. 准备阶段为什么选这组库与项目基础搭建1.1 为什么是 HttpClient HtmlParser一说到爬虫大多数人第一反应是 Python 的 requests 加 BeautifulSoup脚本写起来确实快。但放到 Java 工程里这个组合会多一层跨语言维护成本你要么用 Java 调 Python 脚本要么单独跑一个采集服务两边还得定义接口来回传数据。如果公司内部已经有一套 Java 后端最自然的方式就是把采集逻辑写成一个普通 Maven 模块和业务代码放在同一个工程里依赖管理、日志、监控全部复用。当然Java 生态里也有一堆现成方案比如 Jsoup 本身就能解析 HTML它的 CSS 选择器比 HtmlParser 好用得多。但“HttpClient HtmlParser”是很多老项目里真实存在的组合也是 Java 面试八股里经常被拿出来问的技术栈。拆成两层还有一个好处你能更清楚地理解爬虫的本质无非是“发请求拿响应”和“从响应里提取数据”两个阶段混在一个库时反而容易搞混。为了学习原理这个经典组合非常合适。方案优点缺点Python requests BeautifulSoup上手快、代码短、生态强与 Java 系统集成困难需要跨语言维护HttpClient HtmlParser纯 Java能嵌入现有工程连接池管理成熟HtmlParser 较老对 HTML5 兼容一般Jsoup 单库方案选择器好用容错强HTTP 能力弱通常还得配合 HttpClient1.2 Maven 依赖引入和项目结构先用 Maven 建一个最简单的 Java 工程把两个依赖加进去。HttpClient 我选的是 4.5.14这是 4.x 系列的收尾版本长期使用下来很稳定HtmlParser 版本停在 2.1虽然老但 API 简单、没有复杂传递依赖能跑就行。dependency groupIdorg.apache.httpcomponents/groupId artifactIdhttpclient/artifactId version4.5.14/version /dependency dependency groupIdorg.htmlparser/groupId artifactIdhtmlparser/artifactId version2.1/version /dependency如果你不是 Maven 工程直接下载这两个 jar 包丢进 lib 目录也可以。这里提醒一句最省事的运行环境是 JDK 8。HtmlParser 发布得太早在高版本 JDK 上偶尔会因为模块限制报错我在第 5 章会专门讲这个坑。项目结构也不复杂就四个类src/main/java/ ├── com/example/crawler/ │ ├── HttpUtil.java // 负责发送 HTTP 请求 │ ├── News.java // 数据模型 │ ├── NewsParser.java // 负责解析 HTML │ └── Main.java // 串联整个流程2. 用 HttpClient 封装网页抓取器附完整代码2.1 HttpClient 的基本用法和超时配置HttpClient 的用法很固定创建一个全局的 CloseableHttpClient然后用 HttpGet 包装请求地址执行后从 response 里取出实体内容。下面这个工具类是我在项目里常用的简化版保留了超时设置、User-Agent 伪装和资源自动释放import org.apache.http.client.config.RequestConfig; 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; import java.io.IOException; /** * 最简单的 Http 抓取工具只保留够用的功能。 */ public class HttpUtil { private static final String 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; private static final int TIMEOUT 5000; private final CloseableHttpClient httpClient; public HttpUtil() { RequestConfig config RequestConfig.custom() .setConnectTimeout(TIMEOUT) .setSocketTimeout(TIMEOUT) .setConnectionRequestTimeout(TIMEOUT) .build(); httpClient HttpClients.custom() .setDefaultRequestConfig(config) .build(); } public String getHtml(String url) throws IOException { HttpGet httpGet new HttpGet(url); httpGet.setHeader(User-Agent, USER_AGENT); try (CloseableHttpResponse response httpClient.execute(httpGet)) { int status response.getStatusLine().getStatusCode(); if (status ! 200) { throw new IOException(请求失败HTTP 状态码 status); } return EntityUtils.toString(response.getEntity(), UTF-8); } } public void close() throws IOException { httpClient.close(); } }三个超时参数别嫌多它们管的事不一样连接超时是建立 TCP 连接的最长等待时间Socket 超时是服务端返回数据包之间的最大间隔连接请求超时是从连接池里拿连接的最长等待时间。我曾经用默认配置去访问一个响应很慢的服务结果线程卡在原地快 90 秒才报错把这三个值统一设成 5 秒后问题立刻消失。2.2 模拟浏览器请求头与连接复用还有一个关键点是 User-Agent。HttpClient 默认会带一串“Apache-HttpClient/4.5.14”的标识很多网站看到就直接返回 403换上浏览器的 User-Agent 后成功率会高很多。如果目标是需要登录才能访问的页面可以把 Cookie 串也塞进请求头httpGet.setHeader(Accept, text/html,application/xhtmlxml); httpGet.setHeader(Referer, https://example.com/); httpGet.setHeader(Cookie, sessionidxxx; tokenyyy);在爬虫里连接复用特别重要。新手最容易犯的一个错误是每次请求都 new 一个 HttpClient用完再 close这样底层连接池完全没生效每次都要重新握一次手。更好的做法是像上面的代码一样把 httpClient 做成成员变量同一个实例多次执行 getHtml连接会自动复用。等你想加并发量了再用 PoolingHttpClientConnectionManager 调整连接池大小PoolingHttpClientConnectionManager cm new PoolingHttpClientConnectionManager(); cm.setMaxTotal(50); cm.setDefaultMaxPerRoute(20); httpClient HttpClients.custom().setConnectionManager(cm).build();3. 用 HtmlParser 精准抽取页面数据附完整代码3.1 HtmlParser 的 NodeFilter 过滤与提取抓完 HTML 之后下一步就是从一堆标签里把目标数据抠出来。HtmlParser 的工作机制很直观先把 HTML 解析成一棵节点树然后用 Filter 按条件筛选节点。你可以把 Filter 想象成一道筛子比如TagNameFilter(A)等价于 CSS 选择器里的aHasAttributeFilter(class, news-item)等价于[classnews-item]多个条件可以用AndFilter拼在一起。下面这个例子演示了最基础的用法解析一段 HTML 字符串提取所有链接和文字。注意extractAllNodesThatMatch的第二个参数传true表示递归查找所有后代节点否则只会扫描当前层级很容易漏数据。import org.htmlparser.Node; import org.htmlparser.NodeFilter; import org.htmlparser.Parser; import org.htmlparser.filters.AndFilter; import org.htmlparser.filters.HasAttributeFilter; import org.htmlparser.filters.TagNameFilter; import org.htmlparser.tags.LinkTag; import org.htmlparser.util.NodeList; public class HtmlParserDemo { public static void main(String[] args) throws Exception { String html div classlist div classitema href/news/1.html标题一/a/div div classitema href/news/2.html标题二/a/div /div; Parser parser new Parser(html); parser.setEncoding(UTF-8); NodeFilter itemFilter new AndFilter( new TagNameFilter(DIV), new HasAttributeFilter(class, item)); NodeList items parser.parse(itemFilter); NodeList links items.extractAllNodesThatMatch(new TagNameFilter(A), true); for (int i 0; i links.size(); i) { LinkTag link (LinkTag) links.elementAt(i); System.out.println(link.getStringText().trim() - link.getLink()); } } }很多初学者不知道Parser.parse(filter)内部其实是两件事先完整解析 HTML 成 NodeList再在结果上执行过滤。理解这一点后你就不会纠结“为什么 parse 一次之后还能继续 extract”。实际项目里我通常先解析出外层容器再在容器内继续提取这样比一次性写一个超级复杂的 Filter 可读性好得多也方便单条数据单独做异常兜底。3.2 用 NodeVisitor 处理复杂页面结构如果只是“挑同一类节点”Filter 是首选。但遇到要在遍历过程中统计上下文、或者同时处理多种标签的情况可以用 NodeVisitor。它相当于给解析树挂了个监听器每访问到一个 Tag 就回调一次 visitTagParser parser new Parser(html); parser.setEncoding(UTF-8); parser.visitAllNodesWith(new NodeVisitor() { Override public void visitTag(Tag tag) { if (A.equalsIgnoreCase(tag.getTagName())) { LinkTag link (LinkTag) tag; System.out.println(link.getStringText() - link.getLink()); } } });说句实话平时写简单爬虫用 NodeVisitor 的场景不算多它更适合解析表格、嵌套列表这类结构复杂的页面。但了解这种写法还是值得的因为很多老项目的代码里就是用 Visitor 遍历的面试时能随口说出两种解析方式比只背一个“用 Filter 提取节点”要扎实得多。4. 完整案例新闻标题列表爬虫从零跑通4.1 目标页面分析与数据结构设计纸上谈兵没意思我直接假设一个新闻列表页结构如下。这类服务端渲染的页面是最适合初级爬虫练习的数据都老老实实写在 HTML 里div classnews-list div classnews-item span classdate2025-06-01/span a href/news/1001.html某地发布重要通知/a /div div classnews-item span classdate2025-06-01/span a href/news/1002.html城市交通新规本周实施/a /div /div解析策略很明确先用AndFilter匹配所有 class 为 news-item 的 div再从每个 div 内部提取 a 标签的文本和 href以及 span 的文本。定义一个最简单的 News 类来保存结果public class News { private String title; private String url; private String publishDate; public News(String title, String url, String publishDate) { this.title title; this.url url; this.publishDate publishDate; } Override public String toString() { return News{title title , url url , publishDate publishDate }; } }4.2 串起主流程抓取、解析、落地下面这个 NewsParser 是核心解析逻辑。注意我在单条数据解析外面加了个 try-catch这样即使某一条 HTML 结构不对也不会拖垮整个采集任务最多丢掉一条数据。这个习惯在批量爬取时非常重要目标页面里偶尔出现一两个不规范标签太常见了。import org.htmlparser.Node; import org.htmlparser.NodeFilter; import org.htmlparser.Parser; import org.htmlparser.filters.AndFilter; import org.htmlparser.filters.HasAttributeFilter; import org.htmlparser.filters.TagNameFilter; import org.htmlparser.tags.LinkTag; import org.htmlparser.tags.Tag; import org.htmlparser.util.NodeList; import java.util.ArrayList; import java.util.List; public class NewsParser { public ListNews parseList(String html) throws Exception { ListNews newsList new ArrayList(); Parser parser new Parser(html); parser.setEncoding(UTF-8); NodeFilter itemFilter new AndFilter( new TagNameFilter(DIV), new HasAttributeFilter(class, news-item)); NodeList items parser.parse(itemFilter); for (int i 0; i items.size(); i) { try { Node item items.elementAt(i); NodeList links item.getChildren() .extractAllNodesThatMatch(new TagNameFilter(A), true); if (links.size() 0) { continue; } LinkTag aTag (LinkTag) links.elementAt(0); String title aTag.getStringText().trim(); String url aTag.getLink(); NodeList dateNodes item.getChildren() .extractAllNodesThatMatch(new AndFilter( new TagNameFilter(SPAN), new HasAttributeFilter(class, date)), true); String date dateNodes.size() 0 ? ((Tag) dateNodes.elementAt(0)).toPlainTextString().trim() : ; newsList.add(new News(title, url, date)); } catch (Exception e) { System.err.println(解析单条数据失败 e.getMessage()); } } return newsList; } }最后用 Main 把抓取和解析串起来。下面例子里的 URL 是我虚构的自己练习时替换成实际目标地址就行public class Main { public static void main(String[] args) { HttpUtil httpUtil new HttpUtil(); String listUrl https://example.com/news; try { String html httpUtil.getHtml(listUrl); NewsParser parser new NewsParser(); for (News news : parser.parseList(html)) { System.out.println(news); } } catch (Exception e) { e.printStackTrace(); } finally { try { httpUtil.close(); } catch (Exception ignored) { } } } }跑起来后控制台会打印类似这样的结果News{title某地发布重要通知, url/news/1001.html, publishDate2025-06-01} News{title城市交通新规本周实施, url/news/1002.html, publishDate2025-06-01}到这一步一个最基础的爬虫已经能正常工作了。如果你要抓更多页在外面套一层循环遍历分页 URL 就行for (int page 1; page 5; page) { String url https://example.com/news?page page; // 抓取 解析 Thread.sleep(2000 new Random().nextInt(1000)); }这里特意加个随机休眠不是教你去对抗反爬而是让采集频率尽量接近正常访客不给目标站点增加压力。5. 实操中常见的坑与排查方法5.1 最常见编码不一致导致的乱码乱码排第一因为十个人里有八个会踩。大多数页面是 UTF-8但有些老站还用 GBK如果抓回来以后直接按 UTF-8 解析中文全变问号。判断方法很简单看 HTTP 响应头里的 charset或者看 HTML 的 meta 标签。HttpClient 在EntityUtils.toString时手动指定编码就行。还有一种情况是编码已经错了字符串本身是乱的可以试着用下面这行代码救回来String correct new String(garbled.getBytes(ISO-8859-1), UTF-8);这个技巧的原理是HttpClient 如果不指定编码默认按 ISO-8859-1 把字节还原成字符串导致 UTF-8 的中文字节被“错误翻译”。把乱码字符串重新按 ISO-8859-1 转会为原始字节再用 UTF-8 解码往往就恢复原样了。另外记得让 HtmlParser 的 setEncoding 和页面编码保持一致否则解析出来的文本还是乱码。5.2 被识别成爬虫User-Agent、Cookie 与频率如果你发现程序能正常访问浏览器但用 HttpClient 一抓就是 403 或 418大概率是请求头暴露了“爬虫身份”。排查时先看 User-Agent 是不是浏览器那串再确认有没有带 Referer最后检查访问频率。有些网站的防火墙会统计单 IP 的请求数短时间请求过多就会临时封禁。这里要说得明白一点反爬对抗是一个动态过程我不会教你去搞验证码识别、代理池轮换这类灰色操作。一个体面的爬虫应该尊重网站的 robots.txt 和用户协议只抓公开数据控制频率出现问题先停下来。技术归技术分寸感也很重要。5.3 HtmlParser 的兼容性问题这个库太老了用的时候要降低预期。第一HTML5 新增的标签像 main、section、article它不一定能正确建模过滤时可能漏节点第二很多不规范的自闭合标签和不闭合标签会直接让解析抛异常所以我在第 4 章强调单条解析要加 try-catch第三高版本 JDK 下运行时偶尔会出现类加载或模块访问的报错最简单的处理办法是切回 JDK 8别在高版本上死磕。如果项目允许我会建议把解析引擎换成 Jsoup写起来更顺手但“HttpClient HtmlParser”这套思路完全可以迁移过去因为换的只是解析层抓取层的设计和代码都不用动。5.4 常见问题速查表现象可能原因处理建议返回乱码页面编码与解析编码不一致检查响应头/meta统一 setEncoding403/418UA 被识别或访问频率过高设置浏览器 UA、降低抓取频率拿不到数据动态渲染页面数据由 JS 加载优先找后端 JSON 接口或换无头浏览器连接卡死超时未设置设置连接、Socket、连接请求三类超时HtmlParser 解析抛异常HTML 不规范或 HTML5 标签不支持单条 try-catch考虑换 Jsoup最后再分享一个小技巧日常调试解析代码时先把抓回来的 HTML 保存到本地文件解析器直接从文件读这样不会因为网络波动反复请求目标网站。等解析逻辑稳定了再改成实时抓取。这个习惯帮我节省了大量时间也避免了对目标站点造成不必要的压力。本文还有配套的精品资源点击获取