内容


探索财务数据输入的备选方式

使用开源 OCR 和条形码方法将数据输入到应用程序

Comments

问题

财务数据输入最忌讳的就是拖沓。您将所有的收据和发票扔到一个鞋盒里以备日后处理,记帐的日子一到,数据必须被录入到软件内,而这时您往往需要大半天的时间才能整理清楚。有些数据输入可以取自从某金融机构下载的 Quicken Interchange Format (QIF) 或 Open Financial Exchange (OFX) 文件,但即便如此,也很少能获得所需要的所有数据输入。

技术专家若是知道了这个问题,往往会问:可否使用技术来让这个任务不那么麻烦?毕竟录入的过程对于每个纸质数据或发票都是一样的:

  1. 阅读发票。
  2. 决定数据是否可从其他资源读过来(比如,从银行下载的帐单)。
  3. 评估这是一个个人交易还是公司交易。
  4. 考虑涉及到哪些子帐目。
  5. 开始、继续并结束这次交易的输入。
  6. 检查此次交易的借贷总和是否相符。
  7. 确保所有细节均记录完毕。

这个过程一方面会涉及到一些人的判断和解析,而另一方面,也会涉及到简单机械的阅读和记录。技术应该能够让这个过程不那么痛苦并且更为准确。有两种方式看上去会有所帮助,它们是 OCR 和条形码扫描。

OCR 和条形码

OCR 和条形码是对财务数据输入有所帮助的诸多技术中的两种。对于 OCR,开发人员会尝试机器阅读一个打印收据上的文本。对于条形码,则需要商家能够在收据上打印出编码了恰当信息的条形码以供日后扫描。对于这些工具,问题有两个方面:如果是随便的一个收据,那么能否对它进行充分好的扫描来提取数据?而对于提取出的数据,可否将其直接导入到财务应用程序?有用与否取决于数据输入的备选过程能不能比键盘输入简单,不那么繁琐。如果不是,那么该应用程序就不是解决这个问题的方法。

扫描图像和条形码通常都不会那么困难。不过,收据有自身的一些特殊问题。收据通常都是通过一个色带褪色的小打印机打印在质量不怎么好的纸上(有可能卷曲或反光面不太好), 这会导致图像虽然容易被人阅读,却不容易被机器阅读。以 OCR 为目的的收据扫描能力差异很大,由好到根本不能忍受。如果打印出的条形码的质量很好,那么扫描条形码的成功率通常都会很高。

实际上,一般而言,收据都会遵循一个模式,所以将原始的文本输入编写成最终财务应用程序可以使用的东西就得到了简化。收据的开头一般都会是商家的名称和联系地址,之后是日期,再后是包含了描述、价格以及适用税率的一行或多行数据项。接下来是小计、附加费用,比如运费、处理费、佣金、税以及总计。在某些时候,还会出现税务登记号、支付方式以及其他数据。顺序有可能会不同,但通常只在各商家之间不同。通常,同一商家的 POS 机在各门店间都是一样的。

一个实际的例子

图 1 所示的是一个虚构的收据的例子。虽然它不能代表收据的可读性,但从扫描的角度而言,还是相当不错的。我接触的收据中有些比这难扫描或根本无法扫描。

图 1. 原始收据的图像
示例收据
示例收据

人可以阅读此收据,决定它包含的数据,然后通过键盘输入这次交易。表 1 显示了此收据的一些示例复式记帐的细节。

表 1. 收据的记帐条目
账目
杂项支出64.68
所付税款6.47
MasterCard 帐户71.15
总计71.1571.15

要想与最终财务应用程序链接,需要进行这样的假设:如果创建了一个原型格式的 XML 文件(参见清单 1),那么此应用程序将能够导入它。

清单 1. 发票的简化 XML 格式
<invoice>
  <transaction date="xxxx-xx-xx" currency="XXX">
    <vendor>
      <name></name>
      <phone></phone>
      <tax_reg></tax_reg>
    </vendor>
    <entry>
      <detail></detail>
      <account></account>
      <amount></amount>
    </entry>
  </transaction>
</invoice>

以这种格式,一个发票会创建一个或多个交易,而每个交易都包含有关此商家信息的一个商家块,以及针对每个交易的多个项(与表 1 内的这些行相关)。每项都包含一个细节元素、金额所应用到的账目以及金额本身(可负可正)。输出是 OFX 格式或是 Organization for the Advancement of Structured Information Standards (OASIS) Invoice 格式,只需调整元素和模式的名称即可。

OCR 方式

