Java 正则表达式

正则表达式在线测试

正则表达式是一串字符,描述了一个文本模式,可以方便地处理文本,包括查找、替换、切分等。

正则表达式中的字符有两类:一类是普通字符,匹配字符本身;另一类是元字符,有特殊含义,元字符及其特殊含义构成了正则表达式的语法。

单个字符

大部分的单个字符是用字符本身表示的,比如字符 '0'、'3'、'a'、'马'等,但有一些单个字符使用多个字符表示,这些字符都以斜杠 \ 开头。

  • 特殊字符

比如 tab 字符 '\t'、换行符 '\n'、回车符 '\r' 等。

  • 八进制表示的字符

\0 开头,后跟1~3位数字,比如 \0141,对应的是 ASCII 编码为97的字符,即字符 'a'。

  • 十六进制表示的字符

\x 开头,后跟两位字符,比如 \x6A,对应的是 ASCII 编码为 106 的字符,即字符 'j'。

  • Unicode 编号表示的字符

\u 开头,后跟 4 位字符,比如 \u9A6C,表示的是中文字符'马',只能表示编号在 0xFFFF 以下的字符,如果超出 0xFFFF,使用 \x{...} 形式,比如 \x{1f48e}。5

  • 元字符

如 '\'、'.'、'?' 等,要匹配元字符本身,需要使用转义字符。

字符组

字符组有多种,包括任意字符、多个指定字符之一、字符区间等。

image.png

任意字符

'.' 默认匹配换行符以外的任意字符。比如:正则表达式 a.f 既匹配字符串 "abf",也匹配 "acf"。

可以指定另外一种匹配模式,一般称为单行匹配模式或者点号匹配模式,此模式下,'.' 匹配任意字符,包括换行符。

有两种方式指定匹配模式:

一种是在正则表达式中,以 (? s)开头,s 表示 single line,即单行匹配模式,比如:(? s)a.f

一种是在程序中指定,在 Java 中,单行匹配模式对应的模式常量是 Pattern.DOTALL 。

字符区间

用中括号 [] 表示组,匹配组中的任意一个字符。比如:[abcd],匹配 a、b、c、d 中的任意一个字符;

字符组中可以使用连字符 '-' 表示连续的多个字符,比如:[0-9]、[a-z];

可以有多个连续空间,可以有其他普通字符,比如:[0-9a-zA-Z];

'-' 是一个元字符,如果要匹配它自身,可以使用转义,即 '-',或者把它放在字符组的最前面

