目次


Open Financial Exchange ファイルに XML の力を活用する

XML ではない OFX ファイルで XML 構文解析を実現する

Comments

私が利用している銀行は、会計計算のプログラマーと帳簿管理者のために、非常に便利なサービスを提供してくれます。つまり私は、私の持っている口座の 1 つに関する指定期間内の入出金明細を記載した簡単なファイルをダウンロードすることができます。これらのファイルに含まれる情報には、口座名義と口座番号、口座の種類 (小切手口座か普通預金口座か、それ以外の口座か)、金融機関に関するさまざまな情報、私の口座の残高情報、私が情報を要求した日付と時刻、その口座の完全な入出金明細 (入金か出金か、その金額、入出金の日付と時刻) などがあります。データ入力の大部分は銀行が行ってくれており、私にとって必要なことは、その情報をローカルでの記録用にプログラムで転送することだけです (そうすることでローカル記録の精度を高め、オンライン・データと整合させる手間を省きます)。

私が利用している銀行の Web サイトにログインし、私の口座の入出金情報を含むファイルをダウンロードするためのページに行くと、そのファイル形式の選択肢として、CSV ファイル、あるいは Quicken、Intuit QuickBooks、Microsoft® Money が提示されます。私はさまざまな理由から、こうした主流の会計プログラムを使わずに独自のクラウド・コンピューティング・アプリケーションを使うことにしています。そのため私は、単純な CSV ファイル形式を選択するか、あるいは他のファイル形式でダウンロードしたデータを分解するかのいずれかを選択しなければなりません。

CSV ファイル形式を選択すると、データベースやスプレッドシートへのダウンロードが高速にできて便利ですが、他のファイル形式のデータを分解する方法にも明確な利点があります。実は CSV 以外のどのファイル形式を選択しても、ファイルそのものは同じであり、それぞれのパッケージに合った異なるファイル拡張子が付けられているにすぎません。このファイルは OFX フォーマット (詳細は「参考文献」を参照) によるプレーンテキスト文書です。OFX フォーマットの構造は、銀行やその他の金融機関との取引データを扱う際に十分な情報が正確に提供できるように設計されています。プロフェッショナルのプログラマーは OFX で提供される詳細情報を使って、取引データが適切であることを確認することができます。しかし、こうしたことは CSV を使って行うことはできません。

問題は、OFX バージョン 1 は一見 XML フォーマットのように見えても実際には XML を真似ているにすぎない点です。OFX ファイルを直接 XML パーサーに読み込もうとするとエラーになります。もし OFX バージョン 1 が (OFX バージョン 2 のように) XML フォーマットだったとしたら、PHP などのプログラミング言語に組み込まれている関数の強力さを活用してファイルの情報を素早く容易に読み取ることができます。しかし私が利用している銀行は (そして他の多くの銀行も同じだと思いますが) OFX バージョン 1.xx のファイルしか提供していません。

サンプル

下記は OFX バージョン 1 のファイルのサンプルです。ダウンロードされたまままの状態がリスト 1、改善されたバージョンがリスト 2 です。リスト 2 では XML 化するために追加した部分や変更した部分を太字にするとともに、人間が読みやすいようにインデントを入れてあります。このファイルには 1 つの口座における 2 つの操作 (1 回の出金 (debit) 操作と 1 回の入金 (credit) 操作) しか含まれていません。

リスト 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 &amp; 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> という文字列を検索し、<BALAMT> に関連する数字をレポートすることもできますが、実際には 2 つの残高がレポートされることになり、両者が異なっている可能性もあります。レポートの最初に表れるのは元帳残高であり、次に表れるのは利用可能残高 (hold されている額を考慮に入れた元帳残高) です。要するに、実際に引き出せる金額は元帳残高よりも少ない可能性があるということです。正しい残高を単純なテキスト検索で見つけようとすると複雑になる可能性があります。

ここで XML の強みが発揮されます。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 という要素構造を指しているので、このスクリプトを実行すると下記のような答えが得られます。

256.94

このプログラミングは簡潔で曖昧さがありません。このようにして節約できた時間を、他のもっと重要なこと、例えば人生の意味を考えたり、大統一理論について考えたりすることに費やせるようになります。

変換の基準と目標

XML による処理の厳密さや容易さを活用するためには、ダウンロードしたそのままのファイルを変換する必要があります。PHP には、そうした変換のための一連の関数が用意されています (他のプログラミング言語にもこれと同等の機能が用意されています)。まずダウンロードしたファイル (リスト 1リスト 2 のコード) でパターンを調べ、正確かつ効率的に処理を行えるようにする必要があります。

この記事では以下のような調整を行う必要があります。

  • ヘッダー・セクション。最初の 9 行とそれに続く空白行は OFX としては重要ですが、変更されることは稀です。処理対象がバージョン 102 のファイルであることをチェックすることは有効ですが、バージョン以外には有用な情報が何も提供されていません。この例で示されているように、これらの項目を独自の要素の中に入れることができます。その場合には <START> 要素と <OFX> 要素の両方を含む新しいルート要素が必要になります。
  • 終了タグがない。大きな問題は、最も内側にある要素のいくつかは開始タグはあるものの、(XML に必要な) 終了タグがないことです。
  • 日付。OFX で提供される日付は、日付に関する GNU のガイドライン (詳細は「参考文献」を参照) に準拠していないため、そのまま strtotime() などの関数で読み取ることはできません。特に、日付は 1 つの連続的なストリングであり、日付部分と時刻部分の間に区切りの空白がありません。OFX の日付フォーマットは XML の妥当性に影響しないため、日付の処理は最初に行うことも、後でデータのレポートを作成する際に行うこともできます。
  • 特殊文字。OFX 出力にはアンパサンド (&) などの文字が含まれることがあり (このサンプルには実際に含まれています)、これらの文字があると XML リーダーがエラーを起こします。ファイルの妥当性検証をする前に、そうした文字を XML と互換性のあるフォーマットに変更することが重要です (例えば &&amp; にするなど)。

