本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營連接:http://item.jd.com/12299018.htmlhtml
上節介紹了正則表達式的語法,本節介紹相關的Java API。java
正則表達式相關的類位於包java.util.regex下,有兩個主要的類,一個是Pattern,另外一個是Matcher。Pattern表示正則表達式對象,它與要處理的具體字符串無關。Matcher表示一個匹配,它將正則表達式應用於一個具體字符串,經過它對字符串進行處理。git
字符串類String也是一個重要的類,咱們在29節專門介紹過String,其中提到,它有一些方法,接受的參數不是普通的字符串,而是正則表達式。此外,正則表達式在Java中是須要先以字符串形式表示的。github
下面,咱們先來介紹如何表示正則表達式,而後探討如何利用它實現一些常見的文本處理任務,包括切分、驗證、查找、和替換。正則表達式
表示正則表達式編程
轉義符 '\'swift
正則表達式由元字符和普通字符組成,字符'\'是一個元字符,要在正則表達式中表示'\'自己,須要使用它轉義,即'\\'。數組
在Java中,沒有什麼特殊的語法能直接表示正則表達式,須要用字符串表示,而在字符串中,'\'也是一個元字符,爲了在字符串中表示正則表達式的'\',就須要使用兩個'\',即'\\',而要匹配'\'自己,就須要四個'\',即'\\\\',好比說,以下表達式:安全
<(\w+)>(.*)</\1>
對應的字符串表示就是:微信
"<(\\w+)>(.*)</\\1>"
一個簡單規則是,正則表達式中的任何一個'\',在字符串中,須要替換爲兩個'\'。
Pattern對象
字符串表示的正則表達式能夠被編譯爲一個Pattern對象,好比:
String regex = "<(\\w+)>(.*)</\\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方法的區別是:
驗證
驗證就是檢驗輸入文本是否完整匹配預約義的正則表達式,常常用於檢驗用戶的輸入是否合法。
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, "\\$"));
若是替換字符串是用戶提供的,爲避免元字符的的干擾,可使用Matcher的以下靜態方法將其視爲普通字符串:
public static String quoteReplacement(String s)
String的replaceAll和replaceFirst調用的實際上是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)
這兩個方法用於和find()一塊兒使用,咱們先看個例子:
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()); }
在這個例子中,咱們將前兩個"cat"替換爲了"dog",其餘"cat"不變,輸出爲:
one dog, two dog, three cat
StringBuffer類型的變量sb存放最終的替換結果,Matcher內部除了有一個查找位置,還有一個append位置,初始爲0,當找到一個匹配的子字符串後,appendReplacement()作了三件事情:
appendTail將append位置以後全部的字符append到sb中。
模板引擎
利用Matcher的這幾個方法,咱們能夠實現一個簡單的模板引擎,模板是一個字符串,中間有一些變量,以{name}表示,以下例所示:
String template = "Hi {name}, your code is {code}.";
這裏,模板字符串中有兩個變量,一個是name,另外一個是code。變量的實際值經過Map提供,變量名稱對應Map中的鍵,模板引擎的任務就是接受模板和Map做爲參數,返回替換變量後的字符串,示例實現爲:
private static Pattern templatePattern = Pattern.compile("\\{(\\w+)\\}"); public static String templateEngine(String template, Map<String, Object> params) { 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(); }
代碼尋找全部的模板變量,正則表達式爲:
\{(\w+)\}
'{'是元字符,因此要轉義,\w+表示變量名,爲便於引用,加了括號,能夠經過分組1引用變量名。
使用該模板引擎的示例代碼爲:
public static void templateDemo() { String template = "Hi {name}, your code is {code}."; Map<String, Object> params = new HashMap<String, Object>(); params.put("name", "老馬"); params.put("code", 6789); System.out.println(templateEngine(template, params)); }
輸出爲:
Hi 老馬, your code is 6789.
小結
本節介紹了正則表達式相關的主要Java API,討論瞭如何在Java中表示正則表達式,如何利用它實現文本的切分、驗證、查找和替換,對於替換,咱們演示了一個簡單的模板引擎。
下一節,咱們繼續探討正則表達式,討論和分析一些常見的正則表達式。
(與其餘章節同樣,本節全部代碼位於 https://github.com/swiftma/program-logic,位於包shuo.laoma.regex.c89下)
----------------
未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。