本文使用 Tesseract(参见 参考资料)作为一个示例开源字符识别应用程序。Tesseract 基本上遵循的是常规的 ./configure, make, make install 过程,因此最为重要的是要仔细遵循随其捆绑的 INSTALL 文件内的指导 — 要想安装成功,有很多内容需要牢记。

如下的指令会让 Tesseract 检查存储为 Tagged Image File Format (TIFF) 格式的 图 1,然后使用第二个参数 wm 作为文件名的基础来接收输出。输出自动地去到当前目录内的 wm.txt 文件。

tesseract wm.tif wm

wm.txt 文件会包含清单 2 所示的输出。

清单 2. 来自 Tesseract 的输出
ACME HARDWARE
88 MAIN STREET
ANYTOWN, ST 12345-67890
123-555-6789
TAX NO - 987654-321
CUSTOMER - CASH SALE
ORDER - 000456
DATE - 2010-08-07
DESC - SKU
ITEM - 12345
2 @ 12 34 = 24 68
SECOND - 98765
2 @ 15 00 = 30 00
THIRD - 44887744
2 @ 5 00 = 10 00
SUBTOTAL = 64 68
TAX 10% = 6 47
TOTAL = 71 15
PAYMENT - MASTERCARD
TRANS - 0678453
REGISTER - 22
EMPLOYEE - 456
THANKYOU EOR SHOPPING AT
ACME

在本例中,Tesseract 只会犯两类错误:丢失所有的小数点以及会把 F 错认为 E。我花了些力气来使 图 1 与 Tesseract 尽量兼容,为此,我使用了 Verdana 字体大小 12,并只使用大写字符,且只留单间距。

此时,我们只需一个脚本来清除文本、提取信息、并导出所需的 XML 就大功告成了。不过,由于这些步骤还会在条形码的例子中重复,所以我会将对脚本的解释留到介绍条形码的部分。

条形码方式

条形码是一些旨在清晰传递信息且具错误检查的特殊图像。出于自身的考虑,很多商家都已可以在收据上打印 1-D 的条形码。本节关注的是这样一个格式,QR 码,因为针对这个格式已经有很多开源工具可用,而且阅读器也都可以在便携的电子设备(比如手机上)实现。

QR 码是一些 2-D 图像,用来携带字母数字和其他信息。在一个方形图像内至多可编码约 4,000 个字母数字符号,它们刚刚好可以替代那种窄的收据条。条形码在红酒瓶的标签上得到了很好的应用,这些标签具有编码好的信息,潜在的购买者或饮酒者通过使用内置在手机内的一个扫描器就可以找到酒瓶所含内容的更多信息。在收据的情形下使用条形码要事先假定商家愿意将条形码添加到打印收据上。既然很多商家都愿意考虑采用能够使自己的服务质量为顾客所接受的手段,因此这种方式的采用为时不远了。

图 2 内所示的条形码包含了来自 图 1 的文本信息。

图 2. QR 码的示例
示例 QR 码

请注意此条形码不必直接包含原始信息,但可以包含指回至商家服务器的一个(短很多)URL,间接地从数据库检索信息。

目前,有很多开源工具可用来创建和读 QR 码。本文使用了 qrencode 编码,使用基于 Java™ 的开源 QR Code Library (qrcode) 解码(参见 参考资料)。使用其他的免费测试服务能够实现同样的目的。

qrencode 的安装很直观,使用常规的 ./configure, make, make install 即可。请确保系统上已经有 javac 例程可用,然后从 QR 码解码并运行这些示例,如捆绑提供的 QUICKSTART.txt 文件所示。

如下的命令编码作为常规文本存储于 acmesrc.txt 文件内的来自 图 1 的文本,并将其存储在一个名为 acmeqr.png 的图像文件:

> qrencode -o acmeqr.png < acme.txt

以类似的方式,可以让 QR 码检查 acmeqr.png 的内容是我们所想要的:

> java -cp classes:lib/qrcode.jar example.QRCodeDecoderCUIExample \
      /path-to-image/acmeqr.png

这个来自 qrcode 目录的指令应该会复述 acme.txt 文件的内容。请注意,包含 440 个字符的这个方形图像的大小接近于 POS 打印纸的 8 厘米宽。虽然可以加入更多的信息,但那将意味着收据必须加宽才能适应较大的图像。

映射到 XML

下一步是将扫描的结果(不管是来自 OCR 还是 QR 码)转化成 XML 格式以便导入到财务应用程序。为此,我们将需要一种脚本语言:Python、Perl、PHP 等均能胜任。本例使用的是 PHP 及 SimpleXML 和 Document Object Model (DOM) XML 例程。我选择了 清单 2 的 Tesseract 输出及其错误作为此脚本输入的测试用例。您可以以同样的方式来使用来自条形码的输入,有可能所需的错误检查会更少。

