[JavaScript][Regex] 去除煩人的隱藏字元

這裡要講的是一個真的是內行人才知道的問題及解決方法。其實並沒有什麼太大的學問, 但就是要知其所以然, 才能找到解法。

背景故事

我大概從十幾年前就遇到過類似的狀況。一些內容提供者為了不希望別人去爬他的資料, 於是在文字裡加入一些隱藏的字元; 外表上看起來都是正確的, 但當你試圖去解析時, 就會出現錯誤。最常見的方法, 就是在文字中穿插白色或透明的文字。

但這其實是掩耳盜鈴的做法。網際網路是公開的, 你把資料放上去之後, 任何人都可以查閱; 你怎麼會認為程式設計師這樣就無法解析呢?

儘管如此, 類似的事例仍然經常發生, 以致於在網路上很容易就可以找到這類的討論。

不過我這裡要講的事倒不像是刻意人為的, 只是現象相同而已。

現象

當我們遇到奇怪的事情時, 有時候必須靜下心來, 反覆驗證, 才能找到真正的問題。

以下圖為例, 我怎麼檢查, 都無法理解為何應該找得到的字, 竟然都無法順利地使用 Regex 找到:

上面我使用的是我自己用 JavaScript 寫的 Regex Test Bed。照理說, 這個 pattern 應該能順利找到 "2020年6月18日" 這個字串片段。然而, 不管我怎麼檢查 pattern, 就是找不到。

這時, 我們就不能再繼續懷疑 pattern 本身了。我開始懷疑輸入文字了。

我採用了一個方法, 就是把 Input 欄位的字清空, 然後自行輸入; 它原來是從網頁上複製貼上的純文字。結果果然是可以找到的:

換句話說, pattern 沒有問題, 是受測文字出了問題!

但我又是怎麼發現的? 那是因為我試著透過鍵盤左右鍵, 發現在文字上逐一移動時, 它不會按一下跳一個字。也就是說, 如果我把游標移到最前面, 再按往右鍵, 照理說游標會按一下就移一個位置, 但是它到達「年」、「月」、「日」三個字的前後時, 都必須額外再按一下, 游標才會往右。很明顯地, 它在那六個地方都蔵有隱形的字元。

隱形字元現形記

現在我們知道有隱形字元了。但到底是什麼字元呢?

在網路上有很多可以幫忙將字串編碼的工具網站, 我找到的是 https://www.url-encode-decode.com/。將文字複製起來, 再到網站上將這段文字加以編碼, 結果如下:

我把編碼的結果複製到文字編輯程式後, 再把它拆開:

大家​可以看到, 在「年」、「月」、「日」三個中文字的前後都出現了奇怪的重複字串 "%E2%80%8E"。

把 "%E2%80%8E" 拿去 Google 之後, 果然發現網路上早有許多討論。它實際上就是 &lrm 字元 (\u200e), 原本就是一種不可見的排版符號。這種字元即使以純文字方式複製, 仍然不會消失。而這也就導致了前面無法以 Regex 檢出的結果。如果把 \u200e 當作 pattern, 就能讓這個字元完全現形:

不過另外有一個更簡單、更無腦的方法, 可以輕易地看出字串中有沒有不可見的字元。打開 Chrome, 按下 F12 以叫出開發者工具, 在 Console 裡直接把字串貼上去, 就可以看出來了:

解決的方法

要解決當下的這個問題, 最快的方法, 就是把這個字元從輸入字串中移除, 如下列程式:

function removeInvisibleChars(input) {
  if (!input)
    return '';
  if (input.isString() && !input.trim())
    return '';
  return input.replace(/\u200e/g, '');
}

其中 isString() 是我自己寫的字串型別判斷程式:

Object.prototype.isString = function() {
  return this instanceof String;
}

如果你要考慮的不只是這個字元的話, 可以使用以下這個更複雜的程式:

function removeInvisibleChars(input) {
  if (!input)
    return '';
  if (input.isString() && !input.trim())
    return '';
  return input.replace(/[\u2000-\u200a\u202f\u2800]/g, ' ')
    .replace(/([^\u0020-\u007e\u3400-\uffff])/g, '');
}

當然, 更複雜的程式便意味著更長的執行時間。不過這個程式會把一些不常見的空白字元置換為正常的空白字元, 此外, 只留下常用符號、阿拉伯數字、英文字和正常的中文字, 其餘全部濾掉。如果有需要, 你可以再自行修改。

先把輸入予以預先處理, 然後你就可以依照既有的方式去解析字串了。


Dev 2Share @ 點部落