巴西的兩種稅號經常被放在一起討論,因為它們的校驗邏輯同源,參數卻不同。本文說明 CPF 與 CNPJ 各自的角色、兩種號碼在結構上的差別、校驗位的共通算法思路,以及為什麼「算得出來」與「有登記」是兩件必須分開講的事。
CPF 和 CNPJ 分別是什麼?
CPF 對應的是自然人,也就是個人;CNPJ 對應的是法人,也就是企業與其他登記主體。這是兩者最根本的差別,也決定了它們在表單上出現的場合:填個人資料時收 CPF,填公司資料時收 CNPJ。
兩者都帶校驗位,而且各自是兩位,而不是一位。這一點在設計輸入流程時很實際:只檢查最後一位的實作會少擋掉一整類抄錄錯誤,而只留一個欄位存放校驗結果的做法,也無法回報是哪一位不成立。
還有一個共通點:它們的校驗位都是由前面的位按公開方式算出來的,所以只要抄錯其中一位,重算的結果就不會吻合。這是它們可以被離線驗證的原因。
還有一個實務上的細節:這兩種號碼在文件上經常與其他識別碼並排出現,例如登記證號或營業執照號。排版時若只留一個通用欄位,使用者很容易把兩種稅號填進同一個位置,事後也難以判斷該用哪一組規則去驗。把欄位名稱寫清楚、並在提示文字裡說明各自對應的主體,能省下大量來回詢問。
兩種號碼的結構差異
從校驗實作的角度看,兩者的差別主要落在三個地方:主體部分的分段方式、加權序列的起點,以及需要算幾位校驗位。
| 面向 | CPF | CNPJ |
|---|---|---|
| 對應主體 | 自然人 | 法人 |
| 校驗位數量 | 兩位 | 兩位 |
| 分段方式 | 由制度定義 | 由制度定義,含法人專屬欄位 |
| 加權序列 | 由制度公布的序列 | 由制度公布的序列,與前者不同 |
| 常見展示 | 以分隔符號分組 | 以分隔符號分組 |
表裡刻意不寫具體位數與號段,因為那屬於制度公布的細節,實作時應該照官方說明,而不是照某一份二手整理。真正需要先確定的是:這兩種號碼不能共用同一組參數,因此也不能共用同一個校驗函式而不帶參數。
展示用的分隔符號是兩者共有的現象。點、斜線與連字號都是為了閱讀方便而加上去的,不屬於號碼本身,校驗前必須先清乾淨。
校驗位的算法思路:加權求和再取模
兩種號碼的校驗位用的是同一個骨架:把前面的位依一組權重相乘後相加,對一個模數取餘數,再用餘數或它的補數映射成一位校驗位。第一種號碼要算兩次,第二次計算時會把第一次算出來的那一位也一起納入。
第一次算出來的那一位,必須參與第二次的計算,順序反過來就不成立。這個「後一位依賴前一位」的設計讓抄錄錯誤更難僥倖通過,但也讓除錯變得比較麻煩:若第二位的結果不對,原因可能出在第一位算錯,而不是第二位的參數有問題。
差別在權重序列與模數。這兩個參數在兩種號碼之間不同,甚至在計算第一位與第二位校驗位時也可能不一樣。因此實作時最忌諱的做法,是把一組參數複製到另一種號碼上,然後期待結果正確。
家族層級的比較整理在校驗位演算法家族;本文只談這兩種稅號本身的差異,不重複家族成員之間的橫向比較。
為什麼全同數字要單獨排除?
如果只用簡單的模運算判斷,某些由同一個數字重複組成的序列是有機會通過的。這種序列顯然不是真正被核發過的號碼,所以正式規則通常會把它們明確排除在外。
這件事的意義不只是補一個漏洞,而是提醒我們:校驗位演算法本身不保證唯一性,制度可以在算式之外再加上額外的約束。實作時若只實作算式而漏掉這些附加條件,就會接受一批本來該被擋下的輸入。
反過來也一樣:附加條件的內容屬於制度規定,不該由實作者自行發明。你能確定的是「這類序列在正式規則裡被排除」這個結構性事實,而不是憑自己想像去列舉清單。
格式校驗與稅號登記狀態是兩件事
校驗通過只說明這串數字自洽。號碼有沒有真的被登記、目前狀態是正常還是已註銷、對應的是哪一個主體,這些都屬於另一層查詢,需要向登記體系確認,演算法答不了。
在介面上把三件事分開講,效果差別很大:格式是否吻合、校驗位是否成立、以及是否已向登記體系確認。把三者壓成一句「稅號無效」,最常見的後果是把一個格式完全正確、只是尚未查詢的號碼說成有問題。
把「本站沒有對應規則」也算進去的話,結論其實有四種可能。這種分態的寫法在沒有校驗位的號碼裡有更完整的說明。
還有一個常被忽略的層次是時效。登記狀態會隨時間改變,某個號碼在去年查詢時正常,今年可能已經註銷。校驗的結論不會隨時間變化,因為它只依賴號碼本身的位與算式;查詢的結論則必須附上查詢時間,否則無法解讀。
給開發者:清洗、遮罩與錯誤提示
處理這兩種稅號時,順序與提示語比算式更容易出問題。幾個反覆出現的坑:
- 先清掉點、斜線、連字號與空白,只留制度允許的字元,再進入校驗。
- 大小寫要統一,因為有些輸入會混入小寫字母。
- 錯誤提示要區分「位數不符」與「校驗位不成立」,兩者的修法不同。
- 顯示時可以遮罩中間部分,但比對時必須用完整值,否則重複項判斷會失真。
- 不要在校驗失敗時直接清空使用者的輸入,保留原值才能對照修正。
還有一點與權限有關:遮罩不該只做在畫面層。如果日誌或除錯訊息裡留著完整值,遮罩就只是裝飾。真正要守住的是「完整值不出現在不必要的地方」。
如果同一份表單同時收個人與公司的資料,還要決定「兩者都填」與「只填其一」哪一種是常態。這個決定會影響後端的必填檢查,也會影響錯誤訊息要先報哪一個欄位。先想清楚這件事,比事後在驗證邏輯裡加例外條件便宜得多。
下一步
把你的表單拆成兩個獨立的校驗設定,一個給 CPF、一個給 CNPJ,各自帶自己的權重與模數參數,不要讓它們共用一組。改完之後,用號碼校驗工具貼上一組明顯是構造值的示範號碼,確認它回報的位數與校驗位結果符合你的預期。
文中的稅號範例都是為了示範校驗邏輯而編造的,既不是任何真實自然人或企業的稅號,也不代表任何登記狀態、納稅資格或營業情形。