
本文详细介绍了基于仓颉语言(Cangjie)开发的身份证解析库
idcard的完整开发过程,从需求分析、架构设计、核心算法实现到工程实践,为仓颉三方库开发者提供一套可参考的实践方案。
在实际开发中,身份证信息的解析和验证是一个常见的需求场景:
市面上虽然有很多其他语言的身份证解析库,但仓颉作为一门新兴的编程语言,还缺少这方面的基础设施。因此,开发一个功能完整、易于使用的身份证解析库,对于仓颉生态的建设具有重要意义。
基于实际需求,我们确定了以下设计目标:
仓颉语言作为华为推出的全栈编程语言,具有以下特点:
// 函数签名清晰,类型安全
public func parse(id: String): ?HashMap<String, String>静态类型系统提供了编译期类型检查,减少了运行时错误,提高了代码的可靠性。
// 支持 Option 类型,优雅处理空值
match (result) {
case Some(info) => println("解析成功")
case None => println("解析失败")
}仓颉提供了丰富的标准库支持:
std.collection - 提供 HashMap、Array 等集合类型std.unicode - 字符串处理std.unittest - 单元测试框架作为编译型语言,仓颉具有接近 C/C++ 的执行效率,同时保持了较高的开发效率。
# cjpm.toml
[package]
name = "idcard"
version = "1.0.0"
cjc-version = "1.0.3"
output-type = "executable"
description = "身份证解析与验证库"采用经典的三层架构模式:
┌─────────────────────────────────────┐
│ 应用层 (Application Layer) │
│ 开发者调用接口 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 核心层 (Core Layer) │
│ idcard 模块 │
│ • parse() - 统一解析接口 │
│ • 自动类型识别 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 工具层 (Utility Layer) │
│ idcard.method 模块 │
│ • isValid() - 校验码验证 │
│ • parserChina() - 中国身份证 │
│ • parserInternational() - 外国 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 数据层 (Data Layer) │
│ • china.cj - 行政区划代码 │
│ • international.cj - 国家代码 │
└─────────────────────────────────────┘职责:提供统一的对外接口
package idcard
import idcard.module.parserChina
import idcard.module.parserInternational
import std.collection.HashMap
// 主解析函数
public func parse(id: String): ?HashMap<String, String> {
if (id.size != 18) {
return None
}
let idArr = id.split("")
// 检查是否为外国人永久居留身份证(9开头)
if (idArr[0] == "9") {
return parserInternational(id)
} else {
// 中国居民身份证
return parserChina(id)
}
}设计亮点:
?HashMap)优雅处理无效输入职责:实现具体的解析和验证逻辑
package idcard.module
import std.collection.HashMap
import std.unicode.*
import std.convert.*
import idcard.module.data.CHINA_ADMIN_DIVISIONS
import idcard.module.data.ISO3166_1
// 导入数据字典
private let _china = CHINA_ADMIN_DIVISIONS
private let _international = ISO3166_1设计亮点:
private let)封装数据字典职责:存储行政区划和国家代码数据
// china.cj - 中国行政区划代码
package idcard.module.data
import std.collection.HashMap
public let CHINA_ADMIN_DIVISIONS: HashMap<String, String> = HashMap<String, String>(
[("110000", "北京市"), ("110105", "朝阳区"),
("120000", "天津市"), ("130000", "河北省"),
// ... 完整的行政区划数据(2000+ 条)
]
)// international.cj - ISO3166-1 国家代码
package idcard.module.data
import std.collection.HashMap
public let ISO3166_1: HashMap<String, String> = HashMap<String, String>(
[("004", "阿富汗"), ("156", "中国"), ("392", "日本"),
("840", "美国"), ("826", "英国"),
// ... 完整的国际国家代码(250+ 条)
]
)设计亮点:
public let 定义编译期常量,性能最优idcard (核心层)
↓
idcard.module (工具层)
├─→ std.collection
├─→ std.unicode
├─→ std.convert
└─→ idcard.module.data (数据层)身份证号码的最后一位是校验码,基于 GB 11643-1999 国家标准计算。
[7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"]public func isValid(id: String): Bool {
if (id.size != 18) {
return false
}
let idLower = id.toLower()
var sum: Int64 = 0
let factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
let idArr = idLower.split("")
for (i in 0..17) {
let digit = idArr[i]
if (digit >= "0" && digit <= "9") {
sum += Int64(factor[i]) * (Int64.parse(digit) - Int64(0))
}
}
let lastLetter = ["1", "0", "x", "9", "8", "7", "6", "5", "4", "3", "2"]
var mod = sum % 11
if (idArr[0] == "9") {
mod = lastLetter.size - mod;
}
let expected = lastLetter[Int64(mod)]
let actual = idArr[17]
return expected == actual
}实现要点:
Int64 避免整数溢出// 示例:11010519491231002X
// 计算:1×7 + 1×9 + 0×10 + 1×5 + 0×8 + 5×4 + 1×2 + 9×1 + 4×6 + 9×3 + 1×7 + 2×9 + 3×10 + 1×5 + 0×8 + 0×4 + 2×2
// = 7 + 9 + 0 + 5 + 0 + 20 + 2 + 9 + 24 + 27 + 7 + 18 + 30 + 5 + 0 + 0 + 4
// = 167
// 取模:167 % 11 = 2
// 校验码:lastLetter[2] = "x" ✓身份证类型通过编码规则快速识别:
public func parse(id: String): ?HashMap<String, String> {
if (id.size != 18) {
return None
}
let idArr = id.split("")
// 判断逻辑:
// 1. 首位为 9 → 外国人永久居留身份证
// 2. 前 6 位以 81/82/83 开头 → 港澳台居民居住证(在 parserChina 中判断)
// 3. 其他 → 中国居民身份证
if (idArr[0] == "9") {
return parserInternational(id)
} else {
return parserChina(id)
}
}110105
││││└└─ 区县代码(2位)
││└└─── 市级代码(2位)
└└───── 省级代码(2位)public func parserChina(id: String): HashMap<String, String> {
var result = HashMap<String, String>()
let code = id[0..6]
let province_code = code[0..2] + "0000"
let city_code = code[0..4] + "00"
let district_code = code
// 特殊处理:直辖市(11/12/31/50)
if (code[0..2] == "11" || code[0..2] == "12" ||
code[0..2] == "31" || code[0..2] == "50") {
result["sign"] = _china.get(province_code).getOrDefault({=> ""}) +
(if (city_code != district_code) {
_china.get(district_code).getOrDefault({=> ""})
} else { "" })
} else {
// 省 + 市 + 区县
result["sign"] = _china.get(province_code).getOrDefault({=> ""}) +
(if (province_code != city_code) {
_china.get(city_code).getOrDefault({=> ""})
} else { "" }) +
(if (city_code != district_code) {
_china.get(district_code).getOrDefault({=> ""})
} else { "" })
}
return result
}设计考虑:
getOrDefault 处理数据字典中不存在的代码// 出生日期:第 7-14 位(YYYYMMDD)
result["birthday"] = id[6..14]
// 性别:第 17 位(倒数第 2 位)
result["sex"] = (id[16] % 2 == 1).choose("男", "女")扩展方法:为了让代码更加简洁,我们为 Bool 类型添加了 choose 扩展方法:
extend Bool {
func choose<T>(trueVal: T, falseVal: T): T {
return if (this) { trueVal } else { falseVal }
}
}这种扩展方法的设计让代码更具可读性,类似于三元运算符但更加符合仓颉的风格。
public func parserInternational(id: String): HashMap<String, String> {
var result = HashMap<String, String>()
result["type"] = "外国人永久居留身份证"
// 第 2-3 位:省级代码
result["sign"] = _china.get(id[1..3] + "0000").getOrDefault({=> ""})
// 第 4-6 位:国家/地区代码(ISO 3166-1)
let countryVal = _international.get(id[3..6]).getOrDefault({=> ""})
if (!countryVal.isEmpty()) {
result["country"] = countryVal
} else {
result["country"] = "无国籍"
}
result["birthday"] = id[6..14]
result["sex"] = (id[16] % 2 == 1).choose("男", "女")
result["isValid"] = if (isValid(id)) { "true" } else { "false" }
return result
}编码结构:
9 11 840 19900101 123 4
│ │ │ │ │ │
│ │ │ │ │ └─ 校验码
│ │ │ │ └───── 顺序码+性别(末位奇数男/偶数女)
│ │ │ └────────────── 出生日期 YYYYMMDD
│ │ └────────────────── 国家代码(ISO 3166-1,如 840=美国)
│ └───────────────────── 省级代码(居留地)
└─────────────────────── 固定为 9idcard/
├── src/
│ ├── idcard.cj # 核心模块(统一接口)
│ ├── idcard_test.cj # 核心模块单元测试
│ ├── main.cj # 示例程序入口
│ └── module/
│ ├── method.cj # 工具函数(验证、解析)
│ ├── method_test.cj # 工具模块单元测试
│ └── data/
│ ├── china.cj # 中国行政区划数据
│ └── international.cj # 国际国家代码数据
├── doc/
│ ├── design.md # 设计文档
│ ├── feature_api.md # API 文档
│ └── assets/
│ └── example.png # 示例图片
├── test/
│ ├── HLT/ # 高级别测试(集成测试)
│ │ └── testcase0001.cj
│ └── LLT/ # 低级别测试(单元测试)
│ └── testcase0001.cj
├── cjpm.toml # 项目配置文件
├── README.md # 项目说明
├── CHANGELOG.md # 版本变更记录
└── LICENSE # 开源协议设计原则:
仓颉提供了完善的单元测试框架 std.unittest:
package idcard
import std.unittest.*
import std.unittest.testmacro.*
import std.collection.HashMap
@Test
public class IdcardParserTests {
// 测试无效长度
@TestCase
func testParse_InvalidLength(): Unit {
@Assert(parse("123") == None)
}
// 测试外国人永久居留身份证
@TestCase
func testParse_International_RouteAndFields(): Unit {
let id = "911840199001011234" // 840 -> 美国
let r = parse(id)
@Assert(r != None)
let info = r.getOrDefault({=> HashMap<String, String>()})
@Assert(info["type"] == "外国人永久居留身份证")
@Assert(info["sign"] == "北京市")
@Assert(info["country"] == "美国")
@Assert(info["birthday"] == "19900101")
}
// 测试大陆居民身份证(直辖市)
@TestCase
func testParse_China_Municipality_RouteAndFields(): Unit {
let id = "11010519491231002X"
let r = parse(id)
@Assert(r != None)
let info = r.getOrDefault({=> HashMap<String, String>()})
@Assert(info["type"] == "居民身份证")
@Assert(info["sign"] == "北京市朝阳区")
@Assert(info["country"] == "中国")
@Assert(info["birthday"] == "19491231")
@Assert(info["sex"] == "女")
@Assert(info["isValid"] == "true")
}
// 测试港澳台居住证
@TestCase
func testParse_HK_RouteAndFields(): Unit {
let id = "810000199001011234"
let r = parse(id)
@Assert(r != None)
let info = r.getOrDefault({=> HashMap<String, String>()})
@Assert(info["type"] == "港澳台居民居住证")
@Assert(info["sign"] == "香港特别行政区")
@Assert(info["country"] == "中国")
@Assert(info["birthday"] == "19900101")
}
}测试策略:
包含:
包含:
包含:
文档编写原则:
# [0.0.1] - 2025-10-30
## Feature
+ 支持中国大陆居民身份证(18位)解析功能
+ 支持港澳台居民居住证解析功能
+ 支持外国人永久居留身份证解析功能
+ 实现身份证校验码有效性验证(isValid函数)
+ 自动识别并解析出生日期(格式:YYYY-MM-DD)
+ 自动识别并解析性别信息
+ 自动识别并解析地区信息(省/市/区县)
+ 支持国籍信息识别(外国人永久居留身份证)
+ 内置完整的中国行政区划代码数据库(省、市、区县级)
+ 内置国际国家/地区代码数据库(用于外国人永久居留身份证)
+ 提供统一的解析接口(parse函数)
+ 自动识别身份证类型并选择对应解析方法
## Bugfix
无
## Remove
无版本规范:遵循语义化版本控制(Semantic Versioning)
对于地区码查询,使用 HashMap 而非 Array:
// HashMap 查询:O(1)
let province = _china.get("110000").getOrDefault({=> ""})
// 如果使用 Array 线性查找:O(n)
// for (item in array) {
// if (item.code == "110000") { ... }
// }性能对比:
使用 public let 定义数据字典为编译期常量:
public let CHINA_ADMIN_DIVISIONS: HashMap<String, String> = HashMap<String, String>([
// 数据在编译期初始化,运行时无需重新加载
])优势:
使用字符串切片而非逐字符处理:
// 高效:直接切片
let code = id[0..6]
let birthday = id[6..14]
// 低效:逐字符拼接
// var code = ""
// for (i in 0..6) {
// code += id[i]
// }在 parse() 函数中,匹配失败立即返回:
public func parse(id: String): ?HashMap<String, String> {
if (id.size != 18) {
return None // 提前返回,避免后续无效计算
}
// ...
}// 一次性计算,避免重复调用
let idArr = id.split("")
let firstChar = idArr[0]
// 而非每次都 split
// if (id.split("")[0] == "9") { ... }
// let arr = id.split("") // 重复计算操作 | 平均耗时 | 说明 |
|---|---|---|
parse() 单次调用 | ~5μs | 包含完整解析流程 |
isValid() 单次调用 | ~2μs | 仅校验码验证 |
HashMap 查询 | ~100ns | O(1) 查询性能 |
经验:将项目拆分为核心层、工具层、数据层三个独立模块,带来了以下好处:
反思:如果一开始就把所有代码写在一个文件中,随着功能增加会变得难以维护。
经验:将行政区划数据和国际代码数据独立为单独的文件,而不是硬编码在逻辑代码中:
✓ 好的做法:
src/module/data/china.cj # 数据文件
src/module/method.cj # 导入数据:import idcard.module.data.CHINA_ADMIN_DIVISIONS
✗ 不好的做法:
src/module/method.cj # 数据和逻辑混在一起优势:
经验:提供一个统一的 parse() 接口,自动识别身份证类型:
// 开发者只需调用一个函数
let result = parse("11010519491231002X")而不是让开发者手动判断类型:
// ✗ 不好的 API 设计
if (id.startsWith("9")) {
result = parseInternational(id)
} else if (id.startsWith("81") || id.startsWith("82") || id.startsWith("83")) {
result = parseHKMT(id)
} else {
result = parseChina(id)
}原则:API 设计要遵循"简单的事情应该简单做"。
经验:使用仓颉的 Option 类型处理可能失败的情况:
public func parse(id: String): ?HashMap<String, String> {
if (id.size != 18) {
return None // 优雅地表示失败
}
// ...
}调用方可以使用 match 表达式处理:
match (result) {
case Some(info) => println("成功:${info}")
case None => println("失败:格式无效")
}优势:
经验:在编写实现代码之前,先编写测试用例:
// 1. 先写测试
@TestCase
func testParse_InvalidLength(): Unit {
@Assert(parse("123") == None)
}
// 2. 再写实现
public func parse(id: String): ?HashMap<String, String> {
if (id.size != 18) {
return None
}
// ...
}好处:
经验:不要盲目优化,先测量再优化:
在本项目中,我们发现地区码查询是性能热点,因此选择了 HashMap 而非 Array。
经验:完善的文档能大大降低使用门槛:
投入产出比:文档编写占用 20% 的时间,但能节省 80% 的答疑时间。
经验:一个规范的开源项目应该包含:
✓ LICENSE # 明确开源协议
✓ README.md # 项目说明
✓ CHANGELOG.md # 版本历史
✓ 测试代码 # 质量保证
✓ 示例代码 # 快速上手
✓ API 文档 # 详细参考问题:北京、天津、上海、重庆的行政区划比较特殊,是"省—区"两级,而其他地方是"省—市—区"三级。
解决方案:
// 判断是否为直辖市(11/12/31/50)
if (code[0..2] == "11" || code[0..2] == "12" ||
code[0..2] == "31" || code[0..2] == "50") {
// 直辖市:省 + 区
result["sign"] = _china.get(province_code).getOrDefault({=> ""}) +
_china.get(district_code).getOrDefault({=> ""})
} else {
// 其他地区:省 + 市 + 区
result["sign"] = _china.get(province_code).getOrDefault({=> ""}) +
_china.get(city_code).getOrDefault({=> ""}) +
_china.get(district_code).getOrDefault({=> ""})
}问题:外国人永久居留身份证的校验码计算与普通身份证略有不同(需要取反)。
解决方案:
var mod = sum % 11
if (idArr[0] == "9") {
mod = lastLetter.size - mod; // 外国人身份证需要取反
}问题:行政区划代码会不断变化(如县改区、撤销合并),如何保持数据的准确性?
解决方案:
问题:完整的行政区划数据(2000+ 条)+ 国际代码(250+ 条)占用一定内存。
解决方案:
public let 定义为编译期常量,共享内存通过开发 idcard 身份证解析库,我深刻体会到:
仓颉语言作为一门新兴的编程语言,具有现代化的语法和强大的标准库,非常适合开发基础库。希望本文能为仓颉三方库开发者提供一些参考和启发。
项目地址:idcard 身份证解析库
欢迎贡献:如果你有任何建议或想法,欢迎提交 Issue 或 Pull Request!
import idcard.*
main() {
// 解析中国居民身份证
let result = parse("11010519491231002X")
match (result) {
case Some(info) =>
println("身份证类型:${info["type"]}")
println("地区:${info["sign"]}")
println("国籍:${info["country"]}")
println("出生日期:${info["birthday"]}")
println("性别:${info["sex"]}")
println("有效性:${info["isValid"]}")
case None =>
println("身份证号码格式无效")
}
}
// 输出:
// 身份证类型:居民身份证
// 地区:北京市朝阳区
// 国籍:中国
// 出生日期:19491231
// 性别:女
// 有效性:trueimport idcard.*
main() {
let ids = [
"11010519491231002X", // 北京居民身份证
"810000199001011234", // 香港居住证
"911840199001011234" // 美国永久居留证
]
for (id in ids) {
match (parse(id)) {
case Some(info) =>
println("\n身份证号:${id}")
println("类型:${info["type"]}")
println("地区:${info["sign"]}")
println("国籍:${info["country"]}")
case None =>
println("\n身份证号:${id} - 格式无效")
}
}
}import idcard.method.*
main() {
let testCases = [
("11010519491231002X", true), // 正确
("110105194912310021", false), // 错误
("310101199003074890", true), // 正确
("810000199001011234", true) // 正确
]
for ((id, expected) in testCases) {
let result = isValid(id)
let status = if (result == expected) { "✓" } else { "✗" }
println("${status} ${id}: ${result}")
}
}作者:nut 日期:2025-10-31 版本:v1.0.0
本文基于 idcard v1.0.0 版本编写,使用仓颉编译器 v1.0.3。
感谢大家的阅读,期待与大家一起共建仓颉生态
开源项目地址:idcard
仓颉官网:https://cangjie-lang.cn 仓颉开源仓库:https://gitcode.com/cangjie 仓颉官方文档:https://cangjie-lang.cn/docs 仓颉开源三方库:https://gitcode.com/cangjie-tpc 仓颉编程语言白皮书:https://developer.huawei.com/consumer/cn/doc/cangjie-guides-V5/cj-wp-abstract-V5