专栏名称: 老马说编程
从入门到高级, 深入浅出, 老马和你一起探索编程及计算机技术的本质, 篇篇原创, 用心写作。
目录
相关文章推荐
程序员的那些事  ·  董事长十几刀刺死 ... ·  昨天  
程序员的那些事  ·  西部数据突然宣布:退出 SSD 市场! ·  3 天前  
码农翻身  ·  再这么搞下去,程序员失业是迟早的事! ·  昨天  
OSC开源社区  ·  深度实测Manus,我依然认为这就是AI ... ·  4 天前  
51好读  ›  专栏  ›  老马说编程

(89) 正则表达式 (中) / 计算机程序的思维逻辑

老马说编程  · 公众号  · 程序员  · 2017-06-05 08:39

正文

上节 介绍了正则表达式的语法,本节介绍相关的Java API。


正则表达式相关的类位于包java.util.regex下,有两个主要的类,一个是Pattern,另一个是Matcher。Pattern表示正则表达式对象,它与要处理的具体字符串无关。Matcher表示一个匹配,它将正则表达式应用于一个具体字符串,通过它对字符串进行处理。


字符串类String也是一个重要的类,我们在 29节 专门介绍过String,其中提到,它有一些方法,接受的参数不是普通的字符串,而是正则表达式。此外,正则表达式在Java中是需要先以字符串形式表示的。


下面,我们先来介绍如何表示正则表达式,然后探讨如何利用它实现一些常见的文本处理任务,包括切分、验证、查找、和替换。


表示正则表达式

转义符 '\'

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


在Java中,没有什么特殊的语法能直接表示正则表达式,需要用字符串表示,而在字符串中,'\'也是一个元字符,为了在字符串中表示正则表达式的'\',就需要使用两个'\',即'\\',而要匹配'\'本身,就需要四个'\',即'\\\\',比如说,如下表达式:

(.*)\1>


对应的字符串表示就是:

"(.*)\\1>"


一个简单规则是, 正则表达式中的任何一个'\',在字符串中,需要替换为两个'\'


Pattern对象

字符串表示的正则表达式可以被 编译 为一个Pattern对象,比如:

String regex = "(.*)\\1>";
Pattern pattern = Pattern. compile (regex);


Pattern是正则表达式的面向对象表示,所谓编译,简单理解就是将字符串表示为了一个内部结构,这个结构是一个 有穷自动机 ,关于有穷自动机的理论比较深入,我们就不探讨了。


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


匹配模式

Pattern的compile方法接受一个额外参数,可以指定匹配模式:

public static Pattern compile(String regex, int flags)


上节 ,我们介绍过三种匹配模式:单行模式(点号模式)、多行模式和大小写无关模式,它们对应的常量分别为:Pattern.DOTALL,Pattern.MULTILINE和Pattern.CASE_INSENSITIVE,多个模式可以一起使用,通过'|'连起来即可,如下所示:

Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.DOTALL)


还有一个模式Pattern.LITERAL,在此模式下,正则表达式字符串中的元字符将失去特殊含义,被看做普通字符。Pattern有一个静态方法:

public static String quote(String s)


quote()的目的是类似的,它将s中的字符都看作普通字符。我们在 上节 介绍过\Q和\E,\Q和\E之间的字符会被视为普通字符。quote()基本上就是在字符串s的前后加了\Q和\E,比如,如果s为"\\d{6}",则quote()的返回值就是"\\Q\\d{6}\\E"。


切分

简单情况

文本处理的一个常见需求是根据分隔符切分字符串,比如在处理CSV文件时,按逗号分隔每个字段,这个需求听上去很容易满足,因为String类有如下方法:

public String[] split(String regex)


比如:

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


输出为:

field num: 3
[abc, def,  hello]


不过,有一些重要的细节,我们需要注意。


转义元字符

