id-scanner-lib
Version:
Browser-based ID card, QR code, and face recognition scanner with liveness detection
152 lines (139 loc) • 6.57 kB
text/typescript
import { IDCardType, IDCardInfo } from './types';
/**
* 格式化日期字符串为标准格式 (YYYY-MM-DD)
*/
function formatDateString(dateStr: string): string {
const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/);
if (dateMatch) {
const year = dateMatch[1];
const month = dateMatch[2].padStart(2, "0");
const day = dateMatch[3].padStart(2, "0");
return `${year}-${month}-${day}`;
}
if (/^\d{8}$/.test(dateStr)) {
const year = dateStr.substring(0, 4);
const month = dateStr.substring(4, 6);
const day = dateStr.substring(6, 8);
return `${year}-${month}-${day}`;
}
return dateStr;
}
/**
* 验证身份证号是否符合规则
*/
function validateIDNumber(idNumber: string): boolean {
if (!idNumber || idNumber.length !== 18) return false;
const pattern = /^\d{17}[\dX]$/;
if (!pattern.test(idNumber)) return false;
const year = parseInt(idNumber.substr(6, 4));
const month = parseInt(idNumber.substr(10, 2));
const day = parseInt(idNumber.substr(12, 2));
if (month < 1 || month > 12 || day < 1 || day > 31) return false;
return true;
}
/**
* IDCardTextParser - 统一解析身份证OCR文本
* 提取 ocr-processor.ts 和 ocr-worker.ts 中的解析逻辑
*/
export class IDCardTextParser {
/**
* 解析身份证文本
* @param text OCR识别的原始文本
* @returns 解析后的身份证信息
*/
static parse(text: string): IDCardInfo {
const info: IDCardInfo = {};
const processedText = text.replace(/\s+/g, " ").trim();
const lines = processedText.split("\n").filter((line) => line.trim());
// 1. 解析身份证号码
const idNumberRegex = /(\d{17}[\dX])/;
const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/;
const basicMatch = processedText.match(idNumberRegex);
const prefixMatch = processedText.match(idNumberWithPrefixRegex);
if (prefixMatch && prefixMatch[1]) {
info.idNumber = prefixMatch[1];
} else if (basicMatch && basicMatch[1]) {
info.idNumber = basicMatch[1];
}
// 2. 解析姓名
const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/;
const nameMatch = processedText.match(nameWithLabelRegex);
if (nameMatch && nameMatch[1]) {
info.name = nameMatch[1].trim();
} else {
for (const line of lines) {
if (line.length >= 2 && line.length <= 5 && /^[一-龥]+$/.test(line) &&
!/性别|民族|住址|公民|签发|有效/.test(line)) {
info.name = line.trim();
break;
}
}
}
// 3. 解析性别和民族
const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/;
const genderOnlyRegex = /性别[\s\:]*([男女])/;
const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/;
const genderNationalityMatch = processedText.match(genderAndNationalityRegex);
const genderOnlyMatch = processedText.match(genderOnlyRegex);
const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex);
if (genderNationalityMatch) {
info.gender = genderNationalityMatch[1];
info.ethnicity = genderNationalityMatch[2];
} else {
if (genderOnlyMatch) info.gender = genderOnlyMatch[1];
if (nationalityOnlyMatch) info.ethnicity = nationalityOnlyMatch[1];
}
// 4. 判断身份证类型
if (processedText.includes('出生') || processedText.includes('公民身份号码')) {
info.type = IDCardType.FRONT;
} else if (processedText.includes('签发机关') || processedText.includes('有效期')) {
info.type = IDCardType.BACK;
}
// 5. 解析出生日期
const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/;
const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/;
const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/;
const birthDateMatch = processedText.match(birthDateRegex1) || processedText.match(birthDateRegex2) || processedText.match(birthDateRegex3);
if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
const year = info.idNumber.substring(6, 10);
const month = info.idNumber.substring(10, 12);
const day = info.idNumber.substring(12, 14);
info.birthDate = `${year}-${month}-${day}`;
} else if (birthDateMatch) {
const year = birthDateMatch[1];
const month = birthDateMatch[2].padStart(2, "0");
const day = birthDateMatch[3].padStart(2, "0");
info.birthDate = `${year}-${month}-${day}`;
}
// 6. 解析地址
const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/;
const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/;
const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2);
if (addressMatch && addressMatch[1]) {
info.address = addressMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim();
if (info.address.length > 70) info.address = info.address.substring(0, 70);
if (!/[一-龥]/.test(info.address)) info.address = '';
}
// 7. 解析签发机关
const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/;
const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/;
const authorityMatch = processedText.match(authorityRegex1) || processedText.match(authorityRegex2);
if (authorityMatch && authorityMatch[1]) {
info.issueAuthority = authorityMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim();
}
// 8. 解析有效期限
const validPeriodRegex1 = /有效期限[\s\:]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日\s]*)[-\s]*(至|-)[-\s]*(\d{4}[-\.\u5e74\s]\d{1,2}[-\.\u6708\s]\d{1,2}[日]*|[永久长期]*)/;
const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/;
const validPeriodMatch = processedText.match(validPeriodRegex1) || processedText.match(validPeriodRegex2);
if (validPeriodMatch && validPeriodMatch[1] && validPeriodMatch[3]) {
const startDate = formatDateString(validPeriodMatch[1]);
const endDate = /\d/.test(validPeriodMatch[3]) ? formatDateString(validPeriodMatch[3]) : '长期有效';
info.validFrom = startDate;
info.validTo = endDate;
info.validPeriod = `${startDate}-${endDate}`;
} else if (validPeriodMatch) {
info.validPeriod = validPeriodMatch[0].replace("有效期限", "").trim();
}
return info;
}
}