> 技术文档 > 【JavaSE】正则表达式学习笔记

【JavaSE】正则表达式学习笔记


正则表达式

-为什么要学习正则表达式

  • 极速体验正则表达式威力

    1. 提取文章中所有的英文单词
    2. 提取文章中所有的数字
    3. 提取文章中所有的英文单词和数字
    4. 提取百度热榜标题
  • 实践案例

    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)); } }}
  • 结论

    正则表达式是处理文本匹配与处理的高效工具

  • 解决之道 - 正则表达式

    1. 为解决上述文本处理问题,Java 提供了正则表达式技术,专门用于高效处理此类文本匹配、提取、替换等需求
    2. 简单来说:正则表达式是一种对字符串执行模式匹配与处理的技术
    3. 正则表达式(英文:regular expression,简称:RegExp)

-正则表达式基本介绍

  • 介绍
    1. 一个正则表达式,就是用特定模式匹配字符串的公式。虽然其语法初看可能显得古怪复杂,让人望而却步,但经过练习后会发现,编写这些表达式其实并不困难。而且,一旦掌握正则表达式,原本需要数小时且容易出错的文本处理工作,往往能在几分钟甚至几秒钟内完成。
    2. 这里要特别强调,正则表达式并非 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 方法完成的任务

    1. 根据指定的正则表达式规则,在目标字符串中查找下一个符合条件的子字符串(例如 “1998”)。
    2. 找到匹配项后,将匹配结果的位置信息记录到 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]
      • 依此类推…
    3. 同时更新 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 个捕获组匹配的内容(依此类推)

