UWSCからWin32APIのReadFileを使う
UWSC公式掲示板で、ファイルの後ろから読みたい、という質問を見たので書いてみた。
UWSCのFOPENはたしかすべてをメモリーに展開して、メモリー上で操作を行い、FCLOSEで書き出す。
このため、大きなファイルになると遅くなる、、、はず。
(メモリーが潤沢なら速い気もするけど)
FileSystemObjectはシーケンシャルリードだし、ADODB.Streamも全てメモリのはず。
ということで、Win32APIを使うしかないと判断。
ただ、場合によっては他に良い方法がある気もする。
条件が不明なので、なんとも言えないけど、、、あらかじめ切っておくとかね。
ま、とりあえず私の興味は、Win32APIにあるので、その方向で。
まずは回答
質問は、「コードが短くて処理が早い方法」なので、私にはできなかった。
それに、行毎の処理を書かれていたが、そこは後回し。
コードの長さについては、モジュール化すると、一見短い。
まず、テストに使用したスクリプト。
file.uwsは次に掲載。
OPTION EXPLICIT CALL file.uws // FGETでの最終行 DIM start = GETTIME() * 1000 + G_TIME_ZZ DIM fid = FOPEN(".\ken_all.csv", F_READ) DIM gyou = FGET(fid, -1) print FGET(fid, gyou) FCLOSE(fid) PRINT "FGET: " + (GETTIME() * 1000 + G_TIME_ZZ - start) // Fileモジュールで末尾128Byteを読む start = GETTIME() * 1000 + G_TIME_ZZ File.Open("KEN_ALL.csv") // 末尾128Byteに移動して、テキストとして読む DIM len = 128 File.Seek(-len, File.FILE_END) PRINT File.ReadText(len) File.Close() PRINT "File: " + (GETTIME() * 1000 + G_TIME_ZZ - start)
手元でちょっとやってみた結果としては、11.5MB程度のテキストファイルで、
FGET版が、359msec。
Fileモジュール版は、GETTIMEの精度限界か0が返ってきた。
1.4GBの後ろ128バイトを取得したところ、16msecでした。
ということで早いみたいです。
次にReadFileによるファイル操作用のモジュール。
これをインクルードして使いました。
file.uws
OPTION EXPLICIT IFB GET_UWSC_NAME = "file.uws" THEN PRINT "Open " + File.Open("KEN_ALL.csv") DIM low, high = 0 PRINT "Size " + File.GetSizeEx(low, high) + " " + low + " " + high low = 128 PRINT "Seek " + File.Seek(-low, File.FILE_END) PRINT "Read " + File.ReadText(low) PRINT "Close " + File.Close() ENDIF MODULE File DIM _handle = NULL DEF_DLL CreateFileW(wstring, dword, dword, dword, dword, dword, dword): dword: kernel32.dll CONST GENERIC_READ = $80000000 CONST GENERIC_WRITE = $40000000 CONST FILE_SHARE_READ = 1 CONST FILE_SHARE_WRITE = 2 CONST FILE_SHARE_DELETE = 4 CONST CREATE_NEW = 1 CONST CREATE_ALWAYS = 2 CONST OPEN_EXISTING = 3 CONST OPEN_ALWAYS = 4 CONST TRUNCATE_EXISTING = 5 CONST INVALID_HANDLE_VALUE = $FFFFFFFF DEF_DLL SetFilePointer(dword, long, var long, dword): dword: kernel32.dll CONST FILE_BEGIN = 0 CONST FILE_CURRENT = 1 CONST FILE_END = 2 DEF_DLL GetFileSize(dword, var dword): dword: kernel32.dll DEF_DLL ReadFile(dword, byte[], dword, var dword, dword): bool: kernel32.dll DEF_DLL CloseHandle(dword): bool: kernel32.dll DEF_DLL GetLastError(): dword: kernel32.dll // 文字列変換 CONST CP_ACP = 0 // default to ANSI code page CONST CP_OEMCP = 1 // default to OEM code page CONST CP_MACCP = 2 // default to MAC code page CONST CP_THREAD_ACP = 3 // current thread's ANSI code page CONST CP_SYMBOL = 42 // SYMBOL translations CONST CP_UTF7 = 65000 // UTF-7 translation CONST CP_UTF8 = 65001 // UTF-8 translation CONST ERROR_INVALID_PARAMETER = 87 // dderror CONST ERROR_INSUFFICIENT_BUFFER = 122 // dderror CONST ERROR_INVALID_FLAGS = 1004 CONST ERROR_NO_UNICODE_TRANSLATION = 1113 DEF_DLL MultiByteToWideChar(dword, dword, byte[], int, var wstring, int): int: kernel32.dll CONST MB_PRECOMPOSED = $00000001 // use precomposed chars CONST MB_COMPOSITE = $00000002 // use composite chars CONST MB_USEGLYPHCHARS = $00000004 // use glyph chars, not ctrl chars CONST MB_ERR_INVALID_CHARS = $00000008 // error for invalid chars DEF_DLL WideCharToMultiByte(dword, dword, wstring, int, byte[], int, byte[], var bool): int: kernel32.dll CONST WC_DISCARDNS = $00000010 // discard non-spacing chars CONST WC_SEPCHARS = $00000020 // generate separate chars CONST WC_DEFAULTCHAR = $00000040 // replace w/ default char CONST WC_ERR_INVALID_CHARS = $00000080 // error for invalid chars CONST WC_COMPOSITECHECK = $00000200 // convert composite to precomposed CONST WC_NO_BEST_FIT_CHARS = $00000400 // do not use best fit chars FUNCTION Open(file, acc = GENERIC_READ, create = OPEN_EXISTING, share = FILE_SHARE_READ) IF _handle <> NULL THEN Close() _handle = CreateFileW(file, acc, share, NULL, create, 0, NULL) IFB _handle = INVALID_HANDLE_VALUE THEN RESULT = GetLastError() ELSE RESULT = 0 ENDIF FEND FUNCTION Close() IFB CloseHandle(_handle) THEN RESULT = 0 ELSE RESULT = GetLastError() ENDIF FEND FUNCTION GetSize() DIM low, high, res = GetSizeEx(low, high) IFB res THEN RESULT = INVALID_HANDLE_VALUE ELSE RESULT = low ENDIF FEND FUNCTION GetSizeEx(var low, var high) low = GetFileSize(_handle, high) IFB low = INVALID_HANDLE_VALUE THEN RESULT = GetLastError() ELSE RESULT = 0 ENDIF FEND FUNCTION Seek(i, mode = FILE_CURRENT) DIM low = i, high = 0 IF low < 0 THEN high = -1 DIM res = SeekEx(low, high, mode) IFB res THEN RESULT = INVALID_HANDLE_VALUE ELSE RESULT = low ENDIF FEND FUNCTION SeekEx(var low, var high, mode = FILE_CURRENT) DIM res = SetFilePointer(_handle, low, high, mode) IFB res = INVALID_HANDLE_VALUE THEN RESULT = GetLastError() ELSE RESULT = 0 ENDIF IF RESULT = 0 THEN low = res FEND FUNCTION Read(var data[], var readsize) DIM size = LENGTH(data) IFB ReadFile(_handle, data, size, readsize, NULL) THEN RESULT = 0 ELSE RESULT = GetLastError() ENDIF FEND FUNCTION ReadText(size) DIM data[size - 1], readsize RESULT = Read(data, readsize) IFB !RESULT THEN DIM res = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, data, readsize, NULL, 0) IFB res > 0 THEN DIM ret = FORMAT(" ", res) res = MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, data, readsize, ret, res) RESULT = ret ELSE RESULT = "" ENDIF ENDIF FEND ENDMODULE
少し解説
FGET版は、最終行を表示できます。
Fileモジュール版は、行の概念を組み込んでいないため、後ろから128Byteを表示しています。
「File.Seek(-len, File.FILE_END)」がファイルポインターの移動ですね。
ファイルの末尾からlenバイトの長さ前に移動させています。
この後、「File.ReadText(len)」しているので、末尾まで読み込むことになります。
2GBか4GBか忘れましたが、ある程度の大きさまでは、File.Seek関数は機能します。
それを超える場合は、File.SeekEx関数を使う必要があります。
行毎の概念を入れるには、一定サイズ(一行に想定される最大サイズが良い)読み込んで、
CHR(13)とCHR(10)の組み合わせを検索するのが良さそうです。
ただ、読み込み位置とマルチバイト文字の関係から、File.ReadTextのリターン文字列では、
CHR(13)が見つからない可能性があるため、File.Readで取れる数値の配列から、
13と10の組み合わせを探す方が良いです。
見つかったらそのバイト配列を、File.ReadTextでやっているように、
MultiByteToWideCharすれば、UWSCで扱いやすいUnicode文字列になります。
(UWSCのCHRB関数でも文字変換できるけど、2Byte文字の判定をしないといけなくなる)
あ、もともとUnicodeのファイルなら、MultiByteToWideCharは不要で、CHR関数に渡す必要が出ます。
ASCII文字列のみなら、文字列変換は不要ですね。
ReadFileの宣言も考え直した方が良い、、、かも。
適当なサイズをバッファリングしながら、行毎の処理をするモジュールも可能ですね。
行毎の概念を入れなかったのは、面倒だったのと、実際には固定長で問題ないケースだと、
書くだけ無駄なので、やらなかったまでです。
WriteFileとかもこの調子でやれば、結構使いやすいモジュールになる可能性もありますね。
そこまで書くと、ADODB.Streamでバイナリーの読み書きするスクリプトの存在価値がなくなる、、、。