级别: 中级 Colin Beckingham, 研究人员, Freelance
2009 年 4 月 27 日 如今一些金融机构允许客户下载文件并导入到客户自己所选的财务程序包中,这让日常的记帐任务变得多少有些简单了。但是,这些文件却给财务程序员带来了问题,因为它们往往仅对 Open Financial Exchange (OFX) 格式可用,而这种格式并不是 XML 兼容的。通过本文,了解如何使用 PHP 的字符串替换函数,使 OFX 文件可以与 XML 兼容。利用 XML 解析的强大功能以及对 OFX 文件的解构使财务编程更为精确。
我的银行为我这个财务程序员和记帐员提供了一种非常有用的服务:我能下载一个小文件来列出一段指定时期内我帐户上的所有交易。该文件包含了帐户名和帐户号;帐户类型(支票、储蓄或是其他类型);有关金融机构的信息;帐户的余额信息;我请求的日期和时间;帐户所执行交易的完整列表,其中显示了交易是存还是支、金额以及交易的日期和时间。银行为我做了很多数据输入的工作:我所需要做的只是通过编程将其传递到我的本地记录,提高了准确性并简化了对帐的过程。
 |
常用缩写词
- CSV - 以逗号分割的值
- GNU:GNU's Not UNIX®
- HTML:超文本标记语言(Hypertext Markup Language)
- W3C:万维网联盟(World Wide Web Consortium)
- XML:可扩展标记语言(Extensible Markup Language)
|
|
当我登录到银行的 Web 站点并开始下载包含与我的帐户相关的交易时,我可以选择是使用一个 CSV
文件,还是面向 Quicken、Intuit QuickBooks、Microsoft® Money 或 Simply
Accounting 的一个文件。出于各种原因,我选择不使用主流的财务程序,而是使用我自己的云计算应用程序,因此我要么必须使用纯 CSV 选项,要么需要解构其他一种下载。
虽然 CSV 文件可被快速下载到数据库或电子表格,但是其他的文件格式也具有某些特定的优势。所有其他可选文件实际上都是相同的文件,只是具有不同的文件扩展名以适合相应的包。该文件是一个 OFX 格式的纯文本文档(更多信息的链接,参见 参考资料),这种结构的设计就是为了在进行银行和其他财务交易时提供有效的信息而且更为准确。总体而言,更为专业的做法是使用 OFX 提供的额外信息来确保交易能被正确解析 — 而这些用 CSV 是无法实现的。
问题是 OFX 版本 1 虽然初看上去是 XML 格式的,但实际上它只是非常接近于 XML。如果试图将文件直接读入一个 XML 解析器,就会导致错误。如果文件是真正的 XML 格式(正如 OFX 版本 2 那样),就可以使用编程语言(比如 PHP)中内置函数的强大功能来更快更轻松地解析信息。不过,我的银行 — 可能很多其他银行也是如此 — 只能提供 OFX 版本 1.xx 文件。
一个例子
如下,是版本 1 文件的一个例子,清单 1 是直接下载后的版本,清单 2 是改进后的版本。我用粗体 标注了一些有用的添加和更改以使其更易于读懂。此文件只包含了一个帐户,该帐户具有两笔交易:一支(借)、一存(贷)。
清单 1. 直接下载的 OFX 文件
OFXHEADER:100
DATA:OFXSGML
VERSION:102
SECURITY:TYPE1
ENCODING:USASCII
CHARSET:1252
COMPRESSION:NONE
OLDFILEUID:NONE
NEWFILEUID:NONE
<OFX>
<SIGNONMSGSRSV1>
<SONRS>
<STATUS>
<CODE>0
<SEVERITY>INFO
<MESSAGE>OK
</STATUS>
<DTSERVER>20090211000000[-5:EST]
<USERKEY>--NoUserKey--
<LANGUAGE>ENG
<INTU.BID>00002
</SONRS>
</SIGNONMSGSRSV1>
<BANKMSGSRSV1>
<STMTTRNRS>
<TRNUID>XXXX - 20090211000000
<STATUS>
<CODE>0
<SEVERITY>INFO
<MESSAGE>OK
</STATUS>
<STMTRS>
<CURDEF>CAD
<BANKACCTFROM>
<BANKID>000000000
<ACCTID>000000
<ACCTTYPE>CHECKING
</BANKACCTFROM>
<BANKTRANLIST>
<DTSTART>20090209
<DTEND>20090209000000[-5:EST]
<STMTTRN>
<TRNTYPE>DEBIT
<DTPOSTED>20090209000000[-5:EST]
<TRNAMT>-98.91
<FITID>00000000000000000000000000
<NAME>GROCER A & Z
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT
<DTPOSTED>20090209000000[-5:EST]
<TRNAMT>308.86
<FITID>00000000000000000000000000
<NAME>DEPOSIT 000000
</STMTTRN>
</BANKTRANLIST>
<LEDGERBAL>
<BALAMT>256.94
<DTASOF>20090209000000[-5:EST]
</LEDGERBAL>
<AVAILBAL>
<BALAMT>256.94
<DTASOF>20090211000000[-5:EST]
</AVAILBAL>
</STMTRS>
</STMTTRNRS>
</BANKMSGSRSV1>
</OFX>
|
|
清单 2. XML 化后的 OFX 版本
<START>
<OFXHEADER>100</OFXHEADER>
<DATA>OFXSGML</DATA>
<VERSION>102</VERSION>
<SECURITY>TYPE1</SECURITY>
<ENCODING>USASCII</ENCODING>
<CHARSET>1252</CHARSET>
<COMPRESSION>NONE</COMPRESSION>
<OLDFILEUID>NONE</OLDFILEUID>
<NEWFILEUID>NONE</NEWFILEUID>
</START>
and remove blank line
<OFX>
<SIGNONMSGSRSV1>
<SONRS>
<STATUS>
<CODE>0</CODE>
<SEVERITY>INFO </SEVERITY>
<MESSAGE>OK </MESSAGE>
</STATUS>
<DTSERVER>2009021100000000[-5:EST] </DTSERVER>
<USERKEY>--NoUserKey-- </USERKEY>
<LANGUAGE>ENG </LANGUAGE>
<INTU.BID>00002 </INTU.BID>
</SONRS>
</SIGNONMSGSRSV1>
<BANKMSGSRSV1>
<STMTTRNRS>
<TRNUID>XXXX - 20090211000000000 </TRNUID>
<STATUS>
<CODE>0 </CODE>
<SEVERITY>INFO </SEVERITY>
<MESSAGE>OK </MESSAGE>
</STATUS>
<STMTRS>
<CURDEF>CAD </CURDEF>
<BANKACCTFROM>
<BANKID>000000000 </BANKID>
<ACCTID>0000000 </ACCTID>
<ACCTTYPE>CHECKING </ACCTTYPE>
</BANKACCTFROM>
<BANKTRANLIST>
<DTSTART>20090209 </DTSTART>
<DTEND>20090209000000[-5:EST]</DTEND>
<STMTTRN>
<TRNTYPE>DEBIT</TRNTYPE>
<DTPOSTED>20090209000000[-5:EST]</DTPOSTED>
<TRNAMT>-98.91</TRNAMT>
<FITID>00000000000000000000000000 </FITID>
<NAME>GROCER A & Z</NAME>
</STMTTRN>
<STMTTRN>
<TRNTYPE>CREDIT</TRNTYPE>
<DTPOSTED>20090209000000[-5:EST]</DTPOSTED>
<TRNAMT>308.86</TRNAMT>
<FITID>00000000000000000000000000 </FITID>
<NAME>DEPOSIT 00000000</NAME>
</STMTTRN>
</BANKTRANLIST>
<LEDGERBAL>
<BALAMT>256.94</BALAMT>
<DTASOF>20090209020000[-5:EST]</DTASOF>
</LEDGERBAL>
<AVAILBAL>
<BALAMT>256.94</BALAMT>
<DTASOF>20090211000000[-5:EST]</DTASOF>
</AVAILBAL>
</STMTRS>
</STMTTRNRS>
</BANKMSGSRSV1>
</OFX>
|
|
假设,现在需要知道当前余额。可以在 清单 1 的代码中查找字符串 <BALAMT> 并报告相关的数量。但是,实际上,会报告两个余额,而且二者可能还不太相同。第一个是分类帐余额;第二个是可用余额,可用余额是从分类帐余额中除去不可用的金额。就实际而言,这意味着所能支取的钱数可能会少于分类帐上所显示的。想要通过一个简单的文本搜索过程找到实际余额,有可能会比较复杂。
这就是需要 XML 展露身手的时候了,因为它能够去除这种不明确性,而且能够简化搜索过程。使用 清单 2 内的代码,假设删除
<START>...</START> 部分并在名为 sample.xml 的本地文件内保存其余的部分。根元素仍为
<OFX>。寻找当前的分类帐余额,您可以精确指定想要的余额。清单 3 所示的几行 PHP 代码的作用就是寻找余额。
清单 3. 用来获得分类帐余额的简单代码
<?php
// test ofx
$xmlstr = file_get_contents('sample.xml');
$xml = new SimpleXMLElement($xmlstr);
echo $xml->BANKMSGSRSV1->STMTTRNRS->STMTRS->LEDGERBAL->BALAMT."\n";
?>
|
在上述代码内,先是获得文件的文本内容并通过调用 SimpleXMLElement() 函数将其加载到一个 XML 对象。至于您不熟悉的涉及 ->
操作符的语法,它让您能够指向整个树的某个特定分支。在 PHP 内,让变量 $xml 指向整个字符字符串可以将您带到 XML 树的根。忽略根元素(在本例中,即 <OFX>),之后,就可以顺着此树的这个分支往上到达更小的分支,一直到不能再前进为止。所需数据就在最小分支的尾部。
在本例中,由于它特别地指向元素结构 ...->LEDGERBAL->BALAMT,因此运行此脚本后会生成如下结果:
这种编程准确而清晰。节省下来的时间,可以用到其他更为重要的事情,比如生命的意义以及大统一理论。
转换的标准和目标
 |
