id-scanner-lib 1.3.0 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -25
- package/dist/id-scanner-core.esm.js +273 -0
- package/dist/id-scanner-core.js +273 -0
- package/dist/id-scanner-core.min.js +1 -1
- package/dist/id-scanner-ocr.esm.js +596 -76
- package/dist/id-scanner-ocr.js +596 -76
- package/dist/id-scanner-ocr.min.js +1 -1
- package/dist/id-scanner-qr.esm.js +273 -0
- package/dist/id-scanner-qr.js +273 -0
- package/dist/id-scanner-qr.min.js +1 -1
- package/dist/id-scanner.js +1268 -87
- package/dist/id-scanner.min.js +1 -1
- package/package.json +2 -2
- package/src/id-recognition/anti-fake-detector.ts +698 -0
- package/src/id-recognition/id-detector.ts +166 -19
- package/src/id-recognition/ocr-processor.ts +188 -41
- package/src/id-recognition/ocr-worker.ts +82 -72
- package/src/index.ts +189 -15
- package/src/types/browser-image-compression.d.ts +19 -0
- package/src/types/tesseract.d.ts +37 -0
- package/src/utils/image-processing.ts +364 -0
- package/dist/id-scanner-core.esm.js.map +0 -1
- package/dist/id-scanner-core.js.map +0 -1
- package/dist/id-scanner-core.min.js.map +0 -1
- package/dist/id-scanner-ocr.esm.js.map +0 -1
- package/dist/id-scanner-ocr.js.map +0 -1
- package/dist/id-scanner-ocr.min.js.map +0 -1
- package/dist/id-scanner-qr.esm.js.map +0 -1
- package/dist/id-scanner-qr.js.map +0 -1
- package/dist/id-scanner-qr.min.js.map +0 -1
- package/dist/id-scanner.js.map +0 -1
- package/dist/id-scanner.min.js.map +0 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @file 身份证检测模块
|
|
3
3
|
* @description 提供自动检测和定位图像中的身份证功能
|
|
4
4
|
* @module IDCardDetector
|
|
5
|
+
* @version 1.3.2
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { Camera } from "../utils/camera"
|
|
@@ -53,6 +54,8 @@ export interface IDCardDetectorOptions {
|
|
|
53
54
|
* ```
|
|
54
55
|
*/
|
|
55
56
|
export class IDCardDetector implements Disposable {
|
|
57
|
+
// 身份证标准宽高比(近似黄金比例)
|
|
58
|
+
private static readonly ID_CARD_ASPECT_RATIO = 1.58 // 标准身份证宽高比
|
|
56
59
|
private camera: Camera
|
|
57
60
|
private detecting = false
|
|
58
61
|
private detectTimer: number | null = null
|
|
@@ -270,28 +273,32 @@ export class IDCardDetector implements Disposable {
|
|
|
270
273
|
private async detectIDCard(imageData: ImageData): Promise<DetectionResult> {
|
|
271
274
|
// 1. 图像预处理
|
|
272
275
|
const grayscale = ImageProcessor.toGrayscale(imageData)
|
|
273
|
-
|
|
274
|
-
// 2.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
276
|
+
|
|
277
|
+
// 2. 使用Sobel边缘检测算法检测边缘
|
|
278
|
+
const edgeData = ImageProcessor.detectEdges(grayscale)
|
|
279
|
+
|
|
280
|
+
// 3. 检测矩形和边缘
|
|
281
|
+
// 使用基于边缘的矩形检测
|
|
282
|
+
const rectangles = this.detectRectangles(edgeData)
|
|
283
|
+
|
|
284
|
+
// 4. 评估检测结果 - 检查是否找到了合适的矩形
|
|
285
|
+
const idCardRect = this.findIdCardRectangle(rectangles, imageData.width, imageData.height)
|
|
286
|
+
|
|
280
287
|
const detectionResult: DetectionResult = {
|
|
281
|
-
success:
|
|
282
|
-
message: "
|
|
288
|
+
success: idCardRect !== null,
|
|
289
|
+
message: idCardRect ? "身份证检测成功" : "未检测到身份证",
|
|
283
290
|
}
|
|
284
291
|
|
|
285
|
-
if (detectionResult.success) {
|
|
286
|
-
//
|
|
292
|
+
if (detectionResult.success && idCardRect) {
|
|
293
|
+
// 使用检测到的身份证矩形区域
|
|
287
294
|
const width = imageData.width
|
|
288
295
|
const height = imageData.height
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
const rectWidth =
|
|
292
|
-
const rectHeight =
|
|
293
|
-
const rectX =
|
|
294
|
-
const rectY =
|
|
296
|
+
|
|
297
|
+
// 使用实际检测到的身份证区域
|
|
298
|
+
const rectWidth = idCardRect.width
|
|
299
|
+
const rectHeight = idCardRect.height
|
|
300
|
+
const rectX = idCardRect.x
|
|
301
|
+
const rectY = idCardRect.y
|
|
295
302
|
|
|
296
303
|
// 添加四个角点
|
|
297
304
|
detectionResult.corners = [
|
|
@@ -337,8 +344,8 @@ export class IDCardDetector implements Disposable {
|
|
|
337
344
|
)
|
|
338
345
|
}
|
|
339
346
|
|
|
340
|
-
// 设置置信度
|
|
341
|
-
detectionResult.confidence =
|
|
347
|
+
// 设置置信度 - 基于边缘强度和矩形形状评分
|
|
348
|
+
detectionResult.confidence = this.calculateConfidence(idCardRect, edgeData)
|
|
342
349
|
}
|
|
343
350
|
|
|
344
351
|
return detectionResult
|
|
@@ -360,4 +367,144 @@ export class IDCardDetector implements Disposable {
|
|
|
360
367
|
this.camera.release()
|
|
361
368
|
this.resultCache.clear()
|
|
362
369
|
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 从边缘图像中检测矩形
|
|
373
|
+
* @param edgeData 边缘检测后的图像数据
|
|
374
|
+
* @returns 检测到的矩形数组
|
|
375
|
+
*/
|
|
376
|
+
private detectRectangles(edgeData: ImageData): Array<{
|
|
377
|
+
x: number;
|
|
378
|
+
y: number;
|
|
379
|
+
width: number;
|
|
380
|
+
height: number;
|
|
381
|
+
confidence: number;
|
|
382
|
+
}> {
|
|
383
|
+
const width = edgeData.width;
|
|
384
|
+
const height = edgeData.height;
|
|
385
|
+
const minSize = Math.min(width, height) * 0.2; // 最小矩形尺寸
|
|
386
|
+
const rectangles = [];
|
|
387
|
+
|
|
388
|
+
// 使用积分图像加速边缘密度计算
|
|
389
|
+
const integralImg = new Uint32Array(width * height);
|
|
390
|
+
|
|
391
|
+
// 计算积分图像
|
|
392
|
+
for (let y = 0; y < height; y++) {
|
|
393
|
+
for (let x = 0; x < width; x++) {
|
|
394
|
+
const idx = y * width + x;
|
|
395
|
+
const pixel = (edgeData.data[idx * 4] > 128) ? 1 : 0; // 边缘为白色
|
|
396
|
+
|
|
397
|
+
// 计算积分图
|
|
398
|
+
const above = y > 0 ? integralImg[(y - 1) * width + x] : 0;
|
|
399
|
+
const left = x > 0 ? integralImg[y * width + (x - 1)] : 0;
|
|
400
|
+
const diagonal = (x > 0 && y > 0) ? integralImg[(y - 1) * width + (x - 1)] : 0;
|
|
401
|
+
|
|
402
|
+
integralImg[idx] = pixel + above + left - diagonal;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 滑动窗口检测矩形
|
|
407
|
+
for (let h = minSize; h < height * 0.9; h += Math.max(2, Math.floor(h * 0.05))) {
|
|
408
|
+
// 计算当前高度下,按照标准身份证比例的宽度
|
|
409
|
+
const w = Math.round(h * IDCardDetector.ID_CARD_ASPECT_RATIO);
|
|
410
|
+
if (w > width * 0.9) continue;
|
|
411
|
+
|
|
412
|
+
for (let y = 0; y < height - h; y += Math.max(2, Math.floor(h * 0.1))) {
|
|
413
|
+
for (let x = 0; x < width - w; x += Math.max(2, Math.floor(w * 0.1))) {
|
|
414
|
+
// 计算矩形区域内的边缘密度
|
|
415
|
+
const edgeCount = this.calculateRectSum(integralImg, x, y, w, h, width);
|
|
416
|
+
const avgEdgeDensity = edgeCount / (w * h);
|
|
417
|
+
|
|
418
|
+
// 计算矩形边界的边缘密度
|
|
419
|
+
const perimeterEdgeCount = this.calculateRectPerimeter(integralImg, x, y, w, h, width);
|
|
420
|
+
const perimeterLength = 2 * (w + h);
|
|
421
|
+
const perimeterDensity = perimeterEdgeCount / perimeterLength;
|
|
422
|
+
|
|
423
|
+
// 矩形得分 - 边界边缘密度高且内部适中
|
|
424
|
+
const rectScore = perimeterDensity * 0.7 + (0.3 - Math.abs(0.15 - avgEdgeDensity)) * 0.3;
|
|
425
|
+
|
|
426
|
+
if (rectScore > 0.4) { // 阈值可根据实际项目调整
|
|
427
|
+
rectangles.push({
|
|
428
|
+
x,
|
|
429
|
+
y,
|
|
430
|
+
width: w,
|
|
431
|
+
height: h,
|
|
432
|
+
confidence: rectScore
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// 按得分排序
|
|
440
|
+
return rectangles.sort((a, b) => b.confidence - a.confidence);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* 使用积分图计算矩形区域内的总和
|
|
445
|
+
*/
|
|
446
|
+
private calculateRectSum(integral: Uint32Array, x: number, y: number, w: number, h: number, stride: number): number {
|
|
447
|
+
const x2 = Math.min(x + w - 1, stride - 1);
|
|
448
|
+
const y2 = Math.min(y + h - 1, integral.length / stride - 1);
|
|
449
|
+
|
|
450
|
+
const topLeft = (x > 0 && y > 0) ? integral[(y - 1) * stride + (x - 1)] : 0;
|
|
451
|
+
const topRight = y > 0 ? integral[(y - 1) * stride + x2] : 0;
|
|
452
|
+
const bottomLeft = x > 0 ? integral[y2 * stride + (x - 1)] : 0;
|
|
453
|
+
const bottomRight = integral[y2 * stride + x2];
|
|
454
|
+
|
|
455
|
+
return bottomRight - topRight - bottomLeft + topLeft;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* 计算矩形周长上的边缘点数量
|
|
460
|
+
*/
|
|
461
|
+
private calculateRectPerimeter(integral: Uint32Array, x: number, y: number, w: number, h: number, stride: number): number {
|
|
462
|
+
// 上边缘
|
|
463
|
+
const topEdgeSum = this.calculateRectSum(integral, x, y, w, 1, stride);
|
|
464
|
+
// 下边缘
|
|
465
|
+
const bottomEdgeSum = this.calculateRectSum(integral, x, y + h - 1, w, 1, stride);
|
|
466
|
+
// 左边缘
|
|
467
|
+
const leftEdgeSum = this.calculateRectSum(integral, x, y, 1, h, stride);
|
|
468
|
+
// 右边缘
|
|
469
|
+
const rightEdgeSum = this.calculateRectSum(integral, x + w - 1, y, 1, h, stride);
|
|
470
|
+
|
|
471
|
+
return topEdgeSum + bottomEdgeSum + leftEdgeSum + rightEdgeSum;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* 从检测到的矩形中找出最可能是身份证的矩形
|
|
476
|
+
*/
|
|
477
|
+
private findIdCardRectangle(rectangles: Array<{x: number; y: number; width: number; height: number; confidence: number}>, imageWidth: number, imageHeight: number): {x: number; y: number; width: number; height: number; confidence: number} | null {
|
|
478
|
+
if (rectangles.length === 0) return null;
|
|
479
|
+
|
|
480
|
+
// 筛选符合身份证宽高比的矩形
|
|
481
|
+
const filteredRects = rectangles.filter(rect => {
|
|
482
|
+
const aspectRatio = rect.width / rect.height;
|
|
483
|
+
return Math.abs(aspectRatio - IDCardDetector.ID_CARD_ASPECT_RATIO) < 0.2; // 允许20%的误差
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
if (filteredRects.length === 0) return null;
|
|
487
|
+
|
|
488
|
+
// 返回得分最高的矩形
|
|
489
|
+
return filteredRects[0];
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* 计算身份证检测的置信度
|
|
494
|
+
*/
|
|
495
|
+
private calculateConfidence(rect: {x: number; y: number; width: number; height: number; confidence: number} | null, edgeData: ImageData): number {
|
|
496
|
+
if (!rect) return 0;
|
|
497
|
+
|
|
498
|
+
// 基本得分来自矩形检测
|
|
499
|
+
let score = rect.confidence;
|
|
500
|
+
|
|
501
|
+
// 额外因素:矩形大小相对于图像
|
|
502
|
+
const relativeSize = (rect.width * rect.height) / (edgeData.width * edgeData.height);
|
|
503
|
+
if (relativeSize > 0.1 && relativeSize < 0.7) {
|
|
504
|
+
score += 0.1; // 身份证通常占据图像的合理比例
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 范围限制在0-1之间
|
|
508
|
+
return Math.min(Math.max(score, 0), 1);
|
|
509
|
+
}
|
|
363
510
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* @file OCR处理模块
|
|
3
3
|
* @description 提供身份证文字识别和信息提取功能
|
|
4
4
|
* @module OCRProcessor
|
|
5
|
+
* @version 1.3.2
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { createWorker } from "tesseract.js"
|
|
@@ -231,67 +232,213 @@ export class OCRProcessor implements Disposable {
|
|
|
231
232
|
* @param {string} text - OCR识别到的文本
|
|
232
233
|
* @returns {IDCardInfo} 提取到的身份证信息对象
|
|
233
234
|
*/
|
|
235
|
+
/**
|
|
236
|
+
* 格式化日期字符串为标准格式 (YYYY-MM-DD)
|
|
237
|
+
* @param dateStr 原始日期字符串
|
|
238
|
+
* @returns 格式化后的日期字符串
|
|
239
|
+
*/
|
|
240
|
+
private formatDateString(dateStr: string): string {
|
|
241
|
+
// 先尝试提取年月日
|
|
242
|
+
const dateMatch = dateStr.match(/(\d{4})[-\.\u5e74\s]*(\d{1,2})[-\.\u6708\s]*(\d{1,2})[日]*/);
|
|
243
|
+
if (dateMatch) {
|
|
244
|
+
const year = dateMatch[1];
|
|
245
|
+
const month = dateMatch[2].padStart(2, '0');
|
|
246
|
+
const day = dateMatch[3].padStart(2, '0');
|
|
247
|
+
return `${year}-${month}-${day}`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 如果是纯数字格式如 20220101
|
|
251
|
+
if (/^\d{8}$/.test(dateStr)) {
|
|
252
|
+
const year = dateStr.substring(0, 4);
|
|
253
|
+
const month = dateStr.substring(4, 6);
|
|
254
|
+
const day = dateStr.substring(6, 8);
|
|
255
|
+
return `${year}-${month}-${day}`;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// 如果无法格式化,返回原始字符串
|
|
259
|
+
return dateStr;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 验证身份证号是否符合规则
|
|
264
|
+
* @param idNumber 身份证号
|
|
265
|
+
* @returns 是否有效
|
|
266
|
+
*/
|
|
267
|
+
private validateIDNumber(idNumber: string): boolean {
|
|
268
|
+
// 基本验证,校验位有效性和长度
|
|
269
|
+
if (!idNumber || idNumber.length !== 18) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 检查格式,前17位必须为数字,最后一位可以是数字或'X'
|
|
274
|
+
const pattern = /^\d{17}[\dX]$/;
|
|
275
|
+
if (!pattern.test(idNumber)) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 检查日期部分
|
|
280
|
+
const year = parseInt(idNumber.substr(6, 4));
|
|
281
|
+
const month = parseInt(idNumber.substr(10, 2));
|
|
282
|
+
const day = parseInt(idNumber.substr(12, 2));
|
|
283
|
+
|
|
284
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 更详细的检查可以添加校验位的验证等逻辑...
|
|
289
|
+
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
234
293
|
private parseIDCardText(text: string): IDCardInfo {
|
|
235
294
|
const info: IDCardInfo = {}
|
|
295
|
+
|
|
296
|
+
// 预处理文本,清除多余空白
|
|
297
|
+
const processedText = text.replace(/\s+/g, " ").trim()
|
|
236
298
|
|
|
237
|
-
//
|
|
238
|
-
const lines =
|
|
299
|
+
// 拆分为行,并过滤空行
|
|
300
|
+
const lines = processedText.split("\n").filter((line) => line.trim())
|
|
239
301
|
|
|
240
|
-
//
|
|
302
|
+
// 解析身份证号码 - 多种模式匹配
|
|
303
|
+
// 1. 普通18位身份证号模式
|
|
241
304
|
const idNumberRegex = /(\d{17}[\dX])/
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
305
|
+
// 2. 带前缀的模式
|
|
306
|
+
const idNumberWithPrefixRegex = /公民身份号码[\s\:]*(\d{17}[\dX])/
|
|
307
|
+
|
|
308
|
+
// 尝试所有模式
|
|
309
|
+
let idNumber = null
|
|
310
|
+
const basicMatch = processedText.match(idNumberRegex)
|
|
311
|
+
const prefixMatch = processedText.match(idNumberWithPrefixRegex)
|
|
312
|
+
|
|
313
|
+
if (prefixMatch && prefixMatch[1]) {
|
|
314
|
+
idNumber = prefixMatch[1] // 首选带前缀的匹配,因为最可靠
|
|
315
|
+
} else if (basicMatch && basicMatch[1]) {
|
|
316
|
+
idNumber = basicMatch[1] // 其次是常规匹配
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (idNumber) {
|
|
320
|
+
info.idNumber = idNumber
|
|
245
321
|
}
|
|
246
322
|
|
|
247
|
-
// 解析姓名
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
323
|
+
// 解析姓名 - 使用多种策略
|
|
324
|
+
// 1. 直接匹配姓名标签近的内容
|
|
325
|
+
const nameWithLabelRegex = /姓名[\s\:]*([一-龥]{2,4})/
|
|
326
|
+
const nameMatch = processedText.match(nameWithLabelRegex)
|
|
327
|
+
|
|
328
|
+
// 2. 分析行文本寻找姓名
|
|
329
|
+
if (nameMatch && nameMatch[1]) {
|
|
330
|
+
info.name = nameMatch[1].trim()
|
|
331
|
+
} else {
|
|
332
|
+
// 备用方案:查找短行且内容全是汉字
|
|
333
|
+
for (const line of lines) {
|
|
334
|
+
if (line.length >= 2 && line.length <= 5 && /^[一-龥]+$/.test(line) && !/性别|民族|住址|公民|签发|有效/.test(line)) {
|
|
335
|
+
info.name = line.trim()
|
|
336
|
+
break
|
|
337
|
+
}
|
|
255
338
|
}
|
|
256
339
|
}
|
|
257
340
|
|
|
258
|
-
// 解析性别和民族
|
|
259
|
-
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
341
|
+
// 解析性别和民族 - 多种模式匹配
|
|
342
|
+
// 1. 标准格式匹配
|
|
343
|
+
const genderAndNationalityRegex = /性别[\s\:]*([男女])[\s ]*民族[\s\:]*([一-龥]+族)/
|
|
344
|
+
const genderNationalityMatch = processedText.match(genderAndNationalityRegex)
|
|
345
|
+
|
|
346
|
+
// 2. 只匹配性别
|
|
347
|
+
const genderOnlyRegex = /性别[\s\:]*([男女])/
|
|
348
|
+
const genderOnlyMatch = processedText.match(genderOnlyRegex)
|
|
349
|
+
|
|
350
|
+
// 3. 只匹配民族
|
|
351
|
+
const nationalityOnlyRegex = /民族[\s\:]*([一-龥]+族)/
|
|
352
|
+
const nationalityOnlyMatch = processedText.match(nationalityOnlyRegex)
|
|
353
|
+
|
|
354
|
+
if (genderNationalityMatch) {
|
|
355
|
+
info.gender = genderNationalityMatch[1]
|
|
356
|
+
info.nationality = genderNationalityMatch[2]
|
|
357
|
+
} else {
|
|
358
|
+
// 分开获取
|
|
359
|
+
if (genderOnlyMatch) info.gender = genderOnlyMatch[1]
|
|
360
|
+
if (nationalityOnlyMatch) info.nationality = nationalityOnlyMatch[1]
|
|
267
361
|
}
|
|
268
362
|
|
|
269
|
-
// 解析出生日期
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
363
|
+
// 解析出生日期 - 支持多种格式
|
|
364
|
+
// 1. 标准格式:YYYY年MM月DD日
|
|
365
|
+
const birthDateRegex1 = /出生[\s\:]*(\d{4})年(\d{1,2})月(\d{1,2})[日号]/
|
|
366
|
+
// 2. 美式日期格式:YYYY-MM-DD或YYYY/MM/DD
|
|
367
|
+
const birthDateRegex2 = /出生[\s\:]*(\d{4})[-\/\.](\d{1,2})[-\/\.](\d{1,2})/
|
|
368
|
+
// 3. 带前缀的格式
|
|
369
|
+
const birthDateRegex3 = /出生日期[\s\:]*(\d{4})[-\/\.\u5e74](\d{1,2})[-\/\.\u6708](\d{1,2})[日号]?/
|
|
370
|
+
|
|
371
|
+
let birthDateMatch = processedText.match(birthDateRegex1) ||
|
|
372
|
+
processedText.match(birthDateRegex2) ||
|
|
373
|
+
processedText.match(birthDateRegex3)
|
|
374
|
+
|
|
375
|
+
// 4. 从身份证号码中提取出生日期(如果上述方法失败)
|
|
376
|
+
if (!birthDateMatch && info.idNumber && info.idNumber.length === 18) {
|
|
377
|
+
const year = info.idNumber.substring(6, 10)
|
|
378
|
+
const month = info.idNumber.substring(10, 12)
|
|
379
|
+
const day = info.idNumber.substring(12, 14)
|
|
380
|
+
info.birthDate = `${year}-${month}-${day}`
|
|
381
|
+
} else if (birthDateMatch) {
|
|
382
|
+
// 确保月份和日期是两位数
|
|
383
|
+
const year = birthDateMatch[1]
|
|
384
|
+
const month = birthDateMatch[2].padStart(2, '0')
|
|
385
|
+
const day = birthDateMatch[3].padStart(2, '0')
|
|
386
|
+
info.birthDate = `${year}-${month}-${day}`
|
|
274
387
|
}
|
|
275
388
|
|
|
276
|
-
// 解析地址
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
389
|
+
// 解析地址 - 改进的正则匹配
|
|
390
|
+
// 1. 常规模式
|
|
391
|
+
const addressRegex1 = /住址[\s\:]*([\s\S]*?)(?=公民身份|出生|性别|签发)/
|
|
392
|
+
// 2. 更宽松的模式
|
|
393
|
+
const addressRegex2 = /住址[\s\:]*([一-龥a-zA-Z0-9\s\.\-]+)/
|
|
394
|
+
|
|
395
|
+
const addressMatch = processedText.match(addressRegex1) || processedText.match(addressRegex2)
|
|
396
|
+
|
|
397
|
+
if (addressMatch && addressMatch[1]) {
|
|
398
|
+
// 清理地址中的常见错误和多余空格
|
|
399
|
+
info.address = addressMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim()
|
|
400
|
+
|
|
401
|
+
// 限制地址长度并判断地址合理性
|
|
402
|
+
if (info.address.length > 70) {
|
|
403
|
+
info.address = info.address.substring(0, 70)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 确保地址是合理的(不仅仅包含符号或数字)
|
|
407
|
+
if (!/[一-龥]/.test(info.address)) {
|
|
408
|
+
info.address = ""; // 如果没有中文字符,可能不是有效地址
|
|
409
|
+
}
|
|
281
410
|
}
|
|
282
411
|
|
|
283
412
|
// 解析签发机关
|
|
284
|
-
const
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
413
|
+
const authorityRegex1 = /签发机关[\s\:]*([\s\S]*?)(?=有效|公民|出生|\d{8}|$)/
|
|
414
|
+
const authorityRegex2 = /签发机关[\s\:]*([一-龥\s]+)/
|
|
415
|
+
|
|
416
|
+
const authorityMatch = processedText.match(authorityRegex1) || processedText.match(authorityRegex2)
|
|
417
|
+
|
|
418
|
+
if (authorityMatch && authorityMatch[1]) {
|
|
419
|
+
info.issuingAuthority = authorityMatch[1].replace(/\s+/g, "").replace(/\n/g, "").trim()
|
|
288
420
|
}
|
|
289
421
|
|
|
290
|
-
// 解析有效期限
|
|
291
|
-
|
|
292
|
-
const
|
|
422
|
+
// 解析有效期限 - 支持多种格式
|
|
423
|
+
// 1. 常规格式:YYYY.MM.DD-YYYY.MM.DD
|
|
424
|
+
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}[日]*|[永久长期]*)/
|
|
425
|
+
// 2. 简化格式:YYYYMMDD-YYYYMMDD
|
|
426
|
+
const validPeriodRegex2 = /有效期限[\s\:]*(\d{8})[-\s]*(至|-)[-\s]*(\d{8}|[永久长期]*)/
|
|
427
|
+
|
|
428
|
+
const validPeriodMatch = processedText.match(validPeriodRegex1) || processedText.match(validPeriodRegex2)
|
|
429
|
+
|
|
293
430
|
if (validPeriodMatch) {
|
|
294
|
-
|
|
431
|
+
// 格式化为统一的有效期限形式
|
|
432
|
+
if (validPeriodMatch[1] && validPeriodMatch[3]) {
|
|
433
|
+
const startDate = this.formatDateString(validPeriodMatch[1])
|
|
434
|
+
const endDate = /\d/.test(validPeriodMatch[3]) ?
|
|
435
|
+
this.formatDateString(validPeriodMatch[3]) :
|
|
436
|
+
'长期有效'
|
|
437
|
+
|
|
438
|
+
info.validPeriod = `${startDate}-${endDate}`
|
|
439
|
+
} else {
|
|
440
|
+
info.validPeriod = validPeriodMatch[0].replace('有效期限', '').trim()
|
|
441
|
+
}
|
|
295
442
|
}
|
|
296
443
|
|
|
297
444
|
return info
|