中國居民身份證號碼(18 位)包含地址碼、出生日期碼、順序碼、校驗碼四部分。本文深度解析校驗算法,提供生產就緒的 Java 實現,並探討數據清洗、性能優化等工程應用場景。
| 段落 | 位數 | 說明 | 標準 |
|---|---|---|---|
| 地址碼 | 1-6 | 戶籍地行政區劃代碼 | GB/T 2260 |
| 出生日期碼 | 7-14 | YYYYMMDD 格式 | GB/T 7408 |
| 順序碼 | 15-17 | 同地同日期順序,奇男偶女 | - |
| 校驗碼 | 18 | 根據前 17 位計算 | ISO 7064:1983.MOD 11-2 |
S = Σ(Ai × Wi),i=0..16Wi:[7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]
Y = S mod 11| Y 值 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 校驗碼 | 1 | 0 | X | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 |
public final class IdCardValidator {
// 權重因子常量
private static final int[] WEIGHTS = {7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2};
// 校驗碼映射表,索引 = Y 值
private static final char[] CHECK_CODES = {'1','0','X','9','8','7','6','5','4','3','2'};
private IdCardValidator() {} // 工具類防實例化
/**
* 計算 17 位本體碼對應的校驗碼
* @param id17 前 17 位數字字符串
* @return 校驗碼字符
* @throws IllegalArgumentException 輸入非 17 位數字
*/
public static char calculateCheckCode(CharSequence id17) {
if (id17 == null || id17.length() != 17) {
throw new IllegalArgumentException("本體碼必須為 17 位數字");
}
int sum = 0;
for (int i = 0; i < 17; i++) {
char c = id17.charAt(i);
if (c < '0' || c > '9') {
throw new IllegalArgumentException("本體碼包含非數字字符: " + c);
}
sum += (c - '0') * WEIGHTS[i]; // 字符轉數字比 parseInt 快
}
return CHECK_CODES[sum % 11];
}
/**
* 驗證 18 位身份證完整合法性
* @param id18 18 位身份證號碼
* @return true 表示格式正確且校驗碼匹配
*/
public static boolean isValid(String id18) {
if (id18 == null || id18.length() != 18) return false;
// 前 17 位必須為數字
for (int i = 0; i < 17; i++) {
char c = id18.charAt(i);
if (c < '0' || c > '9') return false;
}
// 第 18 位:數字或 X
char last = id18.charAt(17);
if (!((last >= '0' && last <= '9') || last == 'X' || last == 'x')) return false;
// 校驗碼比對(忽略大小寫)
char expected = calculateCheckCode(id18.subSequence(0, 17));
return Character.toUpperCase(last) == expected;
}
/**
* 從 18 位身份證提取出生日期
* @return LocalDate,格式錯誤返回 null
*/
public static java.time.LocalDate extractBirthDate(String id18) {
if (!isValid(id18)) return null;
try {
int year = Integer.parseInt(id18.substring(6, 10));
int month = Integer.parseInt(id18.substring(10, 12));
int day = Integer.parseInt(id18.substring(12, 14));
return java.time.LocalDate.of(year, month, day);
} catch (Exception e) {
return null;
}
}
/**
* 從 18 位身份證提取性別
* @return 1=男,0=女,-1=無效
*/
public static int extractGender(String id18) {
if (!isValid(id18)) return -1;
int seq = Integer.parseInt(id18.substring(14, 17));
return (seq & 1) == 1 ? 1 : 0; // 奇數男,偶數女
}
}
public class IdCardValidatorTest {
@Test
public void testCalculateCheckCode() {
assertEquals('3', IdCardValidator.calculateCheckCode("14230219700101101"));
}
@Test
public void testValidIdCards() {
assertTrue(IdCardValidator.isValid("142302197001011013")); // 校驗碼 3
assertTrue(IdCardValidator.isValid("11010119900307783X")); // 校驗碼 X
}
@Test
public void testInvalidIdCards() {
assertFalse(IdCardValidator.isValid("142302197001011012")); // 校驗碼錯誤
assertFalse(IdCardValidator.isValid("14230219700101101")); // 長度不足
assertFalse(IdCardValidator.isValid("14230219700101101Y")); // 非法校驗碼
}
}
在 ETL/數據遷移中,將身份證驗證作為第一道關卡:
// Spark/Flink 處理邏輯
dataset.filter(row -> IdCardValidator.isValid(row.getString("id_card")))
文中提到的只存前 17 位策略值得參考:
INNER JOIN 時字符串比較更快,索引更小char[] 而非 String 索引訪問c - '0' 避免 Integer.parseInt() 開銷WEIGHTS 到 long 位運算(SIMD 友好)15 位版本無校驗碼,出生年份為 2 位(如 90 表 1990)。轉換邏輯:
public static String upgrade15to18(String id15) {
if (id15 == null || id15.length() != 15) return null;
String id17 = id15.substring(0, 6) + "19" + id15.substring(6);
char check = calculateCheckCode(id17);
return id17 + check;
}
^\d{17}[\dX]$ 會通過大量偽造號碼