使用百度文字识别实现身份证自动审核
前言
先来说说为什么要做这个东西,这段时间公司的注册用户直线上升,而我们的项目是一个虚拟币交易平台,需要用户经过实名认证才能去交易,之前项目中的实名认证是人工去审核的,每天审核几千上万个,把人搞死了,所以就急需一个自动去审核的功能。
实现思路
我最开始想的是去找一个识别身份证上面的文字的接口,然后把识别出来的姓名和身份证号和用户填写的去比较,完全符合就给他通过,然后我就去找接口了,最后决定使用百度的接口了
一、创建应用
去百度云创建一个应用,创建应用之前是需要认证,认证很简单,按照他提示的一步一步去搞就行了,然后就可以创建应用了,如下:
其中APP_ID
、API_KEY
、SECRET_KEY
是等会需要用到的,其实所有api都需要这个,阿里的也一样,我们可以看到这一个应用就可以使用很多api:
二、创建测试案例测试
有下面这么多,这里我们只需要使用身份证识别就可以了,但是这里有个问题,就是身份证识别每天只能免费使用500次,而我们每天的审核量远远不止500条,虽然我刚开始是那这个身份证的接口做的一个测试案例的,识别效果还不错,返回的是JSON格式的数据,如下:
待识别的证件照【正面】:
正面识别结果:
0 [main] INFO com.baidu.aip.client.BaseClient - get access_token success. current state: STATE_AIP_AUTH_OK
3 [main] DEBUG com.baidu.aip.client.BaseClient - current state after check priviledge: STATE_TRUE_AIP_USER
{
"log_id": 8156922741304040753,
"words_result": {
"姓名": {
"words": "卜振",
"location": {
"top": 236,
"left": 611,
"width": 111,
"height": 53
}
},
"民族": {
"words": "汉",
"location": {
"top": 345,
"left": 832,
"width": 31,
"height": 39
}
},
"住址": {
"words": "安徵省颖上县颖河乡马海村卜老庄队317号",
"location": {
"top": 534,
"left": 588,
"width": 483,
"height": 104
}
},
"公民身份号码": {
"words": "342128197603154716",
"location": {
"top": 774,
"left": 778,
"width": 644,
"height": 48
}
},
"出生": {
"words": "19760315",
"location": {
"top": 434,
"left": 591,
"width": 380,
"height": 42
}
},
"性别": {
"words": "男",
"location": {
"top": 342,
"left": 589,
"width": 36,
"height": 44
}
}
},
"words_result_num": 6,
"image_status": "normal",
"direction": 0
}
待识别的证件照【反面】:
反面识别结果:
0 [main] INFO com.baidu.aip.client.BaseClient - get access_token success. current state: STATE_AIP_AUTH_OK
3 [main] DEBUG com.baidu.aip.client.BaseClient - current state after check priviledge: STATE_TRUE_AIP_USER
{
"log_id": 329680296446600997,
"words_result": {
"失效日期": {
"words": "20340725",
"location": {
"top": 803,
"left": 934,
"width": 232,
"height": 55
}
},
"签发机关": {
"words": "永州市公安局零陵分局",
"location": {
"top": 691,
"left": 629,
"width": 501,
"height": 66
}
},
"签发日期": {
"words": "20140725",
"location": {
"top": 817,
"left": 635,
"width": 254,
"height": 54
}
}
},
"words_result_num": 3,
"image_status": "normal",
"direction": 0
}
可以看到识别还是挺准确的,测试代码如下:
package com.ssh.test;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import org.json.JSONObject;
import com.baidu.aip.ocr.AipOcr;
public class Test {
//设置APPID/AK/SK
public static final String APP_ID = "11634304";
public static final String API_KEY = "wXsELDayARPIbfjnhsunlRWC";
public static final String SECRET_KEY = "EgC8Ug26uP8FGQXdC2HxkbdMsUrTF3m8";
public static void sample(AipOcr client) {
// 传入可选参数调用接口
HashMap<String, String> options = new HashMap<String, String>();
options.put("detect_direction", "true");
options.put("detect_risk", "false");
//front:身份证含照片的一面;back:身份证带国徽的一面
String idCardSide = "back";
// 参数为本地图片路径
String image = "D://idcard/3.jpg";
JSONObject res = client.idcard(image, idCardSide, options);
//System.out.println(res.toString(2));
byte[] data = null;
try {
InputStream inputStream = null;
inputStream = new FileInputStream(image);
data = new byte[inputStream.available()];
inputStream.read(data);
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
// 参数为本地图片二进制数组
byte[] file = data;
res = client.idcard(file, idCardSide, options);
System.out.println(res.toString(2));
}
public static void main(String[] args) {
// 初始化一个AipOcr
AipOcr client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);
// 可选:设置网络连接参数
client.setConnectionTimeoutInMillis(2000);
client.setSocketTimeoutInMillis(60000);
// 可选:设置代理服务器地址, http和socket二选一,或者均不设置
//client.setHttpProxy("proxy_host", proxy_port); // 设置http代理
// client.setSocketProxy("proxy_host", proxy_port); // 设置socket代理
sample(client);
}
}
其实也可以去看百度云官方的案例,代码也得很详细,用之前先导jar包,具体看这里:http://ai.baidu.com/docs#/OCR-Java-SDK/top。
其实这样已经实现了身份证识别,但是仔细一想,每张身份证需要调用两次,那500次就只够250个人使用,太少了,然后我就试着去找其他的接口实现了,下面就来讲讲我是用什么来实现的。
三、使用通用文字识别
通用文字识别每天可以使用5W次,对于我们的项目是足够了,所以我就决定使用他了,
这个通用文字识别返回的数据如下:
识别结果:
2018-08-14 08:56:58 [ INFO | com.baidu.aip.client.BaseClient:209 ]:get access_token success. current state: STATE_AIP_AUTH_OK
0 [main] INFO com.baidu.aip.client.BaseClient - get access_token success. current state: STATE_AIP_AUTH_OK
{"log_id":4969158594526202744,"words_result":[{"probability":{"average":0.986142,"min":0.969767,"variance":1.35E-4},"words":"名凌伟忠"},{"probability":{"average":0.95937,"min":0.834893,"variance":0.003532},"words":"性新男民族汉"},{"probability":{"average":0.99802,"min":0.99302,"variance":6.0E-6},"words":"出生1968年7月8日"},{"probability":{"average":0.99679,"min":0.982513,"variance":3.2E-5},"words":"住址江苏省苏州市平江区陆洞"},{"probability":{"average":0.996753,"min":0.988016,"variance":2.5E-5},"words":"桥45号"},{"probability":{"average":0.996695,"min":0.94748,"variance":1.1E-4},"words":"公民身份号码320511196807080012"}],"words_result_num":6,"language":3,"direction":3}
最后提取出来的文字=======>名凌伟忠性新男民族汉出生1968年7月8日住址江苏省苏州市平江区陆洞桥45号公民身份号码320511196807080012
正面验证通过!
我们可以看到这个通用文字识别出来的结果是一段一段的,我们需要把我们需要的文字提取出来,提取过程看下面的代码:
package com.tradeplatform.common.util;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import com.baidu.aip.ocr.AipOcr;
/**
* 文字识别工具类
* @author asus
*
*/
public class OcrUtils {
//设置APPID/AK/SK
public static final String APP_ID = "11636969";
public static final String API_KEY = "BEPxcrMej7by6EnY6rGPjHsi";
public static final String SECRET_KEY = "pSe8nI2WWHbtQr7Phhr02ICq4m5yWiIY";
public static void main(String[] args) {
for (int i = 0; i < 1; i++) {
// System.err.println(idCardValidate("郑志刚","420112195711032714","http://47.75.43.140:8888/group1/M00/02/95/rB_34FtpWleADkp4AAI06gxuaIs788.jpg"));
System.err.println(idCardValidate("凌伟忠","320511196807080012","https://admin.gdae.xin//group1/M00/02/DA/rB_34Ftr7RGADpSwAAEe1K-ijzY703.jpg"));
}
}
/**
* 身份证文字识别
* 说明:
* <p>
* 创建人: LGQ <br>
* 创建时间: 2018年8月6日 下午4:43:14 <br>
* <p>
* 修改人: <br>
* 修改时间: <br>
* 修改备注: <br>
* </p>
* @throws Exception
*/
private static HashMap<String, String> options;
public static String idCardValidate(String trueName, String idCardNO, String imgSrc) {
if (StringUtils.isNotBlank(idCardNO)) {
idCardNO = idCardNO.toUpperCase();
}
// 传入可选参数调用接口
if (null == options) {
options = new HashMap<String, String>();
options.put("language_type", "CHN_ENG");
options.put("detect_direction", "true");
options.put("detect_language", "true");
options.put("probability", "true");
}
URL url = null;
byte[] data = null;
try {
url = new URL(imgSrc);
DataInputStream dataInputStream = new DataInputStream(url.openStream());
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = dataInputStream.read(buffer)) > 0) {
output.write(buffer, 0, length);
}
data = output.toByteArray();
dataInputStream.close();
} catch (MalformedURLException e) {
return "照片URL协议、格式或者路径错误!";
} catch (IOException e) {
return "照片转换IO流异常!";
}
if (null == data) {
return "获取的照片为空!";
}
JSONObject res = getAipOcr().basicGeneral(data, options);
System.err.println(res);
// 通用文字识别, 图片参数为远程url图片
try {
if (null != res) {
//用于保存全部识别出来的文字
StringBuilder rest = new StringBuilder();
JSONArray jsonArray = res.getJSONArray("words_result");
//使用循环把需要的文字全部提取出来
for(int i = 0;i < jsonArray.length(); i++) {
JSONObject jsonObject = (JSONObject) jsonArray.get(i);
rest.append(jsonObject.getString("words"));
}
if(StringUtils.isNotBlank(rest.append("").toString())) {
//最后提取出来的文字
String restStr = rest.toString();
System.err.println("最后提取出来的文字=======>" + restStr);
//判断是正面照还是反面照
if (StringUtils.isBlank(trueName) && StringUtils.isBlank(idCardNO) && restStr.contains("居民身份证")) {
//反面
//截取过期时间
try {
String expiryTime = restStr.split("\\-")[1];
if(StringUtils.isNotBlank(expiryTime)) {
if("长期".equals(expiryTime)) {
return "反面验证通过!";
}else {
try {
StringBuilder s = new StringBuilder();
char[] expiryChar = expiryTime.toCharArray();
for (char c : expiryChar) {
if ('0' <= c && c <= '9') {
s.append(c);
}
}
Long idcardDate = new SimpleDateFormat("yyyyMMdd").parse(s.toString()).getTime();
Long currDate = new Date().getTime();
Long vlid = idcardDate - currDate;
if(vlid >= 0) {
return "反面验证通过!";
}else {
return "身份证过期!";
}
} catch (ParseException e) {
return "无法识别!";
}
}
}else {
return "无法识别!";
}
} catch (Exception e) {
return "无法识别!";
}
} else if (restStr.contains("出生") && restStr.contains("年")) {
//正面
/**
* 判断证件号是否匹配
*/
if (!restStr.contains(trueName)) {
return "姓名不匹配!";
}else if(idCardNO.length() != 18 || !restStr.contains(idCardNO) ){
return "证件号不匹配!";
}else if((idCardNO.length() != 18 || !restStr.contains(idCardNO)) && !restStr.contains(trueName)){
return "证件号、姓名不匹配!";
}else {
try {
/**
* 判断年龄是否符合要求
*/
//获取索引
int sIndex = restStr.indexOf("出生");
int eIndex = restStr.indexOf("年");
//截取到年
String year = restStr.substring(sIndex + 2, eIndex);
//判断截取到的是不是4个纯数字
Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
if (pattern.matcher(year.trim()).matches()) {
//是纯数字
//判断年龄是否符合18-70
Calendar date = Calendar.getInstance();
//当前年份
int currYear = date.get(Calendar.YEAR);
//年龄
int age = currYear - Integer.valueOf(year.trim());
if (age < 18) {
return "年龄小于18岁!";
}
if (age > 70) {
return "年龄大于70岁!";
}
}else {
return "无法识别!";
}
//整段字符串包含全部通过
if(idCardNO.length() == 18 && restStr.contains(trueName) && restStr.contains(idCardNO)) {
return "正面验证通过!";
}
} catch (Exception e) {
return "无法识别!";
}
}
}else {
return "无法识别!";
}
}
}
} catch (JSONException e) {
return "无法识别!";
}
return "无法识别!";
}
/**
* 获取AipOcr对象
* 说明:
* <p>
* 创建人: LGQ <br>
* 创建时间: 2018年8月6日 下午3:59:21 <br>
* <p>
* 修改人: <br>
* 修改时间: <br>
* 修改备注: <br>
*/
private static AipOcr client;
private static AipOcr getAipOcr() {
if(client == null) {
// 初始化一个AipOcr
// 可选:设置网络连接参数
client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);
client.setConnectionTimeoutInMillis(2000);
client.setSocketTimeoutInMillis(60000);
}
return client;
}
}
上面的这一段代码需要注意:
try {
url = new URL(imgSrc);
DataInputStream dataInputStream = new DataInputStream(url.openStream());
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = dataInputStream.read(buffer)) > 0) {
output.write(buffer, 0, length);
}
data = output.toByteArray();
dataInputStream.close();
} catch (MalformedURLException e) {
return "照片URL协议、格式或者路径错误!";
} catch (IOException e) {
return "照片转换IO流异常!";
}
if (null == data) {
return "获取的照片为空!";
}
JSONObject res = getAipOcr().basicGeneral(data, options);
我们为什么不直接把图片的url传过去呢,而是去读取这个图片地址,将其转换成字节流呢,因为这个接口不支持htpps,我们项目中的图片都是带https的。
四、使用
识别工具类已经做好了,我们怎么去使用,刚开始我是想在审核的时候去进行识别,就是运营人员点击审核按钮,就去识别对应人的信息,后来我发现这个点击过程有点慢,点击一下需要等待1-2秒左右,可能就是将图片转换成字节流这个过程很耗时,在这里我来说说为什么不做成全自动审核,就是完全不需要人去管,因为机器识别始终不可能比人眼识别精准,可能会出现识别错误的情况,目前我还没有经过大量测试,还不清楚识别率高不高。所以只能先做成半自动吧,后来我就想去用一个任务定时去审核,然后把自动审核的结果存起来,当运营人员去审核的时候就不会出现卡顿的情况,因为是提前审核好的,运营人员审核的时候只需要去关心自动审核没有通过的(因为肯能会识别错),如果是自动审核通过了那就是通过了,下面贴出自动审核定时器的代码:
package com.tradeplatform.platform.user.job;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.tradeplatform.common.util.ApiConstant;
import com.tradeplatform.common.util.OcrUtils;
import com.tradeplatform.common.util.SysConfig;
import com.tradeplatform.platform.trade.mapper.TradeConfigMapper;
import com.tradeplatform.platform.trade.service.TradeConfigService;
import com.tradeplatform.platform.user.mapper.UserMapper;
import com.tradeplatform.trade.api.trade.utils.TradeConstants;
import com.tradeplatform.trade.api.user.entity.User;
/**
* 实名认证自动审核任务
* @author asus
*
*/
@Component
@EnableScheduling
@Transactional
public class AutoVerifyTask implements SchedulingConfigurer {
private Logger log = Logger.getLogger(getClass());
private TradeConfigMapper tradeConfigMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private TradeConfigService configService;
/**
* 查询出需要自动审核的实名认证
* <p>
* 创建人:lgq <br>
* 创建时间:2018年5月31日 下午1:55:01 <br>
* <p>
* 修改人: <br>
* 修改时间: <br>
* 修改备注: <br>
* </p>
*
* @return
*/
public List<User> getNeedAutoVerify() {
//每次自动审核条数
String autoVerifyNum = configService.getConfig(TradeConstants.TRADE_CONFIG_AUTO_VERIFY_NUM);
//查询出需要自动审核的用户
List<User> autoVerifyUsers = userMapper.selAutoVerifyUsers(Long.valueOf(autoVerifyNum));
return autoVerifyUsers;
}
private String cron;
@Autowired
public AutoVerifyTask(TradeConfigMapper tradeConfigMapper) {
// 获取每几分钟执行一次自动审核(单位:分钟)
this.tradeConfigMapper = tradeConfigMapper;
String interval = tradeConfigMapper.getConfigByCode(TradeConstants.TRADE_CONFIG_AUTO_VERIFY_TIME_INTERVAL).getConfigValue();
//0 0 3 */1 * ?
this.cron = "0 */" + interval + " * * * ?";
log.info("cron表达式==========>" + cron);
//每十秒钟执行一次
//cron = "*/10 * * * * ?";
//每十分钟执行一次
//cron = "0 */10 * * * ?";
}
//记录自动审核任务执行次数
private int num = 1;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.addTriggerTask(new Runnable() {
@Override
public void run() {
//自动审核是否开启
String autoVerify = configService.getConfig(TradeConstants.TRADE_CONFIG_OPEN_AUTO_VERIFY);
if (autoVerify.equals("1")) {
log.warn("----------自动审核已开启,开始进行第" + num + "次自动审核---------");
//服务内网IP,用于加载身份证前面
// 公测环境、生产环境才使用服务器内网IP
String idCardHost;
if (SysConfig.environment >= ApiConstant.Environment.OBT) {
idCardHost = "http://47.75.43.140:8888";
}else {
idCardHost = "http://192.168.0.189:8888";
}
//查询出需要自动审核的用户
List<User> autoVerifyUsers = getNeedAutoVerify();
//为空不循环
if (CollectionUtils.isNotEmpty(autoVerifyUsers)) {
log.info("需要审核的用户数量:" + autoVerifyUsers.size());
//循环审核
for (int i = 0; i < autoVerifyUsers.size(); i ++) {
User user = autoVerifyUsers.get(i);
log.info("第" + (i + 1) + "个审核的用户信息:" + user);
//获取自动审核结果
//正面照审核结果
String frontVerifyResult = OcrUtils.idCardValidate(user.getTrueName(), user.getIdCard(), idCardHost + user.getFront());
log.info("正面照审核结果:" + frontVerifyResult);
//反面照审核结果
String versoVerifyResult = OcrUtils.idCardValidate("", "", idCardHost + user.getVerso());
log.info("反面照审核结果:" + versoVerifyResult);
//修改自动审核描述
Map<String, Object> map = new HashMap<>();
map.put("desc", "正:" + frontVerifyResult + "|反:" + versoVerifyResult);
map.put("id", user.getId());
userMapper.updateVerifyDesc(map);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num ++;
log.warn("----------自动审核完成---------");
} else {
log.warn("----------没有需要自动审核的用户---------");
}
} else {
log.warn("----------自动审核未开启---------");
}
}
}, new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
// 任务触发,可修改任务的执行周期
CronTrigger trigger = new CronTrigger(cron);
Date nextExec = trigger.nextExecutionTime(triggerContext);
return nextExec;
}
});
}
}
定时器不会用的看这里:
http://note.youdao.com/noteshare?id=09c1497aa12dbd944ee9c5cc103eb07c
因为系统需要,在后台设置了3个参数,分别是:是否开启自动审核、自动审核任务执行时间间隔、每次审核条数,有了这3个参数就可以动态的去控制自动审核了。
经过测试可以发现每分钟去请求百度识别接口20-30次是没有问题的,上面代码中每次识别之前我都让现成睡眠了1s,就是怕接口识别不过来加上的。
使用定时器去做的好处就是不管用户什么提交实名认证,任务都会去自动把他审核掉,提前审核好,然后将结果保存至数据库,最后运营人员看到的界面就是这样的:
运营人员即可根据提示去进行审核,虽然还是手动的,但是比纯手动还是快了不少的。