字符组支持排除的概念,在 [ 后紧跟一个字符 ^,比如:[^abcd],匹配除了a, b, c, d以外的任意一个字符;

^ 只有在字符组的开头才是元字符,如果不在开头,就是普通字符,匹配它自身;

在字符组中,除了 [ ]、\ 、- 、^ 外,其他在字符组外的元字符不再具备特殊含义。

有一些特殊的以 \ 开头的字符,表示一些预定义的字符组:

❑ \d: d 表示 digit,匹配一个数字字符,等同于 [0-9]。

❑ \w: w 表示 word,匹配一个单词字符,等同于 [a-zA-Z_0-9]。

❑ \s: s 表示 space,匹配一个空白字符,等同于 [ \t\n\x0B\f\r]。

❑ \D:匹配一个非数字字符,即 [^\d]

❑ \W:匹配一个非单词字符,即 [^\w]

❑ \S:匹配一个非空白字符,即 [^\s]

量词

量词指的是指定出现次数的元字符,有三个常见的元字符:+、*、? 。

image.png

表示前面字符的一次或多次出现,比如 ab+c,既能匹配 abc,也能匹配 abbc,或 abbbc。

表示前面字符的零次或多次出现,比如 ab*c,既能匹配 abc,也能匹配 ac,或 abbbc。

  • ?

表示前面字符可能出现,也可能不出现,比如 ab? c,既能匹配 abc,也能匹配 ac,但不能匹配 abbc。

  • {m,n}

更通用的语法是 {m,n},出现次数从m 到 n,包括 m 和 n,如果 n 没有限制,可以省略,如果 m 和 n 一样,可以写为 {m}。

语法必须是严格的 {m,n} 形式,逗号左右不能有空格。

量词的默认匹配是贪婪的,如果希望在碰到第一个匹配时就停止,应该使用懒惰量词,在量词的后面加一个符号 '? '。

分组

表达式可以用括号 () 括起来,表示一个分组,比如 a(bc)d, bc 就是一个分组,分组可以嵌套,比如 a(de(fg))。

分组默认都有一个编号,按照括号的出现顺序,从 1 开始,从左到右依次递增。分组 0 是一个特殊分组,内容是整个匹配的字符串。

a(bc)((de)(fg))

字符串 abcdefg 匹配这个表达式,第 1 个分组为 bc,第 2 个为 defg,第 3 个为 de,第 4 个为 fg,分组 0 是 abcdefg 。

分组匹配的子字符串可以在后续访问,好像被捕获了一样,所以默认分组称为捕获分组。

小括号 () 和元字符 '|' 一起,可以表示匹配其中的一个子表达式,如:(http|ftp|file)。

可以使用斜杠 \ 加分组编号引用之前匹配的分组,这称为回溯引用。比如:<(\w+)>(.*)</\1>,\1匹配之前的第一个分组 (\w+)。

使用数字引用分组,容易出现混乱,可以对分组进行命名,通过名字引用,对分组命名的语法是 (?<name>X),引用分组的语法是 \k<name>

比如:<(? <tag>\w+)>(.*)</\k<tag>>

默认分组都称为捕获分组,即分组匹配的内容被捕获了,可以在后续被引用。

实现捕获分组有一定的成本,为了提高性能,如果分组后续不需要被引用,可以改为非捕获分组,语法是 (? :...),比如:(? :abc|def)

特殊边界匹配

边界匹配不同于字符匹配,可以认为,在字符串中,每个字符的两边都是边界。比如:"a cat\n"。

在正则表达式中,除了可以指定字符需满足什么条件,还可以指定字符的边界,常用的表示特殊边界的元字符有 ^、$、\A、\Z、\z 和 \b。

  • ^

匹配整个字符串的开始,^abc 表示整个字符串必须以 abc 开始。注意在字符组中 ^ 表示排除,但在字符组外,它匹配开始。

  • $

默认匹配整个字符串的结束,如果整个字符串以换行符结束,匹配的是换行符之前的边界,比如表达式 abc$,表示整个表达式以 abc 结束,或者以 abc\r\n 或 abc\n 结束。

以上 ^ 和 的含义是默认模式下的,可以指定另外一种匹配模式:多行匹配模式,在此模式下,会以行为单位进行匹配,^ 匹配的是行开始,\ 匹配的是行结束,比如表达式是 ^abc$,字符串是 "abc\nabc\r\n",就会有两个匹配。

可以有两种方式指定匹配模式。一种是在正则表达式中,以(? m)开头,m表示multi-line,即多行匹配模式。另外一种是在程序中指定,在Java中,对应的模式常量是 Pattern.MULTILINE。

单行模式影响的是字符 '.' 的匹配规则,使得 '.' 可以匹配换行符;多行模式影响的是 ^ 和 $ 的匹配规则,使得可以匹配行的开始和结束,两个模式可以一起使用。

  • 开始边界

\A 与 ^ 类似,不管什么模式,匹配的总是整个字符串的开始边界。

  • 结束边界

\Z 和 \z 与 $ 一样,匹配的是换行符之前的边界,而 \z 匹配的总是结束边界。

  • 单词边界

\b 匹配的是单词边界,比如 \bcat\b,匹配的是完整的单词c at,不能匹配 category。\b 匹配的不是一个具体的字符,而是一种边界,这种边界满足一个要求,一边是单词字符,另一边不是单词字符。在Java中,\b 识别的单词字符除了 \w,还包括中文字符。

image.png

环视边界匹配

环视匹配的是一个边界,表达式是对这个边界左边或右边字符串的要求,对同一个边界,可以指定多个要求,即写多个环视。

  • 肯定顺序环视

语法是(? =...),要求右边的字符串匹配指定的表达式。比如表达式 abc(? =def),(? =def) 在字符 c 右面,即匹配 c 右边的边界。对该边界的要求是:它的右边有def,比如 abcdef,如果没有则不匹配,比如 abcd。

  • 否定顺序环视

语法是(? ! ...),要求右边的字符串不能匹配指定的表达式。比如表达式 s(? ! ing),匹配一般的 s,但不匹配后面有 ing 的 s。注意:避免与排除型字符组混淆,比如 s[^ing],匹配的是两个字符,第一个是 s,第二个是 i、n、g 以外的任意一个字符。

  • 肯定逆序环视

语法是(? <=...),要求左边的字符串匹配指定的表达式。比如表达式 (? <=\s)abc,(? <=\s) 在字符 a 左边,即匹配 a 左边的边界。对该边界的要求是:它的左边必须是空白字符。

  • 否定逆序环视

语法是(? <! ...),要求左边的字符串不能匹配指定的表达式。比如表达式 (? <! \w)cat,(? <! \w) 在字符 c 左边,即匹配 c 左边的边界。对该边界的要求是:它的左边不能是单词字符。

环视结构也被称为断言,断言的对象是边界,边界不占用字符,没有宽度,所以也被称为零宽度断言。

顺序环视也可以出现在左边,逆序环视也可以出现在右边。

比如:(? =.*[A-Z])\w+ ,\w+ 匹配多个单词字符,(? =.*[A-Z]) 匹配单词字符的左边界,这是一个肯定顺序环视,对这个左边界的要求是,它右边的字符串匹配表达式 .*[A-Z],就是说,它右边至少要有一个大写字母。

匹配模式

image.png

在正则表达式中,可以指定多个模式。

  • 单行匹配模式

又称点号匹配模式,此模式下,'.' 匹配任意字符,包括换行符。有两种方式指定匹配模式:

一种是在正则表达式中,以 (? s)开头,s 表示 single line,即单行匹配模式,比如:(? s)a.f

一种是在程序中指定,Java 中对应的模式常量是 Pattern.DOTALL 。

  • 多行匹配模式

在此模式下,会以行为单位进行匹配,^ 匹配的是行开始,$ 匹配的是行结束,比如表达式是 ^abc$,字符串是 "abc\nabc\r\n",就会有两个匹配。

有两种方式指定匹配模式:

一种是在正则表达式中,以(? m)开头,m表示multi-line,即多行匹配模式;

一种是在程序中指定,Java 中对应的模式常量是 Pattern.MULTILINE。

  • 不区分大小写的模式

一种是在正则表达式开头使用 (? i), i 为 ignore,比如:(? i)the

一种在程序中指定,Java 中对应的模式常量是 Pattern.CASE_INSENSITIVE。

Java API

正则表达式相关的类位于包 java.util.regex 下,有两个主要的类,一个是 Pattern,另一个是 Matcher。

Pattern

Pattern 表示正则表达式对象,它与要处理的具体字符串无关。

  1. 表示正则表达式

正则表达式由元字符和普通字符组成,在正则表达式中,字符 '\' 是一个元字符,要表示 '\' 本身,需要使用它转义,即 '\\'。

在 Java 中,需要用字符串表示正则表达式,而在字符串中,''也是一个元字符,要表示 '\' 本身,需要使用它转义,即 '\\'。

为了在字符串中表示正则表达式的 '\' 本身,就需要使用四个 '\' 。字符串表示的正则表达式可以被编译为一个 Pattern 对象。

String regex = "<(\\w+)>(.*)</\\1>";
Pattern pattern = Pattern.compile(regex);

编译有一定的成本,Pattern 对象只与正则表达式有关,与要处理的具体文本无关,可以安全地被多线程共享,应该尽量重用 Pattern 对象,避免重复编译。

Pattern 的 compile 方法接受一个额外参数,可以指定匹配模式,多个模式可以一起使用,通过 '|' 连起来即可。

  • Pattern.DOTALL 单行模式(点号模式)

  • Pattern.MULTILINE 多行模式

  • Pattern.CASE_INSENSI-TIVE 大小写无关模式

  • Pattern.LITERAL 正则表达式的元字符将失去特殊含义,被看作普通字符,和 Pattern 的 静态 quote() 方法作用一样。

  1. 切分

String 的 split 将参数 regex 看作正则表达式,而不是普通的字符串,如果分隔符包含元字符,比如 .$ | ( ) [ { ^ ? * + \,就需要转义。

如果分隔符是用户指定的,程序事先不知道,可以通过 Pattern.quote() 将其看作普通字符串。

分隔符就不一定是一个字符,比如,可以将一个或多个空白字符或点号作为分隔符。

String str = " abc def  hello.\n    world";
String[] fields = str.split("[\\s.]+");
System.out.println(Arrays.toString(fields));

需要说明的是,尾部的空白字符串不会包含在返回的结果数组中,但头部和中间的空白字符串会被包含在内。

String str = ", abc, , def, , ";
String[] fields = str.split(", ");
System.out.println("field num: "+fields.length); // field num: 4
System.out.println(Arrays.toString(fields)); // [, abc, , def]

如果字符串中找不到匹配 regex 的分隔符,返回数组长度为1,元素为原字符串。

Pattern 也有 split 方法,与 String 方法的定义类似:

public String[] split(CharSequence input)

1)Pattern接受的参数是CharSequence,更为通用,String、StringBuilder、StringBuffer、CharBuffer 等都实现了该接口。

2)如果 regex 长度大于 1 或包含元字符,String 的 split 方法必须先将 regex 编译为 Pattern 对象,再调用 Pattern 的 split 方法,为避免重复编译,应该优先采用Pattern 的方法。

3)如果 regex 就是一个字符且不是元字符,String 的 split 方法会采用更为简单高效的实现,这时应该优先采用 String 的 split 方法。

  1. 验证