此脚本需要完成的事情有:

  1. 阅读原始输入。
  2. 判断商家的格式。
  3. 检查有无错误。
  4. 将数据智能地映射到变量。
  5. 输出此 XML。

清单 3 显示了一个示例脚本。

清单 3. 映射脚本示例
<?php
// mapper - Colin Beckingham 2010
//
// firstly load the data
if (!$infile = $argv[1]) die("Missing input file!\n");
$raw = file_get_contents($infile);
$inarray = explode("\n",$raw);
$vendor["name"] = $inarray[0];
$sumcheck=0;
switch ($vendor["name"]) {
  case "ACME HARDWARE":
    $invoid = substr($inarray[6],8);
    $invdate = substr($inarray[7],7);
    $index["sku"] = 8;
    $index["subt"] = array_search_substr("SUBTOTAL",$inarray);
    $index["tax"] = $index["subt"]+1;
    $index["tot"] = $index["subt"]+2;
    $index["method"] = $index["subt"]+3;
    $vendor["phone"] = $inarray[3];
    $vendor["tax_reg"] = substr($inarray[4],9);
    $detail = $vendor["name"]." $invoid";
    $items["expdetail"]=$detail;
    $items["expaccount"]=1;
    $merch = substr($inarray[$index["subt"]],strpos($inarray[$index["subt"]],"=")+2);
    $merch = str_replace(" ",".",$merch);
    $items["expamount"]=($merch)*-1;
      $sumcheck += $items["expamount"];
    $items["taxdetail"]=$detail;
    $items["taxaccount"]=2;
    $tax = substr($inarray[$index["tax"]],strpos($inarray[$index["tax"]],"=")+2);
    $tax = str_replace(" ",".",$tax);
    $items["taxamount"]=($tax)*-1;
      $sumcheck += $items["taxamount"];
    $items["astdetail"]=$detail;
    $items["astaccount"]=3;
    $tot = substr($inarray[$index["tot"]],strpos($inarray[$index["tot"]],"=")+2);
    $tot = str_replace(" ",".",$tot);
    $items["astamount"]=$tot;
      $sumcheck += $items["astamount"];
      if ($sumcheck != 0) echo "Unbalanced entries! ($sumcheck)\n";
  break;
  default:
    die("Vendor not recognized!\n");
  break;
}
// secondly create XML and substitute data
$basic = "<?xml version=\"1.0\" ?>";
$basic .= "<invoice></invoice>";
$xml = simplexml_load_string($basic);
$xml->addchild("transaction");
$xml->transaction->addAttribute("date",$invdate);
$xml->transaction->addAttribute("currency","CAD");
$xml->transaction->addchild("vendor");
$xml->transaction->vendor->addchild("name",$vendor["name"]);
$xml->transaction->vendor->addchild("phone",$vendor["phone"]);
$xml->transaction->vendor->addchild("tax_reg",$vendor["tax_reg"]);
$xml->transaction->addchild("entry");
$j=0;
$xml->transaction->entry[$j]->addchild("detail",$items["expdetail"]);
$xml->transaction->entry[$j]->addchild("account",$items["expaccount"]);
$xml->transaction->entry[$j]->addchild("amount",$items["expamount"]);
if ($items["taxamount"] != 0) {
  $xml->transaction->addchild("entry");
  $j++;
  $xml->transaction->entry[$j]->addchild("detail",$items["taxdetail"]);
  $xml->transaction->entry[$j]->addchild("account",$items["taxaccount"]);
  $xml->transaction->entry[$j]->addchild("amount",$items["taxamount"]);
}
$xml->transaction->addchild("entry");
$j++;
$xml->transaction->entry[$j]->addchild("detail",$items["astdetail"]);
$xml->transaction->entry[$j]->addchild("account",$items["astaccount"]);
$xml->transaction->entry[$j]->addchild("amount",$items["astamount"]);
show_nice_xml($xml);
//
function show_nice_xml($xml) {
  $doc = new DOMDocument('1.0');
  $doc->formatOutput = true;
  $domnode = dom_import_simplexml($xml);
  $domnode = $doc->importNode($domnode, true);
  $domnode = $doc->appendChild($domnode);
  echo $doc->saveXML();
}
function array_search_substr($needle,$haystack) {
  foreach ($haystack as $k=>$h) {
    if (substr($h,0,strlen($needle)) == $needle) return $k;
  }
  return false;
}
?>

