我之前用 Electron 写过一个 OCR 识别和翻译的程序,OCR 使用的是百度和腾讯的 API,最近我准备加一个讯飞的 OCR API。讯飞的 OCR API 没有提供 SDK,需要通过 HTTP 请求的方式来调用,下面就是 Node.js 调用 讯飞 OCR API 的方法。

开通 OCR 通用文字识别

访问 https://www.xfyun.cn/services/common-ocr,滚动到下方的价格区域,可以免费领取 10 万次的免费包,如果你是第一次使用讯飞的服务的话,第一次开通可能需要创建一个应用。

开通完成后进入控制台,在左侧侧边栏选择 通用文字识别,讯飞有两种通用文字识别,一种是 通用文字识别,另一种是 通用文字识别intsig,两种 API 的调用方式会有些不一样,我这里使用的就是 通用文字识别

进入 通用文字识别 后,在右侧的 服务接口认证信息 区域可以看到 APPIDAPISecretAPIKey

讯飞 APPID、APISecret、APIKey

APPID、APISecret、APIKey 主要用于身份验证,在发送识别请求的时候也需要一起发送。在左侧的 实时用量 区域可以查看使用次数和剩余次数。

鉴权签名

在发送请求时,地址后面还需要有 authorizatiohostdate 三个 query 参数,下面是参数说明:

  • authorization:签名信息
  • host:请求主机
  • date:时间戳

authorization 包含 api_keyalgorithmheaderssignature 四部分组成。

下面是生成 authorization 的过程:

首先需要生成一个 signaturesignature 又包含了 host 请求主机、date 时间戳、request-line 请求参数,这些参数需要一行一个,如下:

host: api.xf-yun.com
date: Wed, 11 Aug 2021 06:55:18 GMT
POST /v1/private/sf8e6aca1 HTTP/1.1

其中的 date 时间戳需要实时生成,提交后服务器会校验时间。

下面还需要使用 hmac-sha256 算法结合 apiSecret 对上面的 signature 进行签名,签名完成后把 signature 转换为 base64,这样才算是生成了一个完整的 signature

authorization 包含四个部分,上面已经有了 signature,还差 algorithmheadersapi_key,剩下的三个部分都可以写死,algorithm 是加密算法,只支持 hmac-sha256headers 是参与签名的参数名,也是写死的,只支持 host date request-line

一个 authorization 把四个部分拼接后就是下面这样的:

api_key="你的api_key",algorithm="hmac-sha256",headers="host date request-line",signature="你的签名"

最后还需要把上面拼接后的 authorization 转换为 base64,这样才算是生成了一个完整的 authorization

下面就使用 Node.js 生成一个 authorization

const crypto = require('crypto');

const APPId = 'misterma.com'; // app_id 控制台查看
const APISecret = 'www.misterma.com'; // APISecret 控制台查看
const APIKey = 'www.misterma.com'; // APIKey 控制台查看

const host = 'api.xf-yun.com';  // 请求主机
const urlPath = '/v1/private/sf8e6aca1';  // 地址路径
// 生成时间戳,RFC1123格式("EEE, dd MMM yyyy HH:mm:ss z")
const date = new Date().toUTCString();

// 生成 signature
let signature = `host: ${host}
date: ${date}
POST ${urlPath} HTTP/1.1`;
// 使用 hmac-sha256 算法结合 apiSecret 对 signature 签名
signature = crypto.createHmac('sha256', APISecret).update(signature).digest('base64');