检验输入文本是否完整匹配预定义的正则表达式,经常用于检验用户的输入是否合法。

String 的 matches:

public boolean matches(String regex) {
  return Pattern.matches(regex, this);
}

实际调用的是 Pattern 的 matches :

public static boolean matches(String regex, CharSequence input) {
  Pattern p = Pattern.compile(regex);
  Matcher m = p.matcher(input);
  return m.matches();
}

Matcher

Matcher 表示一个匹配,它将正则表达式应用于具体字符串,通过它对字符串进行处理。

  1. 查找
public static void find(){
  String regex = "\\d{4}-\\d{2}-\\d{2}";
  Pattern pattern = Pattern.compile(regex);
  
  String str = "today is 2017-06-02, yesterday is 2017-06-01";
  Matcher matcher = pattern.matcher(str);
  
  while(matcher.find()){
    System.out.println("find " + matcher.group() 
                       +" position: " + matcher.start() + "-" + matcher.end());
  }
}

Matcher 的内部记录有一个位置,起始为 0,find 方法从这个位置查找匹配正则表达式的子字符串,找到后,返回 true,并更新这个内部位置。

匹配到的子字符串信息可以通过如下方法获取:

// 匹配到的完整子字符串,group() 其实调用的是 group(0),表示获取匹配的第0个分组的内容,分组0是一个特殊分组,表示匹配的整个子字符串
public String group()
// 子字符串的起始位置
public int start()
// 子字符串的结束位置加 1
public int end()
  
