原來判斷字串是不是 URL 超難 - 談 Evil Regexes
WriterShelf™ is a unique multiple pen name blogging and forum platform. Protect relationships and your privacy. Take your writing in new directions. ** Join WriterShelf**
WriterShelf™ is an open writing platform. The views, information and opinions in this article are those of the author.
Article info
Categories:
Tags:
Date:
Published: 2019/10/06 - Updated: 2019/10/06
Total: 2052 words
Like
or Dislike
About the Author
很久以前就是個「寫程式的」,其實,什麼程式都不熟⋯⋯
就,這會一點點,那會一點點⋯⋯
More to explore
很多事情,真的是不遇到不知道,寫個電腦程式也真的是太難了。
題目:需求超簡單,給一個字串,判斷是不是合法的 URL,傳回真假。
用 RegExp,竟然有可能超慢,我還以為當機了
第一個直覺就是用 RegExp 寫,比較字串,就是 RegExp 的本業,剛好我的需求是在前端,就用 JavaScript 寫了一個,只是寫好後,問題不斷,原來要判斷一個字串是不是 URL,原來那麼的難。
用 RegExp 的第一個問題就是中文判斷,這個問題還好,網路有高人,原來的 RegExp 加上就好了,只是很不好加,RegExp 寫在 JavaScript 上,只能說好像在寫火星文,一堆反斜線,寫得好累,各位如果有興趣,我是參考這篇。
接下來的問題就大了,也學到一件事,我發現在長字串的情形下,RegExp 有可能會超級慢,慢到你以為電腦當機了,來,看碼:
上面的程式夠簡單吧,就只是判斷 URL 的 host 而已,這是我在 Chrome console 上的測試的結果,第一個
patter.test('query')
只花了不到 0.004 秒,很好,但是可怕的是第二個,patter.test('query-b17.....')
竟然花了 5 分 5 秒,而且在這個 5 分 5 秒中,Chrome 是完全沒有反應的,剛開始我完全沒有預期到這樣的問題,直覺上,我以爲電腦當機了,各位看到的這串碼已經是我用最土的 divide and conquer 除錯法找出的問題點,怎麼看都沒有問題,但是,它卻有著大大的問題,也托這個問題的福,我學到了 RegExp 中更深一層的問題,這是一個典型的「毀滅性回尋(Catastrophic Backtracking)」,這是我的翻譯,更多人就直接用 Evil(惡魔)Regexes 來稱呼了。惡魔 Evil Regexes 簡單來說就是配對時,RegExp 開始不斷的重複找尋,而且這個問題更進一步的延伸成一種攻擊:regular expression denial of service,ReDoS - Wikipedia。
惡魔 Evil Regexes 有點難介紹,網路有人介紹的很好,我就不寫了。要更了解,可以先看這篇:https://stackoverflow.com/questions/12841970/how-can-i-recognize-an-evil-regex/ ,寫的簡單好懂,如果要看長篇大論,這篇不錯:Runaway Regular Expressions: Catastrophic Backtracking 。
好了,重點來了,這個問題非常難解,除非你能限制輸入的字串,只要用 RegExp 你早晚會遇到,唯一能做的就是,小心小心再小心的處理 * 及 +,再多多測試,如果不能接受這種風險,就只能放棄用 RegExp 判斷字串了。
題外話
這讓我想到大學上 Compiler 課時老師所教的 Pumping Lemma,這是定義 Regular Languages 中很重要的基礎理論,開發 Compiler 時,用來判斷新設計的電腦語言中有沒有 ambiguity 的關鍵理論,當時就被 Pumping Lemma 搞得很昏,沒想到多年以後,這個 RegExp 更難相處。 記得以前有個教授說,電腦不用學,因為它只會越來越簡單使用,這句話對了一半,用的人是越來越簡單用了,但是寫程式的人可是越來越難啊。 話說,抓這隻蟲時,又讓我想到我多年前老闆的名言:「程式寫的好,是不需要 debug 的。」我很遜,我到現在程式都寫不好。
用 Server 讀取判斷,有實務困難
RegExp 有危機,那就換用 AJAX 呼叫 Server 用 cURL 或是 NET HTTP 來做實際的 HTTP URL 確定,只要取得 HEAD 就可以判斷 URL 是正確的,多簡單啊,正準備動手寫時,還好先休息了一下,才想到這也不是一個好方法,各位想也知道,會要判斷字串是不是合法的 URL,接下來的動作八九不離十,就是要讀取這個網址,這樣就會照成連續讀取這個 Server,通常,Server 都會有保護機制,連續讀取幾次,很容易就會被判斷成惡意攻擊,我自己的網站都會做判斷,大型網站更是嚴格,而且一旦被列入黑名單,那就不只是 debug 的問題了,要改正,更難,此路不通啊。
用 JavaScript API 或是 DOM 一定要有 protocol
又回到網路找救兵後,發現這個問題其實已經在 StackOverflow 上討論的沸沸揚揚,甚至已經被鎖文規定一定要有 10 點積分的才可以發言,太有趣了,原來大家都有這樣的問題,好,那既然 RegExp 不好用,Server 實際讀取也不實際,我們來看看這則沸沸揚揚的 StackOverflow 上的討輪提供了那些其他選項:
https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url/49849482
我對其中的兩個選項很感到興趣,一個是用 Dom Parser,另一個是用 URL API,我就來試試兩者有何好壞不同。
比較 Dom Parser 與 URL Constructor
用 Dom Parser ,就先建立一個 anchor 後,只要設定它的 href 值後,是不是 URL 就可以用 host 讀出來,如果是 URL,host 就會應相對應的 host name,如果不是,就回應現在的瀏覽器所在的 host name,或是空字串。
用 URL Constructor 就直接 new 一個 URL 就好了,沒有錯,就是對的 URL,來試試,這麼簡單,就直接用 Chrome console 來試就好:
其實兩者的測試結果幾乎相同,很不妙的是,如果開頭沒有 http 就一定不是被認定是 URL,在定義上來說,這是對的,只是在實務上,好像可以在變通一下,對付它有一個簡單的方法,就是將 “http://" 先加上後再做測試,只是.......不行,讓我們看看這些測試中,那些是最有問題的:
所以,如果你要測試的可能 URL 字串是不含 http(s):// 的,這方法不能用,天啊,怎麼測試一個字串是不是 URL 那麼難啊!
用 match,就這個吧
我真的沒輒了,最後用的是這個方法,也是在這篇沸沸揚揚的 StackOverflow 討論中 copy 下來的,不完美,但是我真的也想不到更好的方法了:
最大的問題就是會把 email 當成合法的網址,ip 也不能測試,不過它沒有 Evil(惡魔)Regexes 的「慢」問題,也可以測試不含 http(s):// 的 URL 字串,已知的問題就來給他後面避開,畢竟這應該是我已知的最好解法了。
哎,很多事情,真的是不遇到不知道,原來那麼難。