文字比對工具

您輸入的內容不會被儲存、記錄或分享。

貼上兩段文字,即可逐行、逐字看出變更之處。所有處理都在瀏覽器中完成,兩份內容都不會上傳。

使用方式

  1. 左側貼上原始內容,右側貼上新版本。
  2. 差異會顯示在下方,可選擇並排或統一檢視。
  3. 若空白或大小寫不應計入差異,請使用忽略選項。

比較是如何計算的

兩段文字都會切成行,再以最長共同子序列找出共同的行,這與 Unix diff 指令的基礎概念相同。子序列中的行保留原位,剩下的則回報為從左側刪除或在右側新增。當刪除與新增剛好相鄰時,會配成一對顯示為修改,而不是兩個互不相干的事件。

每一對修改接著會以單字層級再比對一次,所以長行中只有一個數字改變時,只會標出那個數字,而不是整行變紅。這就是「讀得懂的差異」與「得瞇著眼看的差異」之間的差別,也是逐行比對工具用在文章與設定檔上顯得不太有用的原因。

在這一切之前,開頭與結尾完全相同的行會先被去除。因此兩個只有一處不同、各有一千行的檔案,實際上只會比較寥寥數行,既快又清楚。

看起來一樣的兩行卻被標為不同時

這是最常見的意外,而原因永遠是你看不見的字元。最常見的元凶是行尾空格,所以「忽略行尾空白」預設是開啟的。其次是換行字元:在 Windows 上寫的檔案以歸位加換行(CRLF)結尾,在 macOS 或 Linux 上寫的只用換行(LF),直接比較原始位元組的 diff 會判定每一行都有變更。

再來是 Tab 與連續空格的差別,這可以用「忽略空白」選項合併;文書處理軟體與部分網頁用來取代一般空格的不換行空格;自動校正替換成的彎引號與短破折號;以及隱藏在第一行開頭的 BOM。這些都會讓畫面上看起來一模一樣的兩行,在檔案中確實不同。

大小上限

最長共同子序列的時間複雜度是平方級:比較 m 行與 n 行,要填滿一張 m 乘 n 的表格。幾百行時微不足道,一萬行對一萬行就是一億格,填表期間頁面會停止回應。

因此在去除共同的開頭與結尾之後,比較的上限設定為四百萬格,超過的比較會直接顯示訊息拒絕,而不會嘗試執行。實務上,只要內容大致相似,數千行的檔案都能瞬間比較完成,只有兩份真正毫不相干的大型文件才會碰到上限。遇到這種情況,適合的工具是可以串流處理的命令列 diff。

並排檢視或合併檢視

並排檢視把兩個版本放在各自的欄位中,較適合文章、翻譯,以及任何你想把兩段文字當作文字來閱讀的內容。合併檢視則加上標記交錯排列,這是修補檔與程式碼審查使用的格式,當變更少且分散在長檔案各處時更容易追蹤。

兩種檢視都不會改變計算結果,只是同一個結果的兩種呈現方式。

差異比對背後的演算法

經典做法由 Eugene Myers 在 1986 年發表,它會在兩個序列之間找出最短編輯腳本,也就是把其中一個變成另一個所需的最少插入與刪除組合。Unix 的 diff 指令和 Git 的預設實作用的就是它,而就產生最少變更這個意義來說,它是最佳解。

但變更最少不見得最好讀。Myers 演算法可能把剛好長得一樣卻毫無關聯的行對在一起,這裡一個右大括號、那裡一個空行,結果產生的差異把兩個函式交錯在一起,而不是清楚呈現一個被刪掉、一個被新增。Git 的 patience 演算法針對這點做了改善:它以在兩個版本中都只出現一次的行作為錨點,再在錨點之間遞迴處理,通常就能對上讀者心中真正的對應關係。

histogram 演算法是 Git 目前建議的替代方案,它在 patience 的基礎上加入了出現頻率的啟發式判斷,處理大檔案時更快。如果程式碼審查的差異看起來莫名其妙,把 diff.algorithm 設成 histogram,往往能從完全相同的兩個檔案得出更清楚的結果。

雙向比對與三方合併

比較兩個檔案能回答改了什麼。合併則需要第三個輸入,也就是共同祖先,因為少了它就無從判斷哪一邊改了某一行、哪一邊只是保留原狀。這正是版本控制要保存合併基準的原因,也是三方合併工具要顯示三個視窗的原因。

當兩邊相對於那個祖先都改動了同一段內容時,衝突就出現了。其餘的一切都會自動合併,包括相鄰但沒有重疊的改動,這也是為什麼兩個人可以整天編輯同一個檔案卻相安無事,然後在某一行上撞在一起。