// 分组编号为 group 的内容
public String group(int group)
// 分组编号为 group 的起始位置
public int start(int group)
// 分组编号为 group 的结束位置加 1
public int end(int group)

// group(0) 不统计
public int groupCount()
  
// 分组命名为 name 的内容
public String group(String name)
public static void findGroup() {
  String regex = "(\\d{4})-(\\d{2})-(\\d{2})";
  Pattern pattern = Pattern.compile(regex);
  String str = "today is 2017-06-02, yesterday is 2017-06-01";
  Matcher matcher = pattern.matcher(str);
  while (matcher.find()) {
    System.out.println("year:" + matcher.group(1)
                       + ", month:" + matcher.group(2) + ", day:" + matcher.group(3));
  }
}
  1. 替换

String的 replaceAll 和 replaceFirst 调用的其实是 Pattern 和 Matcher 中的方法。

public String replaceAll(String regex, String replacement) {
  return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}

为避免元字符的干扰,可以使用 Matcher 的 quoteReplacement 静态方法将其视为普通字符串。

public static String quoteReplacement(String s)

除了一次性的替换操作外,Matcher 还定义了边查找、边替换的方法。

public Matcher appendReplacement(StringBuffer sb, String replacement)
  
public StringBuffer appendTail(StringBuffer sb)
Pattern pattern = Pattern.compile("cat");
Matcher matcher = pattern.matcher("one cat,two cat,three cat");
StringBuffer sb = new StringBuffer(); // sb 存放最终的替换结果
int foundNum = 0;

