Table of Contents
List of Figures
Table of Contents
メモリ中のデータをファイルに保存する時に、全く同じ「形」で保存する事はできません。
例えば、書籍を管理する簡単なプログラムを作るとします。すると、おそらく次 のような構造体のリンクリスト構造のデータをメモリ中に持つ事になります。
struct book_t { book_t* m_next; char* m_title; char* m_author int m_year; };
構造体を並べるのにリンクリストではなく配列を使う、とか、文字列のメンバ をポインタではなく固定長の文字列配列とする、とうのは×です。(1,1)から ら脱却できません。
このデータをファイルに保存するとします。
void save_data(const book_t* const data, FILE* fp) { for (const book_t* bp = data; bp != NULL; bp = bp->m_next) { fwrite(bp, sizeof(book_t), 1, fp); } }
まさか、こんな風にファイルに保存しようとはしませんよね? 文字列へのポ インタをファイル書き出したらからといって、その実体の文字列が自動的にファ イルに書き出されるわけでは有りません。
void save_data(const book_t* const data, FILE* fp) { for (const book_t* bp = data; bp != NULL; bp = bp->m_next) { fwrite(bp, sizeof(book_t), 1, fp); fwrite(bp->m_title, strlen(bp->m_title) + 1, 1, fp); fwrite(bp->m_author, strlen(bp->m_author) + 1, 1, fp); } }
これでとりあえず保存できました。では読む場合は?
const book_t* load_data(FILE* fp) { book_t* bp = (book_t*)malloc(sizeof(book_t)); fread(bp, sizeof(book_t), 1, fp); ... 文字列の長さが分からないからどれだけバッファを確保したら良いか分からない! }
大きさが決まっている構造体は読む前に領域を確保してそこに読み込めば良い ですが、長さが事前に分からない文字列はそうは行きません....読む前に長さ が分かれば良い!!。じゃあ、保存する時に文字列の長さを記録すれば良いん だ!
void save_str(const char* str, FILE* fp) { size_t str_len = strlen(str); fwrite(&str_len, sizeof(str_len), 1, fp); fwrite(str, str_len, 1, fp); } void save_data(const book_t* const books, FILE* fp) { for (const book_t* bp = books; bp != NULL; bp = bp->m_next) { fwrite(bp, sizeof(book_t), 1, fp); save_str(bp->m_title, fp); save_str(bp->m_author, fp); } } char* load_str(FILE* fp) { size_t str_len; fread(&str_len, sizeof(str_len), 1, fp); char* const str = (char*)malloc(str_len) + 1; fread(str, str_len, 1, fp); str[str_len] = '\0'; return str; } book_t* load_data(FILE* fp) { book_t* head = NULL; book_t* tail = NULL; do { book_t* bp = (book_t*)malloc(sizeof(book_t)); fread(bp, sizeof(book_t), 1, fp); bp->m_title = load_str(fp); bp->m_author = load_str(fp); if (head == NULL) head = bp; if (tail != NULL) tail->m_next = bp; tail = bp; } while (tail->m_next != NULL); return head; }
やた!...でも何か無駄があるような。book_t 構造体をそのまま保存している けど、ポインタの分は無駄じゃん。ロードした後に上書きされるんだから。 それに、データの終りはファイルの終りで判断すれば良いし。無駄を省いて
void save_str(const char* str, FILE* fp) { size_t str_len = strlen(str); fwrite(&str_len, sizeof(str_len), 1, fp); fwrite(str, str_len, 1, fp); } void save_data(const book_t* const data, FILE* fp) { for (const book_t* bp = data; bp != NULL; bp = bp->m_next) { save_str(bp->m_title, fp); save_str(bp->m_author, fp); fwrite(&bp->m_year, sizeof(bp->m_year), 1, fp); } } char* load_str(FILE* fp) { size_t str_len; fread(&str_len, sizeof(str_len), 1, fp); char* const str = (char*)malloc(str_len); fread(str, str_len, 1, fp); str[str_len] = '\0'; return str; } book_t* load_data(FILE* fp) { book_t* head = NULL; book_t* tail = NULL; while (!feof(fp)) { book_t* bp = (book_t*)malloc(sizeof(book_t)); bp->m_next = NULL; bp->m_title = load_str(fp); bp->m_author = load_str(fp); fread(&bp->m_year, sizeof(bp->m_year), 1, fp); if (head == NULL) head = bp; if (tail != NULL) tail->m_next = bp; tail = bp; } return head; }
完璧!....こんな風に考えたあなた。あなたは(1,1) から (2,2) を通らずに (3,1)に行こうとしています。(2,1)というレベルは存在するのですが、(2,1) は袋小路。そこから (3,1) には行けません。なぜか?
上記の例のようにデータの前にデータの長さをいれるというのは良く使われる 技法で、実際に頻繁に使われています。
このメリットは何と言っても、メモリ確保やコピーの無駄を最小限にできるこ と。それに、レコードやフィールドをスキップする時に中身を読まない (read())しないで飛び越し(seek())ができるので、メモリバスやI/Oバスに負 荷かかからない。
一方、欠点は、データの長さを記録するフィールド自体は固定サイズですから 制限が生じます。unsigned char (1バイト)なら256バイト、unsigned short(2 バイト)なら64Kバイト、unsigned long (4バイト)なら4Gバイト。
また、データ長を記録するフィールド自身は、本来保存したい情報ではなく、 入出力を効率化するための付帯情報です。つまり、無駄な情報なのです。
さて、レコードサイズやフィールドサイズの制限を設ける事は、実際にはどん な所が問題となるのでしょうか?
上記の例では、m_title や m_author の上限は 4GB です。本のタイトルが4GB を越えるってのは有り得ないでしょうし、まさか、ジュゲムジュゲム...で名 前が8GBもあるような人はいないでしょう。そもそも、今の32bitOSでは 4GB 以上のデータはメモリに乗せられない。だから、この制限は問題無いと思うで しょう。
良く有ることですがシステムを設計するときに「このフィールドにはそんなに 大きなデータは入らないだろう。無駄にデータ長フィールドに4バイトもとる 必要ないから2バイトにしておこう」という判断をすることが多々あります。 しかし、そのフィールドに入るデータのサイズが、将来においても増えないと いう事をだれが保証するでしょうか?
例えば、画像データファイル形式を設計しているとします。現時点では1枚で 4GB を越えるような画像データは滅多にありません。だから32ビットで大丈夫 だろうと思います。しかし、地球全体の1cm解像度の衛星写真を保存しようと したら32ビットでは入りません。
それに、未来の256ビットパソコンでは4GBなんてCPUの2次キャッシュのサイ ズです。未来の人達は、あなたがデータ長制限を4GBにした事を非難するでしょ う。
ファイルというのは将来読むために作るものです。将来というのは1秒先かも しれませんが、100年先かもしれません。今できないから、将来もできない と考えるのは誤りです。特にサイズに関する事は予想外の展開を見せます。で すから、ファイルの中にサイズに関する制限を埋め込むのは良く有りません。
また、あなたが書いたソースコードのロジックが素晴らしければ未来の256ビッ トパソコンのプログラマもそれを未来の256ビットCPU対応コンパイラでコンパ イルして使いたいと思うかもしれません。しかし、つまらないサイズ制限があ れば、それを使えないのです。
余談になりますが、文字列のメモリ中の形式についても同じ問題があります。 Pascal の文字列は null ターミネートではなく、文字列データの最初にその 文字の長さが1バイトで書かれています。だから、255文字より長いテキスト は文字列としては扱えません。ちなみに、MacOS Classic の API でも文字列 を Pascal 形式で扱っていました。だから、文字列の扱いには本当に苦労しま す。また、Visual Basic も同様で奴らは16bitで表してます。ですから、 32767文字より長い文字列は扱えません。この影響は 32-bit になった Windows にも深く残っています。NT系統ではない Windows のメモ帳の制限な どはこれです。
例えば、この書籍のデータベースは教授からの依頼で、研究室の書籍を管理し ようというものだったとします。教授は気まぐれですから、出来上がってデー タも溜った頃になって「著者が複数ある場合もなんとかしなさい」、「翻訳本 の場合は原書のタイトルや翻訳者も記録しなさい」と言い出すでしょう。
とすると、上記の最後のプログラムでは、フィールドの追加があったら書き直 (書き足)さなければなりません。それくらいならまだ良いのですが、古いバー ジョンのデータを新しいバージョンのプログラムで読めるようにする必要があ ります。もし、使い始めて時間が経ってデータが何百ギガバイトにもなってい たら、コンバージョンするためのディスクスペースや処理するための時間が無 いかもしれません。となると、新しいプログラムは古いプログラムが書いた古 いデータ形式に対応できなければなりません。
上記の形式でフィールドの追加に対応できるようにする方法として、レコード の区切りが分かるようにする事が考えられます。方法は、レコードの先頭にレ コードの長さを書く、レコードの終りに何か特別なマークを書く、です。前者 は圧倒的に効率が良いのですが前節の説明から却下です。すると、後者になり ます。
しかし、これでも問題が残ります。例えばフィールドをどんどん増やしていっ たします。例えば滅多に使われないフィールドがあると、その分無駄な領域が でてきます。この問題は単純な表形式(CSVでよく起こる)に共通の問題として 出てきます。空きばかりの配列。大規模数値計算をやっている人たちにおなじ みの疎行列の問題みたいなものです。
原因はなぜでしょう。これは、順番に意味を持たせてしまったからです。1番 目がタイトル、2番目が著者、3番面が出版年、という具合にです。
また、階層構造をもったデータを扱う事ができません。
フィールドの順番に意味を持たせる事の問題はほかにもあります。それは、そ の「順番の意味」をそのファイルとは別の情報として保存(あるいは転送)しな ければならないという事です。
この問題を解決する方法として「タグ」という考え方が導入されました。デー タの前にそのフィールドが何なのかを示す記号をいれるのです。{タグ,デー タ}の繰り返しという構造にするのです。またサイズ指定も加えて{タグ、 データ長、データ}という構造を取るものもあります。
タグがあれば新しいバージョンのプログラムが作ったデータを古いバージョン のプログラムでも読むことができます。知らないタグは無視するようにしてあ るならば。もちろん、新しいバージョンのプログラムは古いバージョンのデー タを読んで、単にフィールドが不足している、と認識します。デフォルト値を 与えるか、データが無い、と記録するか(データが無いのとデータサイズが0 は意味が違う!)。
さて、1節で「長さファイル構造に埋めるな」と書きました。となると、問題 になるのはレコードやフィールドの終り(区切り)の認識です。
何か特別な「終端記号」を決めてそれが見付かったら区切りとする事が思い付 きます。C 言語(および、POSIX API)で使われる null ターミネートが良い例 です。また、SGML や XML 等ではフィールドの開始だけでなく、終了にもタグ を使っています。
でも、「終端記号」と同じデータ(ビット列)がデータとしてフィールドに入っ ていたらどうしましょう。あるいは、データとして扱いたいとしたら。
例えば、終端記号を "." としたします。そのデータ形式を説明する文書をそ のデータ形式で保存しようとしたら。説明分の中で「このデータ形式では区切 りは"."をつかいます」と書いたら。保存するときはそのまま書き込めるかも しれません。一方、ファイルから読み取る時には「このデータ形式では区切り は"」で区切られてしまいます。
これを解決する方法、回避する記号列(エスケープシーケンス)という技法が必 要になります。
エスケープシーケンスは、プログラミング言語を使っているときには日常的に 出て来ています。CやJavaの文字列リテラルにバックスラッシュ(日本語フォン トセットでは円マーク)をいれる事で、その次の文字列に特別な意味を持たせ ています。良くやるのは " を文字列リテラルの中にいれたい時に \" としま す。
さて、エスケープシーケンスを導入した途端に、文字列と言うものの意味が変 わり始めてしまいます。そこに入っている文字列は本来のデータ(?)では無く なるわけです。エスケープシーケンスを解いた(解釈した、展開した)文字列が 本来のデータでしょう。つまり、本来のデータとコンピュータで扱えるようす るために符号化したデータのギャップが現れてくるわけです。
数値のみを扱うソフトウェアであれば良いですが、普通のソフトウェアでは必 ずテキストを扱います。所で「テキスト」ってなんでしょう?「文字列」?。 まぁ、そうですが、じゃ「文字」って?「文字」の定義(私の私見)をします。 人間どうしがコミュニケーションに使う、目に見えて、動かなくて(もよくて)、 白黒(白赤でもよくて)で、大きさに意味が無くて(大文字小文字は大きさだけ でなく形もちがう)、□に収まって、それを1次元に並べると発語と関連がつ いてちゃんと意味の有るものになる、視覚的な記号のことです。ただ、コン ピュータには視覚がありません(でした)から、コンピュータから視覚的に出力 する事はできても、コンピュータに視覚的に入力することができませんでした し、コンピュータ内部では視覚データをそのまま扱うことができなかったので、 「文字」を数値で代替する事を考えました。だから、文字一つづつに、数値を 割り当てました。それが文字コードです。文字コードの詳細については別のテー マで詳しくやります。
テキストをファイルに保存してして、時間や空間が離れた人に届けること考え るとき、文字コードが問題になります。同じ言語を理解する人に渡すとしても、 書いた文字コードと違う文字コードとして解釈して表示したら、相手はそれを テキストとして読む事ができません。ですから、このファイルのテキストはど の文字コードを使っているかということを何とかして伝えなければなりません。 可能であれば、ファイルの中に記録して置きたいです。
XML は前節で上げたような問題を全て解決したデータ形式です(全てを完全に 解決したとは言いませんが)。先の例をXMLで保存すると、ファイルは下のよう な「テキストファイル」になります。
<?xml version="1.0" encoding="iso-2022-jp" standalone="yes" ?> <!DOCTYPE book-list [ <!ELEMENT book-list (book+) > <!ELEMENT book (title, author, year) > <!ELEMENT title (#PCDATA) > <!ELEMENT author (#PCDATA) > <!ELEMENT year (#PCDATA) > ]> <book-list> <book> <title>Effective X++ 2nd ed.</title> <author>Scott Meyers</author> <year>1998</year> </book> <book> <title>The Annotated C++ Reference Manual</title> <author>Bjarne Stroustrup</author> <year>1990</year> </book> </book-list>
XML の詳しい仕様についてはここでは説明しません(気が向いたら書くかもし れませんが)。各自書籍なりインターネットを検索するなりして調べてくださ い。
XML の X は拡張可能と言っているのですが、その意味は、タグや属性名につ いて自由にして良いということです。意味も決まっていません。つまり、まっ さらな状態なわけです。まっさらな白いキャンバスに自由に書いてくださいと 言っているのです。ですから、つかうためには論理的な構造を考え、それを表 現するか決めなければなりません。
XMLの解釈をするプログラムを自分で書いてみて、かつ、配布して誰かに使っ てもらおうとするのは、かなり勇気がいります。なぜ?テキストファイルを解 釈する事の難しさがわかってます? 何のために、こういう単純で拡張がしや すそうな形式にしたか。XML の基本的な構造 element, attribute を解析(分 離)するプログラムが使い回せるじゃやないですか。XML を読んだり書いたり する時には、自分で printf() や scanf() を使っちゃダメです。もうわかり ますよね?
XML の基本構造に対応したAPIがあります。DOMと呼ばれるものです。DOMは仕 様です。実際にはそれぞれのプログラミング言語やOSやツールキット毎に実装 があります。当然言語が違えば関数のシグネチャは変わります。でも大体同じ 形をしているので、一度覚えれば、他の言語に移っても、なんとかなります (大同小異なので逆に混乱する場合もありますが)。
まず、Java はバイナリのデータ処理に向いていません。バイナリのデータを 扱うというのは、結局メモリの構造を自分で解釈していかなければならないで す。Java はそういうのは不得意です。なぜ?。ポインタ自由に使えないから。 だから、データをファイルにする時にはテキストファイルじゃやないとダメな んです。
逆に C はテキストを扱うのは不得意です。なぜ?。昔は ASCII しか無かった けど、今はマルチリンガルはあたりまえ。char でテキストを操作してはいけ ないのです。また、文字列は可変長が当り前ですので、メモリ管理を自前でや らなければならない C では、メモリ管理のコードばかりになって、本来の処 理がそのなかに埋もれてしまいます。もちろん、ツワモノにとってはそんなこ と大した問題になりません。ツワモンが書くのであれば、もちろん C の方が 圧倒的に性能が良いです。
つまり、Java > C というのは、文字列処理の得意さ&バイナリデータの扱い の不得意さ、なのです。
文字列処理の得意さといえば perl ですが、実は XML を扱うのには Perl は かなり良いのです。また、GNOME プロジェクトでは XML を沢山つかうので、 ツワモノが凄い高性能なライブラリ(libxml)を C で書いてくれました。当然、 別のツワモノが libxml を perl から呼べるようにするライブラリ(パッケー ジ)を書いてくれました(XML::LibXML)。C で書かれた高性能さと、perl の文 字列操作の便利さを両方堪能できます。おすすめです。
XML を一番気軽に扱える言語は実は JavaScript です。Windows では JavaScript のインタプリタが標準で入っていますし、Web ブラウザは JavaScript で実装されているようなものです(ちょっと極論かな?。でも実際 Mozilla はそんな感じ)。XML を使う練習をするのであれば、JavaScript がお 勧めです。もし、JavaScript を使ったことが無いのであれば、この機会に覚 えましょう。
下記のスクリプトは、エクセルのファイルを読んで XML に変換するものです。
function main() { var tag_list = new Array("", "title", "author", "year"); var argv = WScript.Arguments; if (argv.Count() == 0) { WScript.Echo("ファイルを指定してください"); WScript.Quit(); } var xls_file = argv(0); var fso = new ActiveXObject("Scripting.FileSystemObject"); xls_file = fso.GetAbsolutePathName(xls_file); if (!fso.FileExists(xls_file)) { WScript.Echo("ご指定のファイル \"" + xls_file + "\" は存在しませんよ"); WScript.Quit(); } if (xls_file.search(/.+\.(csv)|(xls)$/) < 0) { WScript.Echo("拡張子がエクセルのファイルじゃないよ"); WScript.Quit(); } var dom = new ActiveXObject("MSXML.DOMDocument"); var book_list = dom.createElement("book-list"); dom.appendChild(book_list); var excel_app = new ActiveXObject("Excel.Application"); excel_app.Workbooks.Open(xls_file); var ws = excel_app.Workbooks(1).Sheets(1); for (var r = 1; ws.Cells(r, 1).Value != null; r++) { var book = dom.createElement("book"); book_list.appendChild(book); for (var c = 1; c < tag_list.length; c++) { var value = ws.Cells(r, c).Value; if (value == null) continue; var elm = dom.createElement(tag_list[c]); elm.text = value; book.appendChild(elm); } } excel_app.Quit(); var xml_file = xls_file.replace(/(.+)\.(csv)|(xls)$/, '$1xml'); dom.save(xml_file); } main();
また、以下はその逆で XML から Excel を作るスクリプトです。
function main() { var tag_list = new Array("", "title", "author", "year"); var argv = WScript.Arguments; if (argv.Count() == 0) { WScript.Echo("ファイルを指定してください"); WScript.Quit(); } var xml_file = argv(0); var fso = new ActiveXObject("Scripting.FileSystemObject"); xml_file = fso.GetAbsolutePathName(xml_file); if (!fso.FileExists(xml_file)) { WScript.Echo("ご指定のファイル \"" + xml_file + "\" は存在しませんよ"); WScript.Quit(); } if (xml_file.search(/.+\.xml$/) < 0) { WScript.Echo("拡張子が XML じゃないよ"); WScript.Quit(); } var dom = new ActiveXObject("MSXML.DOMDocument"); dom.load(xml_file); var excel_app = new ActiveXObject("Excel.Application"); var wb = excel_app.Workbooks.Add(); var ws = wb.Sheets(1); var node_list = dom.documentElement.selectNodes("book"); for (var r = 1, e = new Enumerator(node_list); !e.atEnd(); e.moveNext(), r++) { var book = e.item(); for (var c = 1; c < tag_list.length; c++) { var elm = book.selectSingleNode(tag_list[c]); if (elm == null) continue; ws.Cells(r, c).Value = elm.text; } } var xls_file = xml_file.replace(/(.+)\.xml$/, '$1.xls'); wb.SaveAs(xls_file); excel_app.Quit(); } main();