【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="<div style=\"margin-class=\"content_1YWBm\"><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%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.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\"><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 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)(?<g1>\\d)(?<g2>\\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 )。 |
\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.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<br>String result = Pattern.compile("(\\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.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);}// 测试合法邮箱地址@Testpublic 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"));}// 测试用户名部分非法的情况@Testpublic 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")); // 空用户名}// 测试@符号相关错误@Testpublic void testInvalidAtSymbol() {assertFalse(EmailValidator.isEmailValid("userdomain.com")); // 缺少@assertFalse(EmailValidator.isEmailValid("user@domain@com")); // 多个@}// 测试域名部分非法的情况@Testpublic 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")); // 域名开头仅有点}// 测试边缘合法场景(格式合规但可能被部分系统限制)@Testpublic 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);}// 测试合法整数格式@Testpublic void testValidIntegers() {assertTrue(isNumeric("0"));assertTrue(isNumeric("123"));assertTrue(isNumeric("-456"));assertTrue(isNumeric("+789"));assertTrue(isNumeric("001")); // 前导零(合法但可能被视为特殊情况)assertTrue(isNumeric("00")); // 多个前导零}// 测试合法小数字符串@Testpublic 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")); // 前导零(合法)}// 测试非法格式的字符串@Testpublic 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("∞")); // 特殊符号}// 测试边界条件和特殊场景@Testpublic 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")); // 二进制(若不支持)}// 测试大数和极小值(根据方法实现可能通过或失败)@Testpublic void testLargeNumbers() {assertTrue(isNumeric("999999999999999999")); // 极大数(若不考虑溢出)assertTrue(isNumeric("0.000000000000001")); // 极小数assertTrue(isNumeric("999999999999999999.999999999999999999")); // 极高精度小数}// 测试特殊字符和空格@Testpublic void testSpecialCharacters() {assertFalse(isNumeric(" 123")); // 前导空格assertFalse(isNumeric("123 ")); // 尾随空格assertFalse(isNumeric(" 123 ")); // 前后空格assertFalse(isNumeric("1,000")); // 千分位逗号assertFalse(isNumeric("1_000")); // 下划线分隔符(Java 7+ 支持,但此处视为非法)}// 测试正负号的合法性@Testpublic 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和portString 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 {// 验证普通域名或IPv4if (!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 解析@Testpublic void testValidUrls() {// 基础 HTTP URLString[] parts = parseUrl("http://example.com");assertNotNull(parts);assertArrayEquals(new String[]{"http", "", "example.com", "", "", "", ""}, parts);// 带路径的 HTTPS URLparts = parseUrl("https://example.com/path/to/resource");assertNotNull(parts);assertArrayEquals(new String[]{"https", "", "example.com", "", "/path/to/resource", "", ""}, parts);// 带端口的 FTP URLparts = parseUrl("ftp://example.com:21");assertNotNull(parts);assertArrayEquals(new String[]{"ftp", "", "example.com", "21", "", "", ""}, parts);// 带用户信息的 URLparts = parseUrl("http://user:pass@example.com");assertNotNull(parts);assertArrayEquals(new String[]{"http", "user:pass", "example.com", "", "", "", ""}, parts);// 带查询参数的 URLparts = parseUrl("https://example.com/search?q=test&page=1");assertNotNull(parts);assertArrayEquals(new String[]{"https", "", "example.com", "", "/search", "q=test&page=1", ""}, parts);// 带片段的 URLparts = parseUrl("https://example.com/docs#section2");assertNotNull(parts);assertArrayEquals(new String[]{"https", "", "example.com", "", "/docs", "", "section2"}, parts);// 完整 URLparts = 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);// 省略协议的 URLparts = 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@Testpublic 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")); // 不完整的 IPv6assertNull(parseUrl("http://2001:db8::1]")); // 不完整的 IPv6assertNull(parseUrl("http://[invalid-ipv6]")); // 非法 IPv6assertNull(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")); // 缺少主机}// 测试边界情况@Testpublic 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);// 带百分比编码的 URLparts = 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 ↩︎