阿里云ocr识别

阿里云ocr识别笔记

安装扩展包

laravel使用

1
composer require godruoyi/laravel-ocr

原始方式

1
2
3
4
# >= 8.1
composer require "godruoyi/ocr:^3.0"
# >= 7.2
composer require "godruoyi/ocr:^2.1"

配置文件(laravel举例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
<?php

/*
* This file is part of the godruoyi/ocr.
*
* (c) Godruoyi <gmail@godruoyi.com>
*
* This source file is subject to the MIT license that is bundled.
*/

return [

/*
|--------------------------------------------------------------------------
| Default client
|--------------------------------------------------------------------------
|
| 指定一个默认的 client 名称,其值需要在下列的 drivers 数组中配置。
|
*/
'default' => env('OCR_DEFAULT_CLIENT', 'aliyun'),

/*
|--------------------------------------------------------------------------
| Client 配置
|--------------------------------------------------------------------------
|
| Client 配置信息,包括基本密钥等;注意目前 aliyun 暂只支持 appcode 方式。
|
*/
'drivers' => [
'aliyun' => [
'appcode' => env('OCR_CODE', ''),
'secret_id' => '',
'secret_key' => '',
],

'baidu' => [
'access_key' => '',
'secret_key' => '',
],

'tencent' => [
'secret_id' => '',
'secret_key' => '',
],
],

/*
|--------------------------------------------------------------------------
| Cache 配置
|--------------------------------------------------------------------------
|
| Baidu OCR 需要缓存来保存 AccessToken,默认我们使用 Symfony Cache 组件的 FilesystemAdapter
| 你可以在这里设置为使用 Laravel 的缓存组件。
|
*/
'laravel_cache' => true,

];

其中在.env中增加 OCR_CODE = xxxxxxxxxxxxxx数据,这个key来源于阿里云,试用次数100次加1分钱500次机会,足够使用了。

表格识别

调用初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* @param $filePath [图片地址]
* @throws BusinessException
*/
public function handle($filePath, $type = 1)
{
$options = [
'table' => true,
'noStamp' => true
];

$response = OCR::aliyun()->generalAdvanced($filePath, $options);
$res = $response->toArray();
if (isset($res['error_code'])) {
Log::channel('ocr')->error('识别图片:' . $filePath . ' 错误结果:', $res);
throw new BusinessException(CodeResponse::OCR_ERR);
}
if ($type == 15) {
$data = $res['content'];
Log::channel('ocr')->info('识别图片:' . $filePath . '识别结果:' . $data);
return $this->format15($data);
} else {
$data = $res['prism_tablesInfo'][0];
Log::channel('ocr')->info('识别图片:' . $filePath . ' 识别结果:', $data);
return $this->format($data);
}
}

通用解析表格数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
/**
* 通用识别处理
* @throws BusinessException
*/
public function format($data)
{
$table = [];
$res = [];
$items = $data['cellInfos'];
foreach ($items as $item) {
if ($item['xsc'] == $item['xec'] && $item['ysc'] == $item['yec']) {
$table[$item['ysc']][$item['xec']] = $item['word'];
}
}
$data = [
'sn' => ['序号', 'ID'],
'code' => ['编码', '商品编码', '零件编码', '零件号', '配件编码', '零件编号'],
'name' => ['零件名称', '名称', '零件中文名', '配件名称'],
'num' => ['出库数', '数量'],
'quality' => ['产地', '配件品质', '品质'],
'unit' => ['单位'],
'price' => ['售价', '单价'],
'total_price' => ['金额', '价格', '销售价', '配件价格'],
];
$init = 0;//表格数据(除了表头)第一条row索引
if (!isset($table[0])) {
throw new BusinessException(CodeResponse::OCR_ERR);
} else {
$arr = array_values($table[0]);
$intersection = array_intersect($arr, $data['name']);
$user = Auth::guard('supplier')->user();
if (!empty($intersection)) {
$header = $table[0];
// 遍历表格数据(除了表头)
$init = 1;
$keys = array_keys($table[1]);
$user->table_headers = json_encode($header);
$user->save();
} else {
//获取存储的表头
$header = json_decode($user->table_headers);
if (!$header) {
Log::channel('ocr')->error('表头缺失');
throw new BusinessException(CodeResponse::OCR_ERR);
}
// 遍历表格数据(除了表头)
$keys = array_keys($table[0]);
}
}
$initData = array_fill_keys(array_keys($data), ''); // 初始化空的行数据
for ($i = $init; $i < count($table); $i++) {
$row = $table[$i];
$rowData = [];
// 遍历当前行的每个格子
foreach ($keys as $key) {
if (isset($header[$key])) {
$cellData = $row[$key] ?? '';
// 检查当前表头文字是否在 $row 数组中的值中
foreach ($data as $k => $v) {
$header_k = str_replace(' ', '', $header[$key]);
if (in_array($header_k, $v)) {
$rowData[$k] = $cellData;
break;
}
}
}
}
$rowData = array_merge($initData, $rowData);
if ($rowData['name']) {
//数据格式化
$rowData['sn'] = empty($rowData['sn']) ? count($res) + 1 : $rowData['sn'];
$rowData["code"] = str_replace(' ', '', $rowData["code"]);
$rowData["total_price"] = preg_replace("/[^0-9.]+/", "", $rowData["total_price"]);
$res[] = $rowData;
}
}
return $res;
}

Tips:

  1. 上传解析的图片需要有表格线,且单元格没有合并的情况
  2. 当表头说明文字叫法不同,但是都有一一对应的字段,可以通过$data数组进行配置
  3. 当上传第一张图片(有表头)解析成功后,会存储表头到此用户表的table_headers字段,后续没有表头只有数据也会同步使用此表头进行解析
  4. 数据格式化可针对行数据是否有name字段来判断是否需要此条数据,并二次加工数据
  5. 印章水印可在阿里云移除,当前配置可自动识别水印去除处理

图片示例

特殊图片处理

  • 当图片没有竖线,只有横线的情况,没有办法解析到每列数据,可以通过取出所有字符串数据,进行正则匹配方式一一拿到每行数据,然后进行处理
  • 定制化处理针对指定图片,不能随意解析,其他非规则图片可参考此方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 特殊识别处理
* @Author sugar
* @date 2023/8/9
* @param $data
* @return array
*/
public function format15($data)
{

$res = [];
preg_match('/仓位.+?人民币/', $data, $matches);
// 获取中间的字符串
$data = trim($matches[0], '仓位 人民币');
// $pattern = '/ (\d{3}) (\S+) (.*?) (\S+) (\S+) (\S+) (\S+)/';
$pattern = '/\d{3}\s.+?(?=\d{3}\s|$)/';
preg_match_all($pattern, $data, $matches);
for ($i = 0; $i <= count($matches[0]) - 1; $i++) {
$m = $matches[0][$i];
$m_arr = explode(' ', $m);
$m_arr = array_values(array_filter($m_arr));
if (count($m_arr) == 7 || count($m_arr) == 8) {
$n['sn'] = $m_arr[0];
$n['num'] = $m_arr[count($m_arr) - 3];
$n['quality'] = $m_arr[count($m_arr) - 4];
$n['price'] = $m_arr[count($m_arr) - 2];
$n['total_price'] = $this->splitString($m_arr[count($m_arr) - 1]);
if (count($m_arr) == 8) {
$str = $m_arr[1] . $m_arr[2];
$n['code'] = substr($str, 0, 12);
$n['name'] = substr($str, 12);
} else {
$n['code'] = substr($m_arr[1], 0, 12);
$n['name'] = substr($m_arr[1], 12);
}
$res[] = $n;
}
}
return $res;
}

public function splitString($string)
{
// 使用正则表达式匹配小数点后两位的内容
preg_match("/\d+\.\d{2}/", $string, $matches);
if (count($matches) > 0) {
// 提取匹配到的内容作为前面的字符串
$result = $matches[0];
} else {
// 如果没有匹配到,则返回原始字符串
$result = $string;
}
return $result;
}

图片示例

特别技巧:

  • 在前端上传图片后可以进行多张图片数据通过指定key进行合并数据
1
2
3
4
5
6
7
8
9
10
11
12
//更新表格数据
let oldArr = dataSource.value;
let appendArr = res.data
appendArr.forEach((item) => {
const index = oldArr.findIndex((oldItem) => oldItem.sn.toString() === item.sn.toString());
if (index !== -1) {
oldArr[index] = item;
} else {
oldArr.push(item);
}
});
dataSource.value = oldArr;