最近有个需求要用户上传证件信息,所以需要获取证件上面的数据比如身份证号、银行卡号等,去查了一下百度 OCR 发现是有免费额度的,然后就是按次收费的,当然也不贵,不过本着能省则省,加上作为一个技术要多尝试新的技术能力所以就想自己能不能实现这个,查了一下资料发现 PaddleOCR 对这个证件识别效果很不错,这个 PaddleOCR 也是百度开源的,所以还是要感谢百度。
下面开始搭建 PaddleOCR
注意我这里服务器是 CentOS 7,这个很坑,版本太老了,如果你是新服务或者可以选择那么建议选 Ubuntu22.04 + 的版本,因为我在 CentOS 7 折腾了很久最终各种版本冲突都没装好,最后只能选择用 Docker 容器使用 Ubuntu22.04 才安装成功的。
安装 Docker 容器,注意我是 CentOS 7 才需要安装这个,如果是 Ubuntu22.04 + 的跳过
# 先检查一下自己的服务器有没有 Docker ,我服务器是没有的
yum list docker-ce --showduplicates | tail -20
# 没有的话我们就开始安装,先移除原地址使用国内镜像
rm -f /etc/yum.repos.d/docker-ce.repo
# 下载阿里云镜像站提供的 Docker CE(社区版)软件源配置文件
curl -o /etc/yum.repos.d/docker-ce.repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
# 验证 显示 mirrors.aliyun.com 就是正常的
grep baseurl /etc/yum.repos.d/docker-ce.repo
# 清理一下缓存防止有干扰
yum clean all
rm -rf /var/cache/yum
yum makecache
# 安装
yum install -y docker-ce docker-ce-cli containerd.io
# 安装完检查一下
docker --version
systemctl status docker
# 可以看到安装成功了但是容器没有启动
(ocr) [root@ecm-957e ai]# docker --version
Docker version 26.1.4, build 5650f9b (ocr)
[root@ecm-957e ai]# systemctl status docker
● docker.service - Docker Application Container Engine
Loaded: loaded (/usr/lib/systemd/system/docker.service;
disabled;
vendor preset: disabled)
Active: inactive (dead)
Docs: https://docs.docker.com
# 启动容器
systemctl start docker
# 再次查看状态,正常应该能看到 Active: active (running)
systemctl status docker
# 设置开机自启
systemctl enable docker
# 测试下看 Docker 是否正常,能看到 Hello from Docker! 就是正常的
docker run hello-world
# 我们在宿主机创建目录用来进行挂载容器,方便后面操作
mkdir -p /data/ai/{envs,models,apps,logs,upload}
# 创建容器
# -itd 后台运行
# --restart=always 开机自动启动
# -v 映射宿主机目录
# sleep infinity 容器一直运行
docker run -itd --name ocr-server --restart=always -v /data/ai:/data/ai ubuntu:22.04 sleep infinity
# 查看容器
docker ps
# 可以看到下面的内容
(base) [root@ecm-957e ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bf6943b094f9 ubuntu:22.04 "sleep infinity" 18 hours ago Up 18 hours ocr-server
# 进入容器
docker exec -it ocr-server bash
Docker 安装完成后我们就开始安装核心服务了
# 安装必要依赖库
apt update
apt install -y python3 python3-pip python3-venv libgl1 libglib2.0-0 libsm6 libxext6 libxrender1
# 配置清华镜像源,方便下载
mkdir -p ~/.pip
# 安装 vi 命令,新容器一般没有
apt-get update && apt-get install -y vim
# 配置源
vi ~/.pip/pip.conf
# 在里面放入下面内容,注意 wq 保存
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
trusted-host = pypi.tuna.tsinghua.edu.cn
timeout = 600
# 安装 PaddlePaddle
pip install paddlepaddle==2.6.2 -i https://pypi.tuna.tsinghua.edu.cn/simple
# 安装 PaddleOCR
pip install paddleocr==2.7.3 i https://pypi.tuna.tsinghua.edu.cn/simple
# 验证,看到 OCR OK 就是成功了
python3 -c "
from paddleocr import PaddleOCR
print('OCR OK')
"
# 我这里报错了 numpy 版本不对,这个换个版本就可以了
RuntimeError: module compiled against ABI version 0x1000009 but this version of numpy is 0x2000000 RuntimeError: module compiled against ABI version 0x1000009 but this version of numpy is 0x2000000 Traceback (most recent call last): File "<string>", line 2, in <module> File "/usr/local/lib/python3.10/dist-packages/paddleocr/__init__.py", line 14, in <module> from .paddleocr import * File "/usr/local/lib/python3.10/dist-packages/paddleocr/paddleocr.py", line 25, in <module> import cv2 ImportError: numpy.core.multiarray failed to import
# 卸载原版本
pip uninstall -y numpy
# 降级版本
pip install numpy==1.24.4 -i https://pypi.tuna.tsinghua.edu.cn/simple
# 安装 OpenCV
pip install opencv-contrib-python==4.6.0.66 -i https://pypi.tuna.tsinghua.edu.cn/simple
# 安装好后验证
python3 -c "
import cv2
print(cv2.__version__)
print(cv2.__file__)
"
# 接着测试 PaddleOCR,如果提示 OCR OK 就是可以了
python3 -c "
from paddleocr import PaddleOCR
print('OCR OK')
"
# 第一次初始化 PaddleOCR
# 会自动下载检测模型、识别模型、方向分类模型
# 下载完成后输出 OCR INIT OK
python3 -c "
from paddleocr import PaddleOCR
ocr = PaddleOCR(
use_angle_cls=True,
lang='ch'
)
print('OCR INIT OK')
"
安装好后我们开始测试
# 在**宿主机**创建一个目录,上传一张身份证照片进去,这里刚才已经和容器做了映射,容器里面也会看到这个文件
mkdir -p /data/ai/upload/test
# 我们在**容器**里面放一个测试脚本 idcard_parser.py
vi /data/ai/apps/ocr-service/idcard_parser.py
放入下面的内容
from paddleocr import PaddleOCR
import re
ocr = PaddleOCR(
use_angle_cls=True,
lang='ch'
)
def parse_idcard(image_path):
result = ocr.ocr(image_path, cls=True)
# 保留原始OCR结果
texts = [item[1][0].strip() for item in result[0]]
full_text = "".join(texts)
data = {
"side": None,
# 正面
"name": None,
"gender": None,
"nation": None,
"birth": None,
"address": None,
"id_no": None,
# 反面
"issue_authority": None,
"valid_start": None,
"valid_end": None
}
# =========================
# 判断正反面
# =========================
id_match = re.search(r"\d{17}[\dXx]", full_text)
if id_match:
data["side"] = "front"
elif "签发机关" in full_text or "有效期限" in full_text:
data["side"] = "back"
else:
data["side"] = "unknown"
# =========================
# 正面解析
# =========================
if data["side"] == "front":
for t in texts:
if "姓名" in t:
data["name"] = t.replace("姓名", "").strip()
elif "性别" in t:
value = t.replace("性别", "").strip()
if value in ["男", "女"]:
data["gender"] = value
elif "民族" in t:
data["nation"] = t.replace("民族", "").strip()
# 身份证号
if id_match:
data["id_no"] = id_match.group()
# 出生日期(全局匹配)
birth_match = re.search(
r"(\d{4}年\d{1,2}月\d{1,2})",
full_text
)
if birth_match:
birth = birth_match.group(1)
if not birth.endswith("日"):
birth += "日"
data["birth"] = birth
# 地址
addr_lines = []
collecting = False
for t in texts:
if "住址" in t:
collecting = True
addr_lines.append(
t.replace("住址", "").strip()
)
continue
if collecting:
if re.search(r"\d{17}[\dXx]", t):
break
if "公民" in t:
break
addr_lines.append(t)
if addr_lines:
data["address"] = "".join(addr_lines)
# =========================
# 反面解析
# =========================
elif data["side"] == "back":
for idx, t in enumerate(texts):
if "签发机关" in t:
value = t.replace("签发机关", "").strip()
if value:
data["issue_authority"] = value
elif idx + 1 < len(texts):
data["issue_authority"] = texts[idx + 1]
elif "有效期限" in t:
value = t.replace("有效期限", "")
dates = re.findall(
r"\d{4}[.\-年]\d{1,2}[.\-月]\d{1,2}",
value
)
if len(dates) >= 2:
data["valid_start"] = dates[0]
data["valid_end"] = dates[1]
# 全文兜底提取有效期
if not data["valid_start"]:
dates = re.findall(
r"\d{4}[.\-年]\d{1,2}[.\-月]\d{1,2}",
full_text
)
if len(dates) >= 2:
data["valid_start"] = dates[0]
data["valid_end"] = dates[1]
return data
if __name__ == "__main__":
res = parse_idcard(
"/data/ai/upload/test/111.png"
)
print("\n===== 解析结果 =====")
for k, v in res.items():
print(k, ":", v)
执行一下这个脚本
python3 /data/ai/apps/ocr-service/idcard_parser.py
可以看到解析成功了
root@bf6943b094f9:/# python3 /data/ai/apps/ocr-service/idcard_parser.py
[2026/06/16 03:36:40] ppocr DEBUG: Namespace(help='==SUPPRESS==', use_gpu=False, use_xpu=False, use_npu=False, ir_optim=True, use_tensorrt=False, min_subgraph_size=15, precision='fp32', gpu_mem=500, gpu_id=0, image_dir=None, page_num=0, det_algorithm='DB', det_model_dir='/root/.paddleocr/whl/det/ch/ch_PP-OCRv4_det_infer', det_limit_side_len=960, det_limit_type='max', det_box_type='quad', det_db_thresh=0.3, det_db_box_thresh=0.6, det_db_unclip_ratio=1.5, max_batch_size=10, use_dilation=False, det_db_score_mode='fast', det_east_score_thresh=0.8, det_east_cover_thresh=0.1, det_east_nms_thresh=0.2, det_sast_score_thresh=0.5, det_sast_nms_thresh=0.2, det_pse_thresh=0, det_pse_box_thresh=0.85, det_pse_min_area=16, det_pse_scale=1, scales=[8, 16, 32], alpha=1.0, beta=1.0, fourier_degree=5, rec_algorithm='SVTR_LCNet', rec_model_dir='/root/.paddleocr/whl/rec/ch/ch_PP-OCRv4_rec_infer', rec_image_inverse=True, rec_image_shape='3, 48, 320', rec_batch_num=6, max_text_length=25, rec_char_dict_path='/usr/local/lib/python3.10/dist-packages/paddleocr/ppocr/utils/ppocr_keys_v1.txt', use_space_char=True, vis_font_path='./doc/fonts/simfang.ttf', drop_score=0.5, e2e_algorithm='PGNet', e2e_model_dir=None, e2e_limit_side_len=768, e2e_limit_type='max', e2e_pgnet_score_thresh=0.5, e2e_char_dict_path='./ppocr/utils/ic15_dict.txt', e2e_pgnet_valid_set='totaltext', e2e_pgnet_mode='fast', use_angle_cls=True, cls_model_dir='/root/.paddleocr/whl/cls/ch_ppocr_mobile_v2.0_cls_infer', cls_image_shape='3, 48, 192', label_list=['0', '180'], cls_batch_num=6, cls_thresh=0.9, enable_mkldnn=False, cpu_threads=10, use_pdserving=False, warmup=False, sr_model_dir=None, sr_image_shape='3, 32, 128', sr_batch_num=1, draw_img_save_dir='./inference_results', save_crop_res=False, crop_res_save_dir='./output', use_mp=False, total_process_num=1, process_id=0, benchmark=False, save_log_path='./log_output/', show_log=True, use_onnx=False, output='./output', table_max_len=488, table_algorithm='TableAttn', table_model_dir=None, merge_no_span_structure=True, table_char_dict_path=None, layout_model_dir=None, layout_dict_path=None, layout_score_threshold=0.5, layout_nms_threshold=0.5, kie_algorithm='LayoutXLM', ser_model_dir=None, re_model_dir=None, use_visual_backbone=True, ser_dict_path='../train_data/XFUND/class_list_xfun.txt', ocr_order_method=None, mode='structure', image_orientation=False, layout=True, table=True, ocr=True, recovery=False, use_pdf2docx_api=False, invert=False, binarize=False, alphacolor=(255, 255, 255), lang='ch', det=True, rec=True, type='ocr', ocr_version='PP-OCRv4', structure_version='PP-StructureV2')
[2026/06/16 03:36:40] ppocr DEBUG: dt_boxes num : 10, elapsed : 0.09243059158325195
[2026/06/16 03:36:41] ppocr DEBUG: cls num : 10, elapsed : 0.05271768569946289
[2026/06/16 03:36:41] ppocr DEBUG: rec_res num : 10, elapsed : 0.4503798484802246
===== 解析结果 =====
side : front
name : 代用名
gender : 男
nation : 汉
birth : 2013年05月06日
address : 湖南省长沙市开福区巡道街幸福小区居民组
id_no : 430512198908131367
issue_authority : None
valid_start : None
valid_end : None
可以看到和这个图片是一致,然后反面也是支持的,这里就不演示了,大家自己更换图片测试即可

为了方便后续项目对接使用我这边直接封装了一个 Python 服务,所以大家需要代码的自己去拉一下ocr-service
部署很简单,就是把目录里面的文件放到服务器上,然后执行脚本就可以了,目录如下

&spm=1001.2101.3001.5002&articleId=162026679&d=1&t=3&u=a20909f863e14ce5b48982913e613e6f)
1092

被折叠的 条评论
为什么被折叠?



