csvを0落ちしないで、ANSIだろうがUTF-8だろうが、一発でExcelで開きたい

# 課題
* マスタのコードなどに使われる0始まりの番号が .csvをExcelで開くと先頭の0が消えてしまい、これではデータとして壊れてしまう。 手動の場合の対処は[こちら](https://qiita.com/yamashiroakihito/items/24479cf04b22c74a9764)
 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/2ca4129a-69d6-4240-385d-8ca78a3fa176.png)


* UTF-8で保存されることが要件のcsvファイルをExcelで開くと文字化けする。手動の場合の対処は[こちら](https://qiita.com/yamashiroakihito/items/6922661fb6d5753b67d8)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/b3072c6a-3325-78de-a839-5fb3d2d5c768.png)

* ファイルの拡張子を.txtに変更して、テキストファイルウィザードで開けばいいのだが、これは割と面倒なので、自動化したい。

# 対処方法

VBSのスクリプトを作りました。
csvをtxtの拡張子にしたコピーを作り、そのファイルをExcelのテキストウィザードですべての列を文字列属性にして開くことを自動化するスクリプトです。

後述のスクリプトを OpenCSV.vbs というファイル名で(本当はなんでもいいけど、説明の都合、なにかファイル名を決めたいので、このファイル名にします。) デスクトップに保存してください。(これも本当はデスクトップじゃなくてもいいです。)

OpenCsvファイルに開きたい csvファイルあるいはtxtファイルをドラッグしてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/54dbfdba-9114-11b0-4832-a5747a1c2d3a.png)

csvファイルの場合は、以下のメッセージがでるの、「はい」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/279ad53e-7c30-0583-e092-5f5436b924ea.png)

以下の通り、Excelが開きます。
文字化けもせず、0落ちもしません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/956c692e-b351-8285-8268-dc6dbe819deb.png)

# ドラッグ & ドロップすら　面倒くさい場合

そういう場合は、エクスプローラの「送る」に、OpenCSV.vbs を登録してしまいます。
手順
- Windowsキーを押したまま、Rキーを押します。
- 「ファイルを指定して実行」ダイアログが表示されますので、「Shell:Sendto」と入力します。
- ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/825c2fad-c660-848d-7355-afd78b5b822a.png)

表示された 「SendTo」フォルダに、上記で保存したデスクトップ上の「OpenCSV.vbs」ファイルをコピーします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/cc2670f2-0ef9-9372-6497-50a7ab00b822.png)

これで「送る」に OpenCSV.vbsが登録されました。
- 開きたいファイルを右クリックして、「送る」→「OpenCSV.vbs」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/a2a3dfbf-ad83-2f81-45a7-9870074347a8.png)

# コードの保存方法

後述のコードをコピーして、 OpenCSV.vbs として保存してください。

コードを囲っている黒い領域の右上にカーソルをあてると、コピー用のアイコンがでてくるので、それでコピーできます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215956/e8ea668a-1fc3-f823-fbd4-8aa18717f5f4.png)

※ vbs保存時は 文字コードを ANSIにしてください。　メモ帳のデフォルトは 近年、UTF-8になっています。 保存時にANSIに変更してください。

# コード

