第一部分:Java 中的正则表达式
Java 提供了 java.util.regex 包来支持正则表达式,主要包括两个核心类:

Pattern: 编译后的正则表达式模式对象,它是一个不可变的类,代表了一个编译好的正则表达式。Matcher: 对输入字符串进行匹配操作的引擎,它使用Pattern对象来解释和执行匹配操作。
核心概念和常用方法
编译模式
Pattern 类的 compile() 方法用于将一个正则表达式字符串编译成一个 Pattern 对象。
String regex = "\\d+"; // 匹配一个或多个数字 Pattern pattern = Pattern.compile(regex);
创建匹配器
使用 Pattern 对象的 matcher() 方法,可以创建一个 Matcher 对象,用于对具体的输入字符串进行操作。
String input = "我的电话是 123456,还有一部是 98765。"; Matcher matcher = pattern.matcher(input);
执行匹配
-
boolean matches(): 尝试将整个输入序列与模式匹配,如果匹配成功,返回true。
(图片来源网络,侵删)System.out.println(matcher.matches()); // false,因为输入字符串不全是数字
-
boolean find(): 尝试在输入字符串中查找下一个匹配的子序列,这是最常用的方法。while (matcher.find()) { System.out.println("找到数字: " + matcher.group()); } // 输出: // 找到数字: 123456 // 找到数字: 98765 -
boolean lookingAt(): 尝试从输入字符串的起始位置开始匹配模式。matcher.reset("123456 is a number."); System.out.println(matcher.lookingAt()); // true
获取匹配结果
String group(): 返回最近一次匹配的子序列。int start(): 返回最近一次匹配的子序列的起始索引。int end(): 返回最近一次匹配的子序列的结束索引(最后一个字符的索引 + 1)。
matcher.reset("我的电话是 123456,还有一部是 98765。");
while (matcher.find()) {
System.out.println("找到的数字: " + matcher.group());
System.out.println("起始位置: " + matcher.start());
System.out.println("结束位置: " + matcher.end());
System.out.println("--------------------");
}
常用正则表达式元字符(Java 语法)
| 元字符 | 描述 | 示例 |
|---|---|---|
| 匹配除换行符外的任何单个字符 | a.c 匹配 "abc", "aac" |
|
| 匹配前面的子表达式零次或多次 | a*b 匹配 "b", "aab", "aaab" |
|
| 匹配前面的子表达式一次或多次 | a+b 匹配 "ab", "aab",但不匹配 "b" |
|
| 匹配前面的子表达式零次或一次 | a?b 匹配 "b", "ab" |
|
{n} |
精确匹配 n 次 | a{3} 匹配 "aaa" |
{n,} |
至少匹配 n 次 | a{3,} 匹配 "aaa", "aaaa" |
{n,m} |
匹配 n 到 m 次 | a{2,4} 匹配 "aa", "aaa", "aaaa" |
| , , | 非贪婪匹配,匹配尽可能少的字符 | <.*?> 会匹配 <a> 而不是 <a>b</a> |
[] |
字符集,匹配其中的任意一个字符 | [abc] 匹配 "a", "b", "c" |
[^] |
反向字符集,匹配不在其中的任意一个字符 | [^abc] 匹配 "d", "e", "1" |
| 选择,匹配 两边的任意一个表达式 | a\|b 匹配 "a" 或 "b" |
|
| 分组,可以捕获匹配的文本 | (a)(b) 可以用 group(1) 和 group(2) 获取 "a" 和 "b" |
|
\d |
匹配数字,等同于 [0-9] |
\d+ 匹配一个或多个数字 |
\D |
匹配非数字,等同于 [^0-9] |
|
\w |
匹配单词字符(字母、数字、下划线),等同于 [a-zA-Z0-9_] |
|
\W |
匹配非单词字符 | |
\s |
匹配空白字符(空格、制表符、换行等) | |
\S |
匹配非空白字符 | |
^ |
匹配输入字符串的开始位置 | ^abc 匹配 "abc123" 的开头 |
| 匹配输入字符串的结束位置 | 123$ 匹配 "abc123" 的结尾 |
|
\ |
转义字符,用于匹配特殊字符本身 | \. 匹配 "." |
特别注意:
在 Java 字符串中,\ 是一个转义字符,在正则表达式中表示 \ 的元字符(如 \d, \.)在 Java 字符串中需要写成 \\d, \\.。
第二部分:使用 Java 正则表达式处理 HTML
使用正则表达式解析 HTML 是一个经典的话题。重要提示:正则表达式不适合用于解析复杂的、嵌套的 HTML/XML 文档。 HTML 的不规范性和复杂性(如标签属性、嵌套、注释等)很容易让正则表达式失效。
对于非常简单、结构固定的 HTML 片段,或者进行一些简单的文本提取,正则表达式仍然是一个快速有效的工具。
场景 1:提取所有 <a> 标签的 href 属性值
假设我们有如下 HTML 字符串:
<html>
<body>
<a href="https://www.google.com">Google</a>
<a href="/about.html" title="About Us">About</a>
<p>这是一个段落。</p>
</body>
</html>
目标: 提取出 https://www.google.com 和 /about.html。
正则表达式分析:
- 我们需要匹配
<a开头的标签。 - 后面可能跟着任意属性,直到
href="..."。 href的值可能在单引号、双引号或无引号中,但为了安全,我们假设都在双引号中。- 我们需要捕获
href属性的值。
正则表达式:
<a\\s+[^>]*?href="([^"]*)"
<a: 匹配<a。\\s+: 匹配一个或多个空白字符(空格、制表符)。[^>]*?: 匹配除>外的任意字符零次或多次, 使其非贪婪,匹配到第一个>即可。href=": 匹配href="。([^"]*): 这是一个捕获组,匹配并捕获任意数量的非 字符,这就是我们想要的href值。- 匹配结尾的 。
Java 代码实现:
import java.util.regex.*;
public class HtmlRegexExample {
public static void main(String[] args) {
String html = "<html><body><a href=\"https://www.google.com\">Google</a><a href=\"/about.html\" title=\"About Us\">About</a><p>这是一个段落。</p></body></html>";
// 正则表达式,注意 Java 字符串中的转义
String regex = "<a\\s+[^>]*?href=\"([^\"]*)\"";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(html);
System.out.println("找到的链接:");
while (matcher.find()) {
// group(0) 是整个匹配的字符串,group(1) 是第一个捕获组
String href = matcher.group(1);
System.out.println(href);
}
}
}
输出:
找到的链接:
https://www.google.com
/about.html
场景 2:提取所有 <img> 标签的 src 属性值
与场景 1 类似,只需将标签名和属性名修改即可。
HTML:
<img src="image1.png" alt="Image 1"> <img src="https://example.com/image2.jpg" width="200">
正则表达式:
<img\\s+[^>]*?src="([^"]*)"
Java 代码:
// ... (代码框架同上) String html = "<img src=\"image1.png\" alt=\"Image 1\"><img src=\"https://example.com/image2.jpg\" width=\"200\">"; String regex = "<img\\s+[^>]*?src=\"([^\"]*)\""; // ... (使用 pattern 和 matcher 进行查找)
第三部分:为什么不推荐用正则解析 HTML?以及更好的替代方案
正则表达式的局限性
- 无法处理嵌套结构:HTML 的核心是嵌套标签(如
<div><p>...</p></div>),正则表达式很难正确处理这种嵌套关系,它会贪婪地匹配从第一个<div>到最后一个</div>,中间包含所有内容,导致结构混乱。 - HTML 规范的复杂性:
- 属性值可能包含
>或 ,这会破坏简单的正则表达式。 - 标签可以大小写不敏感(
<A>和<a>都有效)。 - 注释
<!-- ... -->和脚本<script>...</script>中的内容会干扰匹配。 - 属性的顺序是任意的。
- 属性值可能包含
- 维护困难:一个看似简单的 HTML 匹配正则表达式,可能在遇到一个微小的变化时就完全失效,编写和调试复杂的正则表达式非常耗时且容易出错。
更好的替代方案:HTML 解析器
对于任何严肃的 HTML 解析任务,都应该使用专门的 HTML 解析库,它们能构建一个 DOM(文档对象模型)树,让你可以像操作对象一样操作 HTML,从而完全避免正则表达式的所有问题。
推荐使用的 Java HTML 解析库:
-
Jsoup (强烈推荐)
- 优点:非常轻量、易用、功能强大,它提供了类似 jQuery 的 CSS 选择器 API,非常直观。
- 官网:https://jsoup.org/
使用 Jsoup 重写上面的例子:
import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.IOException; public class JsoupExample { public static void main(String[] args) { String html = "<html><body><a href=\"https://www.google.com\">Google</a><a href=\"/about.html\" title=\"About Us\">About</a><p>这是一个段落。</p></body></html>"; // 解析 HTML 字符串 Document doc = Jsoup.parse(html); // 使用 CSS 选择器查找所有 a 标签 Elements links = doc.select("a"); System.out.println("找到的链接:"); for (Element link : links) { // 获取 href 属性 String href = link.attr("href"); // 获取标签内的文本 String text = link.text(); System.out.println("链接: " + href + ", 文本: " + text); } // 查找所有 img 标签的 src String imgHtml = "<img src=\"image1.png\" alt=\"Image 1\"><img src=\"https://example.com/image2.jpg\" width=\"200\">"; Document imgDoc = Jsoup.parse(imgHtml); Elements images = imgDoc.select("img"); System.out.println("\n找到的图片:"); for (Element img : images) { System.out.println("图片源: " + img.attr("src")); } } }输出:
找到的链接: 链接: https://www.google.com, 文本: Google 链接: /about.html, 文本: About 找到的图片: 图片源: image1.png 图片源: https://example.com/image2.jpg -
HtmlUnit
- 优点:一个“浏览器程序”,不仅能解析 HTML,还能模拟浏览器执行 JavaScript,适合进行 Web 自动化测试。
- 缺点:比 Jsoup 重,更复杂。
-
Apache NekoHTML
- 优点:一个健壮的解析器,能修复很多 HTML 格式错误,生成一个干净的 DOM 树。
| 特性 | Java 正则表达式 (java.util.regex) |
HTML 解析器 (如 Jsoup) |
|---|---|---|
| 适用场景 | 简单的文本模式匹配、日志分析、数据清洗 | 所有 HTML/XML 解析、数据抓取、网页内容修改 |
| 处理嵌套 | 非常困难,不推荐 | 非常擅长,基于 DOM 树 |
| 健壮性 | 低,对 HTML 的不规范非常敏感 | 高,能处理各种“畸形” HTML |
| 易用性 | 复杂的正则表达式难以编写和调试 | API 直观,类似 CSS 选择器,学习成本低 |
| 性能 | 对于简单模式,速度很快 | 初始化稍慢,但操作 DOM 非常高效 |
最终建议:
- 当你只需要从一小段固定的、简单的 HTML 中提取一两个值时,可以考虑使用正则表达式作为快速解决方案。
- 当你需要处理任何真实的、复杂的、可能变化的网页时,或者需要进行复杂的 DOM 遍历和操作时,请务必使用 Jsoup 这样的专业 HTML 解析器。 这是正确、高效且可维护的做法。