while(matcher.find()){
  matcher.appendReplacement(sb,"dog");
  foundNum++;
  if(foundNum == 2) break;
}

matcher.appendTail(sb);
System.out.println(sb.toString());

Matcher 内部除了查找位置,还有一个 append 位置,初始为0,当找到一个匹配的子字符串后,appendReplacement() 做了三件事情:

1)将 append 位置到当前匹配之前的子字符串 append 到 sb 中,在第一次操作中,为 "one ",第二次为 ", two "(注意空格)。

2)将替换字符串 append 到 sb 中。

3)更新 append 位置为当前匹配之后的位置。

appendTail 将 append 位置之后所有的字符 append 到 sb 中。

模板引擎

模板是一个字符串,中间有一些变量,以 {name} 表示。

String template = "Hi {name}, your code is {code}.";

上述模板字符串中有两个变量:一个是 name,另一个是 code。

变量的实际值通过 Map 提供,变量名称对应 Map 中的键,模板引擎的任务就是接受模板和 Map 作为参数,返回替换变量后的字符串。

public class PatternTemplate {

    private static Pattern templatePattern = Pattern.compile("\\{(\\w+)\\}");

    public static String templateEngine(String template, Map<String,Object> params) {
        StringBuffer sb = new StringBuffer();
        // 寻找所有的模板变量,正则表达式为 \{(\w+)\}
        Matcher matcher = templatePattern.matcher(template);
        while(matcher.find()){
            String key = matcher.group(1);
            Object value = params.get(key);
            matcher.appendReplacement(sb,value != null ? Matcher.quoteReplacement(value.toString()) : "");
        }
        matcher.appendTail(sb);
        return sb.toString();
    }


    public static void main(String[] args) {
        String template = "Hi {name},your code is {code}";
      
        Map<String,Object> params = new HashMap<>();
        params.put("name","JOJO");
        params.put("code","6789");
      
        System.out.println(templateEngine(template, params));
    }

}

参考:《Java 编程的逻辑》马俊昌

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • 正则表达 参考正则表达式的一个参考2 限定符 常用这个代替 x*,零次或多次 ≡{0,} {n,m}表示前面的...
    xmlovecm阅读 1,151评论 0 0
  • 参考文章:正则表达式30分钟入门教程 语法:正则表达式语法手册 正则表达式在Java和其他语法中的区别 在其他语言...
    小石头呢阅读 2,305评论 0 1
  • 正则表达式定义了字符串的模式。 正则表达式可以用来搜索、编辑或处理文本。 正则表达式并不仅限于某一种语言,但是在每...
    代码墨白阅读 141评论 0 0
  • 元字符 代码说明.匹配除换行符以外的任意字符\w匹配字母或数字或下划线或汉字\s匹配任意的空白符\d匹配数字^匹配...
    Mr_Fly阅读 609评论 0 0
  • 本文总结了有关Java 正则表达式主要问题。由于他们经常被问到,你可能发现他们很有用。 1.如何从字符串中提取数字...
    明翼阅读 725评论 2 3