```vb
OpenTxtAsStringColumns

'指定されたファイルを標準列属性ではなく文字列属性で開くためのvbsです。
'CSVがUTF-8かどうかも判断します。
Sub OpenTxtAsStringColumns

        If WScript.Arguments.Count = 0 Then
                Exit Sub
        End If

        TargetFilePath = WScript.Arguments(0)

        'ファイルの拡張子がcsvだとExcelが変な自動判定をしてしまうので
        'うまくうごかない。よって、拡張子が .csvかを調べる
        Set FSO = CreateObject("Scripting.FileSystemObject")
        FileExt = FSO.GetExtensionName(TargetFilePath)

        If UCase(FileExt) = "CSV" Then
           NewTargetFilePath = TargetFilePath & ".txt"
           Response = MsgBox( _
                      "拡張子csvはExcelの標準列制御を回避できません。" & vbCrLf & _
                      "同じフォルダに拡張子txtを付与した下記ファイルを自動作成して" & vbCrLf & _
                      "それを開きますか?" &vbCrLf & _
                      TargetFilePath, vbYesNo + vbDefaultButton2 , "確認" )

           If Response = vbNo Then
               MsgBox "処理が取り消されました"
               Exit Sub
           End If
           
           On Error Resume Next

           FSO.CopyFile TargetFilePath, NewTargetFilePath

           If Err Then
              MsgBox "コピー先のファイルが他のプログラムで開かれていますので" & _
                     "コピーができません。処理を中止します。" & _
                     vbCrLf & vbCrlf & NewTargetFilePath , vbExclamation
              Exit Sub
           End If
           On Error Goto 0

           TargetFilePath = NewTargetFilePath
        End If

        Const xlDoubleQuote = 1
        Const xlWindows = 2
        Const xlDelimited = 1

        Dim Origin
        Origin = xlWindows 'ANSI CodePage

        If CanBeUTF8(TargetFilePath) Then
                Origin = 65001 'UTF-8
        End If

        'ファイルの内容をみて列数分の列情報を作るのは手間なので
        '最大列数分の列情報を作っておきます。
        'すべての列を文字列属性で読み込ませます。
        Const MaxColInExcel = 16384
        ReDim FieldInfo(MaxColInExcel-1)
        For i = 1 To MaxColInExcel
                'Array(4,1) は 4列(A列)が標準属性(1)であることを示します。
                'Array(5,2) は 5列(B列)が文字列属性(2)であることを示します。
                'Array(6,3) は 6列(C列)が日付属性(3)であることを示します。
                FieldInfo(i-1) = Array(i, 2)   
        Next
        Set XL = CreateObject("Excel.Application")
        
        XL.Application.Visible = True

        XL.Application.WorkBooks.OpenText _
                TargetFilePath, Origin, _
                , xlDelimited, xlDoubleQuote, _
                False, False, False, True, _
                False, False,, FieldInfo
                                                        
End Sub

Function CanBeUTF8(TestFilePath)

    'UTF8の規定に従ったファイルでないときにこの既定値で処理を抜けます。
    'VBSでは As Boolean宣言ができず、Variant型の扱いなので、既定値を明示します。
    'VBAの場合は、Function ... As Boolean で宣言しておけば、この1行は不要です。
    CanBeUTF8 = False

    'ファイルをバイナリデータとして読み取るために ADODB.Streamを用います。
    Const adTypeBinary = 1
    Set Ado = CreateObject("ADODB.Stream")
    Ado.Type = adTypeBinary
    Ado.Open

    'バイトデータの並びを文字列型変数に読み取ります。
    '読み取ったデータは、配列としてはアクセスできません。
    '代わりにMidB を使って取得、代入ができます。
    Ado.LoadFromFile TestFilePath
    ByteArrayAsString = Ado.Read
    Ado.Close

    '1バイト目  2バイト目以降
    '00..7F     なし
    'C2..DF     80..BF
    'E0..EF     80..BF      80..BF
    'F0..F4     80..BF      80..BF      80..BF

    '文字列型の形式をとっていますが、中に入っているのはUnicode文字列ではなくByteの並びなので
    'その長さは、Lenではなく、LenBで判断します。
    For i = 1 To LenB(ByteArrayAsString)

        'MidBで各バイトにアクセス可能です。 さらにAscBに代入して、数値として扱うことができます。
        FirstByte = AscB(MidB(ByteArrayAsString,i,1))

        '1バイト目の値によって、２バイト目以降が何バイトあるかが決まります。
        If &h00 <= FirstByte And FirstByte <= &h7F Then
            FollowingBytesCount = 0

        ElseIf &hC2 <= FirstByte And FirstByte <= &hDF Then
            FollowingBytesCount = 1

        ElseIf &hE0 <= FirstByte And FirstByte <= &hEF Then
            FollowingBytesCount = 2

        ElseIf &hF0 <= FirstByte And FirstByte <= &hF4 Then
            FollowingBytesCount = 3
        Else
            'UTF-8として解釈可能ではありません。
            Exit Function
        End If

        '2バイト目以降が決められたバイトの数だけ 80-BFの間の値で続いているかを確認します。
        For j = 1 To FollowingBytesCount
            i = i + 1

            If i > LenB(ByteArrayAsString) Then
                'UTF-8として解釈可能ではありません。読み取るデータがもうありませんでした。
                Exit Function
            End If

            '後続1バイトを数値として読み取ります。
            FollowingByte = AscB(MidB(ByteArrayAsString,i,1))

            '後続バイトは 0x80 ～ 0xBF の範囲のバイトでなければなりません。     
            If &h80 <= FollowingByte And FollowingByte <= &hBF Then
            Else
                'UTF-8として解釈可能ではありません。
                Exit Function
            End If
        Next
        'ここに来た時、変数iは jのループの中で後続バイト数の分加算されているので
        '次のバイトの組み合わせの直前のバイト位置を指し示しています。
    Next
    'すべてのバイトデータがUTF-8の規則に準じていました。
    CanBeUTF8 = True
End Function
```

# 解説
あまり、このコードの解説には興味はもたらないかと思いますが
今月作ったqiitaの記事の集大成となっています。
UTF-8か判断する部分には、
[UTF-8ファイルとして解釈可能かを判断するVBS / VBA 関数](https://qiita.com/yamashiroakihito/items/d7b23e6cd125c050d6ab) の記事で作った関数を使っています。

肝になるのは、以下の箇所です。
この列情報の設定が Excelの「マクロの記録」では記録されないので、これを実現できてない人が多いんだろうと思います。
列番号を指定した配列をさらに上の配列につっこむのがミソです。
列番号を配列の第１列で指定しているので、本当は順不同でも大丈夫です。
また指定しない列は標準列として処理されるので、文字列にしたい列があらかじめ特定されているなら、全列分の情報を用意する必要もないです。

```vb
        Const MaxColInExcel = 16384
        ReDim FieldInfo(MaxColInExcel-1)
        For i = 1 To MaxColInExcel
                'Array(4,1) は 4列(A列)が標準属性(1)であることを示します。
                'Array(5,2) は 5列(B列)が文字列属性(2)であることを示します。
                'Array(6,3) は 6列(C列)が日付属性(3)であることを示します。
                FieldInfo(i-1) = Array(i, 2)   
        Next
```

# おことわり
このコードによる損害に対しては、一切、責任を負いません。
自由に改変して配布していただいてかまいません。