Java編程的邏輯 (88) - 正則表達式 (上)

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營連接:http://item.jd.com/12299018.htmlhtml


上節咱們提到了正則表達式,它提高了文本處理的表達能力,本節就來討論正則表達式,它是什麼?有什麼用?各類特殊字符都是什麼含義?如何用Java藉助正則表達式處理文本?都有哪些經常使用正則表達式?因爲內容較多,咱們分爲三節進行探討,本節先簡要探討正則表達式的語法。git

正則表達式是一串字符,它描述了一個文本模式,利用它能夠方便的處理文本,包括文本的查找、替換、驗證、切分等。正則表達式

正則表達式中的字符有兩類,一類是普通字符,就是匹配字符自己,另外一類是元字符,這些字符有特殊含義,這些元字符及其特殊含義就構成了正則表達式的語法。編程

正則表達式有一個比較長的歷史,各類與文本處理有關的工具、編輯器和系統都支持正則表達式,大部分編程語言也都支持正則表達式。雖然都叫正則表達式,但因爲歷史緣由,不一樣語言、系統和工具的語法不太同樣,本文主要針對Java語言,其餘語言可能有所差異。微信

下面,咱們就來簡要介紹正則表達式的語法,咱們先分爲如下部分分別介紹:編程語言

  • 單個字符
  • 字符組
  • 量詞
  • 分組
  • 特殊邊界匹配
  • 環視邊界匹配

最後針對轉義、匹配模式和各類語法進行總結。編輯器

單個字符工具

大部分的單個字符就是用字符自己表示的,好比字符'0','3','a','馬'等,但有一些單個字符使用多個字符表示,這些字符都以斜槓'\'開頭,好比:性能

  • 特殊字符,好比tab字符'\t',換行符'\n',回車符'\r'等;
  • 八進制表示的字符,以\0開頭,後跟1到3位數字,好比\0141,對應的是ASCII編碼爲97的字符,即字符'a';
  • 十六進制表示的字符,以\x開頭,後跟兩位字符,好比\x6A,對應的是ASCII編碼爲106的字符,即字符'j';
  • Unicode編號表示的字符,以\u開頭,後跟四位字符,好比\u9A6C,表示的是中文字符'馬',這隻能表示編號在0xFFFF如下的字符,若是超出0XFFFF,使用\x{...}形式,好比對於字符'💎',可使用\x{1f48e};
  • 斜槓\自己,斜槓\是一個元字符,若是要匹配它自身,使用兩個斜槓表示,即'\\';
  • 元字符自己,除了'\',正則表達式中還有不少元字符,好比. * ? +等,要匹配這些元字符自身,須要在前面加轉義字符'\',好比'\.'。

字符組
編碼

任意字符

點號字符'.'是一個元字符,默認模式下,它匹配除了換行符之外的任意字符,好比正則表達式:

a.f

既匹配字符串"abf",也匹配"acf"。

能夠指定另一種匹配模式,通常稱爲單行匹配模式或者叫點號匹配模式,在此模式下,'.'匹配任意字符,包括換行符。

能夠有兩種方式指定匹配模式,一種是在正則表達式中,以(?s)開頭,s表示single line,即單行匹配模式,好比:

(?s)a.f

另一種是在程序中指定,在Java中,對應的模式常量是Pattern.DOTALL,下節咱們再介紹Java API。

指定的多個字符之一

在單個字符和任意字符之間,有一個字符組的概念,匹配組中的任意一個字符,用中括號[]表示,好比:

[abcd]

匹配a, b, c, d中的任意一個字符。

[0123456789]

匹配任意一個數字字符。

字符區間

爲方便表示連續的多個字符,字符組中可使用連字符'-',好比:

[0-9]
[a-z]

能夠有多個連續空間,能夠有其餘普通字符,好比:

[0-9a-zA-Z_]

在字符組中,'-'是一個元字符,若是要匹配它自身,可使用轉義,即'\-',或者把它放在字符組的最前面,好比:

[-0-9]

排除型字符組

