iT邦幫忙

0

應用系統建置前準備工具 - TaiwanIDAddress 臺灣地址與身分證檢核工具

  • 分享至 

  • xImage
  •  

TaiwanIDAddress 臺灣地址與身分證工具類別

概述

TaiwanIDAddress 是一個用於處理臺灣地址格式化和身分證/居留證驗證的工具類別。此類別提供地址正規化、地址解析和驗證,以及身分證號碼和居留證號碼的驗證功能,特別適合需要處理臺灣地址和身分證資料的應用系統。

專案相關程式

  • GlobalConstants

第三方元件(Dependency)

  • org.apache.commons:commons-lang3

主要功能

1. 地址正規化

基本正規化

// 正規化地址字串
String rawAddress = "臺北市中山區 123號";
String normalized = TaiwanIDAddress.normalize(rawAddress);
// 輸出:臺北市中山區123號

地址格式驗證

// 驗證地址格式
String address = "臺北市中山區南京東路一段128號";
boolean isValid = TaiwanIDAddress.isValid(address);
// 輸出:true

2. 地址解析

解析地址組成

// 解析地址各部分
String address = "106臺北市大安區仁愛路四段1號";
Map<String, String> parts = TaiwanIDAddress.parse(address);
// 輸出:
// postal: "106"
// city: "臺北市"
// district: "大安區"
// rest: "仁愛路四段1號"

3. 身分證號碼驗證

身分證/居留證驗證

// 驗證身分證號碼
String id = "A123456789";
boolean isValid = TaiwanIDAddress.isValidIDorRCNumber(id);

// 驗證居留證號碼
String rcNo = "AB12345678";
boolean isValidRC = TaiwanIDAddress.checkResidentID(rcNo);

系統需求

相依套件

  • org.apache.commons:commons-lang3

運行環境

  • 支援 Unicode NFKC 正規化

格式規則

1. 地址正規化規則

  • 全形轉半形(數字、英文字母、空白)
  • 統一「臺」字使用
  • 移除多餘空白與標點
  • Unicode 正規化 (NFKC)

2. 地址驗證規則

  • 必須包含縣/市與鄉/鎮/市/區
  • 或包含道路/門牌關鍵字(路、街、巷、弄、號等)
  • 允許有郵遞區號(3-5碼)

3. 身分證規則

  • 第一碼:英文字母(A-Z,代表縣市)
  • 第二碼:1(男)或 2(女)
  • 第三到九碼:數字
  • 第十碼:檢查碼

注意事項

1. 地址處理

  • 不保證完整涵蓋所有地址形式
  • 某些特殊地址可能需要人工處理
  • 建議搭配官方行政區資料使用

2. 身分證驗證

  • 支援新舊式居留證格式
  • 僅驗證格式,不代表實際存在
  • 注意資料保護與隱私考量

3. 效能考量

  • 大量地址處理時注意正規表示式效能
  • 考慮快取常用地址解析結果
  • Unicode 正規化可能影響效能

4. 相容性

  • 支援繁簡體混合地址
  • 處理各種標點符號變體
  • 相容新舊地址書寫習慣

進階用法

自定義地址解析

// 使用自定義的 Pattern 處理特殊格式
Pattern customPattern = Pattern.compile("您的pattern");
Matcher matcher = customPattern.matcher(address);
if (matcher.matches()) {
    String city = matcher.group("city");
    // 處理解析結果
}

地址格式化範例

// 結合解析結果重新格式化地址
Map<String, String> parts = TaiwanIDAddress.parse(address);
String formatted = String.format("%s%s%s%s",
    parts.get("postal") != null ? parts.get("postal") + " " : "",
    parts.get("city"),
    parts.get("district"),
    parts.get("rest")
);

身分證字號驗證範例

// 驗證身分證或居留證
if (TaiwanIDAddress.isValidIDorRCNumber(id)) {
    // 驗證通過,進一步處理
    if (id.charAt(1) == '1') {
        System.out.println("男性身分證");
    } else if (id.charAt(1) == '2') {
        System.out.println("女性身分證");
    } else if (id.charAt(1) >= '8') {
        System.out.println("新式居留證");
    }
}

