【JavaSE】正则表达式学习笔记
正则表达式
-为什么要学习正则表达式
- 
极速体验正则表达式威力
- 提取文章中所有的英文单词
 - 提取文章中所有的数字
 - 提取文章中所有的英文单词和数字
 - 提取百度热榜标题
 
 - 
实践案例
package com.xijie;import java.util.regex.Matcher;import java.util.regex.Pattern;/** * 体验正则表达式的威力 */public class regexpFirst { public static void main(String[] args) { //假设用爬虫获取了百度百科的一段文字 String content= \"1998年12月8日,第二代Java平台的企业版J2EE发布。1999年6月,Sun公司发布了第二代Java平台(简称为Java2)的3个版本:J2ME(Java2 Micro Edition,Java2平台的微型版),应用于移动、无线及有限资源的环境;J2SE(Java 2 Standard Edition,Java 2平台的标准版),应用于桌面环境;J2EE(Java 2Enterprise Edition,Java 2平台的企业版),应用于基于Java的应用服务器。Java 2平台的发布,是Java发展过程中最重要的一个里程碑,标志着Java的应用开始普及。\"; String contentHot=\"跟着总书记的足迹探寻中华文脉<a href=\\\"https://www.baidu.com/s?wd=%E8%B7%9F%E7%9D%80%E6%80%BB%E4%B9%A6%E8%AE%B0%E7%9A\"; //提取文章中所有的英文单词 //传统方式:使用遍历方式,代码量高,效率低,难以维护 //正则表达式技术 getAllWord(content); //提取所有数字 getAllNumber(content); //提取所有数字和英文单词 getAllWordNumber(content); //提取所有热搜标题 getHotTitle(contentHot); } private static void getAllWord(String content){ Pattern pattern=Pattern.compile(\"[a-zA-Z]+\"); Matcher matcher=pattern.matcher(content); while(matcher.find()){ System.out.println(\"找到 \"+matcher.group(0)); } } private static void getAllNumber(String content){ Pattern pattern=Pattern.compile(\"[0-9]+\"); Matcher matcher=pattern.matcher(content); while(matcher.find()){ System.out.println(\"找到 \"+matcher.group(0)); } } private static void getAllWordNumber(String content){ Pattern pattern=Pattern.compile(\"([0-9]+)|([a-zA-Z]+)\"); Matcher matcher=pattern.matcher(content); while(matcher.find()){ System.out.println(\"找到 \"+matcher.group(0)); } } private static void getHotTitle(String content){ Pattern pattern=Pattern.compile(\"target=\\\"_blank\\\">(.*?) <!--\"); Matcher matcher=pattern.matcher(content); while(matcher.find()){ System.out.println(\"找到 \"+matcher.group(1)); } }}结论
正则表达式是处理文本匹配与处理的高效工具
解决之道 - 正则表达式
- 为解决上述文本处理问题,Java 提供了正则表达式技术,专门用于高效处理此类文本匹配、提取、替换等需求
 - 简单来说:正则表达式是一种对字符串执行模式匹配与处理的技术
 - 正则表达式(英文:regular expression,简称:RegExp)
 
-正则表达式基本介绍
- 介绍
- 一个正则表达式,就是用特定模式匹配字符串的公式。虽然其语法初看可能显得古怪复杂,让人望而却步,但经过练习后会发现,编写这些表达式其实并不困难。而且,一旦掌握正则表达式,原本需要数小时且容易出错的文本处理工作,往往能在几分钟甚至几秒钟内完成。
 - 这里要特别强调,正则表达式并非 Java 独有,实际上很多编程语言都支持通过正则表达式进行字符串操作,例如Javascript、php。。。
 
 
-正则表达式底层实现
- 
实例分析
package com.xijie.regexp;import java.util.regex.Matcher;import java.util.regex.Pattern;public class RegTheory { public static void main(String[] args) { String content = \"1998年12月8日,第二代Java平台的企业版J2EE发布。1999年6月,Sun公司发布了\" + \"第二代Java平台(简称为Java2)的3个版本:J2ME(Java2 MicroEdition,Java2平台的微型\" + \"版),应用于移动、无线及有限资源的环境;J2SE(Java2StandardEdition,Java2平台的\" + \"标准版),应用于桌面环境;J2EE(Java2EnterpriseEdition,Java2平台的企业版),应\" + \"用于基于Java的应用服务器。Java2平台的发布,是Java发展过程中最重要的一个\" + \"里程碑,标志着Java的应用开始普及9889\"; //筛选四个数字 String regStr=\"(\\\\d\\\\d)(\\\\d\\\\d)\"; //1. 设置模板对象 Pattern pattern=Pattern.compile(regStr); //2. 设置模式对象 Matcher matcher=pattern.matcher(content); //3. 匹配结果 while(matcher.find()){ System.out.println(\"找到\"+matcher.group(0)); System.out.println(\"第1组匹配:\"+matcher.group(1)); System.out.println(\"第2组匹配:\"+matcher.group(2)); } }} - 
Matcher.find 方法完成的任务
- 根据指定的正则表达式规则,在目标字符串中查找下一个符合条件的子字符串(例如 “1998”)。
 - 找到匹配项后,将匹配结果的位置信息记录到 Matcher 对象内部的 int [] groups 数组中:
2.1 groups [0] = 子字符串的起始索引(例如 31)
2.2 groups [1] = 子字符串的结束索引 + 1(例如 35,表示实际结束位置为 34)
2.3 对于每个捕获组:- 第 1 个捕获组的起始索引存入 groups [2]
 - 第 1 个捕获组的结束索引 + 1 存入 groups [3]
 - 第 2 个捕获组的起始索引存入 groups [4]
 - 第 2 个捕获组的结束索引 + 1 存入 groups [5]
 - 依此类推…
 
 - 同时更新 last 匹配位置,将其设置为当前子字符串的结束索引 + 1(例如 35),以便下次调用 find () 时从该位置继续搜索。
 
 - 
Matcher.group 方法
根据 groups 数组中记录的位置信息,从目标字符串中提取并返回对应的子字符串:
- group (0):返回整个匹配的子字符串(例如根据 groups [0]=31 和 groups [1]=35,截取索引 [31,35) 的内容)
 - group (1):返回第 1 个捕获组匹配的内容(例如根据 groups [2] 和 groups [3] 的位置信息截取)
 - group (2):返回第 2 个捕获组匹配的内容(依此类推)
 
 
-正则表达式语法
- 
基本介绍
若要灵活运用正则表达式,需了解各类元字符的功能。元字符从功能上大致分为以下几类:
- 限定符
 - 选择匹配符
 - 分组组合和反向引用符
 - 特殊字符
 - 字符匹配符
 - 定位符
 
 - 
元字符-转义号 \\\\
- 
符号说明: 在使用正则表达式检索某些特殊字符时,需用转义符号
\\,否则可能无法检索到结果,甚至报错。 - 
案例:
- 
若直接用
$匹配字符串 “abc ( (”,由于 ‘ ((”,由于` ((”,由于‘是正则中的特殊元字符(表示行尾),会导致匹配逻辑错误,无法正确匹配字符串中的$`。 - 
若直接用
(匹配字符串 “abc$(\"”,由于(是正则中的分组符号,同样会导致匹配异常,无法正确识别字符串中的(。 
 - 
 - 
再次提示: 在 Java 的正则表达式中,需用两个反斜杠
\\\\表示其他语言中的一个反斜杠\\(例如,匹配$需写成\\\\$,匹配(需写成\\\\()。 - 
需要用到转义符号的字符有以下:.*+()$/?[]^{}
 - 
匹配特殊字符的案例
package com.xijie.regexp;import java.util.regex.Matcher;import java.util.regex.Pattern;public class EscChar { public static void main(String[] args) { //需要用到转义符号的字符有以下:.*+()$/\\?[]^{} String content=\"123.123*123+123(213)123$123/123\\\\123?123[123]123^123{123}\"; //匹配. findStr(content,\"\\\\.\"); //匹配* findStr(content,\"\\\\*\"); //匹配+ findStr(content,\"\\\\+\"); //匹配( findStr(content,\"\\\\(\"); //匹配) findStr(content,\"\\\\)\"); //匹配$ findStr(content,\"\\\\$\"); //匹配/ findStr(content,\"\\\\/\"); //匹配\\ findStr(content,\"\\\\\\\\\"); //匹配? findStr(content,\"\\\\?\"); //匹配[ findStr(content,\"\\\\[\"); //匹配] findStr(content,\"\\\\]\"); //匹配^ findStr(content,\"\\\\^\"); //匹配{ findStr(content,\"\\\\{\"); //匹配} findStr(content,\"\\\\}\"); } private static void findStr(String content,String reg){ Pattern pattern=Pattern.compile(reg); Matcher matcher=pattern.matcher(content); System.out.println(\"匹配:\"+reg); while(matcher.find()){ System.out.println(\"找到:\"+matcher.group(0)); } }} 
 - 
 - 
元字符-字符匹配符
符号 含义 示例 说明 匹配输入示例 [] 可接收的字符列表 [a-zA-Z0-9_] 匹配字母(大小写)、数字或下划线中的任意一个字符 A、3、_、z[^] 不接收的字符列表 [^a-zA-Z] 匹配除字母外的任意字符(包括数字、特殊符号、空格等) 5、@、(空格)、#- 连字符 [0-9a-fA-F] 匹配十六进制字符(0-9、小写 a-f、大写 A-F 中的任意一个) 3、a、F、7. 匹配除\\n外的任意字符 a.b 匹配 “a” 和 “b” 之间有 1 个任意字符(除换行),中间无字符或换行不匹配 a1b、a@b、a b(空格)\\d 匹配单个数字字符,相当于[0-9] \\d{3} 匹配连续 3 个数字 123、456、789\\D 匹配单个非数字字符,相当于[ ^0-9] \\D{2} 匹配连续 2 个非数字字符 ab、@#、_$\\w 匹配单个数字、大小写字母字符,相当于[0-9a-zA-Z] \\w+ 匹配 1 个或多个连续的单词字符(字母、数字、下划线) user123、_name、A_B\\W 匹配单个非数字、大小写字母字符,相当于[^0-9a-zA-Z] \\W* 匹配 0 个或多个连续的非单词字符(允许空字符串) @#、(空格)、(空)- 
应用案例
- [a-z]:[a-z]表示可以匹配a-z中任意一个字符
 - (?i):后续字符都不区分大小写
 - Pattern pat = Pattern.compile(regEx, Pattern.CASE_INSENSITIVE):本匹配不区分大小写,这意味着对小写字符的匹配也对大写字符生效,反之亦然
 - [^a-z]:表示匹配不是a-z的任意一个字符, [ ^a-z]{2}表示匹配连续2个不是a-z的字符
 - [abcd]表示可以匹配abcd中的任意一个字符。
 - [^abcd]表示可以匹配不是abcd中的任意一个字符
 - \\\\d表示可以匹配0-9的任意一个数字,相当于[0-9]。
 - \\\\D表示可以匹配不是0-9中的任意一个数字,相当于[ ^0-9]
 - \\\\w匹配任意英文字符、数字和下划线,相当于[a-zA-Z0-9]
 - \\\\W相当于[ ^a-zA-Z0-9]是\\\\w刚好相反.
 - \\\\s匹配任何空白字符(空格,制表符等)
 - \\\\S匹配任何非空白字符,和\\\\s刚好相反
 - .匹配出\\n之外的所有字符,如果要匹配。本身则需要使用\\\\
 
package com.xijie.regexp;import java.util.regex.Matcher;import java.util.regex.Pattern;/** * 元字符-字符匹配符 */public class CharMatch { public static void main(String[] args) { String content=\"Abc@123_def 你好#\"; //* 1. [a-z]:[a-z]表示可以匹配a-z中任意一个字符 findStr(content,\"[a-z]\"); //* 2. (?i):后续字符都不区分大小写 findStr(content,\"(?i)[a-z]\"); //* 3. Pattern pat = Pattern.compile(regEx, Pattern.CASE_INSENSITIVE):本匹配不区分大小写,这意味着对小写字符的匹配也对大写字符生效,反之亦然 findStr(content,\"[a-z]\"); //* 4. [^a-z]:表示匹配不是a-z的任意一个字符, [ ^a-z]{2}表示匹配连续2个不是a-z的字符 findStr(content,\"[^a-z]\"); //* 5. [abcd]表示可以匹配abcd中的任意一个字符。 findStr(content,\"[ce]\"); //* 6. [^abcd]表示可以匹配不是abcd中的任意一个字符 findStr(content,\"[Abcdef]\"); //* 7. \\\\\\\\d表示可以匹配0-9的任意一个数字,相当于[0-9]。 findStr(content,\"\\\\d\"); //* 8. \\\\\\\\D表示可以匹配不是0-9中的任意一个数字,相当于[ ^0-9] findStr(content,\"\\\\D\"); //* 9. \\\\\\w匹配任意英文字符、数字和下划线,相当于[a-zA-Z0-9] findStr(content,\"\\\\w\"); //* 10. \\\\\\\\W相当于[ ^a-zA-Z0-9]是\\\\\\w刚好相反. findStr(content,\"\\\\W\"); //* 11. \\\\\\\\s匹配任何空白字符(空格,制表符等) findStr(content,\"\\\\s\"); //* 12. \\\\\\\\S匹配任何非空白字符,和\\\\\\\\s刚好相反 findStr(content,\"\\\\S\"); //* 13. .匹配出\\n之外的所有字符,如果要匹配。本身则需要使用\\\\\\ findStr(content,\".\"); } private static void findStr(String content,String reg){ Pattern pattern=Pattern.compile(reg); Matcher matcher=pattern.matcher(content); System.out.println(\"在\"+content+\"中匹配:\"+reg); while(matcher.find()){ System.out.print(\"找到:\"+matcher.group(0)+\"|\"); } System.out.println(); } private static void findStrCaseInsensitive(String content,String reg){ Pattern pattern=Pattern.compile(reg,Pattern.CASE_INSENSITIVE); Matcher matcher=pattern.matcher(content); System.out.println(\"在\"+content+\"中大小写不敏感匹配:\"+reg); while(matcher.find()){ System.out.println(\"找到:\"+matcher.group(0)); } }} 
 - 
 - 
元字符-选择匹配符
在匹配某个字符串的时候是选择性的,即:既可以匹配这个,又可以匹配那个,这时你需
要用到选择匹配符号符号 符号 示例 解释 | 匹配之前或之后的表达式 ab|cd ab或者cd - 
应用案例
package com.xijie.regexp;import java.util.regex.Matcher;import java.util.regex.Pattern;/** * 选择匹配符 */public class ChooseMatch { public static void main(String[] args) { String content=\"Abc@123_def 你好#\"; findStr(content,\"bc|12|_|好#\"); } private static void findStr(String content,String reg){ Pattern pattern=Pattern.compile(reg); Matcher matcher=pattern.matcher(content); System.out.println(\"在\"+content+\"中匹配:\"+reg); while(matcher.find()){ System.out.print(\"找到:\"+matcher.group(0)+\"|\"); } System.out.println(); }} 
 - 
 - 
元字符-限定符
- 用于指定其前面的字符或组合连续出现多少次
 
符号 含义 示例 说明 匹配输入 * 指定字符重复 0 次或 n 次(无要求)0 到多 (abc)* 仅包含任意个 abc 的字符串 abc,abcabcabc + 指定字符重复 1 次或 n 次(至少一次)1 到多 m+(abc)* 以至少一个 m 开头,后接任意个 abc 字符串 m,mabc,mmabcabc ? 指定字符重复 0 次或 1 次(至多一次)0 或 1 m+abc? 以至少 1 个 m 开头,后接 ab 或 abc 的字符串 mab,mmabc,mmmab {n} 刚好 n 遍 [abcd]{3} 由 abcd 中字母组成的任意长度为 3 的字符串 acd,aaa,dca,ddb {n,} 指定字符重复至少 n 次(n 到多次) a{2,} 由至少 2 个 a 组成的字符串 aa,aaa,aaaa,aaaaa {n,m} 指定字符重复至少 n 次且至多 m 次(n 到 m 次) [0-9]{2,4} 由 2 到 4 个数字组成的字符串 12,345,6789 - 
注意点
- 贪心匹配:匹配符合要求尽可能长的字符串
 - 不重复匹配:检查过的字符就不重复检查,例如在abc中查找\\\\w{2},只会查找出ab
 
 - 
应用案例
package com.xijie.regexp;import java.util.regex.Matcher;import java.util.regex.Pattern;public class RegexQuantifierDemo { public static void main(String[] args) { // 测试数据 String[] testStrings = { \"abc\", \"abcabcabc\", \"m\", \"mabc\", \"mmabcabc\", \"mab\", \"mmabc\", \"mmmab\", \"acd\", \"aaa\", \"dca\", \"ddb\", \"aa\", \"aaa\", \"aaaa\", \"12\", \"345\", \"6789\" }; // 定义正则表达式及其描述 String[][] regexPatterns = { { \".*\", \"任意字符串(包含空字符串)\" }, { \"(abc)*\", \"仅包含任意个abc的字符串\" }, { \"m+(abc)*\", \"以至少一个m开头,后接任意个abc的字符串\" }, { \"m+abc?\", \"以至少1个m开头,后接ab或abc的字符串\" }, { \"[abcd]{3}\", \"由abcd中字母组成的任意长度为3的字符串\" }, { \"a{2,}\", \"由至少2个a组成的字符串\" }, { \"[0-9]{2,4}\", \"由2到4个数字组成的字符串\" } }; // 执行匹配测试 for (String[] patternInfo : regexPatterns) { String regex = patternInfo[0]; String description = patternInfo[1]; Pattern pattern = Pattern.compile(regex); System.out.println(\"\\n正则表达式: \" + regex); System.out.println(\"描述: \" + description); System.out.println(\"匹配结果:\"); for (String testStr : testStrings) { Matcher matcher = pattern.matcher(testStr); if (matcher.matches()) { System.out.printf(\" ✅ 匹配: \\\"%s\\\"\\n\", testStr); } } } }} 
 - 
元字符-定位符
- 
定位符,规定要匹配的字符串出现的位置,比如在字符串的开始还是在结束的位置,这个也是相当有用的,必须掌握
符号 示例 匹配输入示例 说明 含义 ^ 1+[a-zA-Z_]*$ 123abc、456_、789Xyz 以至少 1 个数字开头,后接任意个大小写字母或下划线,且必须以字母或下划线结尾的字符串 指定起始字符 $ 2\\d{2}-[a-z]+$ A12-xyz、Z99-test 以 1 个大写字母开头,后接 2 个数字和连字符 “-”,并以至少 1 个小写字母结尾的字符串 指定结束字符 \\b \\bjava\\b “I love java programming” 精确匹配单词 “java”,前后必须是边界(如空格、标点或字符串首尾) 匹配目标字符串的边界 \\B \\B@\\w+\\B “@username”, “user@domain.com” 匹配被非边界字符包围的 @符号(如邮箱中的 @,但排除句首 @标签) 匹配目标字符串的非边界  - 
应用实例
package com.xijie.regexp;import java.util.regex.Matcher;import java.util.regex.Pattern;/** * 元字符-字符匹配符 */public class PositionMatch { public static void main(String[] args) { findStr(\"123456@qq.com\",\"^[\\\\d]+@[\\\\w]+\\\\.(com)$\"); findStr(\"java 12345java\",\"\\\\bjava\\\\b\"); findStr(\"java 1111java666\",\"[\\\\d]+\\\\B\"); } private static void findStr(String content,String reg){ Pattern pattern=Pattern.compile(reg); Matcher matcher=pattern.matcher(content); System.out.println(\"在\"+content+\"中匹配:\"+reg); while(matcher.find()){ System.out.print(\"找到:\"+matcher.group(0)+\"|\"); } System.out.println(); }} 
 - 
 - 
分组-捕获分组
- 
常用分组
常用分组构造形式 说明 (pattern) 非命名捕获。捕获匹配的子字符串。编号为零的第一个捕获是由整个正则表达式模式匹配的文本,其它捕获结果则根据左括号的顺序从 1 开始自动编号。 (?pattern) 命名捕获。将匹配的子字符串捕获到一个组名称或编号名称中。用于 name 的字符串不能包含任何标点符号,并且不能以数字开头。可以使用单引号替代尖括号,例如(?‘name’)。  - 
应用实例
package com.xijie.regexp;import java.util.regex.Matcher;import java.util.regex.Pattern;public class CatchGroup { public static void main(String[] args) { findStrAndGroup(\"sss123456\",\"(\\\\d\\\\d)(\\\\d)(\\\\d\\\\d)\",null); findStrAndGroup(\"sss123456\",\"(\\\\d\\\\d)(?\\\\d)(?\\\\d\\\\d)\",new String[]{\"g1\",\"g2\",\"g3\"}); } private static void findStrAndGroup(String content,String reg,String[] groupName){ Pattern pattern=Pattern.compile(reg); Matcher matcher=pattern.matcher(content); System.out.println(\"在\"+content+\"中匹配:\"+reg); while(matcher.find()){ for (int i = 0; i <=matcher.groupCount(); i++) { System.out.print(\"分组\"+i+\":\"+matcher.group(i)+\"|\"); } if(groupName!=null){ for (String s : groupName) { try { System.out.print(\"分组\"+s+\":\"+matcher.group(s)+\"|\"); } catch (IllegalArgumentException e) { System.out.print(\"没有名为\"+s+\"的分组|\"); } } } } System.out.println(); }} 
 - 
 - 
分组-非捕获分组
- 
特别分组
常用分组构造形式 说明 (?:pattern) 非捕获匹配,匹配 pattern 但不存储匹配结果,用于用 “or” 字符(|)组合模式部件,例如`industr(?:y (?=pattern) 正向预查,非捕获匹配,例如`Windows(?=95 (?!pattern) 负向预查,非捕获匹配,例如`Windows(?!95  - 
应用实例
package com.xijie.regexp;import java.util.regex.Matcher;import java.util.regex.Pattern;public class NonCatchGroup { public static void main(String[] args) { findStrAndGroup(\"win98win2000win7win10win11\",\"win(?:98|11)\",null); findStrAndGroup(\"win98win2000win7win10win11\",\"win(?=2000)\",null); findStrAndGroup(\"win98win2000win7win10win11\",\"win(?!2000)\",null); } private static void findStrAndGroup(String content,String reg,String[] groupName){ Pattern pattern=Pattern.compile(reg); Matcher matcher=pattern.matcher(content); System.out.println(\"在\"+content+\"中匹配:\"+reg); while(matcher.find()){ for (int i = 0; i <=matcher.groupCount(); i++) { System.out.print(\"分组\"+i+\":\"+matcher.group(i)+\"|\"); } if(groupName!=null){ for (String s : groupName) { try { System.out.print(\"分组\"+s+\":\"+matcher.group(s)+\"|\"); } catch (IllegalArgumentException e) { System.out.print(\"没有名为\"+s+\"的分组|\"); } } } } System.out.println(); }} 
 - 
 - 
非贪心匹配
?表示非贪心匹配
package com.xijie.regexp;import java.util.regex.Matcher;import java.util.regex.Pattern;public class NonGreedyMatch { public static void main(String[] args) { findStrAndGroup(\"aaa123465\",\"[a]+\\\\d+\",null); findStrAndGroup(\"aaa123465\",\"[a]+?\\\\d+?\",null); } private static void findStrAndGroup(String content,String reg,String[] groupName){ Pattern pattern=Pattern.compile(reg); Matcher matcher=pattern.matcher(content); System.out.println(\"在\"+content+\"中匹配:\"+reg); while(matcher.find()){ for (int i = 0; i <=matcher.groupCount(); i++) { System.out.print(\"分组\"+i+\":\"+matcher.group(i)+\"|\"); } if(groupName!=null){ for (String s : groupName) { try { System.out.print(\"分组\"+s+\":\"+matcher.group(s)+\"|\"); } catch (IllegalArgumentException e) { System.out.print(\"没有名为\"+s+\"的分组|\"); } } } } System.out.println(); }} 
-正则表达式元字符详细说明
以下是 Java 正则表达式中常见元字符及其说明的详细表格:
元字符 说明 .匹配除换行符 \\n外的任意单个字符。若启用Pattern.DOTALL标志,则可匹配包括换行符在内的所有字符。^匹配输入字符串的开始位置。若启用 Pattern.MULTILINE标志,则也可匹配行的开头(\\n之后)。$匹配输入字符串的结束位置。若启用 Pattern.MULTILINE标志,则也可匹配行的结尾(\\n之前)。*匹配前面的子表达式零次或多次。等价于 {0,}。例如,a*可匹配空字符串、a、aa等。+匹配前面的子表达式一次或多次。等价于 {1,}。例如,a+可匹配a、aa,但不匹配空字符串。?匹配前面的子表达式零次或一次。等价于 {0,1}。例如,colou?r可匹配color或colour。{n}匹配前面的子表达式恰好 n次。例如,a{3}匹配aaa。{n,}匹配前面的子表达式至少 n次。例如,a{2,}匹配aa、aaa等。{n,m}匹配前面的子表达式至少 n次,至多m次(含边界)。例如,a{2,4}匹配aa、aaa、aaaa。*?,+?,??,{n,}?,{n,m}?使量词变为非贪婪模式,即尽可能少地匹配字符。例如, a+?匹配aaa时仅匹配a。\\转义字符,用于取消元字符的特殊含义。例如, \\.匹配字面点号,\\\\匹配单个反斜杠。[]字符组,匹配方括号中指定的任意一个字符。例如, [abc]匹配a、b或c。[^]否定字符组,匹配不在方括号中的任意一个字符。例如, [^abc]匹配除a、b、c外的任意字符。[a-z]范围字符组,匹配指定范围内的任意字符。例如, [a-z]匹配任意小写字母,[0-9A-F]匹配十六进制数字。[a-zA-Z0-9]组合范围,匹配字母或数字。 [^0-9]等价于\\D。` ` 逻辑或,匹配 ` ()捕获组,将括号内的表达式作为一个整体,并捕获匹配的内容供后续引用。例如, (ab)+匹配abab,并将ab作为一个组捕获。(?:)非捕获组,仅将括号内的表达式作为一个整体,但不捕获匹配的内容。例如, (?:ab)+匹配abab,但不存储ab。(?=)正向预查(零宽断言),要求后面的字符满足指定模式,但不消耗字符。例如, \\d+(?= dollars)匹配紧跟dollars的数字。(?!负向预查(零宽断言),要求后面的字符不满足指定模式。例如, \\d+(?! dollars)匹配不紧跟dollars的数字。(?<=)正向后顾,要求前面的字符满足指定模式。例如, (?<=\\$)\\d+匹配前面是$的数字。(?<!负向后顾,要求前面的字符不满足指定模式。例如, (?<!\\$)\\d+匹配前面不是$的数字。\\d匹配任意数字,等价于 [0-9]。\\D匹配任意非数字字符,等价于 [^0-9]。\\w匹配任意字母、数字或下划线,等价于 [a-zA-Z0-9_]。\\W匹配任意非字母、数字或下划线的字符,等价于 [^a-zA-Z0-9_]。\\s匹配任意空白字符,包括空格、制表符、换行符等,等价于 [ \\t\\n\\r\\f]。\\S匹配任意非空白字符,等价于 [^ \\t\\n\\r\\f]。\\b匹配单词边界,即单词字符( \\w)和非单词字符(\\W)的交界处。例如,\\bcat\\b匹配独立的cat,但不匹配category。\\B匹配非单词边界。例如, \\Bcat\\B匹配category中的cat,但不匹配独立的cat。\\A匹配整个输入字符串的开始位置,不受 Pattern.MULTILINE影响。\\Z匹配整个输入字符串的结束位置(或最后一个换行符之前)。 \\z匹配整个输入字符串的绝对结束位置,忽略换行符。 \\G匹配上一次匹配结束的位置,常用于连续匹配。 Java 特殊转义序列
转义序列 说明 \\t制表符( \\u0009)。\\n换行符( \\u000A)。\\r回车符( \\u000D)。\\f换页符( \\u000C)。\\a警报(响铃)符( \\u0007)。\\eEscape 符( \\u001B)。\\cx控制字符,例如 \\cM表示 Ctrl-M(等价于\\r)。\\xhh十六进制字符,例如 \\x61表示a。\\uhhhhUnicode 字符,例如 \\u0061表示a。\\0nn八进制字符,例如 \\0141表示a。预定义字符类
字符类 等价表示 说明 \\p{Lower}[a-z]匹配小写字母。 \\p{Upper}[A-Z]匹配大写字母。 \\p{ASCII}[\\x00-\\x7F]匹配 ASCII 字符。 \\p{Alpha}[\\p{Lower}\\p{Upper}]匹配字母。 \\p{Digit}[0-9]匹配数字。 \\p{Alnum}[\\p{Alpha}\\p{Digit}]匹配字母或数字。 \\p{Punct}[^\\\\p{Alnum}]匹配标点符号(非字母数字)。 \\p{Blank}[ \\t]匹配空格或制表符。 \\p{Space}[ \\t\\n\\x0B\\f\\r]匹配所有空白字符,等价于 \\s。\\p{javaLowerCase}匹配 Java 中的小写字母。 \\p{javaUpperCase}匹配 Java 中的大写字母。 \\p{javaWhitespace}匹配 Java 中的空白字符。 \\p{javaMirrored}匹配具有镜像属性的 Unicode 字符。 -正则表达式三个常用类及常用方法
- 
Pattern 类
pattern对象是一个正则表达式对象。Pattern类没有公共构造方法。要创建一个Pattern对
象,调用其公共静态方法,它返回一个Pattern对象。该方法接受一个正则表达式作为它的第
一个参数,比如:Pattern r=Pattern.compile(pattern);方法名 说明 static Pattern compile(String regex)编译给定的正则表达式字符串,返回 Pattern对象。用于创建正则表达式模式。static Pattern compile(String regex, int flags)编译正则表达式字符串,并指定匹配标志(如 CASE_INSENSITIVE、MULTILINE等)。Matcher matcher(CharSequence input)创建一个 Matcher对象,用于在指定输入字符串中执行匹配操作。String pattern()返回编译后的正则表达式字符串。 String[] split(CharSequence input)将输入字符串按正则表达式匹配的位置分割成字符串数组。 String[] split(CharSequence input, int limit)同上,但最多分割成 limit个元素,超出的部分不再分割。static boolean matches(String regex, CharSequence input)静态方法,快速判断输入字符串是否完全匹配给定的正则表达式。等价于: Pattern.compile(regex).matcher(input).matches()int flags()返回编译时指定的匹配标志。  - 
Matcher 类
Matcher对象是对输入字符串进行解释和匹配的引擎。与Pattern类一样,Matcher也没有
公共构造方法。你需要调用Pattern对象的matcher方法来获得一个Matcher对象方法签名 说明 public int start()返回上一次匹配的起始索引位置。例如,若匹配到 abc,则返回a的索引。public int start(int group)返回上一次匹配中指定捕获组(如 (\\\\d+))的起始索引。若未匹配或组不存在,返回-1。public int end()返回上一次匹配的最后一个字符的后一个位置的索引。例如,匹配 abc,返回c的索引 + 1。public int end(int group)返回上一次匹配中指定捕获组的结束位置的后一个索引。例如,捕获组匹配 123,返回3的索引 + 1。public boolean lookingAt()尝试从输入序列的起始位置开始匹配模式,但不要求匹配整个输入。例如,模式 abc匹配输入abcdef返回true。public boolean find()尝试在输入序列中查找下一个与模式匹配的子序列。例如,模式 \\\\d+在a123b456中可多次调用find()分别匹配123和456。public boolean find(int start)从指定索引位置开始查找下一个匹配的子序列。例如, find(3)从索引 3 开始匹配。public boolean matches()尝试将整个输入序列与模式匹配。例如,模式 \\\\d+匹配123返回true,匹配a123返回false。public String replaceAll(String replacement)将输入序列中所有匹配的子序列替换为指定字符串。支持使用 $1、$2等引用捕获组。例如:java
String result = Pattern.compile(\"(\\\\d+)-(\\\\d+)\")
.matcher(\"123-456\")
.replaceAll(\"$2-$1\");
// 结果:\"456-123\" - 
PatternSyntaxException
PatternSyntaxException是一个非强制异常类,它表示一个正则表达式模式中的语法错误。 
-分组、捕获、反向引用
- 
介绍
要解决前面的问题,我们需要了解正则表达式的以下几个概念:
- 分组
用圆括号()组成的复杂匹配模式,每个括号内的部分可看作一个子表达式或分组。 - 捕获
将正则表达式中子表达式 / 分组匹配的内容,保存到内存中以数字编号或显式命名的组里,方便后续引用:- 分组编号从左向右,以分组的左括号为标志:
- 第一个出现的分组组号为 
1,第二个为2,依此类推。 - 组号 
0代表整个正则表达式。 
 - 第一个出现的分组组号为 
 
 - 分组编号从左向右,以分组的左括号为标志:
 - 反向引用
圆括号捕获的内容可在后续被引用,用于构建更灵活的匹配模式:- 内部反向引用:在正则表达式内部使用 
\\\\分组号(如\\\\1)。 - 外部反向引用:在替换字符串或代码中使用 
$分组号(如$1)。 
 - 内部反向引用:在正则表达式内部使用 
 
 - 分组
 
-在Sting类中使用正则表达式
- 
替换功能
String 类 public StringReplaceAll(String regex,String replacement)
 - 
判断功能
String类 public boolean matches(String regex)
 - 
分割功能
String 类 public String[] split(String regex)
 
-正则表达式练习
- 验证电子邮件格式是否合法,基本结构:
local-part@domain 
 核心规则
local-part(用户名)
- 允许字符:
大小写字母(a-z,A-Z)、数字(0-9)、特殊符号(! # $ % & \' * + - / = ? ^ _{ | } ~`)、点号(`.`)。- 
限制:
 - 
点号(
.)不能作为开头或结尾,也不能连续出现(如..)。- 特殊符号需用引号包裹(如 
\"john+doe\"@example.com),但实际中很少使用。 
 - 特殊符号需用引号包裹(如 
 - 
长度:理论上无限制,但部分邮件服务器限制为 64 个字符。
 
 - 
 
@符号
- 必须存在且只能出现一次,用于分隔用户名和域名。
 
- 
domain(域名)- 结构:
主机名.顶级域名(如example.com)。- 允许字符:
大小写字母、数字、连字符(-),但连字符不能作为开头或结尾。 - 顶级域名(TLD):
必须至少包含一个点号(.),如.com,.org,.co.uk等。 
 - 允许字符:
 
 - 结构:
 
实现代码
package com.xijie.regexp;import org.junit.Test;import java.util.regex.Pattern;import static org.junit.Assert.assertFalse;import static org.junit.Assert.assertTrue;/** * 检测一个字符串是否是合法的电子邮箱 * * 电子邮件格式基本结构:local-part@domain * 核心规则: * 1. local-part(用户名) * - 允许字符: * 英文字母(a-z,不区分大小写,多数服务商将大写视为小写,如 User 和 user 视为同一用户名); * 数字(0-9); * 部分特殊字符: * 点(.):如 user.name(但需注意:不能连续出现,如 user..name 无效;多数服务商禁止开头或结尾为点,如 .user 或 user. 无效); * 下划线(_):如 user_name,可以放在标签的开头或结尾,与英文字母数字同等 * 连字符(-):如 user-name,不可以放在标签的开头或结尾 * - 限制: * - 点号(.)不能作为开头或结尾,也不能连续出现(如 ..)。 * 2. @ 符号 * - 必须存在且只能出现一次,用于分隔用户名和域名。 * 3. domain(域名) * - 结构:主机名.顶级域名(如 example.com)。 * - 允许字符: * 大小写字母、数字、连字符(-),但连字符不能作为开头或结尾,连字符不能连续出现。 * - 顶级域名(TLD): * 必须至少包含一个点号(.),如 .com, .org, .co.uk 等。 */public class EmailValidator { public static boolean isEmailValid(String str){ //邮箱不区分大小写,因此转成小写后识别 //主机名分为两部分:非结尾标签与结尾标签 //第一部分:非结尾标签可以出现\\\\w或连字符,但是出现连字符必须后跟\\\\w,非结尾标签后必须跟一个.,非结尾标签可以出现0次或多次 //第二部分:结尾标签就是非结尾标签去掉.,结尾标签必须刚好出现一次 //域名分为两部分:主机名与顶级域名 //第一部分,主机名由一个或多个xxx.构成,xxx可以由大小写字母、数字、连字符组成,但连字符不能作为开头或结尾且不能连续出现 //xxx.的思路:开头必须是大小写字母或数字,若有连字符,连字符必须后跟大小写字母或数字,结尾必须是. //第二部分:顶级域名的规范与主机名的xxx部分相同 str=str.toLowerCase(); String reg=\"([\\\\w]([\\\\w]|(-\\\\w))*\\\\.)*([\\\\w]([\\\\w]|(-\\\\w))*)@([\\\\da-z]([\\\\da-z]|(-[\\\\da-z]))*\\\\.)+([\\\\da-z]([\\\\da-z]|(-[\\\\da-z]))*)\"; return Pattern.matches(reg,str); } // 测试合法邮箱地址 @Test public void testValidEmails() { // 基础合法格式 assertTrue(EmailValidator.isEmailValid(\"user@domain.com\")); assertTrue(EmailValidator.isEmailValid(\"user.name@domain.co.uk\")); assertTrue(EmailValidator.isEmailValid(\"user_name@domain.com\")); assertTrue(EmailValidator.isEmailValid(\"user-name@domain.com\")); // 数字与特殊字符组合(合法) assertTrue(EmailValidator.isEmailValid(\"123@domain.com\")); assertTrue(EmailValidator.isEmailValid(\"user.name123@domain.com\")); assertTrue(EmailValidator.isEmailValid(\"user.name_tag@domain.com\")); assertTrue(EmailValidator.isEmailValid(\"user.name-tag@domain.com\")); // 下划线在开头/结尾(合法) assertTrue(EmailValidator.isEmailValid(\"_user@domain.com\")); assertTrue(EmailValidator.isEmailValid(\"user_@domain.com\")); assertTrue(EmailValidator.isEmailValid(\"_user_@domain.com\")); // 多段域名(合法) assertTrue(EmailValidator.isEmailValid(\"user@sub.domain.co.uk\")); assertTrue(EmailValidator.isEmailValid(\"user@domain.co.jp\")); // 顶级域名含数字(格式合法) assertTrue(EmailValidator.isEmailValid(\"user@domain.123\")); assertTrue(EmailValidator.isEmailValid(\"user@domain.co123\")); // 示例中提到的合法地址 assertTrue(EmailValidator.isEmailValid(\"a-a-b.aa_-a.aaa@8-a-c-c.a.a-c.aaa.c-c.cc.asd.88\")); } // 测试用户名部分非法的情况 @Test public void testInvalidUsernames() { // 点号相关错误 assertFalse(EmailValidator.isEmailValid(\".user@domain.com\")); // 点在开头 assertFalse(EmailValidator.isEmailValid(\"user.@domain.com\")); // 点在结尾 assertFalse(EmailValidator.isEmailValid(\"user..name@domain.com\")); // 连续点 assertFalse(EmailValidator.isEmailValid(\"user...name@domain.com\")); // 多个连续点 // 连字符相关错误 assertFalse(EmailValidator.isEmailValid(\"-user@domain.com\")); // 连字符在开头 assertFalse(EmailValidator.isEmailValid(\"user-@domain.com\")); // 连字符在结尾 assertFalse(EmailValidator.isEmailValid(\"user--name@domain.com\")); // 连续连字符 // 含禁止的特殊字符(包括+) assertFalse(EmailValidator.isEmailValid(\"user+name@domain.com\")); // 含+(禁止) assertFalse(EmailValidator.isEmailValid(\"user@name@domain.com\")); // 含额外@ assertFalse(EmailValidator.isEmailValid(\"user!name@domain.com\")); // 含! assertFalse(EmailValidator.isEmailValid(\"user#name@domain.com\")); // 含# assertFalse(EmailValidator.isEmailValid(\"user$name@domain.com\")); // 含$ assertFalse(EmailValidator.isEmailValid(\"user%name@domain.com\")); // 含% // 空格或空白字符 assertFalse(EmailValidator.isEmailValid(\"user name@domain.com\")); // 含空格 assertFalse(EmailValidator.isEmailValid(\"user\\tname@domain.com\")); // 含制表符 // 空用户名 assertFalse(EmailValidator.isEmailValid(\"@domain.com\")); // 空用户名 } // 测试@符号相关错误 @Test public void testInvalidAtSymbol() { assertFalse(EmailValidator.isEmailValid(\"userdomain.com\")); // 缺少@ assertFalse(EmailValidator.isEmailValid(\"user@domain@com\")); // 多个@ } // 测试域名部分非法的情况 @Test public void testInvalidDomains() { // 连字符相关错误 assertFalse(EmailValidator.isEmailValid(\"user@-domain.com\")); // 域名开头含连字符 assertFalse(EmailValidator.isEmailValid(\"user@domain-.com\")); // 域名结尾含连字符 assertFalse(EmailValidator.isEmailValid(\"user@domain--name.com\")); // 域名含连续连字符 // 缺少顶级域名(无点) assertFalse(EmailValidator.isEmailValid(\"user@domain\")); // 无点分隔 assertFalse(EmailValidator.isEmailValid(\"user@domain.\")); // 结尾仅有点 // 域名含非法字符 assertFalse(EmailValidator.isEmailValid(\"user@domain_com\")); // 域名含下划线(非法) assertFalse(EmailValidator.isEmailValid(\"user@domain!com\")); // 域名含! assertFalse(EmailValidator.isEmailValid(\"user@.com\")); // 域名开头仅有点 } // 测试边缘合法场景(格式合规但可能被部分系统限制) @Test public void testEdgeValidCases() { // 顶级域名含连字符(如.co-op是合法TLD) assertTrue(EmailValidator.isEmailValid(\"user@domain.co-op\")); // 域名标签含数字 assertTrue(EmailValidator.isEmailValid(\"user@123.domain.com\")); }}- 
验证是不是整数或小数
package com.xijie.regexp;import org.junit.Test;import static org.junit.Assert.assertFalse;import static org.junit.Assert.assertTrue;/** * 验证字符串是否是整数或小数 * 整数或小数的构成 * (符号部分)+(整数部分)+(点)+(小数部分) * * 符号部分:只能是+或-或无 * 整数部分:可以有;若没有整数部分,则必须有(点)+(小数部分) * 点:若没有点,则必须有整数部分;若有点则整数部分和小数部分至少有一部分 * 小数部分:若有小数部分,则小数部分前必须有点;小数部分可以没有 */public class NumericValidator { public static boolean isNumeric(String str){ String reg=\"[+-]?((\\\\d+\\\\.?\\\\d*)|(\\\\.\\\\d+))\"; return str.matches(reg); } // 测试合法整数格式 @Test public void testValidIntegers() { assertTrue(isNumeric(\"0\")); assertTrue(isNumeric(\"123\")); assertTrue(isNumeric(\"-456\")); assertTrue(isNumeric(\"+789\")); assertTrue(isNumeric(\"001\")); // 前导零(合法但可能被视为特殊情况) assertTrue(isNumeric(\"00\")); // 多个前导零 } // 测试合法小数字符串 @Test public void testValidDecimals() { assertTrue(isNumeric(\"0.1\")); assertTrue(isNumeric(\".1\")); // 省略整数部分(合法) assertTrue(isNumeric(\"1.\")); // 省略小数部分(合法) assertTrue(isNumeric(\"1.0\")); assertTrue(isNumeric(\"-0.1\")); assertTrue(isNumeric(\"+.1\")); assertTrue(isNumeric(\"123.456\")); assertTrue(isNumeric(\"123.\")); assertTrue(isNumeric(\".456\")); assertTrue(isNumeric(\"00.1\")); // 前导零(合法) } // 测试非法格式的字符串 @Test public void testInvalidFormats() { assertFalse(isNumeric(\"\")); // 空字符串 assertFalse(isNumeric(\".\")); // 仅有小数点 assertFalse(isNumeric(\"..\")); // 多个小数点 assertFalse(isNumeric(\"1..2\")); // 多个小数点 assertFalse(isNumeric(\"+\")); // 仅有符号 assertFalse(isNumeric(\"-\")); // 仅有符号 assertFalse(isNumeric(\"abc\")); // 包含非数字字符 assertFalse(isNumeric(\"1a2\")); // 数字间包含非数字字符 assertFalse(isNumeric(\"1,234\")); // 包含逗号(部分地区使用,但此处视为非法) assertFalse(isNumeric(\"1 2\")); // 包含空格 assertFalse(isNumeric(\"1+2\")); // 错误的符号位置 assertFalse(isNumeric(\"+-1\")); // 多个符号 assertFalse(isNumeric(\"1.2.3\")); // 多个小数点 assertFalse(isNumeric(\"1e3\")); // 科学计数法(若不支持) assertFalse(isNumeric(\"∞\")); // 特殊符号 } // 测试边界条件和特殊场景 @Test public void testEdgeCases() { assertTrue(isNumeric(\"0.\")); // 零+小数点(合法) assertTrue(isNumeric(\".0\")); // 小数点+零(合法) assertTrue(isNumeric(\"00000\")); // 全零(合法) assertTrue(isNumeric(\"000.000\")); // 全零带小数点(合法) assertFalse(isNumeric(\".\")); // 单独的小数点 assertFalse(isNumeric(\"+.\")); // 符号+小数点 assertFalse(isNumeric(\"-.1.\")); // 非法的小数格式 assertFalse(isNumeric(\"0x10\")); // 十六进制(若不支持) assertFalse(isNumeric(\"0b10\")); // 二进制(若不支持) } // 测试大数和极小值(根据方法实现可能通过或失败) @Test public void testLargeNumbers() { assertTrue(isNumeric(\"999999999999999999\")); // 极大数(若不考虑溢出) assertTrue(isNumeric(\"0.000000000000001\")); // 极小数 assertTrue(isNumeric(\"999999999999999999.999999999999999999\")); // 极高精度小数 } // 测试特殊字符和空格 @Test public void testSpecialCharacters() { assertFalse(isNumeric(\" 123\")); // 前导空格 assertFalse(isNumeric(\"123 \")); // 尾随空格 assertFalse(isNumeric(\" 123 \")); // 前后空格 assertFalse(isNumeric(\"1,000\")); // 千分位逗号 assertFalse(isNumeric(\"1_000\")); // 下划线分隔符(Java 7+ 支持,但此处视为非法) } // 测试正负号的合法性 @Test public void testSignCharacters() { assertTrue(isNumeric(\"+1\")); // 正号 assertTrue(isNumeric(\"-1\")); // 负号 assertFalse(isNumeric(\"+-1\")); // 多个符号 assertFalse(isNumeric(\"-+1\")); // 多个符号 assertFalse(isNumeric(\"1-\")); // 符号位置错误 assertFalse(isNumeric(\"1+\")); // 符号位置错误 }} - 
解析URL:协议、域名、端口、文件名
package com.xijie.regexp;import org.junit.Test;import static org.junit.Assert.*;/** * 解析URL:协议、域名、端口、文件名 * * 先规定URL组成:scheme://[userinfo@]host[:port]/path[?query][#fragment] * * scheme:协议 * userinfo:用户信息,可选 * host:主机名 * port:端口 * path:路径 * query:查询参数 * fragment:片段 */public class URLValidator { public static String[] parseUrl(String url) { if (url == null || url.isEmpty()) { return null; } // 定义正则表达式模式 String regex = \"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\\\?([^#]*))?(#(.*))?\"; java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(regex); java.util.regex.Matcher matcher = pattern.matcher(url); if (!matcher.matches()) { return null; } // 提取各部分 String scheme = matcher.group(2); String authority = matcher.group(4); String path = matcher.group(5); String query = matcher.group(7); String fragment = matcher.group(9); // 进一步解析authority部分为userInfo、host和port String userInfo = null; String host = null; String port = null; if (authority != null) { int atIndex = authority.lastIndexOf(\'@\'); if (atIndex != -1) { userInfo = authority.substring(0, atIndex); authority = authority.substring(atIndex + 1); } int colonIndex = authority.indexOf(\':\'); if (colonIndex != -1 && colonIndex < authority.length() - 1) { host = authority.substring(0, colonIndex); port = authority.substring(colonIndex + 1); } else { host = authority; } } // 验证scheme是否合法 if (scheme != null && !scheme.matches(\"^[a-zA-Z][a-zA-Z0-9+.-]*$\")) { return null; } // 验证host是否合法 if (host != null) { // 检查是否为IPv6地址格式(带方括号) if (host.startsWith(\"[\") && host.endsWith(\"]\")) { String ipv6 = host.substring(1, host.length() - 1); // 简单验证IPv6格式(实际应更严格) if (!ipv6.matches(\"^[0-9a-fA-F:]+$\")) { return null; } } else { // 验证普通域名或IPv4 if (!host.matches(\"^[a-zA-Z0-9-]+(\\\\.[a-zA-Z0-9-]+)*$\") && !host.matches(\"^(\\\\d{1,2}|1\\\\d\\\\d|2[0-4]\\\\d|25[0-5])\\\\.(\\\\d{1,2}|1\\\\d\\\\d|2[0-4]\\\\d|25[0-5])\\\\.(\\\\d{1,2}|1\\\\d\\\\d|2[0-4]\\\\d|25[0-5])\\\\.(\\\\d{1,2}|1\\\\d\\\\d|2[0-4]\\\\d|25[0-5])$\")) { return null; } } } // 验证port是否合法 if (port != null && !port.matches(\"^\\\\d{1,5}$\")) { return null; } // 返回结果数组 return new String[]{ scheme != null ? scheme : \"\", userInfo != null ? userInfo : \"\", host != null ? host : \"\", port != null ? port : \"\", path != null ? path : \"\", query != null ? query : \"\", fragment != null ? fragment : \"\" }; } // 测试合法 URL 解析 @Test public void testValidUrls() { // 基础 HTTP URL String[] parts = parseUrl(\"http://example.com\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"example.com\", \"\", \"\", \"\", \"\"}, parts); // 带路径的 HTTPS URL parts = parseUrl(\"https://example.com/path/to/resource\"); assertNotNull(parts); assertArrayEquals(new String[]{\"https\", \"\", \"example.com\", \"\", \"/path/to/resource\", \"\", \"\"}, parts); // 带端口的 FTP URL parts = parseUrl(\"ftp://example.com:21\"); assertNotNull(parts); assertArrayEquals(new String[]{\"ftp\", \"\", \"example.com\", \"21\", \"\", \"\", \"\"}, parts); // 带用户信息的 URL parts = parseUrl(\"http://user:pass@example.com\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"user:pass\", \"example.com\", \"\", \"\", \"\", \"\"}, parts); // 带查询参数的 URL parts = parseUrl(\"https://example.com/search?q=test&page=1\"); assertNotNull(parts); assertArrayEquals(new String[]{\"https\", \"\", \"example.com\", \"\", \"/search\", \"q=test&page=1\", \"\"}, parts); // 带片段的 URL parts = parseUrl(\"https://example.com/docs#section2\"); assertNotNull(parts); assertArrayEquals(new String[]{\"https\", \"\", \"example.com\", \"\", \"/docs\", \"\", \"section2\"}, parts); // 完整 URL parts = parseUrl(\"https://user:pass@example.com:8080/api/users?page=1#top\"); assertNotNull(parts); assertArrayEquals(new String[]{ \"https\", \"user:pass\", \"example.com\", \"8080\", \"/api/users\", \"page=1\", \"top\" }, parts); // 省略协议的 URL parts = parseUrl(\"//example.com/path\"); assertNotNull(parts); assertArrayEquals(new String[]{\"\", \"\", \"example.com\", \"\", \"/path\", \"\", \"\"}, parts); // IPv4 地址 parts = parseUrl(\"http://192.168.1.1\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"192.168.1.1\", \"\", \"\", \"\", \"\"}, parts); // IPv6 地址 parts = parseUrl(\"http://[2001:db8::1]/path\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"2001:db8::1\", \"\", \"/path\", \"\", \"\"}, parts); // 带端口的 IPv6 地址 parts = parseUrl(\"http://[2001:db8::1]:8080\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"2001:db8::1\", \"8080\", \"\", \"\", \"\"}, parts); // 复杂路径和查询 parts = parseUrl(\"https://example.com/a/b/c?x=1&y=2#z\"); assertNotNull(parts); assertArrayEquals(new String[]{\"https\", \"\", \"example.com\", \"\", \"/a/b/c\", \"x=1&y=2\", \"z\"}, parts); // 空路径 parts = parseUrl(\"http://example.com?query=1\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"example.com\", \"\", \"\", \"query=1\", \"\"}, parts); // 特殊协议 parts = parseUrl(\"ftp://ftp.example.com\"); assertNotNull(parts); assertArrayEquals(new String[]{\"ftp\", \"\", \"ftp.example.com\", \"\", \"\", \"\", \"\"}, parts); parts = parseUrl(\"mailto:user@example.com\"); assertNotNull(parts); assertArrayEquals(new String[]{\"mailto\", \"\", \"\", \"\", \"user@example.com\", \"\", \"\"}, parts); } // 测试非法 URL @Test public void testInvalidUrls() { assertNull(parseUrl(null)); assertNull(parseUrl(\"\")); assertNull(parseUrl(\"http://\")); // 缺少主机 assertNull(parseUrl(\"http://:80\")); // 缺少主机 assertNull(parseUrl(\"http://user@:80\")); // 缺少主机 assertNull(parseUrl(\"http://example.com:port\")); // 非法端口 assertNull(parseUrl(\"http://example.com:65536\")); // 端口超出范围 assertNull(parseUrl(\"http://[2001:db8::1\")); // 不完整的 IPv6 assertNull(parseUrl(\"http://2001:db8::1]\")); // 不完整的 IPv6 assertNull(parseUrl(\"http://[invalid-ipv6]\")); // 非法 IPv6 assertNull(parseUrl(\"http://invalid_host\")); // 非法主机名 assertNull(parseUrl(\"http://invalid_host:80\")); // 非法主机名 assertNull(parseUrl(\"ht tp://example.com\")); // 非法协议 assertNull(parseUrl(\"http://example.com/path?query#fragment#extra\")); // 多个片段符号 assertNull(parseUrl(\"http://example.com/path?query?extra\")); // 多个查询符号 assertNull(parseUrl(\"://example.com\")); // 空协议 assertNull(parseUrl(\"123://example.com\")); // 协议不以字母开头 assertNull(parseUrl(\"http://user:pass@:80/path\")); // 缺少主机 } // 测试边界情况 @Test public void testEdgeCases() { // 带点的主机名 String[] parts = parseUrl(\"http://sub.domain.co.uk\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"sub.domain.co.uk\", \"\", \"\", \"\", \"\"}, parts); // 带破折号的主机名 parts = parseUrl(\"http://my-host.com\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"my-host.com\", \"\", \"\", \"\", \"\"}, parts); // 带多个点的路径 parts = parseUrl(\"http://example.com/path/to/file.txt\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"example.com\", \"\", \"/path/to/file.txt\", \"\", \"\"}, parts); // 复杂查询参数 parts = parseUrl(\"http://example.com?key1=value1&key2=value2&key3=\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"example.com\", \"\", \"\", \"key1=value1&key2=value2&key3=\", \"\"}, parts); // 带加号的查询参数(URL 编码中 + 表示空格) parts = parseUrl(\"http://example.com?search=java+programming\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"example.com\", \"\", \"\", \"search=java+programming\", \"\"}, parts); // 带百分比编码的 URL parts = parseUrl(\"http://example.com/path%20with%20spaces?param=value%26extra\"); assertNotNull(parts); assertArrayEquals(new String[]{ \"http\", \"\", \"example.com\", \"\", \"/path%20with%20spaces\", \"param=value%26extra\", \"\" }, parts); // 端口为 0(理论合法) parts = parseUrl(\"http://example.com:0\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"example.com\", \"0\", \"\", \"\", \"\"}, parts); // 端口为 65535(最大合法端口) parts = parseUrl(\"http://example.com:65535\"); assertNotNull(parts); assertArrayEquals(new String[]{\"http\", \"\", \"example.com\", \"65535\", \"\", \"\", \"\"}, parts); }} 
- 
0-9 ↩︎
 - 
A-Z ↩︎
 
 


