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でバイナリーの読み書きするスクリプトの存在価値がなくなる、、、。