// 拼接 authorization
let authorization = `api_key="${APIKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
// authorization base64编码
authorization = Buffer.from(authorization).toString('base64');

生成的 authorization 发送请求时需要放到地址后面,通过 query 参数的方式发送,如下:

https://api.xf-yun.com/v1/private/sf8e6aca1?authorization=authorization&host=api.xf-yun.com&date=时间戳

发送请求

图片文件在发送之前需要转换为 base64,图片转换为 base64 后不能超过 4MB,发送的数据格式为 JSON 字符串,如下:

{
  "header": {
    "app_id": "你的app_id",
    "status": 3
  },
  "parameter": {
    "sf8e6aca1": {
      "category": "ch_en_public_cloud",
      "result": {
        "encoding": "utf8",
        "compress": "raw",
        "format": "json"
      }
    }
  },
  "payload": {
    "sf8e6aca1_data_1": {
      "encoding": "jpg",
      "status": 3,
      "image": "图片base64"
    }
  }
}  

其中的 payload.sf8e6aca1_data_1.image 就是图片base64。

payload.sf8e6aca1_data_1.encoding 是图片类型,可以是 jpgpngbmp

parameter.sf8e6aca1.category 是识别语言,上面的 ch_en_public_cloud 是中文和英文。

我这里发送 HTTP 请求会用到 axios,关于 axios 的使用可以看 JavaScript 使用 Axios 发送 GET 和 POST 请求

下面是调用讯飞 OCR 通用文字识别的完整过程,包含生成签名和发送请求:

const crypto = require('crypto');
const axios = require('axios').default;
const fs = require('fs');
const path = require('path');

const APPId = 'misterma.com'; // app_id 控制台查看
const APISecret = 'www.misterma.com'; // APISecret 控制台查看
const APIKey = 'www.misterma.com'; // APIKey 控制台查看

const host = 'api.xf-yun.com';  // 请求主机
const urlPath = '/v1/private/sf8e6aca1';  // 地址路径
// 生成时间戳,RFC1123格式("EEE, dd MMM yyyy HH:mm:ss z")
const date = new Date().toUTCString();

// 生成 signature
let signature = `host: ${host}
date: ${date}
POST ${urlPath} HTTP/1.1`;
// 使用 hmac-sha256 算法结合 apiSecret 对 signature 签名
signature = crypto.createHmac('sha256', APISecret).update(signature).digest('base64');

// 拼接 authorization
let authorization = `api_key="${APIKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`;
// authorization base64编码
authorization = Buffer.from(authorization).toString('base64');

// 读取当前目录下的 img.jpg 图片,然后转换为 base64
const image = fs.readFileSync(path.join(__dirname, 'img.jpg'), 'base64');

// 要发送的数据
const submitData = {
  header: { app_id: APPId, status: 3 },
  parameter: {
    sf8e6aca1: {
      category: 'ch_en_public_cloud',
      result: {
        encoding: 'utf8',
        compress: 'raw',
        format: 'json'
      }
    }
  },
  payload: {
    sf8e6aca1_data_1: {
      encoding: 'jpg',
      status: 3,
      image: image
    }
  }
};

// 使用 axios 发送 post 请求
axios({
  url: `https://${host}${urlPath}?authorization=${authorization}&host=${host}&date=${date}`,
  method: 'post',
  data: JSON.stringify(submitData),
  headers: {
    'Content-Type': 'application/json'
  }
}).then(result => {
  if (result.data.header.message !== 'success') {
    // 如果没有识别成功就输出错误信息,然后返回
    console.log(result.data.header.message);
    return false;
  }
  // 获取识别结果
  let text = Buffer.from(result.data.payload.result.text, 'base64').toString('utf-8');
  // 把识别结果转换为对象
  text = JSON.parse(text);
  const textList = [];
  // 获取每一行的文字
  text.pages[0].lines.forEach(val => {
    textList.push(val.words[0].content);
  });
  console.log(textList);
}).catch(error => {
  // 输出错误信息
  console.log(error.message);
  // 输出讯飞服务器返回的结果
  console.log(error.response.data);
});

鉴权信息和识别配置都需要发送,鉴权信息写在地址后面,通过 query 参数的方式发送,识别配置使用 JSON String,需要写在请求 body 中发送。

识别结果

讯飞的服务器会通过 JSON 格式返回识别结果,如果识别成功会返回如下的 JSON:

{
  "header": {
    "code": 0,
    "message": "success",
    "sid": "as0922903c05c3882"
  },
  "payload": {
    "result": {
      "compress": "raw",
      "encoding": "utf8",
      "format": "json",
      "text": "识别结果base64"
    }
  }
}

识别后的文字和位置信息在 payload.result.text,这是一个 base64 格式的 JSON 字符串,要获取识别文字需要先对 payload.result.text 进行 base64 解码,然后转换为对象。

