一般業務系の単調なASPシステム開発では、通常、テキストベースのデータ送受信が中心なので、オラクルなどのRDBMSでVARCHAR2の最大文字数(4000バイト)を超える巨大テキストデータをバイナリ化して送るなどのケース以外、バイナリデータの取り扱いで時間を取られることはほとんどありません。
しかし、HTTPを使った画像データの送受信においては、ブラウザから送信されたバイナリデータについて、ASP(Active Server Pages)側でファイル名などのテキストデータと画像データなどのバイナリデータ(さらに他のINPUTタグがある場合は各データ)をそれぞれ取り出す必要があります。
ASPでは、バイナリデータを取り込むために、Requestオブジェクトに対するBinaryReadメソッドが用意されていますが、VBScriptの現バージョン(5.5)では、BinaryReadで得られたバイト型配列の構造体を解析できるメソッド等は用意されていません。
そのため、ユーザー側で構造体解析のプロシージャや関数を作成した上で、得られたバイナリデータの中で文字列が含まれている場合は、バイナリ文字列変換がどうしても必要となります。
ブラウザから画像ファイルを送信させるHTMLは、次の例であるとします。
<HTML> <BODY> <FORM NAME="PostData" METHOD="POST" ENCTYPE="multipart/form-data" ACTION="./upload.asp"> <P><INPUT TYPE="text" NAME="E-Mail" VALUE="webmaster@yumenokunisha.com"></P> <P><INPUT TYPE="file" NAME="Picture"></P> <P><INPUT TYPE="submit" NAME="Enter" VALUE="submit"></P> </FORM> </BODY> </HTML>
この場合、ENCTYPE="multipart/form-data"と指定しているので、受信側のASPでは、フォームPostDataに含まれる様々なデータ形式のデータをうまく処理する必要があります。
例えば、INPUTタグ「Pictute」はTYPEが「file」なので、VALUEにはテキストでクライアント側のファイル名がフルパスで入りますが、実際にブラウザからの送信内容は、次のとおりとなります。
-----------------------------7d011112101a0 Content-Disposition: form-data; name="E-Mail" webmaster@hoge -----------------------------7d011112101a0 Content-Disposition: form-data; name="Picture"; filename="S:\Test.gif" Content-Type: image/gif バイナリデータがここに並ぶ -----------------------------7d011112101a0 Content-Disposition: form-data; name="Enter" Submit Query -----------------------------7d011112101a0--
普通のINPUTタグ(TYPEが「text」)の場合、上記のNAMEが「E-Mail」のように、二つの改行(vbCrLfが二つ、合計4バイト)の次にVALUEが現れますが、TYPEが「file」の場合は、「filename=」とその値と改行、「Content-Type:」とその値の次に、二つの改行があって、やっとデータが並びます。
受信側では、まず送られたバイナリデータ全体をRequest.BinaryReadを使って、バイト型配列変数に格納します。その際、BinaryReadメソッドの引数として、配列のサイズを渡す必要があります。
'lngTotalBytes = Request.ServerVariables("CONTENT_LENGTH")
lngTotalBytes = Request.TotalBytes
bytePostData = Request.BinaryRead(lngTotalBytes)
上記の例にあるように、Requestオブジェクトに対するTotalBytesメソッドで得られるバイト数と、CGIの環境変数「CONTENT_LENGTH」で得られるバイト数は同じになります。
さて、この変数bytePostDataにはブラウザからの送信内容がすべて入っているわけですが、これをうまく解析できるプロシージャを用意する必要があります。
取り出す項目は、次のとおりです。
TYPE="text"の場合
Name
Value
TYPE="file"の場合
Name
FileName
ContentType
Value
これを具体化するために、Dictionaryオブジェクトをあらかじめ作成しておいて、その中に値を書き込むプロシージャを作成します。
Dim dicObj
Set dicObj = CreateObject("Scripting.Dictionary")
SetUploadData bytePostData
strEmail = dicObj.Item("E-Mail").Item("Value")
strFilePathName = dicObj.Item("Picture").Item("FileName")
strContentType = dicObj.Item("Picture").Item("ContentType")
binPicture = dicObj.Item("Picture").Item("Value")
Sub SetUploadData(byteData())
'最初に、POSTされたデータの区切りとなる境界文字列を求めます。
'キャリッジリターンが最初に含まれる位置まで文字列が、
'次の例のように、境界文字列として繰り返し用いられます。
'-----------------------------7d011112101a0
'最後の境界文字列には、次の例のように、最後に"--"が付加されています。
'-----------------------------7d011112101a0--
lngBegin = 1
lngEnd = InStrB(lngBegin, byteData, Str2Bin(Chr(13)))
strBoundary = MidB(byteData, lngBegin, lngEnd - lngBegin)
lngCurrentToken = InStrB(1, byteData, strBoundary)
Do Until (lngCurrentToken = InStrB(byteData, strBoundary & Str2Bin("--")))
Dim dicTempObj
Set dicTempObj = CreateObject("Scripting.Dictionary")
'ダブルクォーテーションに囲まれた文字列が
'POSTされたINPUTタグのNAMEの値として、
'"name="の次に現れますので、値を記憶します。
lngPosition = InStrB(lngCurrentToken, byteData, Str2Bin("Content-Disposition"))
lngPosition = InStrB(lngPosition, byteData, Str2Bin("name="))
lngBegin = lngPosition + 6
lngEnd = InStrB(lngBegin, byteData, Str2Bin(Chr(34)))
strName = Bin2Str(MidB(byteData, lngBegin, lngEnd - lngBegin))
'POSTされたINPUTタグのタイプがfileの場合、
'"name="の次に、ファイルネームが"filename="の次に現れます。
'その記述位置をメモリに記憶します。
'この場合、必ずしもカレントのトークンの位置とは限りません。
lngFileName=InStrB(lngCurrentToken, byteData, Str2Bin("filename="))
'次の境界文字列までの間が
'INPUTタグのタイプfileに関するデータとなります。
lngNextBoundary = InStrB(lngEnd, byteData, strBoundary)
'カレントのトークンのINPUTタグのタイプがfileである場合、
'必ず、次の境界文字列との位置関係が正しくなります。
If lngFileName <> 0 AND (lngFileName < lngNextBoundary) Then
'ファイル名を取得します。
lngBegin = lngFileName + 10
lngEnd = InStrB(lngBegin, byteData, Str2Bin(Chr(34)))
strFileName = Bin2Str(MidB(byteData, lngBegin, lngEnd - lngBegin)) 'Debug
'一時的な辞書オブジェクトに記憶させます。
dicTempObj.Add "FileName", strFileName
lngPosition = InStrB(lngEnd, byteData, Str2Bin("Content-Type:"))
lngBegin = lngPosition + 14
lngEnd = InStrB(lngBegin, byteData, Str2Bin(Chr(13)))
'一時的な辞書オブジェクトにContent-Typeを記憶させます。
strContentType = Bin2Str(MidB(byteData, lngBegin, lngEnd - lngBegin))
dicTempObj.Add "ContentType", strContentType
'カーソルを動かし、値のバイナリデータを取得します。
lngBegin = lngEnd + 4
lngEnd = InStrB(lngBegin, byteData, strBoundary) - 2
strValue = MidB(byteData, lngBegin, lngEnd - lngBegin)
Else
'filenameとConten-Typeは存在しないので、
'カーソルを動かし、値のバイナリデータを取得します。
lngPosition = InStrB(lngPosition, byteData, Str2Bin(Chr(13)))
lngBegin = lngPosition + 4
lngEnd = InStrB(lngBegin, byteData, strBoundary) - 2
strValue = Bin2Str(MidB(byteData, lngBegin, lngEnd - lngBegin))
End If
'一時的な辞書オブジェクトに値を記憶させます。
dicTempObj.Add "Value", strValue
'永続的な辞書オブジェクトに、
'NAMEのキーペアとして一時的な辞書オブジェクトの中身を記憶します。
dicObj.Add strName, dicTempObj
'次のINPUTタグPOSTデータに移ります。
lngCurrentToken = InStrB(lngCurrentToken + LenB(strBoundary), byteData, strBoundary)
Loop
End Sub
' http://www.asptoday.com/articles/20000316.htm
' Philippe Collignon氏のコーディングを参考
長い引用となってしまいました。これでそれぞれの値が得れるわけですが、上記の関数で多用されている関数「Bin2Str」と「Str2Bin」が今日のお題であります、バイナリ文字列関数であるわけです。
上記掲載の関数に使用している「Bin2Str」と「Str2Bin」は、シングルバイトの英語環境では、シンプルな内容になります。同じく、Philippe Collignon氏のコーディングを参考にすると、次のようなものになります。
Function Bin2Str(binData)
Bin2Str =""
For intCount = 1 To LenB(binData)
Bin2Str = Bin2Str & Chr(AscB(MidB(binData, intCount, 1)))
Next
End Function
Function Str2Bin(strData)
For i = 1 To Len(strData)
strChar = Mid(strData, i, 1)
Str2Bin = Str2Bin & ChrB(AscB(strChar))
Next
End Function
まず「Bin2Str」では、バイナリ文字列(VarTypeがvbArray + vbByte=8209のバイト型配列)をシングルバイト単位で文字に変換してつなぎ合わせていくわけですが、英語環境では、引数の「binData」に含まれる文字は、常にASCIIコード&H7以下であることが暗黙の前提になっています。
また、「Str2Bin」も同様に、その逆を行う、というものです。
ところが、日本語環境では、このままではエラーとなってしまいます。これは、日本語環境(コードページ932)では、ASCIIコード&H81から&H9Fまで、あるいは&HE0から&HFCは、必ずマルチバイトの文字を構成するための先行バイト(Lead Byte)と認識されてしまうからです(先行バイトの範囲は、使用するコードページにより異なります)。
また、単に「インターネットは7bit(0〜127)の世界。&H7(127)より大きい場合はシングルバイト文字として扱ってはいけない」、とだけ評価してしまうと、例の「半角カナ」を正しく評価できない、という情けない症状が出てしまいます。
この問題を回避するには、バイナリ文字列について、バイト単位でASCIIコードを評価し、マルチバイトにおける先行バイトである場合は、次に続くバイトと組み合わせてChr()関数でマルチバイト文字を構成する必要があります。
その逆は、1文字を構成しているASCIIコードが十六進で2桁か4桁かを評価し、4桁の場合は、2桁ずつシングルバイトに分割していく必要があります。
コーディング例は、少々トリッキーですが、次のとおりになります。
Function Bin2Str(byteData)
Bin2Str =""
i = 1
Do While i <= LenB(byteData)
u = Hex(AscB(MidB(byteData, i, 1)))
If ((CInt("&H" & u) >= &H81) And (CInt("&H" & u) <= &H9F)) _
Or ((CInt("&H" & u) >= &HE0) And (CInt("&H" & u) <= &HFC)) Then 'Code Page 932
l = Hex(AscB(MidB(byteData, i + 1, 1)))
intChar = CInt("&H" & u & l)
s = Chr(intChar)
i = i + 2
Else
intChar = CInt("&H" & u)
s = Chr(intChar)
i = i + 1
End If
Bin2Str = Bin2Str & s
Loop
End Function
Function Str2Bin(strData)
For i = 1 To Len(strData)
strChar = Mid(strData, i, 1)
strHex = CStr(Hex(Asc(strChar)))
Select Case Len(strHex)
Case 1 '1Byte
Str2Bin = Str2Bin & ChrB(CInt("&H" & Mid(strHex, 1, 1)))
Case 2 '1Byte
Str2Bin = Str2Bin & ChrB(CInt("&H" & Mid(strHex, 1, 2)))
Case 4 '2Byte
Str2Bin = Str2Bin & ChrB(CInt("&H" & Mid(strHex, 1, 2)))
Str2Bin = Str2Bin & ChrB(CInt("&H" & Mid(strHex, 3, 2)))
End Select
Next
End Function
VBScriptを使ったASP(もちろんVisual Basicでも)では、通常、何気なく文字列操作を行っていますが、バイナリデータを扱うと、とたんにC言語的格闘が始まります(例えば、すべてを最初からVC等で書いていたら、もっとあきらめはつくのですが)。
ここからは、補足説明ですが、まず最初に、ASPで受信したバイナリデータをそのままクライアントに返す場合の処理について考えます。
ASPでは、BinaryReadの逆のメソッドとして、BinaryWriteメソッドが用意されています。コーディング例は、次のとおりとなります。
Response.ContentType = strContentType Response.BinaryWrite binPicture
BinaryWriteメソッドで吐き出す前に、ブラウザに対し、「image/gif」などの「Content-Type」を明示する必要があります。実際のデータは、次のとおりとなります。
HTTP/1.1 200 OK Content-Type:image/gif '空の行 '空の行
なお、バイナリデータをSeesionオブジェクトでページ間渡しをして利用することも良いでしょう。その場合も、「Content-Type」も忘れずに渡すようにします。
Session("ContentType") = strContentType
Session("Picture") = binPicture
Response.Redirect "./upload-redirect.asp"
一方、テキストファイルをPUTした場合は、「Content-Type」が「text/plain」や「application/octet-stream」のバイナリ文字列となりますが、このままBinaryWriteしようとすると、ブラウザ側では、ファイルのダウンロード待ち状態となってしまいます。
この問題を避けるためには、HTML内での文字列の吐き出しにより、ブラウザが処理できるようにしなければなりません。コーディング例は、次のとおりです。
If strContentType = "text/plain" Then
strData = Bin2Str(binPicture)
Response.Write Server.HTMLEncode(strData)
End If
ここでは、まずバイナリ文字列を変換した後、さらにHTML内で特殊文字等を正しく表示させるためのエンコード処理であるHTMLEncodeメソッド(Serverオブジェクト)を用います。また、改行やスペースを正しく表示させるには、当然ながら、この吐き出し前後に、<PRE></PRE>のべた書き指定をする必要があります。
ただし、VBScript上での「Bin2Str」(「Str2Bin」も同様)は負荷が大きく、大きなバイナリデータの変換ではタイムアウトが発生してしまう恐れがありますので、注意してください。
次に、データベースへの書き込みや読み出しについて考えます。
画像ファイルをデータベースに直接バイナリデータとして書き込むことは、一般的にあまり推奨されませんが、VBScriptの現バージョンのFileSystemObjectで、バイナリデータの読み書きがサポートされていない(マイクロソフトのアナウンスでは「将来サポートする予定」とのこと)現状では、フリーのActiveXオブジェクト(有名な「basp21」など)を活用する以外にVBScriptからの処理はできないわけですから、マイクロソフト的に言うと、ASPにおいてはデータベース処理が唯一の「正式な」データ保存方法ということになります。
ADOの手法に関する詳細は別の機会に考えるとして、ここでは、基本的な処理についてのコーディング例を示します。
strDSNString = "DSN=ora_svr;UID=scott;PWD=tiger"
Set objCon = CreateObject("ADODB.Connection")
objCon.Open strDSNString
Set objCmd = CreateObject("ADODB.Command")
objCmd.ActiveConnection = objCon
strSQL = "SELECT * FROM ImageMaster"
objCmd.CommandText = strSQL
objCmd.CommandType = adCmdText
objCmd.CommandTimeout = 30
Set objRS = CreateObject("ADODB.Recordset")
objRS.CursorType = adOpenStatic
objRS.LockType = adLockOptimistic
objRS.Open objCmd
If (LenB(binPicture) Mod 2) = 1 Then
binPictureChunk = binPicture & ChrB(0)
Else
binPictureChunk = binPicture
End If
objRS.AddNew
objRS.Fields("ID") = lngNewID
objRS.Fields("ContentType") = strContentType
objRS.Fields("Image").AppendChunk binPictureChunk
objRS.Update
objRS.Close
objCon.Close
Set objRS = Nothing
Set objCmd = Nothing
Set objCon = Nothing
まず、レコードセットの各プロパティを設定します。レコードセットに対するUpdateメソッド使う場合の「adOpenStatic」や「adLockOptimistic」などは、ADOのタイプライブラリでの定数で、仮想ルートに置く「global.asa」ファイルの「METADATA TYPE="TypeLib"」タグで、「FILE="C:\Program Files\Common Files\System\ado\msado21.tlb"」を指定するか、各ASPファイル内で「C:\Program Files\Common Files\System\ado\adovbs.inc」ファイルをインクルード宣言して用います(Visual BasicやVCなどでの「参照設定」や「#import」と同様です)。「global.asa」については、また別の機会に考えてみます。
また、途中で、バイナリデータのサイズが偶数か奇数かを計り、奇数バイトなら無理やり1byteを足していますが、これは、前述のPhilippe Collignon氏が指摘しているとおり、ADOのAppendChunkのバグ(奇数バイトの場合、最後のバイトが欠けてしまう)を回避するための苦肉の策です。
一方、読み出し方はWHERE句でレコードを選択してから、次のとおりとなります。
strContentType = objRS.Fields("ContentType")
lngSize = objRS.Fields("Image").ActualSize
binPicture = objRS.Fields("Image").GetChunk(lngSize)
バイナリデータを扱うには、相当程度に詰めを行い、安定して動作するようにチューニングを繰り返さなければなりませんし、面倒なこともたくさんあります。
業務で使う告知板システムをASPで開発した時に、書き込みをテキストファイルに落とすか、データベースにするかでかなり悩みました。ファイルの場合は、画像データやワープロ文書など、バイナリの添付ファイルがあった場合には大変だし、一方、データベースの場合、サイズを気にしなければならない(VARCHAR2のフィールドを二つ以上用意したり)、といった悩みにぶつかりました。
また、データベース接続についても、「オラクルなら『oo4o』を使わないといけない」、とずいぶん脅され、それではデータベースの互換性が乏しくなる、といった悩みの日々が続きました。
落ち着いて考えてみれば、オラクルでは「BLOB型」、アクセスの場合なら「OLE オブジェクト型」のフィールドに対して、バイナリの読み書きをして、必要に応じて文字列変換をかければ良い、というところになると思います。
もうしばらくして、バイナリデータを自由自在に扱えるオブジェクトが用意されれば、本稿は役目を終えることになりますが、やはり、この類のお話は、学習経験として誰もが必ず通過しなければいけない道のようにも思います。