简介
我们一般使用正则表达式是用来处理字符串的,不管是实际的开发中还是我们的算法竞赛中,使用正则表达式绝对可以大大提升我们的效率。
正则表达式(regular expression)其实就是对字符串进行模式匹配的技术。
快速入门
我们这里演示一个案例,使用正则表达式匹配下面字符串中的所有英文单词:
import java.util.regex.Matcher; import java.util.regex.Pattern; public class RegexDemo01 { public static void main(String[] args) { String content = "1995年,互联网的蓬勃发展给了Oak机会。业界为了使死板、单调的" + "静态网页能够“灵活”起来,急需一种软件技术来开发一种程序,这种程序可以通过" + "网络传播并且能够跨平台运行。于是,世界各大IT企业为此纷纷投入了大量的人力" + "、物力和财力。这个时候,Sun公司想起了那个被搁置起来很久的Oak,并且重新审" + "视了那个用软件编写的试验平台,由于它是按照嵌入式系统硬件平台体系结构进行编" + "写的,所以非常小,特别适用于网络上的传输系统,而Oak也是一种精简的语言,程" + "序非常小,适合在网络上传输。Sun公司首先推出了可以嵌入网页并且可以随同网页" + "在网络上传输的Applet(Applet是一种将小程序嵌入到网页中进行执行的技术)" + ",并将Oak更名为Java。5月23日,Sun公司在Sun world会议上正式发布Java和" + "HotJava浏览器。IBM、Apple、DEC、Adobe、HP、Oracle、Netscape和微软" + "等各大公司都纷纷停止了自己的相关开发项目,竞相购买了Java使用许可证,并为自" + "己的产品开发了相应的Java平台。"; // 提取文章中所有英文单词 Pattern pattern = Pattern.compile("[a-zA-Z]+"); Matcher matcher = pattern.matcher(content); while (matcher.find()){ // 匹配到的内容都会放到 matcher.group(0)里面 System.out.println(matcher.group(0)); } } }
运行结果:
Oak IT Sun Oak Oak Sun Applet Applet Oak Java Sun Sun world Java HotJava IBM Apple DEC Adobe HP Oracle Netscape Java Java
同理,如果我们想要获取字符串中所有的数字、获取字符串中所有字符串或者数字,只需要这样修改:
Pattern pattern = Pattern.compile("[0-9]+"); Pattern pattern = Pattern.compile("([0-9]+)|([a-zA-Z]+)");
要知道,我们自己实现的话会很复杂,我自己尝试过,比如实现提取所有英文字符,我们需要这样来设计:
- 把字符串转为 char 数组
- 遍历数组,判断字符的 ASCII码是否在 [a,z] , [A,Z]范围内
- 判断英文字符前的字符是否为非英文字符,如果是在字符前添加空格(防止结果连成一片)
- 判断英文字符后的字符是否为非英文字符,如果是在字符后添加空格(防止结果连成一片)
可以看到,我们自己编写程序去实现确实是十分复杂,所以为什么不学习正则表达式呢,取英文字符是比较简单的案例,如果遇到验证邮箱、手机号、身份证号、ip地址、提取字符串等需要各种字符串处理算法的时候,手写算法是十分烧脑的,最好的办法就是找到规律使用正则表达式,提高开发效率!
除此之外,我们学习的网络爬虫在做数据处理的时候,比如各种新闻标题、产品标题、商品评论,这些文本通常都是在超链接或者一些特殊标签内部,这时候我们直接使用正则表达式就可以很轻松地实现标签内文本的提取,这样,我们只需要专心爬虫的代码,而不需要过于担心爬到数据后的数据处理问题了。
底层实现
我们这里主要讨论一下 Java正则表达式中,matcher.find() 和 matcher.group(int group) 的底层是怎么实现的。
案例
找出四个数字连在一起的子串
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平台的企业版),应用3443于基于Java的应用服务器。Java 2平台的发布 ,是Java发展过程中最重要的一个里程碑,标志着Java的应用开始普及9889
代码:
// 1. 找出四个数字连在一起的子串 // \\d 代表数字 String regex = "\\d\\d\\d\\d"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(content); while (matcher.find()){ System.out.println(matcher.group()); // 无参时默认就是 group(0) }
输出结果:
1998 1999 3443 9889
我们开始分析代码以及通过debug分析源码:
matcher.find() & matcher.group() 原理
matcher.group(0)
- 首先,根据我们给它的规则去匹配,定位到满足要求的子字符串位置(比如1998)
- 然后,将子字符串的开始的索引记录到 matcher 对象的属性中去(int[] groups)。
- groups[0] = 0 , 把该子字符串结束的索引+1的值记录到 groups[1]中去,groups[1] = 4。
我们Matcher类的属性 int[] groups 的初始大小为 20 ,初始值均为 -1.
我们Matcher类的属性 int[] groups 的初始大小为 20 ,初始值均为 -1.
- 同时记录属性oldLast 的值为 该子字符串结束的索引+1的值, 即 4,即下次执行matcher.find() 的时候从 4 开始。
- 接下来我们分析 matcher.group(0) 的源码:
public String group(int group) { if (first < 0) throw new IllegalStateException("No match found"); if (group < 0 || group > groupCount()) throw new IndexOutOfBoundsException("No group " + group); if ((groups[group*2] == -1) || (groups[group*2+1] == -1)) return null; return getSubSequence(groups[group * 2], groups[group * 2 + 1]).toString(); }
此时我们调用 mactcher.group(0) 的话,很明显会返回 getSubSequence(groups[0],groups[1]).toString(); 相当于根据 groups[0]=0 和 groups[1]=4 记录的位置来截取字符串 ,注意是左闭右开的 [0,4)。 此时,输出 1998.
继续下一次 matcher.find() 方法,这次定位到了 1999 这个位置:
- 此时,groups[0] = 31, groups[1] = 35 ,oldLast = 35 。
- 同样再次调用 matcher.group(0) , 输出 1999
到这里,我们基本了解了 group(0) 的含义,每次调用matcher.group(0) 它都会去查找字符串中的子串首尾下标,这些下标存在 groups数组中 ,而且首下标永远都是 groups[0] ,尾下标永远都是 groups[1] 。
但是如果我们调用的是 group(1)、或者group(n) 又会是怎样的情况呢?
matcher.group(n)
其实,group(n) 涉及到的是一个分组的概念,体现在代码中的匹配语句上就是括号,我们对上面的匹配语句做一个修改:
String regex = "(\\d\\d)(\\d\\d)"; Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(content); while (matcher.find()){ System.out.println(matcher.group()); // 无参时默认就是 group(0) System.out.println(matcher.group(1)); System.out.println(matcher.group(2)); }
输出结果:
group(0) = 1998 group(1) = 19 group(2) = 98 group(0) = 1999 group(1) = 19 group(2) = 99 group(0) = 3443 group(1) = 34 group(2) = 43 group(0) = 9889 group(1) = 98 group(2) = 89
此时,我们再从 matcher.find() 开始分析:
- 首先,根据我们给它的规则去匹配,定位到满足要求的子字符串位置(比如(19)(98))
- 然后,将子字符串的开始的索引记录到 matcher 对象的属性中去(int[] groups)。
- groups[0] = 0 , 把该子字符串结束的索引+1的值记录到 groups[1]中去,groups[1] = 4。
- 记录1组()匹配到的子串下标到 groups[2] = 0 , groups[3] = 2。
- 记录2组()匹配到的子串下标到 groups[4] = 2 , groups[5] = 4。
- 如果还有更多组,以此类推...
到这里,我们基本了解了分组的实现原理:groups数组负责存储子字符串的下标以及子字符串内每组子串的首尾下标,我们的 getSubSequence(groups[group * 2], groups[group * 2 + 1]) 方法会去根据 matcher.group(int group) 给的参数 group 去查找对应groups数组的首尾下标,从而调用String.substring(start,end) 截取出每组对应的子串。
不得不说,getSubSequence(groups[group * 2], groups[group * 2 + 1]),这个参数的设置确实十分巧妙!
总结
如果正则表达式中有() 即分组
取出匹配的字符串规则如下:
- group(0) 代表匹配到的子字符串,不分组
- group(1) 代表匹配到的子字符串第1组
- group(2) 代表匹配到的子字符串第2组
- ...
- 但是参数不能越界,不能超过分组数