使用 PHP
请注意本文中我对 PHP 的使用,我是在命令行使用的 PHP,而输出则在一个终端窗口。如果在浏览器上下文内使用 PHP,如果合适,可以在我使用换行符(\n)的地方使用 HTML 换行(<br />)。
|
|
所以,如果想要享受处理 XML 的效力和简便性,直接下载的这个文件必须要进行转换。PHP(其他编程语言提供了同等的处理能力)具有一组函数,可以为此提供帮助。首先必须检查此文件(使用 清单 1 和 清单 2 中的代码)中的模式以便处理能够有效和准确。
如下列出的是本例中需要的一些调整:
- 头部分。在 OFX 上下文内很重要的前九行和随后的空白行很少改变。检查所处理的是不是一个 102 版本的文件会很有帮助,但除此之外,没有提供其他任何有用信息。还可以将这些项目放入它们自己的元素,在这种情况下,一个新的根元素将需要包含
<START> 和
<OFX> 这两个元素。
- 缺失结束符。最主要的问题是某些最里面的元素具有一个开始标记,却没有结束标记,而这是 XML 所必需的。
- 日期。OFX 所提供的日期并不符合 GNU 中对日期
的规定(更多信息的链接,请参阅 参考资料),因此不能被诸如
strtotime() 之类的函数立即读取。尤其是,日期是一种连续字符串,在日期和时间这两个部分之间没有分割空白。可以在初始处理或在之后报告数据时解决这个问题,因为 OFX 日期格式并不影响 XML 的有效性。
- 特殊字符。OFX 输出可能会 — 如本例所示 — 包含诸如 ampersand(
&)这样的字符,这些字符会导致 XML 阅读器报告错误。在验证文件之前,需要减少这类字符以使格式与 XML 兼容(比如,& =
&)。
脚本
清单 4 提供了一个能够实现这种转换的建议脚本。它接受文件 sample.ofx 作为其输入,该文件就是从银行下载的那个文件。
清单 4. 用来 XML 化 OFX 文件的脚本
<?php
// 1. Read in the file
$cont = file_get_contents('sample.ofx');
// 2. Separate out and remove the header
$bline = strpos($cont,"<OFX>");
$head = substr($cont,0,$bline-2);
$ofx = substr($cont,$bline-1);
// 3. Examine tags that might be improperly terminated
$ofxx = $ofx;
$tot=0;
while ($pos = strpos($ofxx,'<')) {
$tot++;
$pos2 = strpos($ofxx,'>');
$ele = substr($ofxx,$pos+1,$pos2-$pos-1);
if (substr($ele,0,1) =='/') $sla[] = substr($ele,1);
else $als[] = $ele;
$ofxx = substr($ofxx,$pos2+1);
}
$adif = array_diff($als,$sla);
$adif = array_unique($adif);
$ofxy = $ofx;
// 4. Terminate those that need terminating
foreach ($adif as $dif) {
$dpos = 0;
while ($dpos = strpos($ofxy,$dif,$dpos+1)) {
$npos = strpos($ofxy,'<',$dpos+1);
$ofxy = substr_replace($ofxy,"</$dif>\n<",$npos,1);
$dpos = $npos+strlen($ele)+3;
}
}
// 5. Deal with special characters
$ofxy = str_replace('&','&',$ofxy);
// 6. write the resulting string to the screen
echo $ofxy;
?>
|
该脚本在步骤 1 读取文件的内容,在步骤 2 扫描文本寻找根元素并去掉开始部分,以便让人第一眼就能看到根元素。在步骤 3,它循环遍历其余的文本,查找引入或结束某个元素的 < and > 字符。开始标记存储于
$als 数组,结束标记存储于 $sla 数组。array_diff() 函数比较这两个数组并标注出哪些元素没有结束符,将内容放入数组 $adif。在步骤 4,它遍历包含问题标记的整个数组,插入缺失的结束符。在步骤 5,如果需要,特殊的 and 符号可被 & 替代,最后,新的字符串被写到屏幕。当然,也可以用 file_put_contents() 函数直接将它写到一个新文件。
此脚本只是实现此任务的一种方式。其他语言和算法可能更好,但我更愿意采用这种方式。惟一为我带来问题的一个特殊字符是 and 符号,所以我只需处理该字符而没有采用 htmlentities()
函数的方式。
结果产生的就是一个与 XML 兼容的文件。如果您决定将其应用到来自您的银行或信用卡公司的下载文件中,结果应该可以在浏览器内查看。即便浏览器无法找到样式表,它也应该能显示出这个树。而且您现在用 XML 函数还能处理这个树。
从新结构中获益
现在,假设将 清单 4 的输出存储为一个称为 proc.xml 的新 XML 文件。清单 5 以一次查看一个交易的例子展示了该如何处理此文件。
清单 5. 用 XML 从 OFX 文件提取信息的示例代码
<?php
// test ofx
$xmlstr = file_get_contents('proc.xml');
$xml = new SimpleXMLElement($xmlstr);
// Let's get the balance first
$bal = $xml->BANKMSGSRSV1->STMTTRNRS->STMTRS->LEDGERBAL->BALAMT;
$dat = $xml->BANKMSGSRSV1->STMTTRNRS->STMTRS->LEDGERBAL->DTASOF;
$data = strtotime(substr($dat,0,8));
$datb = date('Y-m-d',$data);
echo "My balance is $bal as at $datb\n";
// Now point at the array of transactions and show the detail for each
$trans = $xml->BANKMSGSRSV1->STMTTRNRS->STMTRS->BANKTRANLIST->STMTTRN;
foreach ($trans as $tran) {
$trandate = trim($tran->DTPOSTED);
$tdate = date("Y-m-d",strtotime(substr($trandate,0,8)));
$tranamt = $tran->TRNAMT;
$trancrdr = $tran->TRNTYPE;
echo "$tdate $tranamt $trancrdr\n";
}
?>
|
应用到这个 XML 化了的 OFX 文件后,清单 5 内的代码现在完全可以识别以 XML 格式组织的文本,并且您还可以应用一些功能强大的 PHP 函数库,包括能获得所需数据的 -> 操作符。一种最为简便的编程捷径是接受一种深度嵌入分支,比如能够捕获交易的 STMTTRN 元素的数组,并将此引用存储于一个变量,类似上述的 $trans = $xml->BANKMSGSRSV1->...。之后,对于此数组的进一步引用(比如在 foreach() 迭代内)就会十分直观,并且会减少代码键入错误。
此脚本的输出类似于 清单 6。您可能需要从头到尾地删除空白或者 “修剪” 这些变量以使其能清晰显示。
清单 6. 清单 5 内代码的输出
My balance is 256.94 as at 2009-02-09
2009-02-09 -98.91 DEBIT
2009-02-09 308.86 CREDIT
|
这里,只使用了完整日期的日期部分。如果需要,通过额外编码,时间部分也可使用。虽然,在本例中,只是简单将数据报告回屏幕,您尽可以使用您应用程序内的信息来更新数据库、重新计算余额等 — 所有这些,根据用户反馈,脚本都能有条件地完成。
结束语
上述这些原理可以应用到任何一个文本文件,而这些文件 — 只需很少工作 — 就能成为 XML 格式的文件。
虽然本文的重点是从一个 OFX 文件导出数据,但是请记住,由于 OFX 标准被广泛认可,您可以使用它从应用程序将信息导出到能识别此格式的其他应用程序。所存在的惟一问题是您必须要决定是直接将它导出为标准的 OFX 版本 1 格式,还是将其导出为一个兼容 XML 的标准,之后再转换成版本 1 的格式。
毫无疑问,在将来,金融机构将会与软件发行商更为紧密地合作,并提供与大众化商业产品兼容的可下载 OFX 文件。如果此举能最终应用到 OFX 版本 2 的可下载文件,您将能够立即享受到直接进行 XML 解析的乐趣。在那之前,财务程序员必须要随时关注银行所提供的下载文件的变化并相应调整编程。
参考资料 学习
获得产品和技术
- 用于产品评估的 IBM 试用软件:使用可从 developerWorks 直接下载的试用软件构建您的下一个项目,这些软件包括来自 DB2®、Lotus®、Rational®、Tivoli® 和 WebSphere® 的应用程序开发工具和中间件产品。
讨论
关于作者  | |  | Colin Beckingham 居住在加拿大安大略省,是一位自由研究人员、作家和程序员。他拥有金斯顿皇后大学的学位,对园艺、赛马、教育、公共服务、零售和旅游/观光领域都有涉猎。他是数据库应用程序的作者,也是大量报纸、杂志和在线文章的撰稿人,他的研究兴趣包括 Linux ® 上的开源编程以及语音控制应用程序。 |
对本文的评价
|