小さい頃はエラ呼吸

いつのまにやら肺で呼吸をしています。


ADODB.Streamオブジェクトを使ってBOMなしUTF-8のファイルを作成する方法

はじめに

ADODB.StreamオブジェクトのSaveToFileメソッドでUTF-8形式のファイルを作成した場合、ファイルの先頭にBOM(Byte Order Mark)が付加されてしまいます。
この記事では、BOMを付加せずに、UTF-8のファイルを作成する方法を紹介します。

2009.12.13追記

一時ファイルを作成しなくても、BOMなしのUTF-8ファイルを出力する方法が見つかりました。
BOMなしのUTF-8ファイルを作成するには、まずBOM付きの一時ファイルを作成します。そして、一時ファイルをバイナリモードで読み込み、BOMの部分(先頭3バイト)を読み飛ばした4バイト目からバイナリモードで目的のファイルに書き込むという手法を用います。

一応説明しておくと、一度Streamに対してUTF-8でデータを書き込みます。その後で、バイナリとして最初の3バイト(BOM分)を読み飛ばしてバイナリを読み出します。読み出したバイナリを新しいStreamにバイナリとして食わせてファイルに保存しました。
JavaScriptでファイルの書き込みをUTF-8で行う(htaまたは、wsh用) - jiroの日記 はてなブックマーク - JavaScriptでファイルの書き込みをUTF-8で行う(htaまたは、wsh用) - jiroの日記

上記のサイトは、JavaScriptで書かれていますが、僕はVBScriptでリライトしてみました。

BOMなしのUTF-8でファイルに書き込む
Sub saveFile(filename, text)
  On Error Resume Next
  
  ' ADODB.Streamのモード
  Dim adTypeBinary : adTypeBinary = 1
  Dim adTypeText : adTypeText = 2
  Dim adSaveCreateOverWrite : adSaveCreateOverWrite = 2
  
  ' ADODB.Streamを作成
  Dim pre : Set pre = CreateObject("ADODB.Stream")
  ' 最初はテキストモードでUTF-8で書き込む
  pre.Type = adTypeText
  pre.Charset = "UTF-8"
  pre.Open()
  pre.WriteText(text)
  ' バイナリモードにするためにPositionを一度0に戻す
  ' Readするためにはバイナリタイプでないといけない
  pre.Position = 0
  pre.Type = adTypeBinary
  ' Positionを3にしてから読み込むことで最初の3バイトをスキップする
  ' つまりBOMをスキップします
  pre.Position = 3
  Dim bin : bin = pre.Read()
  pre.Close()
  
  ' 読み込んだバイナリデータをバイナリデータとしてファイルに出力する
  ' ここは一般的な書き方なので説明を省略
  Dim stm : Set stm = CreateObject("ADODB.Stream")
  stm.Type = adTypeBinary
  stm.Open()
  stm.Write(bin)
  stm.SaveToFile filename, adSaveCreateOverWrite ' force overwrite
  stm.Close()
  
End Sub

一時ファイルを用いてBOMなしUTF-8ファイルを作成する方法

「UTF-8」を指定した場合、BOM付きのUTF-8で保存されてしまう。
BOMなしUTF-8は指定できないため、BOMなしにするにはBOMを除去する処理が必要になる。
具体的には以下のような処理を行う。
1. 一時ファイルに「UTF-8(BOM付き)」で出力する
2. 一時ファイルを4バイト目から読み込む(BOMが先頭3バイトのため)
3. 読み込んだ4バイト目以降を、出力ファイルに書き出す
4. 一時ファイルを削除する

忘れな〜い録 UTF-8(BOMなし)でファイルを保存する。(VBA) はてなブックマーク - 忘れな〜い録 UTF-8(BOMなし)でファイルを保存する。(VBA)

上記のサイトは、VBAで書かれていますが、僕はVBScriptで書いてみました。

UTF-8ファイルの書き込み用クラス
Class UTF8FileWriter
  Private name, errNo, errDesc
  
  ' コンストラクタ
  Private Sub Class_Initialize
    name = "UTF8FileWriter"
  End Sub

  ' UTF-8で保存する(BOMあり)
  Public Sub WriteAll(text, fileName)
    On Error Resume Next
    Dim medthodName : medthodName = name & "." & "WriteAll"
    
    With CreateObject("ADODB.Stream")
      .Type = 2
      .Charset = "UTF-8"
      .Open
      .writeText text
      .SaveToFile fileName, 2
      .Close
    End With
    If Err.Number <> 0 Then
      errNo = Err.Number
      errDesc = Err.Description
      On Error GoTo 0
      Call Err.Raise(errNo, medthodName, errDesc)
    End If
  End Sub
  
  ' UTF-8で保存する(BOMなし)
  Public Sub WriteAllWithoutBOM(text, fileName)
    On Error Resume Next
    
    Dim medthodName : medthodName = name & "." & "WriteAllWithoutBOM"
    Dim tmpFile : tmpFile = fileName & ".tmp"
    
    ' 一時ファイルにUTF-8形式で書き込む
    Call WriteAll(text, tmpFile)
    If Err.Number <> 0 Then
      errNo = Err.Number
      errDesc = Err.Description
      On Error GoTo 0
      Call Err.Raise(errNo, medthodName, errDesc)
    End If
    
    With CreateObject("ADODB.Stream")
      .Type = 1
      .Open
      .LoadFromFile(tmpFile) ' 一時ファイルをバイナリで読み取る
      .Position = 3          ' BOMの3バイトを読み飛ばす
      ' 4バイト目から目的のファイルにバイナリで書き込む
      Dim ws : Set ws = CreateObject("ADODB.Stream")
      ws.Type = 1
      ws.Open
      ws.Write(.Read(-1))
      ws.SaveToFile fileName, 2
      ws.Close
      .Close
    End With
    If Err.Number <> 0 Then
      errNo = Err.Number
      errDesc = Err.Description
      On Error GoTo 0
      Call Err.Raise(errNo, medthodName, errDesc)
    End If
    
    ' 一時ファイルの削除
    Call CreateObject("Scripting.FileSystemObject").DeleteFile(tmpFile)
    If Err.Number <> 0 Then
      errNo = Err.Number
      errDesc = Err.Description
      On Error GoTo 0
      Call Err.Raise(errNo, medthodName, errDesc)
    End If
  End Sub
  
End Class
呼び出し方
Dim fw : Set fw = New UTF8FileWriter
Call fw.WriteAllWithoutBOM("ほげ","out.txt")