讀古今文學網 > 精通正則表達式(第3版) > HTML相關範例 >

HTML相關範例

HTML-Related Examples

在第2章,我們曾討論過把純文本轉換為HTML的例子(☞67),其中要使用正則表達式從文本中提取E-mail地址和http URL。本節來看一些與HTML相關的其他處理。

匹配HTML Tag

Matching an HTML Tag

最常見的辦法就是用「<[^>]+>」來匹配 HTML 標籤。它通常都能工作,例如下面這段用來去除標籤的Perl語句:

$html=~s/<[^>]+>//g;

如果tag中含有『>』,它就不能正常匹配了,而這樣的tag明明是合乎HTML規範的:<input name=dir value=〞>〞>。雖然這種情況很少見,也不為大家推薦,但HTML語言確實容許在引號內的tag屬性中出現非轉義的『<』和『>』。這樣,簡單的「<[^>]+>」就無法匹配了,得想個聰明點的辦法。

『<…>』中能夠出現引用文本和非引用形式的「其他文本(other stuff)」,其中包括除了『>』和引號之外的任意字符。HTML 的引文可以用單引號,也可以用雙引號。但不容許轉義嵌套的引號,所以我們可以直接用「〞[^〞]*〞」和「\'[^\']*\'」來匹配。

把這些和「其他文本」表達式「[^\'〞>]」合起來,我們得到:

「<(〞[^〞]*〞|\'[^\']*\'|[^\'〞>])*>」

這個表達式可能有點難看懂,那麼加上註釋,按寬鬆排列格式來看:

這個表達式相當漂亮,它會把每個引用部分單作為一個單元,而且清楚地說明了在匹配的什麼位置容許出現什麼字符。這個表達式的各部分不會匹配重複的字符,因此不存在模糊性,也就不需要擔心發生前面例子中出現的,「不小心冒出來(sneaking in)」非期望匹配。

不知你是否注意到了,最開始的兩個多選分支的引號中使用了「*」,而不是「+」。引用字符串可能為空(例如『alt=〞〞』),所以要用「*」來處理這種情況。但不要在第三個多選分支中用「*」取代「+」,因為「[^\'〞>]」只接受括號外的「*」的限定。給它添加一個加號得到「([^\'〞>]+)*」,可能導致非常奇怪的結果,我不期望讀者現在就能理解,下一章(☞226)會詳細講解它。

在使用NFA引擎時,我們還需要考慮關於效率的問題:既然沒有用到括號匹配的文本,我們可以把它們改為非捕獲型括號(☞137)。因為多選分支之間不存在重複,如果最後的「>」無法匹配,那麼回頭來嘗試其他的多選分支也是徒勞的。如果一個多選分支能夠在某個位置匹配,那麼其他多選分支肯定無法在這裡匹配。所以,不保存狀態也無所謂,這樣做還可以更快地導致失敗,如果找不到匹配結果的話。我們可以用固化分組「(?>…)」而不是非捕獲型括號(或者用佔有優先的星號限定)。

匹配HTML Link

Matching an HTML Link

假設我們需要從一份文檔中提取URL和鏈接文本,例如下面的文本中標記的內容:

因為<A> tag 的內容可能相當複雜,我會分兩步實現這個任務。第一個是提取<A> tag內部的內容,也就是鏈接文本,然後從<A> tag中提取URL地址。

實現第一步有個簡單辦法,就是在點號通配模式下應用不區分大小寫的「<ab([^>]+)>(.*?)</a>」,這裡使用了忽略優先量詞。它會把<A>的內容放入$1,把鏈接文本放入$2。當然,像之前一樣,我不應該用「[^>]+」,而應該使用前幾節中的表達式。不過在本節,我會繼續使用這個簡單的形式,因為這樣正則表達式更短,也更容易講解。

<A>的內容存入字符串之後,就可以用獨立的正則表達式來檢查它們。其中,URL 是href=value屬性的值。之前已經說過,HTML容許等號的任意一側出現空白字符,值可以以引用形式出現,也可以以非引用形式出現。下面的 Perl 代碼用來輸出變量$Html 中的鏈接。

有幾點需要注意:

●我們為匹配值的每個多選結構都添加了括號,來捕獲確切的文本。

●因為我使用了某些括號來捕獲文本,在不需要捕獲的地方我使用非捕獲型括號,這樣做既清楚又高效。

●「其他字符」部分排除了空白字符,也排除了引號和『>』。

●因為需要捕獲整個 href的值,這裡使用了「+」來限制「其他文本」多選分支。這是否會和第200頁對其他字符應用「+」一樣導致「非常奇怪的結果」呢?不會,因為這外面沒有直接作用於整個多選結構的量詞。其中的細節同樣會在下一章討論。

根據具體文本的不同,最後,URL 可能保存在$1、$2 或者$3 中。此時其他捕獲型括號就為空或是未定義。Perl提供了特殊變量$+,代表$1、$2之類中編號最靠後的捕獲文本。在本例中,這就是我們真正需要的URL。

Perl 中的$+很方便,其他語言也提供了其他辦法來選擇捕獲的 URL。常用的程序語言結構就可以檢查捕獲型括號,找到需要的內容。如果能夠支持,命名捕獲(☞138)最適用於幹這個,就像204頁的VB.NET的例子那樣(幸虧.NET提供了命名捕獲,因為它的$+有問題,☞424)。

檢查HTTP URL

Examining an HTTP URL

現在我們得到了 URL 地址,來看看它是否是 HTTP URL,如果是,就把它分解為主機名(hostname)和路徑(path)兩部分。因為已經有了URL,任務就比從隨機文本中識別URL要簡單許多,識別的程序要難許多,這將在後文介紹。

所以,如果拿到一個URL,我們需要能夠將它拆分為各個部分。主機名是「^http://」之後和第一個反斜線(如果有的話)之前的內容,而路徑就是除此之外的內容:「^http://([^/]+)(/.*)?$」。

URL 中有可能包含端口號,它位於主機名和路徑之間,以一個冒號開頭:「^http://([^/:]+)(:(d+))?(/.*)?$」。

下面是一個分解URL的Perl代碼:

驗證主機名

Validating a Hostname

在上面的例子中,我們用「[^/:]+」來匹配主機名。不過,在第 2 章中(☞76)我們使用的是更複雜的「[-a-z]+(.[-a-z]+)*.(com|edu|…|info)」。做同樣的事情,複雜程度為什麼會有這麼大的差別?

而且,雖然二者都用來「匹配主機名」,方法卻大不相同。從已知文本(例如,從現成的URL中)中提取一些信息是一回事,從隨機文本中準確提取同樣信息是另一回事。

而且,在上例中我們假設,『http://』之後就是主機名,所以用「[^/:]+」來匹配就是理所當然的。但是在第 2 章的例子中,我們使用正則表達式從隨機文本中尋找主機名,所以它必須更加複雜。

現在從另外一個角度來看主機名的匹配,我們可以用正則表達式來驗證主機名。也就是說,我們需要知道,一串字符是否是形式規範、語意正確的主機名。按規定,主機名由點號分隔的部分組成,每個部分可以包括 ASCII 字符、數字和連字符,但是不能以連字符作為開頭和結尾。所以,我們可以在不區分大小寫的模式下使用這個正則表達式:「[a-z0-9]|[a-z0-9][-a-z0-9]*[a-z0-9]」。結尾的後綴部分(『com』、『edu』、『uk』等)只有有限多個可能,這在第 2 章的例子中提到過。結合起來,下面的正則表達式就能夠匹配一個語意正確的主機名:

因為存在長度的限制,能夠由這個正則表達式匹配的可能並不是合法的主機名:每個部分不能超過63個字符。也就是說,「[-a-z0-9]*」應該改為「[-a-z0-9]{0,61}」。

還需要做最後的改動。按規定,只包括後綴的主機名同樣是語意正確的。但實踐證明,這些「主機名」不存在,但是對於兩個字母的後綴來說情況可不是如此。例如,安哥拉的域名『ai』就有一個Web服務器http://ai/。我見過其他這樣的鏈接:cc、co、dk、mm、ph、tj、tv和tw。

如果希望匹配這些特殊情況,應該把中間的「(?:…)+」改為「(?:…)*」:

現在它可以用來驗證包含主機名的字符串了。因為這是我們想出的與主機名相關的三個正則表達式中最複雜的,你也許會想,不要這些錨點,可能比之前那個從隨機文本中提取主機名的表達式更好。但情況並非如此。這個正則表達式能匹配任意雙字母單詞,正因為如此,第 2 章中不那麼精妙的正則表達式的實際效果更好。但是在下一節我們會看到,某些情況下它仍然不夠完善。

在真實世界中提取URL

Plucking Out a URL in the Real World

供職於Yahoo!Finance時,我曾寫過處理收錄的財經新聞和數據的程序。新聞通常是以純文本格式提供的,我的程序將其轉化為HTML格式以便於顯示(如果你在過去10年中曾經在http://finance.yahoo.com瀏覽過財經新聞,沒準看過我處理過的新聞)。

因為接受的數據的「格式」(其實是無格式)很雜亂,從純文本中識別(recognize)出hostname和URL又比驗證(validate)它們困難得多,這任務就很不輕鬆。前面的內容並沒有體現這一點,在本節,你會看到我在Yahoo!用來解決這個問題的程序。

這個程序從文本中提取幾種類型的URL——mailto、http、https和ftp。如果我們在文本中找到『http://』,就知道這肯定是一個URL的開頭,所以我們可以直接用「http://[-w]+(.w[-w]*)+」來匹配主機名。我們知道,要處理的文本肯定是ASCII編碼的英文字母,所以完全可以用「-w」來取代「-a-z0-9」。「w」同樣可以匹配下畫線,在某些系統中,它還可以匹配所有的Unicode字符,但是我們知道,這個程序在運行時不會遇到這些問題。

不過,URL通常不是以http://或者mailto:開頭的,例如:

…visit us at www.oreilly.com or mail to [email protected]

在這種情況下,我們需要加倍小心。我在Yahoo!使用的正則表達式與前面那節的非常相似,只是有一點點不同:

在這個正則表達式中,我們用「(?i:…)」和「(?-i:…)」用來規定正則表達式的某個部分是否區分大小寫(☞135)。我們希望匹配『www.OReilly.com』,但不是『NT.TO』這樣的股票代碼(NT.TO是北電網絡在多倫多證券交易市場的代號,因為要處理的是財經新聞和數據,這樣的股票代碼很多)。按規定,URL的結尾部分(例如『.com』)可能是大寫的,但我不準備處理這些情況。因為我需要保持平衡——匹配期望的文本(盡可能多的URL),忽略不期望的文本(股票代碼)。我希望「(?-i:...)」只包括國家代碼,但是在現實中,我們沒有遇到大寫的URL地址,所以不必這麼做。

下面是從純文本中查找URL的框架,我們可以在其中添加匹配主機名的子表達式:

我還沒有談論過正則表達式的 path(路徑)部分,它接在主機名後面(例如 http://www.中的劃線部分)。path是最難正確匹配的文本,因為它需要一些猜測才能做得很漂亮。我們在第 2 章說過,通常出現在 URL 之後的文本也能被作為URL的一部分。例如:

Read his comments at http://www.oreilly.com/ask_tim/index.html.He...我們觀察之後就會發現,在『index.html』之後的句號是一個標點,不應該作為URL的一部分,但是『index.html』中的點號卻是URL的一部分。

肉眼很容易分辨這兩種情況,但程序做起來卻很難,所以必須想些聰明的辦法來盡可能好地解決問題。第2章的例子使用逆序環視來確保URL不會以句末的句號結尾。我在Yahoo!Finance寫程序時還沒有逆序環視,所以我用的辦法要複雜的多,不過效果是一樣的。代碼在下一頁。

示例5-1:從財經新聞中提取URL

這裡用到的辦法與第2章第75頁用到的辦法有很多不同,比較起來也很有意思。下一頁裡使用此表達式的Java程序詳細介紹了它的構造。

在實際生活中,我懷疑自己是否會寫這樣繁雜的正則表達式,但是作為取代,我會建立一個正則表達式「庫(library)」,需要時取用。這方面一個簡單的例子就是第 76 頁的$HostnameRegex,以及下面的補充內容。