スクリプト

リスト 4 は、この変換を行うためのスクリプトの一案です。このスクリプトの入力は、銀行のサイトからダウンロードしたままのファイルである sample.ofx ファイルです。

リスト 4. OFX ファイルを XML 化するスクリプト
<?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('&','&amp;',$ofxy);
  // 6. write the resulting string to the screen
  echo $ofxy;
?>

このスクリプトはステップ 1 でファイルの中身を読み取ります。ステップ 2 ではテキストをスキャンしてルート要素を探し、ルート要素の先頭部分が最初に来るように、ファイルの先頭部分を削除します。ステップ 3 では残りのテキスト部分に対してループ処理を行い、要素の開始または終了を示す < 記号と > 記号を探します。そして、開始タグを $als 配列に保存し、終了タグを $sla 配列に保存します。array_diff() 関数はこの 2 つの配列を比較し、どの要素が終了されていないかの注記を付け、それらの要素の中身を $adif 配列に入れます。ステップ 4 では、問題のタグの配列に対して繰り返し処理を行い、足りない終了タグを挿入します。ステップ 5 では、必要に応じて特殊文字のアンパサンドが &amp; で置き換えられ、最後に新しいストリングが画面に書き込まれます。もちろん、file_put_contents() 関数を使って新しいファイルに直接書き込むこともできます。

このスクリプトは変換を行うための 1 つの方法にすぎません。他の言語やアルゴリズムの方が適切かもしれませんが、私は長年この方法を使ってうまく行っています。私にとって問題のあった唯一の特殊文字はアンパサンドなので、アンパサンドのみに対して置換処理を行っており、htmlentities() 関数を強制的に使う方法は取りませんでした。

この変換の結果、整形式の XML ファイルが出来上がります。これを銀行やクレジットカード会社のサイトからダウンロードしたファイルに適用してみると、その結果をブラウザーで表示できるはずです。たとえブラウザーがスタイルシートを見つけられないという警告を表示したとしても、ツリーの表示はできるはずです。そしてこのツリーが得られると、XML 関数を使ってツリーを操作することができます。

新しい構造によるメリット

例えばリスト 4 の出力を proc.xml という新しい XML ファイルとして保存したとしましょう。リスト 5 はこのファイルの処理方法の例として、入出金明細を 1 件ずつ処理する方法を示しています。

リスト 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 関数ライブラリー (必要なデータを取得する -> 演算子など) を適用することができます。プログラミングのショートカットとして最も便利なものの 1 つに、深くネストされた枝 (入出金処理を取り込む 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 フォーマットを認識する別のアプリケーションに情報をエクスポートする場合にも OFX 標準が使えることを忘れないでください。唯一の問題は、標準的なバージョン 1 の OFX フォーマットに直接エクスポートするのか、あるいは整形式の XML としてエクスポートしてからバージョン 1 の OFX フォーマットに変換するのかを決めることです。

金融機関は今後主要なソフトウェア・パブリッシャーと協力しながら作業を行い、商用のマス・マーケット製品と互換性のあるダウンロード可能な OFX ファイルを提供することは間違いありません。そうした動きの最終的な結果として、ダウンロード可能なファイルが OFX バージョン 2 で得られるようになれば、XML を即座に直接構文解析できるようになります。その時まで、会計計算のプログラマーは銀行のサイトから提供されるダウンロード・ファイルの変更に目を光らせ、それに合わせてプログラムを調整する必要があります。


ダウンロード可能なリソース


関連トピック

  • Open Financial Exchange のメイン Web サイトを訪れ、金融機関やビジネス、コンシューマー同士がインターネットを介して会計データを電子交換するための統一仕様について学んでください。
  • 日付ストリングで使われる数字に関する GNU のページを訪れ、日付ストリングのコンテキストを正確に解釈し、必要な情報を得るための方法を学んでください。
  • developerWorks の XML ゾーンXML の技術ライブラリーとして、広範な話題を網羅した技術記事やヒント、チュートリアル技術標準、IBM Redbooks などを用意しています。
  • ウィキペディアで XML の項目を調べ、XML について学んでください。
  • W3 Schools の XML Tutorials を利用して XML に関する基本から JavaScript その他の高度なトピックまでのスキルを磨き、またスキルをテストしてください。
  • W3C の XML 仕様を読み、Web その他の場での大規模な電子出版や多種多様なデータの交換に有効な、この柔軟なテキスト・フォーマットについて学んでください。
  • XML および関連技術において IBM 認定技術者になる方法については、IBM XML certification を参照してください。
  • developerWorks podcasts ではソフトウェア開発者のための興味深いインタビューや議論を聞くことができます。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=XML, Open source
ArticleID=381394
ArticleTitle=Open Financial Exchange ファイルに XML の力を活用する
publish-date=03172009