split将参数regex看做正则表达式,而不是普通的字符,如果分隔符是元字符,比如. $ | ( ) [ { ^ ? * + \,就需要转义,比如按点号'.'分隔,就需要写为:

String[] fields = str.split("\\.");


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


将多个字符用作分隔符

既然是正则表达式,分隔符就不一定是一个字符,比如,可以将一个或多个空白字符或点号作为分隔符,如下所示:

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


fields内容为:

[abc, def, hello, world]


空白字符串

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

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


输出为:

field num: 4
[, abc, , def]


找不到分隔符

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


切分数目限制

split方法接受一个额外的参数limit,用于限定切分的数目:

public String[] split(String regex, int limit)


不带limit参数的split,其limit相当于0。关于limit的含义,我们通过一个例子说明下,比如字符串是"a:b:c:",分隔符是":",在limit为不同值的情况下,其返回数组如下表所示:

Pattern的split方法

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

public String[] split(CharSequence input)

public String[] split(CharSequence input, int limit)


与String方法的区别是:

  • Pattern接受的参数是CharSequence,更为通用,我们知道String, StringBuilder, StringBuffer, CharBuffer等都实现了该接口;

  • 如果regex长度大于1或包含元字符,String的split方法会先将regex编译为Pattern对象,再调用Pattern的split方法,这时,为避免重复编译,应该优先采用Pattern的方法;

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


验证

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


String有如下方法:

public boolean matches(String regex)


比如:

String regex = "\\d{8}";
String str = "12345678";
System.out.println(str.matches(regex));


检查输入是否是8位数字,输出为true。


String的matches实际调用的是Pattern的如下方法:

public static boolean matches(String regex, CharSequence input)


这是一个静态方法,它的代码为:

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


就是先调用compile编译regex为Pattern对象,再调用Pattern的matcher方法生成一个匹配对象Matcher,Matcher的matches()返回是否完整匹配。


查找

查找就是在文本中寻找匹配正则表达式的子字符串,看个例子:

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());
}
}


代码寻找所有类似"2017-06-02"这种格式的日期,输出为:

find 2017-06-02 position: 9-19
find 2017-06-01 position: 34-44


Matcher的内部记录有一个位置,起始为0,find()方法从这个位置查找匹配正则表达式的子字符串,找到后,返回true,并更新这个内部位置,匹配到的子字符串信息可以通过如下方法获取:

//匹配到的完整子字符串

public String group()

//子字符串在整个字符串中的起始位置

public int start()

//子字符串在整个字符串中的结束位置加1

public int end()


group()其实调用的是group(0),表示获取匹配的第0个分组的内容。我们在 上节 介绍过捕获分组的概念,分组0是一个特殊分组,表示匹配的整个子字符串。除了分组0,Matcher还有如下方法,获取分组的更多信息:

//分组个数

public int groupCount()

//分组编号为group的内容

public String group(int group)
//分组命名为name的内容

public String group(String name)

//分组编号为group的起始位置

public int start(int group)

//分组编号为group的结束位置加1

public int end(int group)


比如:

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));
}
}


输出为:

year:2017,month:06,day:02
year:2017,month:06,day:01


替换

replaceAll和replaceFirst

查找到子字符串后,一个常见的后续操作是替换。String有多个替换方法:

public String replace(char oldChar, char newChar)

public String replace(CharSequence target, CharSequence replacement)

public String replaceAll(String regex, String replacement)

public String replaceFirst(String regex, String replacement)


第一个replace方法操作的是单个字符,第二个是CharSequence,它们都是将参数看做普通字符。而replaceAll和replaceFirst则将参数regex看做正则表达式,它们的区别是,replaceAll替换所有找到的子字符串,而replaceFirst则只替换第一个找到的,看个简单的例子,将字符串中的多个连续空白字符替换为一个:

String regex = "\\s+";
String str = "hello    world       good";
System.out.println(str.replaceAll(regex, " "));


输出为:

hello world good


在replaceAll和replaceFirst中,参数replacement也不是被看做普通的字符串,可以使用美元符号加数字的形式,比如$1,引用捕获分组,我们看个例子:

String regex = "(\\d{4})-(\\d{2})-(\\d{2})";
String str = "today is 2017-06-02.";
System.out.println(str.replaceFirst(regex, "$1/$2/$3"));


输出为:

today is 2017/06/02.


这个例子将找到的日期字符串的格式进行了转换。所以,字符'$'在replacement中是元字符,如果需要替换为字符'$'本身,需要使用转义,看个例子:

String regex = "#";
String str = "#this is a test";
System.out.println(str.replaceAll(regex, "\\$"







请到「今天看啥」查看全文