本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結
88節介紹了正規表示式的語法,上節介紹了正規表示式相關的Java API,本節來討論和分析一些常用的正規表示式,具體包括:
- 郵編
- 電話號碼,包括手機號碼和固定電話號碼
- 日期和時間
- 身份證
- IP地址
- URL
- Email地址
- 中文字元
對於同一個目的,正規表示式往往有多種寫法,大多沒有唯一正確的寫法,本節的寫法主要是示例。此外,寫一個正規表示式,匹配希望匹配的內容往往比較容易,但讓它不匹配不希望匹配的內容,則往往比較困難,也就是說,保證精確性經常是很難的,不過,很多時候,我們也沒有必要寫完全精確的表示式,需要寫到多精確與你需要處理的文字和需求有關,另外,正規表示式難以表達的,可以通過寫程式進一步處理。這麼描述可能比較抽象,下面,我們會具體討論分析。
郵編
郵編比較簡單,就是6位數字,所以表示式可以為:
[0-9]{6}
複製程式碼
這個表示式可以用於驗證輸入是否為郵編,比如:
public static Pattern ZIP_CODE_PATTERN = Pattern.compile(
"[0-9]{6}");
public static boolean isZipCode(String text) {
return ZIP_CODE_PATTERN.matcher(text).matches();
}
複製程式碼
但如果用於查詢,這個表示式是不夠的,看個例子:
public static void findZipCode(String text) {
Matcher matcher = ZIP_CODE_PATTERN.matcher(text);
while (matcher.find()) {
System.out.println(matcher.group());
}
}
public static void main(String[] args) {
findZipCode("郵編 100013,電話18612345678");
}
複製程式碼
文字中只有一個郵編,但輸出卻為:
100013
186123
複製程式碼
這怎麼辦呢?可以使用88節介紹的環視邊界匹配,對於左邊界,它前面的字元不能是數字,環視表示式為:
(?<![0-9])
複製程式碼
對於右邊界,它右邊的字元不能是數字,環視表示式為:
(?![0-9])
複製程式碼
所以,完整的表示式可以為:
(?<![0-9])[0-9]{6}(?![0-9])
複製程式碼
使用這個表示式,也就是說,將ZIP_CODE_PATTERN
改為:
public static Pattern ZIP_CODE_PATTERN = Pattern.compile(
"(?<![0-9])" // 左邊不能有數字
+ "[0-9]{6}"
+ "(?![0-9])"); // 右邊不能有數字
複製程式碼
就可以輸出期望的結果了。
非0開頭的6位數字就一定是郵編嗎?答案當然是否定的,所以,這個表示式也不是精確的,如果需要更精確的驗證,可以寫程式進一步檢查。
手機號碼
中國的手機號碼都是11位數字,所以,最簡單的表示式就是:
[0-9]{11}
複製程式碼
不過,目前手機號第1位都是1,第2位取值為3、4、5、7、8之一,所以,更精確的表示式是:
1[34578|][0-9]{9}
複製程式碼
為方便表達手機號,手機號中間經常有連字元(即減號'-'),形如:
186-1234-5678
複製程式碼
為表達這種可選的連字元,表示式可以改為:
1[34578][0-9]-?[0-9]{4}-?[0-9]{4}
複製程式碼
在手機號前面,可能還有0、+86或0086,和手機號碼之間可能還有一個空格,比如:
018612345678
+86 18612345678
0086 18612345678
複製程式碼
為表達這種形式,可以在號碼前加如下表示式:
((0|\+86|0086)\s?)?
複製程式碼
和郵編類似,如果為了抽取,也要在左右加環視邊界匹配,左右不能是數字。所以,完整的表示式為:
(?<![0-9])((0|\+86|0086)\s?)?1[34578][0-9]-?[0-9]{4}-?[0-9]{4}(?![0-9])
複製程式碼
用Java表示的程式碼為:
public static Pattern MOBILE_PHONE_PATTERN = Pattern.compile(
"(?<![0-9])" // 左邊不能有數字
+ "((0|\\+86|0086)\\s?)?" // 0 +86 0086
+ "1[34578][0-9]-?[0-9]{4}-?[0-9]{4}" // 186-1234-5678
+ "(?![0-9])"); // 右邊不能有數字
複製程式碼
固定電話
不考慮分機,中國的固定電話一般由兩部分組成:區號和市內號碼,區號是3到4位,市內號碼是7到8位。區號以0開頭,表示式可以為:
0[0-9]{2,3}
複製程式碼
市內號碼錶達式為:
[0-9]{7,8}
複製程式碼
區號可能用括號包含,區號與市內號碼之間可能有連字元,如以下形式:
010-62265678
(010)62265678
複製程式碼
整個區號是可選的,所以整個表示式為:
(\(?0[0-9]{2,3}\)?-?)?[0-9]{7,8}
複製程式碼
再加上左右邊界環視,完整的Java表示為:
public static Pattern FIXED_PHONE_PATTERN = Pattern.compile(
"(?<![0-9])" // 左邊不能有數字
+ "(\\(?0[0-9]{2,3}\\)?-?)?" // 區號
+ "[0-9]{7,8}"// 市內號碼
+ "(?![0-9])"); // 右邊不能有數字
複製程式碼
日期
日期的表示方式有很多種,我們只看一種,形如:
2017-06-21
2016-11-1
複製程式碼
年月日之間用連字元分隔,月和日可能只有一位。
最簡單的正規表示式可以為:
\d{4}-\d{1,2}-\d{1,2}
複製程式碼
年一般沒有限制,但月只能取值1到12,日只能取值1到31,怎麼表達這種限制呢?
對於月,有兩種情況,1月到9月,表示式可以為:
0?[1-9]
複製程式碼
10月到12月,表示式可以為:
1[0-2]
複製程式碼
所以,月的表示式為:
(0?[1-9]|1[0-2])
複製程式碼
對於日,有三種情況:
- 1到9號,表示式為:
0?[1-9]
- 10號到29號,表示式為:
[1-2][0-9]
- 30號和31號,表示式為:
3[01]
所以,整個表示式為:
\d{4}-(0?[1-9]|1[0-2])-(0?[1-9]|[1-2][0-9]|3[01])
複製程式碼
加上左右邊界環視,完整的Java表示為:
public static Pattern DATE_PATTERN = Pattern.compile(
"(?<![0-9])" // 左邊不能有數字
+ "\\d{4}-" // 年
+ "(0?[1-9]|1[0-2])-" // 月
+ "(0?[1-9]|[1-2][0-9]|3[01])"// 日
+ "(?![0-9])"); // 右邊不能有數字
複製程式碼
時間
考慮24小時制,只考慮小時和分鐘,小時和分鐘都用固定兩位表示,格式如下:
10:57
複製程式碼
基本表示式為:
\d{2}:\d{2}
複製程式碼
小時取值範圍為0到23,更精確的表示式為:
([0-1][0-9]|2[0-3])
複製程式碼
分鐘取值範圍為0到59,更精確的表示式為:
[0-5][0-9]
複製程式碼
所以,整個表示式為:
([0-1][0-9]|2[0-3]):[0-5][0-9]
複製程式碼
加上左右邊界環視,完整的Java表示為:
public static Pattern TIME_PATTERN = Pattern.compile(
"(?<![0-9])" // 左邊不能有數字
+ "([0-1][0-9]|2[0-3])" // 小時
+ ":" + "[0-5][0-9]"// 分鐘
+ "(?![0-9])"); // 右邊不能有數字
複製程式碼
身份證
身份證有一代和二代之分,一代是15位數字,二代是18位,都不能以0開頭,對於二代身份證,最後一位可能為x或X,其他是數字。
一代身份證表示式可以為:
[1-9][0-9]{14}
複製程式碼
二代身份證可以為:
[1-9][0-9]{16}[0-9xX]
複製程式碼
這兩個表示式的前面部分是相同的,二代身份證多瞭如下內容:
[0-9]{2}[0-9xX]
複製程式碼
所以,它們可以合併為一個表示式,即:
[1-9][0-9]{14}([0-9]{2}[0-9xX])?
複製程式碼
加上左右邊界環視,完整的Java表示為:
public static Pattern ID_CARD_PATTERN = Pattern.compile(
"(?<![0-9])" // 左邊不能有數字
+ "[1-9][0-9]{14}" // 一代身份證
+ "([0-9]{2}[0-9xX])?" // 二代身份證多出的部分
+ "(?![0-9])"); // 右邊不能有數字
複製程式碼
符合這個要求的就一定是身份證號碼嗎?當然不是,身份證還有一些更為具體的要求,本文就不探討了。
IP地址
IP地址格式如下:
192.168.3.5
複製程式碼
點號分隔,4段數字,每個數字範圍是0到255。最簡單的表示式為:
(\d{1,3}\.){3}\d{1-3}
複製程式碼
\d{1,3}太簡單,沒有滿足0到255之間的約束,要滿足這個約束,就要分多種情況考慮。
值是1位數,前面可能有0到2個0,表示式為:
0{0,2}[0-9]
複製程式碼
值是兩位數,前面可能有一個0,表示式為:
0?[0-9]{2}
複製程式碼
值是三位數,又要分為多種情況。以1開頭的,後兩位沒有限制,表示式為:
1[0-9]{2}
複製程式碼
以2開頭的,如果第二位是0到4,則第三位沒有限制,表示式為:
2[0-4][0-9]
複製程式碼
如果第二位是5,則第三位取值為0到5,表示式為:
25[0-5]
複製程式碼
所以,\d{1,3}
更為精確的表示為:
(0{0,2}[0-9]|0?[0-9]{2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])
複製程式碼
所以,加上左右邊界環視,IP地址的完整Java表示為:
public static Pattern IP_PATTERN = Pattern.compile(
"(?<![0-9])" // 左邊不能有數字
+ "((0{0,2}[0-9]|0?[0-9]{2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}"
+ "(0{0,2}[0-9]|0?[0-9]{2}|1[0-9]{2}|2[0-4][0-9]|25[0-5])"
+ "(?![0-9])"); // 右邊不能有數字
複製程式碼
URL
URL的格式比較複雜,其規範定義在tools.ietf.org/html/rfc173…,我們只考慮http協議,其通用格式是:
http://<host>:<port>/<path>?<searchpart>
複製程式碼
開始是http://
,接著是主機名,主機名之後是可選的埠,再之後是可選的路徑,路徑後是可選的查詢字串,以?開頭。
一些例子:
http://www.example.com
http://www.example.com/ab/c/def.html
http://www.example.com:8080/ab/c/def?q1=abc&q2=def
複製程式碼
主機名中的字元可以是字母、數字、減號和點號,所以表示式可以為:
[-0-9a-zA-Z.]+
複製程式碼
埠部分可以寫為:
(:\d+)?
複製程式碼
路徑由多個子路徑組成,每個子路徑以/開頭,後跟零個或多個非/的字元,簡單的說,表示式可以為:
(/[^/]*)*
複製程式碼
更精確的說,把所有允許的字元列出來,表示式為:
(/[-\w$.+!*'(),%;:@&=]*)*
複製程式碼
對於查詢字串,簡單的說,由非空字串組成,表示式為:
\?[\S]*
複製程式碼
更精確的,把所有允許的字元列出來,表示式為:
\?[-\w$.+!*'(),%;:@&=]*
複製程式碼
路徑和查詢字串是可選的,且查詢字串只有在至少存在一個路徑的情況下才能出現,其模式為:
(/<sub_path>(/<sub_path>)*(\?<search>)?)?
複製程式碼
所以,路徑和查詢部分的簡單表示式為:
(/[^/]*(/[^/]*)*(\?[\S]*)?)?
複製程式碼
精確表示式為:
(/[-\w$.+!*'(),%;:@&=]*(/[-\w$.+!*'(),%;:@&=]*)*(\?[-\w$.+!*'(),%;:@&=]*)?)?
複製程式碼
HTTP的完整Java表示式為:
public static Pattern HTTP_PATTERN = Pattern.compile(
"http://" + "[-0-9a-zA-Z.]+" // 主機名
+ "(:\\d+)?" // 埠
+ "(" // 可選的路徑和查詢 - 開始
+ "/[-\\w$.+!*'(),%;:@&=]*" // 第一層路徑
+ "(/[-\\w$.+!*'(),%;:@&=]*)*" // 可選的其他層路徑
+ "(\\?[-\\w$.+!*'(),%;:@&=]*)?" // 可選的查詢字串
+ ")?"); // 可選的路徑和查詢 - 結束
複製程式碼
Email地址
完整的Email規範比較複雜,定義在tools.ietf.org/html/rfc822,我們先看一些實際中常用的。
比如新浪郵箱,它的格式如:
abc@sina.com
複製程式碼
對於使用者名稱部分,它的要求是:4-16個字元,可使用英文小寫、數字、下劃線,但下劃線不能在首尾。
怎麼驗證使用者名稱呢?可以為:
[a-z0-9][a-z0-9_]{2,14}[a-z0-9]
複製程式碼
新浪郵箱的完整Java表示式為:
public static Pattern SINA_EMAIL_PATTERN = Pattern.compile(
"[a-z0-9]"
+ "[a-z0-9_]{2,14}"
+ "[a-z0-9]@sina\\.com");
複製程式碼
我們再來看QQ郵箱,它對於使用者名稱的要求為:
- 3-18字元,可使用英文、數字、減號、點或下劃線
- 必須以英文字母開頭,必須以英文字母或數字結尾
- 點、減號、下劃線不能連續出現兩次或兩次以上
如果只有第一條,可以為:
[-0-9a-zA-Z._]{3,18}
複製程式碼
為滿足第二條,可以改為:
[a-zA-Z][-0-9a-zA-Z._]{1,16}[a-zA-Z0-9]
複製程式碼
怎麼滿足第三條呢?可以使用邊界環視,左邊加如下表示式:
(?![-0-9a-zA-Z._]*(--|\.\.|__))
複製程式碼
完整表示式可以為:
(?![-0-9a-zA-Z._]*(--|\.\.|__))[a-zA-Z][-0-9a-zA-Z._]{1,16}[a-zA-Z0-9]
複製程式碼
QQ郵箱的完整Java表示式為:
public static Pattern QQ_EMAIL_PATTERN = Pattern.compile(
"(?![-0-9a-zA-Z._]*(--|\\.\\.|__))" // 點、減號、下劃線不能連續出現兩次或兩次以上
+ "[a-zA-Z]" // 必須以英文字母開頭
+ "[-0-9a-zA-Z._]{1,16}" // 3-18位 英文、數字、減號、點、下劃線組成
+ "[a-zA-Z0-9]@qq\\.com"); // 由英文字母、數字結尾
複製程式碼
以上都是特定郵箱服務商的要求,一般的郵箱是什麼規則呢?一般而言,以@作為分隔符,前面是使用者名稱,後面是域名。
使用者名稱的一般規則是:
- 由英文字母、數字、下劃線、減號、點號組成
- 至少1位,不超過64位
- 開頭不能是減號、點號和下劃線
比如:
h_llo-abc.good@example.com
複製程式碼
這個表示式可以為:
[0-9a-zA-Z][-._0-9a-zA-Z]{0,63}
複製程式碼
域名部分以點號分隔為多個部分,至少有兩個部分。最後一部分是頂級域名,由2到3個英文字母組成,表示式可以為:
[a-zA-Z]{2,3}
複製程式碼
對於域名的其他點號分隔的部分,每個部分一般由字母、數字、減號組成,但減號不能在開頭,長度不能超過63個字元,表示式可以為:
[0-9a-zA-Z][-0-9a-zA-Z]{0,62}
複製程式碼
所以,域名部分的表示式為:
([0-9a-zA-Z][-0-9a-zA-Z]{0,62}\.)+[a-zA-Z]{2,3}
複製程式碼
完整的Java表示為:
public static Pattern GENERAL_EMAIL_PATTERN = Pattern.compile(
"[0-9a-zA-Z][-._0-9a-zA-Z]{0,63}" // 使用者名稱
+ "@"
+ "([0-9a-zA-Z][-0-9a-zA-Z]{0,62}\\.)+" // 域名部分
+ "[a-zA-Z]{2,3}"); // 頂級域名
複製程式碼
中文字元
中文字元的Unicode編號一般位於\u4e00和\u9fff之間,所以匹配任意一箇中文字元的表示式可以為:
[\u4e00-\u9fff]
複製程式碼
Java表示式為:
public static Pattern CHINESE_PATTERN = Pattern.compile(
"[\\u4e00-\\u9fff]");
複製程式碼
小結
本節詳細討論和分析了一些常見的正規表示式,在實際開發中,有些可以直接使用,有些需要根據具體文字和需求進行調整。
至此,關於正規表示式,我們就介紹完了,相信你對正規表示式一定有了一個更為清晰透徹的理解!
在之前的章節中,我們都是基於Java 7討論的,從下節開始,我們探討Java 8的一些特性,尤其是函數語言程式設計。
(與其他章節一樣,本節所有程式碼位於 github.com/swiftma/pro…,位於包shuo.laoma.java8.c91下)
未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),從入門到高階,深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。