下面是 payload.result.text 的 JSON:

{
  "category": "ch_en_public_cloud",
  "version": "3.5.0.2094",
  "pages": [
    {
      "lines": [
        {
          "coord": [{ "x": 4, "y": 6 }, { "x": 47, "y": 6 }, { "x": 47, "y": 27 }, { "x": 4, "y": 27 }],
          "exception": 0,
          "words": [
            {
              "content": "新年",
              "conf": 0.997823,
              "coord": [{ "x": 4, "y": 6 }, { "x": 46, "y": 6 },{ "x": 46, "y": 28 },{ "x": 4, "y": 28 }]
            }
          ],
          "conf": 0.997823,
          "word_units": [
            {
              "content": "新",
              "conf": 0.997561812,
              "coord": [{ "x": 4, "y": 6 },{ "x": 26, "y": 6 },{ "x": 26, "y": 28 },{ "x": 4, "y": 28 }],
              "center_point": { "x": 15, "y": 16 }
            },
            {
              "content": "年",
              "conf": 0.998084188,
              "coord": [{ "x": 27, "y": 6 }, { "x": 46, "y": 6 }, { "x": 46, "y": 28 }, { "x": 27, "y": 28 }],
              "center_point": { "x": 36, "y": 17 }
            }
          ],
          "angle": 0
        },
        {
          "coord": [{ "x": 20, "y": 56 }, { "x": 20, "y": 50 }, { "x": 24, "y": 50 }, { "x": 24, "y": 56 }],
          "exception": -1,
          "conf": -1.10607679e32,
          "angle": 270
        },
        {
          "coord": [{ "x": 4, "y": 34 }, { "x": 47, "y": 34 }, { "x": 47, "y": 56 }, { "x": 4, "y": 56 }],
          "exception": 0,
          "words": [
            {
              "content": "快乐",
              "conf": 0.997530699,
              "coord": [{ "x": 4, "y": 34 }, { "x": 46, "y": 34 }, { "x": 46, "y": 57 }, { "x": 4, "y": 57 }]
            }
          ],
          "conf": 0.997530699,
          "word_units": [
            {
              "content": "快",
              "conf": 0.995663464,
              "coord": [{ "x": 4, "y": 34 }, { "x": 24, "y": 34 }, { "x": 24, "y": 57 }, { "x": 4, "y": 57 }],
              "center_point": { "x": 14, "y": 45 }
            },
            {
              "content": "乐",
              "conf": 0.999397874,
              "coord": [{ "x": 25, "y": 34 }, { "x": 46, "y": 34 }, { "x": 46, "y": 57 }, { "x": 25, "y": 57 }],
              "center_point": { "x": 35, "y": 45 }
            }
          ],
          "angle": 0
        }
      ],
      "exception": 0,
      "angle": 0,
      "height": 63,
      "width": 56
    }
  ]
}

识别文字会包含很多位置信息,我上面只是识别了 新年快乐 4个字,一共两行,每行两个字。

如果你不需要位置信息,只想获取每一行的文字的画,可以像下面这样获取:

const result = '识别结果 JSON';
// 把 base64 的识别文字转换为 JSON 字符串
let text = Buffer.from(result.payload.result.text, 'base64').toString('utf-8');
// 把 JSON 字符串的识别文字转换为可操作的对象
text = JSON.parse(text);
// 在控制台输出每一行的识别文字
text.pages[0].lines.forEach(val => {
  console.log(val.words[0].content);
});

更详细的识别结果说明可以查看官方文档 https://www.xfyun.cn/doc/words/universal_character_recognition/API.html

错误处理

如果是鉴权出错,服务器会返回 401 或 403,通过获取服务器返回的数据可以查看错误信息,服务器返回的数据是一个 JSON,就像下面这样:

{ "message": "HMAC signature does not match" }

如果是识别出错可能会返回 400,错误信息可能是下面这样:

{
  "header": {
    "code": 10163,
    "message": "base64 decode error",
    "sid": "ase00084fcab05bf882"
  }
}

更详细的错误信息说明可以查看官方文档 https://www.xfyun.cn/doc/words/universal_character_recognition/API.html