-正则表达式语法

  • 基本介绍

    若要灵活运用正则表达式,需了解各类元字符的功能。元字符从功能上大致分为以下几类:

    1. 限定符
    2. 选择匹配符
    3. 分组组合和反向引用符
    4. 特殊字符
    5. 字符匹配符
    6. 定位符
  • 元字符-转义号 \\\\

    • 符号说明: 在使用正则表达式检索某些特殊字符时,需用转义符号\\,否则可能无法检索到结果,甚至报错。

    • 案例:

      • 若直接用$匹配字符串 “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_] 匹配字母(大小写)、数字或下划线中的任意一个字符 A3_z [^] 不接收的字符列表 [^a-zA-Z] 匹配除字母外的任意字符(包括数字、特殊符号、空格等) 5@、(空格)、# - 连字符 [0-9a-fA-F] 匹配十六进制字符(0-9、小写 a-f、大写 A-F 中的任意一个) 3aF7 . 匹配除\\n外的任意字符 a.b 匹配 “a” 和 “b” 之间有 1 个任意字符(除换行),中间无字符或换行不匹配 a1ba@ba b(空格) \\d 匹配单个数字字符,相当于[0-9] \\d{3} 匹配连续 3 个数字 123456789 \\D 匹配单个非数字字符,相当于[ ^0-9] \\D{2} 匹配连续 2 个非数字字符 ab@#_$ \\w 匹配单个数字、大小写字母字符,相当于[0-9a-zA-Z] \\w+ 匹配 1 个或多个连续的单词字符(字母、数字、下划线) user123_nameA_B \\W 匹配单个非数字、大小写字母字符,相当于[^0-9a-zA-Z] \\W* 匹配 0 个或多个连续的非单词字符(允许空字符串) @#、(空格)、(空)
    • 应用案例

      1. [a-z]:[a-z]表示可以匹配a-z中任意一个字符
      2. (?i):后续字符都不区分大小写
      3. Pattern pat = Pattern.compile(regEx, Pattern.CASE_INSENSITIVE):本匹配不区分大小写,这意味着对小写字符的匹配也对大写字符生效,反之亦然
      4. [^a-z]:表示匹配不是a-z的任意一个字符, [ ^a-z]{2}表示匹配连续2个不是a-z的字符
      5. [abcd]表示可以匹配abcd中的任意一个字符。
      6. [^abcd]表示可以匹配不是abcd中的任意一个字符
      7. \\\\d表示可以匹配0-9的任意一个数字,相当于[0-9]。
      8. \\\\D表示可以匹配不是0-9中的任意一个数字,相当于[ ^0-9]
      9. \\\\w匹配任意英文字符、数字和下划线,相当于[a-zA-Z0-9]
      10. \\\\W相当于[ ^a-zA-Z0-9]是\\\\w刚好相反.
      11. \\\\s匹配任何空白字符(空格,制表符等)
      12. \\\\S匹配任何非空白字符,和\\\\s刚好相反
      13. .匹配出\\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* 可匹配空字符串、aaa 等。 + 匹配前面的子表达式一次或多次。等价于 {1,}。例如,a+ 可匹配 aaa,但不匹配空字符串。 ? 匹配前面的子表达式零次或一次。等价于 {0,1}。例如,colou?r 可匹配 colorcolour{n} 匹配前面的子表达式恰好 n 次。例如,a{3} 匹配 aaa{n,} 匹配前面的子表达式至少 n 次。例如,a{2,} 匹配 aaaaa 等。 {n,m} 匹配前面的子表达式至少 n 次,至多 m 次(含边界)。例如,a{2,4} 匹配 aaaaaaaaa*?, +?, ??, {n,}?, {n,m}? 使量词变为非贪婪模式,即尽可能少地匹配字符。例如,a+? 匹配 aaa 时仅匹配 a\\ 转义字符,用于取消元字符的特殊含义。例如,\\. 匹配字面点号,\\\\ 匹配单个反斜杠。 [] 字符组,匹配方括号中指定的任意一个字符。例如,[abc] 匹配 abc[^] 否定字符组,匹配不在方括号中的任意一个字符。例如,[^abc] 匹配除 abc 外的任意字符。 [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_INSENSITIVEMULTILINE等)。 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+))的起始索引。若未匹配或组不存在,返回-1public int end() 返回上一次匹配的最后一个字符的后一个位置的索引。例如,匹配abc,返回c的索引 + 1。 public int end(int group) 返回上一次匹配中指定捕获组的结束位置的后一个索引。例如,捕获组匹配123,返回3的索引 + 1。 public boolean lookingAt() 尝试从输入序列的起始位置开始匹配模式,但不要求匹配整个输入。例如,模式abc匹配输入abcdef返回truepublic boolean find() 尝试在输入序列中查找下一个与模式匹配的子序列。例如,模式\\\\d+a123b456中可多次调用find()分别匹配123456public boolean find(int start) 从指定索引位置开始查找下一个匹配的子序列。例如,find(3)从索引 3 开始匹配。 public boolean matches() 尝试将整个输入序列与模式匹配。例如,模式\\\\d+匹配123返回true,匹配a123返回falsepublic 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. 捕获
      将正则表达式中子表达式 / 分组匹配的内容,保存到内存中以数字编号显式命名的组里,方便后续引用:
      • 分组编号从左向右,以分组的左括号为标志:
        • 第一个出现的分组组号为 1,第二个为 2,依此类推。
        • 组号 0 代表整个正则表达式
    3. 反向引用
      圆括号捕获的内容可在后续被引用,用于构建更灵活的匹配模式:
      • 内部反向引用:在正则表达式内部使用 \\\\分组号(如 \\\\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

核心规则

  1. local-part(用户名)
  • 允许字符
    大小写字母(a-z, A-Z)、数字(0-9)、特殊符号(! # $ % & \' * + - / = ? ^ _ { | } ~`)、点号(`.`)。
    • 限制:

    • 点号(.)不能作为开头或结尾,也不能连续出现(如 ..)。

      • 特殊符号需用引号包裹(如 \"john+doe\"@example.com),但实际中很少使用。
    • 长度:理论上无限制,但部分邮件服务器限制为 64 个字符。

  1. @ 符号
  • 必须存在且只能出现一次,用于分隔用户名和域名。
  1. 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); }}

  1. 0-9 ↩︎

  2. A-Z ↩︎