第一节着眼于命令行内提供的必须参数,这个参数会给出输出自 清单 2 内 Tesseract 的文本文件的名称。此脚本将此文件文本载入到一个字符串变量,将其 explode 到一个使用了新行的数组并根据此数组的第一个元素决定商家的名称。如果商家名称被认可,switch 语句就会遵照该商家的期望模式。如果此商家未知,它就会停止此脚本。此商家的细节、日期及订单 ID 号处于此数组的预期位置,但小计及后续行的数组索引则依赖于所详细说明的项的数量。通过搜索子字符串 SUBTOTAL 可以决定 SUBTOTAL 元素的索引。确定了此索引后,也就知道了项的数量,也就能够确定小计行之后元素的索引了。使用 for 语句可以迭代多个项以提取细节信息,但为了简便起见,我在此省去了对它的介绍。

有了索引之后,就可以从此数组提取商品的小计金额、税额以及最终的合计金额。由于我预想到此商家会遗漏金额的小数点和方法,因此脚本在需要的时候插入了它们。此脚本保存的是金额的流动合计,如果最终的和不为零,它就会发出一个警告。它还为 detail 的需要使用了商家名以及订单 ID。

在将所需变量分配到 items 数组后,此脚本会生成 XML。它先创建字符串类型的根 invoice 元素并将其载入到一个 SimpleXML 对象。然后添加具有两个属性的所需 transaction 元素以及具有子元素的 vendor 元素。然后它使用此信息生成三个 entry 元素(分别对应于支出、税以及资产三个类别),其中每个元素具有 detailaccountamount 子元素。如果此项不必纳税,那么就不会创建对应于税的元素。最后,此脚本输出一个错落有致的 XML,这一步只在测试时需要。

可以以如下的方式调用此脚本来生成最终的映射输出:

> php /path-to/mapper.php /path-to/acme.txt

清单 4 显示了 XML 输出,其格式是输入到财务应用程序所要求的(与 清单 1 对比)。

清单 4. 映射脚本的 XML 结果
<?xml version="1.0"?>
<invoice>
  <transaction date="2010-08-07" currency="CAD">
    <vendor>
      <name>ACME HARDWARE</name>
      <phone>123-555-6789</phone>
      <tax_reg>987654-321</tax_reg>
    </vendor>
    <entry>
      <detail>ACME HARDWARE 000456</detail>
      <account>1</account>
      <amount>-64.68</amount>
    </entry>
    <entry>
      <detail>ACME HARDWARE 000456</detail>
      <account>2</account>
      <amount>-6.47</amount>
    </entry>
    <entry>
      <detail>ACME HARDWARE 000456</detail>
      <account>3</account>
      <amount>71.15</amount>
    </entry>
  </transaction>
</invoice>

使用 QR 码输出需要较少的错误检查。基本的过程现在就差不过完成了。

更进一步的可能性也很清晰,其中最为重要的一点是需要此脚本逐一地查看各项并将其映射到一个已知帐目,而这一点是脚本可以通过将新项匹配到之前发票上的那些项来逐渐“掌握”的。

结束语

本文充分展示了使用 OCR 和 QR 码输入财务数据是可能的。这类输入财务数据的替代方法是否值得一试取决于来自 OCR 的输出的清晰程度、省力程度以及准确度提高的程度。如果需要键入复杂的字母数字项(比如 SKU 号和税务登记细节)且可以采用多收据的批量输入,那么自动化的输入则物有所值。

作为开发人员,我们的目标是让产品尽量对终端用户有用。如果打印收据的主要目的是便于用户通过 OCR 扫描和阅读,那么有如下几点需要牢记:

  • 纸张应该尽量白、打印字迹应尽量黑且尽量连续。
  • 如果日期靠近收据的顶部,那么将非常有助于人和机器阅读。
  • 字体应该与大多数 OCR 解码器兼容。
  • 收据上的商家名称应该是可解码的格式。很多商家的图标很花哨,容易被人辨识,但却无法被 OCR 辨识。如果非要使用图标,那也没有关系,但请以标准的字体重复商家的名称。
  • 广告应该与数据信息分离开来。在收据底部加上一条广告无可厚非,但是如果广告插入到收据的主体,那么就会使人和 OCR 均很难找到真正信息。
  • 将非数字符号,比如货币符号和税务应用程序的标记,与货币值清晰分开。

OCR 和 QR 码绝不是惟一的替代。另一个 2-D 条形码格式是 PDF417,在宏模式下,它能将一个大的条形码拆分成多个小的条形码,但只有极少数几个非专门的便携设备支持这种格式。另一种很酷的可能性是使用一种智能语音交互例程让一个阅读者输入细节信息。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source
ArticleID=604249
ArticleTitle=探索财务数据输入的备选方式
publish-date=12202010