上節(jié)介紹了正則表達(dá)式的語法,本節(jié)介紹相關(guān)的Java API。
正則表達(dá)式相關(guān)的類位于包java.util.regex下,有兩個(gè)主要的類,一個(gè)是Pattern,另一個(gè)是Matcher。Pattern表示正則表達(dá)式對(duì)象,它與要處理的具體字符串無關(guān)。Matcher表示一個(gè)匹配,它將正則表達(dá)式應(yīng)用于一個(gè)具體字符串,通過它對(duì)字符串進(jìn)行處理。
字符串類String也是一個(gè)重要的類,我們?cè)?9節(jié)專門介紹過String,其中提到,它有一些方法,接受的參數(shù)不是普通的字符串,而是正則表達(dá)式。此外,正則表達(dá)式在Java中是需要先以字符串形式表示的。
下面,我們先來介紹如何表示正則表達(dá)式,然后探討如何利用它實(shí)現(xiàn)一些常見的文本處理任務(wù),包括切分、驗(yàn)證、查找、和替換。
表示正則表達(dá)式
轉(zhuǎn)義符 ''\''
正則表達(dá)式由元字符和普通字符組成,字符''\''是一個(gè)元字符,要在正則表達(dá)式中表示''\''本身,需要使用它轉(zhuǎn)義,即''\\''。
在Java中,沒有什么特殊的語法能直接表示正則表達(dá)式,需要用字符串表示,而在字符串中,''\''也是一個(gè)元字符,為了在字符串中表示正則表達(dá)式的''\'',就需要使用兩個(gè)''\'',即''\\'',而要匹配''\''本身,就需要四個(gè)''\'',即''\\\\'',比如說,如下表達(dá)式:
<(\w+)>(.*)
對(duì)應(yīng)的字符串表示就是:
''<(\\w+)>(.*)''
一個(gè)簡(jiǎn)單規(guī)則是,正則表達(dá)式中的任何一個(gè)''\'',在字符串中,需要替換為兩個(gè)''\''。
Pattern對(duì)象
字符串表示的正則表達(dá)式可以被編譯為一個(gè)Pattern對(duì)象,比如:
String regex = ''<(\\w+)>(.*)'';
Pattern pattern = Pattern.compile(regex);
Pattern是正則表達(dá)式的面向?qū)ο蟊硎荆^編譯,簡(jiǎn)單理解就是將字符串表示為了一個(gè)內(nèi)部結(jié)構(gòu),這個(gè)結(jié)構(gòu)是一個(gè)有窮自動(dòng)機(jī),關(guān)于有窮自動(dòng)機(jī)的理論比較深入,我們就不探討了。
編譯有一定的成本,而且Pattern對(duì)象只與正則表達(dá)式有關(guān),與要處理的具體文本無關(guān),它可以安全地被多線程共享,所以,在使用同一個(gè)正則表達(dá)式處理多個(gè)文本時(shí),應(yīng)該盡量重用同一個(gè)Pattern對(duì)象,避免重復(fù)編譯。
匹配模式
Pattern的compile方法接受一個(gè)額外參數(shù),可以指定匹配模式:
public static Pattern compile(String regex, int flags)
上節(jié),我們介紹過三種匹配模式:?jiǎn)涡心J?點(diǎn)號(hào)模式)、多行模式和大小寫無關(guān)模式,它們對(duì)應(yīng)的常量分別為:Pattern.DOTALL,Pattern.MULTILINE和Pattern.CASE_INSENSITIVE,多個(gè)模式可以一起使用,通過''|''連起來即可,如下所示:
Pattern.compile(regex, Pattern.CASE_INSENSITIVE | Pattern.DOTALL)
還有一個(gè)模式Pattern.LITERAL,在此模式下,正則表達(dá)式字符串中的元字符將失去特殊含義,被看做普通字符。Pattern有一個(gè)靜態(tài)方法:
public static String quote(String s)
quote()的目的是類似的,它將s中的字符都看作普通字符。我們?cè)谏瞎?jié)介紹過\Q和\E,\Q和\E之間的字符會(huì)被視為普通字符。quote()基本上就是在字符串s的前后加了\Q和\E,比如,如果s為''\\d{6}'',則quote()的返回值就是''\\Q\\d{6}\\E''。
切分
簡(jiǎn)單情況
文本處理的一個(gè)常見需求是根據(jù)分隔符切分字符串,比如在處理CSV文件時(shí),按逗號(hào)分隔每個(gè)字段,這個(gè)需求聽上去很容易滿足,因?yàn)镾tring類有如下方法:
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]
不過,有一些重要的細(xì)節(jié),我們需要注意。
轉(zhuǎn)義元字符
split將參數(shù)regex看做正則表達(dá)式,而不是普通的字符,如果分隔符是元字符,比如. $ | ( ) [ { ^ ? * + \,就需要轉(zhuǎn)義,比如按點(diǎn)號(hào)''.''分隔,就需要寫為:
String[] fields = str.split(''\\.'');
如果分隔符是用戶指定的,程序事先不知道,可以通過Pattern.quote()將其看做普通字符串。
將多個(gè)字符用作分隔符
既然是正則表達(dá)式,分隔符就不一定是一個(gè)字符,比如,可以將一個(gè)或多個(gè)空白字符或點(diǎn)號(hào)作為分隔符,如下所示:
String str = ''abc def hello.\n world'';
String[] fields = str.split(''[\\s.]+'');
fields內(nèi)容為:
[abc, def, hello, world]
空白字符串
需要說明的是,尾部的空白字符串不會(huì)包含在返回的結(jié)果數(shù)組中,但頭部和中間的空白字符串會(huì)被包含在內(nèi),比如:
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的分隔符,返回?cái)?shù)組長(zhǎng)度為1,元素為原字符串。
切分?jǐn)?shù)目限制
split方法接受一個(gè)額外的參數(shù)limit,用于限定切分的數(shù)目:
public String[] split(String regex, int limit)
不帶limit參數(shù)的split,其limit相當(dāng)于0。關(guān)于limit的含義,我們通過一個(gè)例子說明下,比如字符串是''a:b:c:'',分隔符是'':'',在limit為不同值的情況下,其返回?cái)?shù)組如下表所示:
Pattern的split方法
Pattern也有兩個(gè)split方法,與String方法的定義類似:
public String[] split(CharSequence input)
public String[] split(CharSequence input, int limit)
與String方法的區(qū)別是:
Pattern接受的參數(shù)是CharSequence,更為通用,我們知道String, StringBuilder, StringBuffer, CharBuffer等都實(shí)現(xiàn)了該接口;
如果regex長(zhǎng)度大于1或包含元字符,String的split方法會(huì)先將regex編譯為Pattern對(duì)象,再調(diào)用Pattern的split方法,這時(shí),為避免重復(fù)編譯,應(yīng)該優(yōu)先采用Pattern的方法;
如果regex就是一個(gè)字符且不是元字符,String的split方法會(huì)采用更為簡(jiǎn)單高效的實(shí)現(xiàn),所以,這時(shí),應(yīng)該優(yōu)先采用String的split方法。
驗(yàn)證
驗(yàn)證就是檢驗(yàn)輸入文本是否完整匹配預(yù)定義的正則表達(dá)式,經(jīng)常用于檢驗(yàn)用戶的輸入是否合法。
String有如下方法:
public boolean matches(String regex)
比如:
String regex = ''\\d{8}'';
String str = ''12345678'';
System.out.println(str.matches(regex));
檢查輸入是否是8位數(shù)字,輸出為true。
String的matches實(shí)際調(diào)用的是Pattern的如下方法:
public static boolean matches(String regex, CharSequence input)
這是一個(gè)靜態(tài)方法,它的代碼為:
public static boolean matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}
就是先調(diào)用compile編譯regex為Pattern對(duì)象,再調(diào)用Pattern的matcher方法生成一個(gè)匹配對(duì)象Matcher,Matcher的matches()返回是否完整匹配。
查找
查找就是在文本中尋找匹配正則表達(dá)式的子字符串,看個(gè)例子:
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的內(nèi)部記錄有一個(gè)位置,起始為0,find()方法從這個(gè)位置查找匹配正則表達(dá)式的子字符串,找到后,返回true,并更新這個(gè)內(nèi)部位置,匹配到的子字符串信息可以通過如下方法獲?。?/p>
//匹配到的完整子字符串
public String group()
//子字符串在整個(gè)字符串中的起始位置
public int start()
//子字符串在整個(gè)字符串中的結(jié)束位置加1
public int end()
group()其實(shí)調(diào)用的是group(0),表示獲取匹配的第0個(gè)分組的內(nèi)容。我們?cè)谏瞎?jié)介紹過捕獲分組的概念,分組0是一個(gè)特殊分組,表示匹配的整個(gè)子字符串。除了分組0,Matcher還有如下方法,獲取分組的更多信息:
//分組個(gè)數(shù)
public int groupCount()
//分組編號(hào)為group的內(nèi)容
public String group(int group)
//分組命名為name的內(nèi)容public String group(String name)
//分組編號(hào)為group的起始位置
public int start(int group)
//分組編號(hào)為group的結(jié)束位置加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
查找到子字符串后,一個(gè)常見的后續(xù)操作是替換。String有多個(gè)替換方法:
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)
第一個(gè)replace方法操作的是單個(gè)字符,第二個(gè)是CharSequence,它們都是將參數(shù)看做普通字符。而replaceAll和replaceFirst則將參數(shù)regex看做正則表達(dá)式,它們的區(qū)別是,replaceAll替換所有找到的子字符串,而replaceFirst則只替換第一個(gè)找到的,看個(gè)簡(jiǎn)單的例子,將字符串中的多個(gè)連續(xù)空白字符替換為一個(gè):
String regex = ''\\s+'';
String str = ''hello world good'';
System.out.println(str.replaceAll(regex, '' ''));
輸出為:
hello world good
在replaceAll和replaceFirst中,參數(shù)replacement也不是被看做普通的字符串,可以使用美元符號(hào)加數(shù)字的形式,比如$1,引用捕獲分組,我們看個(gè)例子:
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.
這個(gè)例子將找到的日期字符串的格式進(jìn)行了轉(zhuǎn)換。所以,字符''$''在replacement中是元字符,如果需要替換為字符''$''本身,需要使用轉(zhuǎn)義,看個(gè)例子:
String regex = ''#'';
String str = ''#this is a test'';
System.out.println(str.replaceAll(regex, ''\\$''));
如果替換字符串是用戶提供的,為避免元字符的的干擾,可以使用Matcher的如下靜態(tài)方法將其視為普通字符串:
public static String quoteReplacement(String s)
String的replaceAll和replaceFirst調(diào)用的其實(shí)是Pattern和Matcher中的方法,比如,replaceAll的代碼為:
public String replaceAll(String regex, String replacement) {
return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}
邊查找邊替換
replaceAll和replaceFirst都定義在Matcher中,除了一次性的替換操作外,Matcher還定義了邊查找、邊替換的方法:
public Matcher appendReplacement(StringBuffer sb, String replacement)
public StringBuffer appendTail(StringBuffer sb)
這兩個(gè)方法用于和find()一起使用,我們先看個(gè)例子:
public static void replaceCat() {
Pattern p = Pattern.compile(''cat'');
Matcher m = p.matcher(''one cat, two cat, three cat'');
StringBuffer sb = new StringBuffer();
int foundNum = 0;
while (m.find()) {
m.appendReplacement(sb, ''dog'');
foundNum++;
if (foundNum == 2) {
break;
}
}
m.appendTail(sb);
System.out.println(sb.toString());
}
在這個(gè)例子中,我們將前兩個(gè)''cat''替換為了''dog'',其他''cat''不變,輸出為:
one dog, two dog, three cat
StringBuffer類型的變量sb存放最終的替換結(jié)果,Matcher內(nèi)部除了有一個(gè)查找位置,還有一個(gè)append位置,初始為0,當(dāng)找到一個(gè)匹配的子字符串后,appendReplacement()做了三件事情:
將append位置到當(dāng)前匹配之前的子字符串a(chǎn)ppend到sb中,在第一次操作中,為''one '',第二次為'', two '';
將替換字符串a(chǎn)ppend到sb中;
更新append位置為當(dāng)前匹配之后的位置。
appendTail將append位置之后所有的字符append到sb中。
模板引擎
利用Matcher的這幾個(gè)方法,我們可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的模板引擎,模板是一個(gè)字符串,中間有一些變量,以{name}表示,如下例所示:
String template = ''Hi {name}, your code is {code}.'';
這里,模板字符串中有兩個(gè)變量,一個(gè)是name,另一個(gè)是code。變量的實(shí)際值通過Map提供,變量名稱對(duì)應(yīng)Map中的鍵,模板引擎的任務(wù)就是接受模板和Map作為參數(shù),返回替換變量后的字符串,示例實(shí)現(xiàn)為:
private static Pattern templatePattern = Pattern.compile(''\\{(\\w+)\\}'');
public static String templateEngine(String template, Mapparams) {
StringBuffer sb = new StringBuffer();
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();
}
代碼尋找所有的模板變量,正則表達(dá)式為:
\{(\w+)\}
''{''是元字符,所以要轉(zhuǎn)義,\w+表示變量名,為便于引用,加了括號(hào),可以通過分組1引用變量名。
使用該模板引擎的示例代碼為:
public static void templateDemo() {
String template = ''Hi {name}, your code is {code}.'';
Mapparams = new HashMap ();
params.put(''name'', ''老馬'');
params.put(''code'', 6789);
System.out.println(templateEngine(template, params));
}
輸出為:
Hi 老馬, your code is 6789.
小結(jié)
本節(jié)介紹了正則表達(dá)式相關(guān)的主要Java API,討論了如何在Java中表示正則表達(dá)式,如何利用它實(shí)現(xiàn)文本的切分、驗(yàn)證、查找和替換,對(duì)于替換,我們演示了一個(gè)簡(jiǎn)單的模板引擎。
下一節(jié),我們繼續(xù)探討正則表達(dá)式,討論和分析一些常見的正則表達(dá)式。
(與其他章節(jié)一樣,本節(jié)所有代碼位于 https://github.com/swiftma/program-logic,位于包shuo.laoma.dynamic.c89下)
聯(lián)系客服