## 單元測試範例

以下測試範例示範地址正規化、解析與身分證驗證:

### 1. 地址正規化與解析
```java
@Test
void testNormalizeAndParse() {
    String raw = "臺北市中山區 123號";
    String normalized = TaiwanIDAddress.normalize(raw);
    assertEquals("臺北市中山區123號", normalized);

    Map<String,String> parts = TaiwanIDAddress.parse("106臺北市大安區仁愛路四段1號");
    assertEquals("106", parts.get("postal"));
    assertEquals("臺北市", parts.get("city"));
}

2. 身分證/居留證驗證

@Test
void testIdValidation() {
    assertTrue(TaiwanIDAddress.isValidIDorRCNumber("A123456789"));
    assertFalse(TaiwanIDAddress.isValidIDorRCNumber("A12345678X"));
}

測試說明

  • 驗證地址正規化流程與解析結果
  • 驗證身分證與居留證的格式與檢查碼

程式碼 TaiwanIDAddress.java


package tw.lewishome.webapp.base.utility.common;

import java.text.Normalizer;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;

/**
 * 臺灣地址正規化與簡易檢核工具
 *
 * 功能: - normalize: 將輸入地址做基本正規化(全形轉半形、統一「臺->臺」、整理空白與連字符等) - isValid:
 * 依簡單規則判斷是否為合理的臺灣地址(可擴充) - parse: 嘗試抽出 郵遞區號、縣市、鄉鎮區 與其餘地址部分
 *
 * 注意:本類提供的是實用的輔助方法,不保證涵蓋所有例外與語意正確性。若需要精準分解(多音字、行政區最新變更),建議搭配官方行政區資料或專門的地址解析庫。
 */
public class TaiwanIDAddress {
    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new TaiwanIDAddress instance.
     * This is the default constructor, implicitly provided by the compiler
     * and can be used to create a new instance of the class.
     * 
     */
    public TaiwanIDAddress() {
        // Default constructor
    }

    // 常用道路/門牌關鍵字,用於簡易檢核
    private static final String[] ROAD_KEYWORDS = { "路", "街", "巷", "弄", "號", "段", "樓", "樓層" };

    // 抽出郵遞區號、縣市、鄉鎮區、其餘地址的簡單正則
    private static final Pattern PARSE_PATTERN = Pattern.compile(
            "^\\s*(?<postal>\\d{3,5})?\\s*(?<city>[^\\s\\d,,]{2,}?(?:縣|市))\\s*(?<district>[^\\s\\d,,]{1,}?(?:鄉|鎮|市|區))\\s*(?<rest>.*)$");

    // 更寬鬆的整體檢核(至少要有縣市與鄉鎮區)
    private static final Pattern BASIC_VALIDATE_PATTERN = Pattern.compile(".*(縣|市).*?(鄉|鎮|市|區).*");

    /**
     * 正規化地址字串: - trim - 將全形字元轉半形(含數字、英文字母與空白) - 消除多重空白、統一破折號 - 將「臺」替換為「臺」 - Unicode
     * 正規化 (NFKC) 以降低異形字影響
     *
     * @param rawAddress 原始地址
     * @return 正規化後的地址
     */
    public static String normalize(String rawAddress) {
        if (rawAddress == null)
            return null;
        // Unicode 正規化,先合併組合字元等
        String s = Normalizer.normalize(rawAddress, Normalizer.Form.NFKC);

        // 全形->半形轉換(數字、英文字母、部分符號、空白)
        s = toHalfWidth(s);

        // 統一臺->臺
        s = s.replace('臺', '臺');

        // 統一各種長短破折號為 "-"
        s = s.replace('-', '-').replace('—', '-').replace('–', '-').replace('〜', '~');

        // 移除多餘的標點前後空白,以及重複空白
        s = s.replaceAll("\\s*[,,:\\:]+\\s*", " ").replaceAll("\\s+", "").trim();

        return s;
    }

    /**
     * 簡單檢核地址是否看起來像臺灣地址 規則(可視需求擴充): - 需包含「縣/市」與「鄉/鎮/市/區」 - 或者整個字串符合常見路/街/巷/弄/號等關鍵字
     *
     * @param address 任意地址字串(會在檢核前做 normalize)
     * @return 若通過簡易檢核回傳 true;否則 false
     */
    public static boolean isValid(String address) {
        if (address == null)
            return false;
        String s = normalize(address);
        if (s.isEmpty())
            return false;

        // 必須包含縣市與鄉鎮區
        Matcher m = BASIC_VALIDATE_PATTERN.matcher(s);
        if (!m.find()) {
            // 若沒有縣市+鄉鎮區,仍嘗試看是否至少包含門牌/路名等
            for (String k : ROAD_KEYWORDS) {
                if (s.contains(k))
                    return true;
            }
            return false;
        }

        // 若包含縣市與鄉鎮區,進一步確認是否有路名或門牌(若無也視為較弱但仍可接受)
        for (String k : ROAD_KEYWORDS) {
            if (s.contains(k))
                return true;
        }

        // 沒有路關鍵字也算基本通過(例如僅縣市+區+裏?視情境可嚴格化)
        return true;
    }

    /**
     * 嘗試解析地址成 基本組成:postal, city, district, rest
     *
     * @param address 任意地址字串(會在解析前做 normalize)
     * @return Map (ordered):若無對應欄位則為 null 或空字串;無法解析則回傳空的 Map
     */
    public static Map<String, String> parse(String address) {
        Map<String, String> result = new LinkedHashMap<>();
        if (address == null)
            return result;
        String s = normalize(address);
        Matcher m = PARSE_PATTERN.matcher(s);
        if (m.matches()) {
            String postal = nullSafeTrim(m.group("postal"));
            String city = nullSafeTrim(m.group("city"));
            String district = nullSafeTrim(m.group("district"));
            String rest = nullSafeTrim(m.group("rest"));

            if (postal != null && postal.isEmpty())
                postal = null;
            result.put("postal", postal);
            result.put("city", city);
            result.put("district", district);
            result.put("rest", rest);
        }
        return result;
    }

    // ----- Helper methods -----

    private static String nullSafeTrim(String s) {
        return s == null ? null : s.trim();
    }

    /**
     * 將可能的全形字元轉為半形。 針對數字、英文字母、部分符號與全形空白做轉換。
     */
    private static String toHalfWidth(String input) {
        if (input == null || input.isEmpty())
            return input;
        StringBuilder sb = new StringBuilder(input.length());
        for (char ch : input.toCharArray()) {
            // 全形空白 U+3000 -> half-width space
            if (ch == '\u3000') {
                sb.append(' ');
                continue;
            }
            // 全形數字與字母範圍
            if (ch >= '\uFF01' && ch <= '\uFF5E') {
                // 轉回 ASCII 範圍
                char converted = (char) (ch - 0xFEE0);
                sb.append(converted);
                continue;
            }
            // 常見全形符號單獨處理
            switch (ch) {
            case ',':
                sb.append(',');
                break;
            case '。':
                sb.append('.');
                break;
            case ':':
                sb.append(':');
                break;
            case ';':
                sb.append(';');
                break;
            case '(':
                sb.append('(');
                break;
            case ')':
                sb.append(')');
                break;
            case '「':
                sb.append('\"');
                break;
            case '」':
                sb.append('\"');
                break;
            case '『':
                sb.append('\"');
                break;
            case '』':
                sb.append('\"');
                break;
            default:
                sb.append(ch);
            }
        }
        return sb.toString();
    }

     /*
     * 依據內政部: 1. 身分證編碼原則 2. 外來人口統一證號編碼原則(居留證)
     * https://www.immigration.gov.tw/ct_cert.asp?xItem=1106801&ctNode=32601&mp=1
     * 身分證及居留證通用 第一碼 縣市編碼原則: A=10 臺北市 J=18 新竹縣 S=26 高雄縣 B=11 臺中市 K=19 苗栗縣 T=27 屏東縣
     * C=12 基隆市 L=20 臺中縣 U=28 花蓮縣 D=13 臺南市 M=21 南投縣 V=29 臺東縣 E=14 高雄市 N=22 彰化縣 W=32
     * 金門縣 F=15 臺北縣 O=35 新竹市 X=30 澎湖縣 G=16 宜蘭縣 P=23 雲林縣 Y=31 陽明山 H=17 桃園縣 Q=24 嘉義縣
     * Z=33 連江縣 I=34 嘉義市 R=25 臺南縣
     * 
     * 第二碼 身分證: 1 男姓 2 女生 居留證: 臺灣地區無戶籍國民、大陸地區人民、港澳居民: A 男性 B 女性 外國人: C 男性 D 女性
     * 
     * 第三碼 身分證: 目前均為 0 居留證: 1 男性 2 女性
     * 
     * 其餘 4~9 碼均為數字 0...9
     * 
     * 最末碼 檢核碼 檢核碼為根據前九碼編碼加權後之計算產生,用以核對 (checksum) 字號正確性 檢核碼意義可以參考
     * http://finalfrank.pixnet.net/blog/post/19639058-身分證字號驗證方法
     * 
     * 身分證字號產生器 https://people.debian.org/~paulliu/ROCid.html,可用以檢核程式是否正確
     * 居留證號碼也是利用類似的概念,祇是第二碼英文字依第一碼原則取對照數字,祇取個位數,也就是 A 取 0, B 取 1, C 取 2, D 取 3
     *
     * 
     * @param str a  String  object
     * 
     * @return a boolean
     */
    /**
     * 檢查臺灣身分證或居留證號碼是否合法
     *
     * @param str 身分證或居留證號碼
     * @return boolean 是否合法
     */
    public static boolean isValidIDorRCNumber(String str) {

        if (StringUtils.isBlank(str)) {
            return false;
        }

        final char[] pidCharArray = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
                'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };

        // 原身分證英文字應轉換為10~33,這裡直接作個位數*9+10
        final int[] pidIDInt = { 1, 10, 19, 28, 37, 46, 55, 64, 39, 73, 82, 2, 11, 20, 48, 29, 38, 47, 56, 65, 74, 83,
                21, 3, 12, 30 };

        // 原居留證第一碼英文字應轉換為10~33,十位數*1,個位數*9,這裡直接作[(十位數*1) mod 10] + [(個位數*9) mod 10]
        final int[] pidResidentFirstInt = { 1, 10, 9, 8, 7, 6, 5, 4, 9, 3, 2, 2, 11, 10, 8, 9, 8, 7, 6, 5, 4, 3, 11, 3,
                12, 10 };

        // 原居留證第二碼英文字應轉換為10~33,並僅取個位數*8,這裡直接取[(個位數*8) mod 10]
        final int[] pidResidentSecondInt = { 0, 8, 6, 4, 2, 0, 8, 6, 2, 4, 2, 0, 8, 6, 0, 4, 2, 0, 8, 6, 4, 2, 6, 0, 8,
                4 };

        str = str.toUpperCase();// 轉換大寫
        final char[] strArr = str.toCharArray();// 字串轉成char陣列
        int verifyNum = 0;

        /* 檢查身分證字號 */
        if (str.matches("[A-Z]{1}[1-2]{1}[0-9]{8}")) {
            // 第一碼
            verifyNum = verifyNum + pidIDInt[Arrays.binarySearch(pidCharArray, strArr[0])];
            // 第二~九碼
            for (int i = 1, j = 8; i < 9; i++, j--) {
                verifyNum += Character.digit(strArr[i], 10) * j;
            }
            // 檢查碼
            verifyNum = (10 - (verifyNum % 10)) % 10;

            return verifyNum == Character.digit(strArr[9], 10);
        }

        /* 檢查統一證(居留證)編號 (2031/1/1停用) */
        verifyNum = 0;
        if (str.matches("[A-Z]{1}[A-D]{1}[0-9]{8}")) {
            // 第一碼
            verifyNum += pidResidentFirstInt[Arrays.binarySearch(pidCharArray, strArr[0])];
            // 第二碼
            verifyNum += pidResidentSecondInt[Arrays.binarySearch(pidCharArray, strArr[1])];
            // 第三~八碼
            for (int i = 2, j = 7; i < 9; i++, j--) {
                verifyNum += Character.digit(strArr[i], 10) * j;
            }
            // 檢查碼
            verifyNum = (10 - (verifyNum % 10)) % 10;

            return verifyNum == Character.digit(strArr[9], 10);
        }

        /* 檢查統一證(居留證)編號 (2021/1/2實施) */
        verifyNum = 0;
        if (str.matches("[A-Z]{1}[89]{1}[0-9]{8}")) {
            // 第一碼
            verifyNum += pidResidentFirstInt[Arrays.binarySearch(pidCharArray, strArr[0])];
            // 第二~八碼
            for (int i = 1, j = 8; i < 9; i++, j--) {
                verifyNum += Character.digit(strArr[i], 10) * j;
            }
            // 檢查碼
            verifyNum = (10 - (verifyNum % 10)) % 10;

            return verifyNum == Character.digit(strArr[9], 10);
        }

        return false;
    }

    /**
     * 檢查臺灣居留證號碼是否合法(支援舊式與新式)
     *
     * @param idNo 居留證號碼
     * @return boolean 是否合法
     */
    public static boolean checkResidentID(String idNo) {
        if (StringUtils.isBlank(idNo)) {
            return false;
        }
        idNo = idNo.toUpperCase();
        // 舊式: 第一碼英文,第二碼 A~D,後8碼數字
        // 新式: 第一碼英文,第二碼 8或9,後8碼數字
        if (!idNo.matches("^[A-Z][A-D89][0-9]{8}$")) {
            return false;
        }
        String alphabet = "ABCDEFGHJKLMNPQRSTUVXYWZIO";
        char firstLetter = idNo.charAt(0);
        char secondLetter = idNo.charAt(1);
        String num = idNo.substring(2);

        if ("ABCD".indexOf(secondLetter) >= 0) {
            // 舊式
            int firstCode = alphabet.indexOf(firstLetter) + 10;
            int secondCode = (alphabet.indexOf(secondLetter) + 10) % 10;
            String transferIdNo = String.valueOf(firstCode) + secondCode + num;
            int[] idNoArray = new int[transferIdNo.length()];
            for (int i = 0; i < transferIdNo.length(); i++) {
                idNoArray[i] = Character.getNumericValue(transferIdNo.charAt(i));
            }
            int sum = idNoArray[0];
            int[] weight = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 1 };
            for (int i = 0; i < weight.length; i++) {
                sum += weight[i] * idNoArray[i + 1];
            }
            return sum % 10 == 0;
        } else {
            // 新式
            int firstCode = alphabet.indexOf(firstLetter) + 10;
            String transferIdNo = String.valueOf(firstCode) + idNo.substring(1);
            int[] idNoArray = new int[transferIdNo.length()];
            for (int i = 0; i < transferIdNo.length(); i++) {
                idNoArray[i] = Character.getNumericValue(transferIdNo.charAt(i));
            }
            int sum = idNoArray[0];
            int[] weight = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 1 };
            for (int i = 0; i < weight.length; i++) {
                sum += (weight[i] * idNoArray[i + 1]) % 10;
            }
            return sum % 10 == 0;
        }
    }
}


單元測試程式碼 TaiwanIDAddressTest.java

package tw.lewishome.webapp.base.utility.common;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.Map;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class TaiwanIDAddressTest {

    @Test
    public void testNormalize() {
        assertNull(TaiwanIDAddress.normalize(null));
        assertEquals("", TaiwanIDAddress.normalize(""));
        assertEquals("台北市信義區", TaiwanIDAddress.normalize("台北市信義區"));
        assertEquals("台北市信義區信義路5段", TaiwanIDAddress.normalize("台北市信義區信義路5段"));
        assertEquals("台北市信義區信義路5段7號", TaiwanIDAddress.normalize("台北市,信義區,信義路5段7號"));
    }

    @Test
    public void testIsValid() {
        assertFalse(TaiwanIDAddress.isValid(null));
        assertFalse(TaiwanIDAddress.isValid(""));
        assertFalse(TaiwanIDAddress.isValid("abc123"));
        assertTrue(TaiwanIDAddress.isValid("台北市信義區"));
        assertTrue(TaiwanIDAddress.isValid("新北市板橋區中山路"));
        assertTrue(TaiwanIDAddress.isValid("台中市西屯區文心路二段"));
    }

    @Test
    public void testParse() {
        Map<String, String> result = TaiwanIDAddress.parse("40669 台中市北屯區文心路四段955號");
        assertEquals("40669", result.get("postal"));
        assertEquals("台中市", result.get("city"));
        assertEquals("北屯區", result.get("district"));
        assertEquals("文心路四段955號", result.get("rest"));

        result = TaiwanIDAddress.parse("台北市信義區信義路五段7號");
        assertNull(result.get("postal"));
        assertEquals("台北市", result.get("city"));
        assertEquals("信義區", result.get("district"));
        assertEquals("信義路五段7號", result.get("rest"));

        result = TaiwanIDAddress.parse(null);
        assertTrue(result.isEmpty());

        result = TaiwanIDAddress.parse("invalid address");
        assertTrue(result.isEmpty());
    }


    @Test
    @DisplayName("isValidIDorRCNumber: test valid and invalid IDs")
    void testIsValidIDorRCNumber() {

        // Valid Taiwan ID
        assertTrue(TaiwanIDAddress.isValidIDorRCNumber("A123456789"));
        // Invalid Taiwan ID
        assertFalse(TaiwanIDAddress.isValidIDorRCNumber("A123456788"));
        // Valid old RC number
        assertTrue(TaiwanIDAddress.isValidIDorRCNumber("AB12345677"));
        // Invalid old RC number
        assertFalse(TaiwanIDAddress.isValidIDorRCNumber("AB12345679"));
        // Valid new RC number
        assertTrue(TaiwanIDAddress.isValidIDorRCNumber("A812345671"));
        // Invalid new RC number
        assertFalse(TaiwanIDAddress.isValidIDorRCNumber("A812345679"));
        // Null and empty
        assertFalse(TaiwanIDAddress.isValidIDorRCNumber(null));
        assertFalse(TaiwanIDAddress.isValidIDorRCNumber(""));
        // Invalid format
        assertFalse(TaiwanIDAddress.isValidIDorRCNumber("1234567890"));
    }

    @Test
    @DisplayName("checkResidentID: validate resident ID numbers")
    void testCheckResidentID() {
        // Test old format resident IDs (with A-D as second character)
        assertTrue(TaiwanIDAddress.checkResidentID("AC12345679"));
        assertFalse(TaiwanIDAddress.checkResidentID("AD12345670")); // Invalid checksum

        // Test new format resident IDs (with 8-9 as second character)
        assertTrue(TaiwanIDAddress.checkResidentID("A800000014"));
        assertFalse(TaiwanIDAddress.checkResidentID("A912345670")); // Invalid checksum

        // Test invalid formats
        assertFalse(TaiwanIDAddress.checkResidentID(null));
        assertFalse(TaiwanIDAddress.checkResidentID(""));
        assertFalse(TaiwanIDAddress.checkResidentID("123456789")); // No letters
        assertFalse(TaiwanIDAddress.checkResidentID("AE12345678")); // Invalid second character
        assertFalse(TaiwanIDAddress.checkResidentID("A1234567")); // Too short
        assertFalse(TaiwanIDAddress.checkResidentID("AB123456789")); // Too long
        assertFalse(TaiwanIDAddress.checkResidentID("A912A45678")); // Invalid number format
    }
}

圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言