字符組支持排除的概念,在[後緊跟一個字符^,好比:

[^abcd]

表示匹配除了a, b, c, d之外的任意一個字符。

[^0-9]

表示匹配一個非數字字符。

排除不是不能匹配,而是匹配一個指定字符組之外的字符,要表達不能匹配的含義,須要使用後文介紹的環視語法。

^只有在字符組的開頭纔是元字符,若是不在開頭,就是普通字符,匹配它自身,好比:

[a^b]

就是匹配字符a, ^或b。

字符組內的元字符

在字符組中,除了^ - [ ] \外,其餘在字符組外的元字符再也不具有特殊含義,變成了普通字符,好比'.',[.*]就是匹配'.'或者'*'自己。

字符組運算

字符組內能夠包含字符組,好比:

[[abc][def]]

最後的字符組等同於[abcdef],內部多個字符組等同於並集運算。

字符組內還支持交集運算,語法是使用&&,好比:

[a-z&&[^de]]

匹配的字符是a到z,但不能是d或e。

須要注意的是,其餘語言可能不支持字符組運算。

預約義的字符組

有一些特殊的以\開頭的字符,表示一些預約義的字符組,好比:

  • \d:d表示digit,匹配一個數字字符,等同於[0-9] ;
  • \w:w表示word,匹配一個單詞字符,等同於[a-zA-Z_0-9];
  • \s:s表示space,匹配一個空白字符,等同於[ \t\n\x0B\f\r]。

它們都有對應的排除型字符組,用大寫表示,即:

  • \D:匹配一個非數字字符,即[^\d] ;
  • \W:匹配一個非單詞字符,即[^\w];
  • \S:匹配一個非空白字符,即[^\s]。

POSIX字符組

還有一類字符組,稱爲POSIX字符組,POSIX是一個標準,POSIX字符組是POSIX標準定義的一些字符組,在Java中,這些字符組的形式是\p{...},好比:

  • \p{Lower}:小寫字母,等同於[a-z];
  • \p{Upper}:大寫字母,等同於[A-Z];
  • \p{Digit}:數字,等同於[0-9];
  • \p{Punct} :標點符號,匹配!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~中的一個。

POSIX字符組比較多,本文就不列舉了。

量詞

經常使用量詞 + * ?

量詞指的是指定出現次數的元字符,有三個常見的元字符+ * ?:

  • +:表示前面字符的一次或屢次出現,好比正則表達式ab+c,既能匹配abc,也能匹配abbc,或abbbc;
  • *:表示前面字符的零次或屢次出現,好比正則表達式ab*c,既能匹配abc,也能匹配ac,或abbbc;
  • ?:表示前面字符可能出現,也可能不出現,好比正則表達式ab?c,既能匹配abc,也能匹配ac,但不能匹配abbc。

通用量詞 {m,n}

更爲通用的表示出現次數的語法是{m,n},出現次數從m到n,包括m和n,若是n沒有限制,能夠省略,若是m和n同樣,能夠寫爲{m},好比:

  • ab{1,10}c:b能夠出現1次到10次
  • ab{3}c:b必須出現三次,即只能匹配abbbc
  • ab{1,}c:與ab+c同樣
  • ab{0,}c:與ab*c同樣
  • ab{0,1}c:與ab?c同樣

須要注意的是,語法必須是嚴格的{m,n}形式,逗號左右不能有空格。

?, *, +, {是元字符,若是要匹配這些字符自己,須要使用'\'轉義,好比

a\*b

匹配字符串"a*b"。

這些量詞出如今字符組中時,不是元字符,好比表達式

[?*+{]

就是匹配其中一個字符自己。

貪婪與懶惰

關於量詞,它們的默認匹配是貪婪的,什麼意思呢?看個例子,正則表達式是:

<a>.*</a>

若是要處理的字符串是:

<a>first</a><a>second</a>

目的是想獲得兩個匹配,一個匹配:

<a>first</a>

另外一個匹配:

<a>second</a>

但默認狀況下,獲得的結果卻只有一個匹配,匹配全部內容。

這是由於.*能夠匹配第一個<a>和最後一個</a>之間的全部字符,只要能匹配,.*就儘可能日後匹配,它是貪婪的。若是但願在碰到第一個匹配時就中止呢?應該使用懶惰量詞,在量詞的後面加一個符號'?',針對上例,將表達式改成:

<a>.*?</a>

就能獲得指望的結果。

全部量詞都有對應的懶惰形式,好比:x??, x*?, x+?, x{m,n}?等。

分組

表達式能夠用括號()括起來,表示一個分組,好比a(bc)d,bc就是一個分組,分組能夠嵌套,好比a(de(fg))。

捕獲分組

分組默認都有一個編號,按照括號的出現順序,從1開始,從左到右依次遞增,好比表達式:

a(bc)((de)(fg))

字符串abcdefg匹配這個表達式,第1個分組爲bc,第2個爲defg,第3個爲de,第4個爲fg。分組0是一個特殊分組,內容是整個匹配的字符串,這裏是abcdefg。

分組匹配的子字符串能夠在後續訪問,好像被捕獲了同樣,因此默認分組被稱爲捕獲分組。關於如何在Java中訪問和使用捕獲分組,咱們下節再介紹。

分組量詞

能夠對分組使用量詞,表示分組的出現次數,好比a(bc)+d,表示bc出現一次或屢次。

分組多選

中括號[]表示匹配其中的一個字符,括號()和元字符'|'一塊兒,能夠表示匹配其中的一個子表達式,好比

(http|ftp|file)

匹配http或ftp或file。

須要注意區分|和[],|用於[]中再也不有特殊含義,好比

[a|b]

它的含義不是匹配a或b,而是a或|或b。

回溯引用

在正則表達式中,可使用斜槓\加分組編號引用以前匹配的分組,這稱之爲回溯引用,好比:

<(\w+)>(.*)</\1>

\1匹配以前的第一個分組(\w+),這個表達式能夠匹配相似以下字符串:

<title>bc</title>

這裏,第一個分組是"title"。

命名分組

使用數字引用分組,可能容易出現混亂,能夠對分組進行命名,經過名字引用以前的分組,對分組命名的語法是(?<name>X),引用分組的語法是\k<name>,好比,上面的例子能夠寫爲:

<(?<tag>\w+)>(.*)</\k<tag>>

非捕獲分組

默認分組都稱之爲捕獲分組,即分組匹配的內容被捕獲了,能夠在後續被引用,實現捕獲分組有必定的成本,爲了提升性能,若是分組後續不須要被引用,能夠改成非捕獲分組,語法是(?:...),好比:

(?:abc|def)

特殊邊界匹配

在正則表達式中,除了能夠指定字符需知足什麼條件,還能夠指定字符的邊界需知足什麼條件,或者說匹配特定的邊界,經常使用的表示特殊邊界的元字符有^, $, \A, \Z, \z和\b。

邊界 ^

默認狀況下,^匹配整個字符串的開始,^abc表示整個字符串必須以abc開始。

須要注意的是^的含義,在字符組中它表示排除,但在字符組外,它匹配開始,好比表達式^[^abc],表示以一個不是a,b,c的字符開始。

邊界 $

默認狀況下,$匹配整個字符串的結束,不過,若是整個字符串以換行符結束,$匹配的是換行符以前的邊界,好比表達式abc$,表示整個表達式以abc結束,或者以abc\r\n或abc\n結束。

多行匹配模式

以上^和$的含義是默認模式下的,能夠指定另一種匹配模式,多行匹配模式,在此模式下,會以行爲單位進行匹配,^匹配的是行開始,$匹配的是行結束,好比表達式是^abc$,字符串是"abc\nabc\r\n",就會有兩個匹配。

能夠有兩種方式指定匹配模式,一種是在正則表達式中,以(?m)開頭,m表示multiline,即多行匹配模式,上面的正則表達式能夠寫爲:

(?m)^abc$

另一種是在程序中指定,在Java中,對應的模式常量是Pattern.MULTILINE,下節咱們再介紹Java API。

須要說明的是,多行模式和以前介紹的單行模式容易混淆,其實,它們之間沒有關係,單行模式影響的是字符'.'的匹配規則,使得'.'能夠匹配換行符,多行模式影響的是^和$的匹配規則,使得它們能夠匹配行的開始和結束,兩個模式能夠一塊兒使用。

邊界 \A

\A與^相似,但無論什麼模式,它匹配的老是整個字符串的開始邊界。

邊界 \Z和\z

\Z和\z與$相似,但無論什麼模式,它們匹配的老是整個字符串的結束,\Z與\z的區別是,若是字符串以換行符結束,\Z與$同樣,匹配的是換行符以前的邊界,而\z匹配的老是結束邊界。在進行輸入驗證的時候,爲了確保輸入最後沒有多餘的換行符,可使用\z進行匹配。

單詞邊界 \b

\b匹配的是單詞邊界,好比\bcat\b,匹配的是完整的單詞cat,它不能匹配category,\b匹配的不是一個具體的字符,而是一種邊界,這種邊界知足一個要求,即一邊是單詞字符,另外一邊不是單詞字符。在Java中,\b識別的單詞字符除了\w,還包括中文字符。

到底什麼是邊界匹配?

邊界匹配可能難以理解,咱們強調下,到底什麼是邊界匹配。邊界匹配不一樣於字符匹配,能夠認爲,在一個字符串中,每一個字符的兩邊都是邊界,而上面介紹的這些特殊字符,匹配的都不是字符,而是特定的邊界,看個例子:

上面的字符串是"a cat\n",咱們用粗線顯示出了每一個字符兩邊的邊界,而且顯示出了每一個邊界與哪些邊界元字符匹配。

環視邊界匹配

定義

對於邊界匹配,除了使用上面介紹的邊界元字符,還有一種更爲通用的方式,那就是環視,環視的字面意思就是左右看看,須要左右符合一些條件,本質上,它也是匹配邊界,對邊界有一些要求,這個要求是針對左邊或右邊的字符串的,根據要求不一樣,分爲四種環視:

  • 確定順序環視,語法是(?=...),要求右邊的字符串匹配指定的表達式,好比表達式abc(?=def),(?=def)在字符c右面,即匹配c右面的邊界,對這個邊界的要求是,它的右邊有def,好比abcdef,若是沒有,好比abcd,則不匹配;
  • 否認順序環視,語法是(?!...),要求右邊的字符串不能匹配指定的表達式,好比表達式s(?!ing),匹配通常的s,但不匹配後面有ing的s;
  • 確定逆序環視,語法是(?<=...),要求左邊的字符串匹配指定的表達式,好比表達式(?<=\s)abc,(?<=\s)在字符a左邊,即匹配a左邊的邊界,對這個邊界的要求是,它的左邊必須是空白字符;
  • 否認逆序環視,語法是(?<!...),要求左邊的字符串不能匹配指定的表達式,好比表達式(?<!\w)cat,(?<!\w)在字符c左邊,即匹配c左邊的邊界,對這個邊界的要求是,它的左邊不能是單詞字符。

能夠看出,環視也使用括號(),不過,它不是分組,不佔用分組編號。

這些環視結構也被稱爲斷言,斷言的對象是邊界,邊界不佔用字符,沒有寬度,因此也被稱爲零寬度斷言。

否認順序環視與排除型字符組

關於否認順序環視,咱們要避免與排除型字符組混淆,即區分s(?!ing)與s[^ing],s[^ing]匹配的是兩個字符,第一個是s,第二個是i, n, g之外的任意一個字符。還要注意,寫法s(^ing)是不對的,^匹配的是起始位置。

出如今左邊的順序環視

順序環視也能夠出如今左邊,好比表達式:

(?=.*[A-Z])\w+

這個表達式是什麼意思呢?

\w+匹配多個單詞字符,(?=.*[A-Z])匹配單詞字符的左邊界,這是一個確定順序環視,對這個邊界的要求是,它右邊的字符串匹配表達式:

.*[A-Z]

也就是說,它右邊至少要有一個大寫字母。

出如今右邊的逆序環視

逆序環視也能夠出如今右邊,好比表達式:

[\w.]+(?<!\.)

[\w.]+匹配單詞字符和字符'.'構成的字符串,好比"hello.ma"。(?<!\.)匹配字符串的右邊界,這是一個逆序否認環視,對這個邊界的要求是,它左邊的字符不能是'.',也就是說,若是字符串以'.'結尾,則匹配的字符串中不能包括這個'.',好比,若是字符串是"hello.ma.",則匹配的子字符串是"hello.ma"。

並行環視

環視匹配的是一個邊界,裏面的表達式是對這個邊界左邊或右邊字符串的要求,對同一個邊界,能夠指定多個要求,即寫多個環視,好比表達式:

(?=.*[A-Z])(?=.*[0-9])\w+

\w+的左邊界有兩個要求,(?=.*[A-Z])要求後面至少有一個大寫字母,(?=.*[0-9])要求後面至少有一位數字。

轉義與匹配模式

轉義

咱們知道,字符'\'表示轉義,轉義有兩種:

  • 把普通字符轉義,使其具有特殊含義,好比'\t', '\n', '\d', '\w', '\b', '\A'等,也就是說,這個轉義把普通字符變爲了元字符;
  • 把元字符轉義,使其變爲普通字符,好比'\.', '\*', '\?','\(', '\\'等。

記住全部的元字符,並在須要的時候進行轉義,這是比較困難的,有一個簡單的辦法,能夠將全部元字符看作普通字符,就是在開始處加上\Q,在結束處加上\E,好比:

\Q(.*+)\E

\Q和\E之間的全部字符都會被視爲普通字符。

正則表達式用字符串表示,在Java中,字符'\'也是字符串語法中的元字符,這使得正則表達式中的'\',在Java字符串表示中,要用兩個'\',即'\\',而要匹配字符'\'自己,在Java字符串表示中,要用四個'\',即'\\\\',關於這點,下節咱們會進一步說明。

匹配模式

前面提到了兩種匹配模式,還有一種經常使用的匹配模式,就是不區分大小寫的模式,指定方式也有兩種,一種是在正則表達式開頭使用(?i),i爲ignore,好比:

(?i)the

既能夠匹配the,也能夠匹配THE,還能夠匹配The。

也能夠在程序中指定,Java中對應的變量是Pattern.CASE_INSENSITIVE。

須要說明的是,匹配模式間不是互斥的關係,它們能夠一塊兒使用,在正則表達式中,能夠指定多個模式,好比(?smi)。

語法總結

下面,咱們用表格的形式簡要彙總下正則表達式的語法。

 

小結

本節簡要介紹了正則表達式中的語法,下一節,咱們來探討相關的Java API。

----------------

未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索