理解了這點,解衝突的方式也會跟著改變。衝突檔案裡的標記顯示的是你的版本和對方的版本,但真正有用的問題是:相對於基準版本,雙方各自想做的是什麼。git checkout --conflict=diff3 會把基準版本也加進標記中,通常能讓解法變得顯而易見,而不是在兩個選項之間瞎猜。

比對那些不是文字行的東西

以行為單位的比較,前提是行的邊界本身有意義。壓縮過的 JavaScript、擠成一行的 JSON 內容、一長段散文,全都打破了這個前提:只改一個字元,整行都會被報成不同。先把兩邊都做美化排版,就能還原差異比對所需要的結構;而對 JSON 來說,轉成正規形式,也就是鍵排序、縮排一致,能讓比較結果真正有意義,而不是碰運氣。

CSV 的問題正好相反:以行為單位的比對能動,卻會把欄位順序調換報成每一列都改過了。先用某個鍵欄位排序,或改用理解 CSV 結構的工具,就能把你真正在意的改動和雜訊分開。

二進位或接近二進位的格式,各自需要專屬的轉換器。.docx 其實是一包 XML 的 zip,notebook 是內嵌輸出結果的 JSON,而 PDF 大致上是看不透的。Git 支援 textconv 過濾器,可以在比對前先把檔案轉成文字,團隊就是靠這個方式,在不必另外存一份副本的情況下,得到文件和 notebook 可讀的差異。

讀別人寫出來的差異

一份好審查的差異,關鍵多半在於裡面沒有什麼。格式噪音,像是對整個檔案跑了格式化工具、換行符號被改掉、套用了 import 排序器,會把真正重要的三行埋在三百行無關緊要的內容底下,而通常的解法是把機械性的提交和實質性的提交分開,讓兩者都能以各自合適的速度被閱讀。

如果你還是拿到了混在一起的差異,忽略空白的比較是最快的穿越方式:git diff -w,或是這裡提供的忽略選項。核准之前記得把它關掉,因為在某些檔案裡空白就是內容,例如 Python 的縮排、YAML 的結構、Makefile 的 tab,以及任何對輸出做精確比對的測試。

另一個值得特別處理的情況是大規模的檔案搬移與更名。一個被移動又編輯過的檔案,如果工具沒有偵測到更名,就會呈現成一次刪除加一次新增;Git 的 -M 參數,或是懂得辨識更名的審查介面,能把上千行的差異變回真正改動的那十行。

把文字比對當成編輯工作

不是每一次比對都跟程式碼有關。比較兩份合約草稿、兩份翻譯,或是一份文件編輯前後的樣子,都是常見用途,而並排檢視就是為此而存在的:你想把兩邊當成文章來讀,而不是當成一份修補檔。

對散文而言,以詞為單位的標示承載了大部分價值,因為改寫過的句子會以改寫過的句子呈現,而不是一行被刪、一行被加。能在同一行內把這件事做好的工具,才是大家覺得真正能拿來做編輯工作的工具,這也是為什麼這裡的比較會在每一組有變動的行內,再跑一次以詞為單位的比對。

關於文件,還有兩個實務上的提醒。從文書處理軟體貼過來的文字,常常帶著另一份副本沒有的彎引號、連接號和不斷行空格,於是明明看起來一模一樣,卻冒出差異。另外,同一份原稿匯出兩次,也可能只是斷行方式不同,所以比較前先把換行符號正規化,可以省下很多困惑。

常見問題

差異是如何計算的?
透過尋找行的最長共同子序列,與 diff 指令的原理相同。兩邊都有的行維持原位,其餘則標示為新增或刪除;當刪除與新增相鄰時,會配對成一次「變更」。
為什麼變更行中的個別單字也會標示?
只知道某一行有變動通常不夠。工具會對每組變更再做一次單字層級的比對,因此長行中單一處的更動不會被埋沒。
忽略選項的作用是什麼?
「忽略大小寫」將大小寫視為相同。「忽略空白」會合併連續的空格與定位字元,適合縮排改變但內容未變的情況。「忽略行尾空白」預設開啟,因為看不見的行尾空白幾乎不是真正的差異。
有大小限制嗎?
比對的複雜度與差異行數的平方成正比,因此會先略過開頭與結尾相同的部分,並在過大時直接拒絕而非讓頁面凍結。實務上數千行的檔案可即時比對。
我的文字會被上傳嗎?
不會。兩個版本都留在您的瀏覽器中,因此可安全比對合約、日誌與尚未發布的程式碼。

最後更新:

回報問題

請提供操作步驟、預期結果及實際結果。請移除機密與個人資料。

相關工具