正则表达式
-为什么要学习正则表达式
-
极速体验正则表达式威力
- 提取文章中所有的英文单词
- 提取文章中所有的数字
- 提取文章中所有的英文单词和数字
- 提取百度热榜标题
-
实践案例
package ***.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="<div style=\"margin-class=\"content_1YWBm\"><a href=\"https://www.baidu.***/s?wd=%E8%B7%9F%E7%9D%80%E6%80%BB%E4%B9%A6%E8%AE%B0%E7%9A%84%E8%B6%B3%E8%BF%B9%E6%8E%A2%E5%AF%BB%E4%B8%AD%E5%8D%8E%E6%96%87%E8%84%89&sa=fyb_news&rsv_dl=fyb_news\" class=\"title_dIF3B \" target=\"_blank\"><div class=\"c-single-text-ellipsis\"> 跟着总书记的足迹探寻中华文脉 <!--36--></div><div class=\"c-text hot-tag_1G080\"></div><!--37--></a><!--s-frag--><div class=\"hot-desc_1m_jR small_Uvkd3 \"><a href=\"https://www.baidu.***/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.***pile("[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.***pile("[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.***pile("([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.***pile("target=\"_blank\"><div class=\"c-single-text-ellipsis\"> (.*?) <!--"); Matcher matcher=pattern.matcher(content); while(matcher.find()){ System.out.println("找到 "+matcher.group(1)); } } } -
结论
正则表达式是处理文本匹配与处理的高效工具
-
解决之道 - 正则表达式
- 为解决上述文本处理问题,Java 提供了正则表达式技术,专门用于高效处理此类文本匹配、提取、替换等需求
- 简单来说:正则表达式是一种对字符串执行模式匹配与处理的技术
- 正则表达式(英文:regular expression,简称:RegExp)
-正则表达式基本介绍
- 介绍
- 一个正则表达式,就是用特定模式匹配字符串的公式。虽然其语法初看可能显得古怪复杂,让人望而却步,但经过练习后会发现,编写这些表达式其实并不困难。而且,一旦掌握正则表达式,原本需要数小时且容易出错的文本处理工作,往往能在几分钟甚至几秒钟内完成。
- 这里要特别强调,正则表达式并非 Java 独有,实际上很多编程语言都支持通过正则表达式进行字符串操作,例如Javascript、php。。。
-正则表达式底层实现
-
实例分析
package ***.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.***pile(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 ***.xijie.regexp; import java.util.regex.Matcher; import java.util.regex.Pattern; public class Es***har { 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.***pile(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.***pile(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 ***.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.***pile(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.***pile(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.***pile(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 ***.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.***pile(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 ***.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.***pile(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.***” 匹配被非边界字符包围的 @符号(如邮箱中的 @,但排除句首 @标签) 匹配目标字符串的非边界 -
应用实例
package ***.xijie.regexp; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 元字符-字符匹配符 */ public class PositionMatch { public static void main(String[] args) { findStr("123456@qq.***","^[\\d]+@[\\w]+\\.(***)$"); findStr("java 12345java","\\bjava\\b"); findStr("java 1111java666","[\\d]+\\B"); } private static void findStr(String content,String reg){ Pattern pattern=Pattern.***pile(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 ***.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)(?<g1>\\d)(?<g2>\\d\\d)",new String[]{"g1","g2","g3"}); } private static void findStrAndGroup(String content,String reg,String[] groupName){ Pattern pattern=Pattern.***pile(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 ***.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.***pile(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 ***.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.***pile(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)。 |
\e |
Escape 符(\u001B)。 |
\cx |
控制字符,例如 \cM 表示 Ctrl-M(等价于 \r)。 |
\xhh |
十六进制字符,例如 \x61 表示 a。 |
\uhhhh |
Unicode 字符,例如 \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.***pile(pattern);方法名 说明 static Pattern ***pile(String regex)编译给定的正则表达式字符串,返回 Pattern对象。用于创建正则表达式模式。static Pattern ***pile(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.***pile(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<br>String result = Pattern.***pile("(\\d+)-(\\d+)")<br> .matcher("123-456")<br> .replaceAll("$2-$1");<br>// 结果:"456-123"<br> -
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.***),但实际中很少使用。
- 特殊符号需用引号包裹(如
-
长度:理论上无限制,但部分邮件服务器限制为 64 个字符。
-
@符号
- 必须存在且只能出现一次,用于分隔用户名和域名。
-
domain(域名)-
结构:
主机名.顶级域名(如example.***)。-
允许字符:
大小写字母、数字、连字符(-),但连字符不能作为开头或结尾。 -
顶级域名(TLD):
必须至少包含一个点号(.),如.***,.org,.co.uk等。
-
允许字符:
-
结构:
实现代码
package ***.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.***)。
* - 允许字符:
* 大小写字母、数字、连字符(-),但连字符不能作为开头或结尾,连字符不能连续出现。
* - 顶级域名(TLD):
* 必须至少包含一个点号(.),如 .***, .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.***"));
assertTrue(EmailValidator.isEmailValid("user.name@domain.co.uk"));
assertTrue(EmailValidator.isEmailValid("user_name@domain.***"));
assertTrue(EmailValidator.isEmailValid("user-name@domain.***"));
// 数字与特殊字符组合(合法)
assertTrue(EmailValidator.isEmailValid("123@domain.***"));
assertTrue(EmailValidator.isEmailValid("user.name123@domain.***"));
assertTrue(EmailValidator.isEmailValid("user.name_tag@domain.***"));
assertTrue(EmailValidator.isEmailValid("user.name-tag@domain.***"));
// 下划线在开头/结尾(合法)
assertTrue(EmailValidator.isEmailValid("_user@domain.***"));
assertTrue(EmailValidator.isEmailValid("user_@domain.***"));
assertTrue(EmailValidator.isEmailValid("_user_@domain.***"));
// 多段域名(合法)
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.***.asd.88"));
}
// 测试用户名部分非法的情况
@Test
public void testInvalidUsernames() {
// 点号相关错误
assertFalse(EmailValidator.isEmailValid(".user@domain.***")); // 点在开头
assertFalse(EmailValidator.isEmailValid("user.@domain.***")); // 点在结尾
assertFalse(EmailValidator.isEmailValid("user..name@domain.***")); // 连续点
assertFalse(EmailValidator.isEmailValid("user...name@domain.***")); // 多个连续点
// 连字符相关错误
assertFalse(EmailValidator.isEmailValid("-user@domain.***")); // 连字符在开头
assertFalse(EmailValidator.isEmailValid("user-@domain.***")); // 连字符在结尾
assertFalse(EmailValidator.isEmailValid("user--name@domain.***")); // 连续连字符
// 含禁止的特殊字符(包括+)
assertFalse(EmailValidator.isEmailValid("user+name@domain.***")); // 含+(禁止)
assertFalse(EmailValidator.isEmailValid("user@name@domain.***")); // 含额外@
assertFalse(EmailValidator.isEmailValid("user!name@domain.***")); // 含!
assertFalse(EmailValidator.isEmailValid("user#name@domain.***")); // 含#
assertFalse(EmailValidator.isEmailValid("user$name@domain.***")); // 含$
assertFalse(EmailValidator.isEmailValid("user%name@domain.***")); // 含%
// 空格或空白字符
assertFalse(EmailValidator.isEmailValid("user name@domain.***")); // 含空格
assertFalse(EmailValidator.isEmailValid("user\tname@domain.***")); // 含制表符
// 空用户名
assertFalse(EmailValidator.isEmailValid("@domain.***")); // 空用户名
}
// 测试@符号相关错误
@Test
public void testInvalidAtSymbol() {
assertFalse(EmailValidator.isEmailValid("userdomain.***")); // 缺少@
assertFalse(EmailValidator.isEmailValid("user@domain@***")); // 多个@
}
// 测试域名部分非法的情况
@Test
public void testInvalidDomains() {
// 连字符相关错误
assertFalse(EmailValidator.isEmailValid("user@-domain.***")); // 域名开头含连字符
assertFalse(EmailValidator.isEmailValid("user@domain-.***")); // 域名结尾含连字符
assertFalse(EmailValidator.isEmailValid("user@domain--name.***")); // 域名含连续连字符
// 缺少顶级域名(无点)
assertFalse(EmailValidator.isEmailValid("user@domain")); // 无点分隔
assertFalse(EmailValidator.isEmailValid("user@domain.")); // 结尾仅有点
// 域名含非法字符
assertFalse(EmailValidator.isEmailValid("user@domain_***")); // 域名含下划线(非法)
assertFalse(EmailValidator.isEmailValid("user@domain!***")); // 域名含!
assertFalse(EmailValidator.isEmailValid("user@.***")); // 域名开头仅有点
}
// 测试边缘合法场景(格式合规但可能被部分系统限制)
@Test
public void testEdgeValidCases() {
// 顶级域名含连字符(如.co-op是合法TLD)
assertTrue(EmailValidator.isEmailValid("user@domain.co-op"));
// 域名标签含数字
assertTrue(EmailValidator.isEmailValid("user@123.domain.***"));
}
}
-
验证是不是整数或小数
package ***.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 ***.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.***pile(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.***"); assertNotNull(parts); assertArrayEquals(new String[]{"http", "", "example.***", "", "", "", ""}, parts); // 带路径的 HTTPS URL parts = parseUrl("https://example.***/path/to/resource"); assertNotNull(parts); assertArrayEquals(new String[]{"https", "", "example.***", "", "/path/to/resource", "", ""}, parts); // 带端口的 FTP URL parts = parseUrl("ftp://example.***:21"); assertNotNull(parts); assertArrayEquals(new String[]{"ftp", "", "example.***", "21", "", "", ""}, parts); // 带用户信息的 URL parts = parseUrl("http://user:pass@example.***"); assertNotNull(parts); assertArrayEquals(new String[]{"http", "user:pass", "example.***", "", "", "", ""}, parts); // 带查询参数的 URL parts = parseUrl("https://example.***/search?q=test&page=1"); assertNotNull(parts); assertArrayEquals(new String[]{"https", "", "example.***", "", "/search", "q=test&page=1", ""}, parts); // 带片段的 URL parts = parseUrl("https://example.***/docs#section2"); assertNotNull(parts); assertArrayEquals(new String[]{"https", "", "example.***", "", "/docs", "", "section2"}, parts); // 完整 URL parts = parseUrl("https://user:pass@example.***:8080/api/users?page=1#top"); assertNotNull(parts); assertArrayEquals(new String[]{ "https", "user:pass", "example.***", "8080", "/api/users", "page=1", "top" }, parts); // 省略协议的 URL parts = parseUrl("//example.***/path"); assertNotNull(parts); assertArrayEquals(new String[]{"", "", "example.***", "", "/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.***/a/b/c?x=1&y=2#z"); assertNotNull(parts); assertArrayEquals(new String[]{"https", "", "example.***", "", "/a/b/c", "x=1&y=2", "z"}, parts); // 空路径 parts = parseUrl("http://example.***?query=1"); assertNotNull(parts); assertArrayEquals(new String[]{"http", "", "example.***", "", "", "query=1", ""}, parts); // 特殊协议 parts = parseUrl("ftp://ftp.example.***"); assertNotNull(parts); assertArrayEquals(new String[]{"ftp", "", "ftp.example.***", "", "", "", ""}, parts); parts = parseUrl("mailto:user@example.***"); assertNotNull(parts); assertArrayEquals(new String[]{"mailto", "", "", "", "user@example.***", "", ""}, 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.***:port")); // 非法端口 assertNull(parseUrl("http://example.***: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.***")); // 非法协议 assertNull(parseUrl("http://example.***/path?query#fragment#extra")); // 多个片段符号 assertNull(parseUrl("http://example.***/path?query?extra")); // 多个查询符号 assertNull(parseUrl("://example.***")); // 空协议 assertNull(parseUrl("123://example.***")); // 协议不以字母开头 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.***"); assertNotNull(parts); assertArrayEquals(new String[]{"http", "", "my-host.***", "", "", "", ""}, parts); // 带多个点的路径 parts = parseUrl("http://example.***/path/to/file.txt"); assertNotNull(parts); assertArrayEquals(new String[]{"http", "", "example.***", "", "/path/to/file.txt", "", ""}, parts); // 复杂查询参数 parts = parseUrl("http://example.***?key1=value1&key2=value2&key3="); assertNotNull(parts); assertArrayEquals(new String[]{"http", "", "example.***", "", "", "key1=value1&key2=value2&key3=", ""}, parts); // 带加号的查询参数(URL 编码中 + 表示空格) parts = parseUrl("http://example.***?search=java+programming"); assertNotNull(parts); assertArrayEquals(new String[]{"http", "", "example.***", "", "", "search=java+programming", ""}, parts); // 带百分比编码的 URL parts = parseUrl("http://example.***/path%20with%20spaces?param=value%26extra"); assertNotNull(parts); assertArrayEquals(new String[]{ "http", "", "example.***", "", "/path%20with%20spaces", "param=value%26extra", "" }, parts); // 端口为 0(理论合法) parts = parseUrl("http://example.***:0"); assertNotNull(parts); assertArrayEquals(new String[]{"http", "", "example.***", "0", "", "", ""}, parts); // 端口为 65535(最大合法端口) parts = parseUrl("http://example.***:65535"); assertNotNull(parts); assertArrayEquals(new String[]{"http", "", "example.***", "65535", "", "", ""}, parts); } }
-
0-9 ↩︎
-
A-Z ↩︎