スキップしてメイン コンテンツに移動

Excel VBA: Output Sheet as UTF-8 CSV

Save Excel Sheet as UTF-8 CSV file

The biggest problem of Excel is that Excel does not support saving file as UTF-8 CSV format.
I have googled and tried to find the solution for saving excel as UTF-8 CSV but all of them requires 2 steps - 1) saving Excel file as Unicode csv 2) Open it by text editor and save it again as UTF-8 CSV.

If you are a software developer, it is easy to write Java or C# program to manipulate Excel.
But it means people (non developer users) need to install or launch the application.
So I have wrriten Excel VBA addin for saving Excel sheet as UTF-8 CSV file so that we can save UTF-8 excel files only using Excel.

Code for saving UTF-8 CSV

Okie!, here is my solution!
Sme notes for the code:

  • The code saves an active sheet to UTF-8 CSV file in temp folder.
  • The error handring part is not robust. I have only wrriten minimum error handling. You might need to add your custome error handring.
  • If you are okay to add BOM on the file, please remove the corresponding part for adding BOM.

Sub SaveAsCSV()
On Error GoTo ErrorHandler

    Const ROW_LIMIT As Long = 1000
    Const OUTPUT_DIR = "C:\temp\"
    
    '==============================
    ' Get Range
    '==============================
    Dim maxRow As Long
    Dim maxCol As Long
    
    If ActiveSheet Is Nothing Then
       MsgBox "Sheet Not Found!", vbExclamation
       Exit Sub
    End If
    
 
    With ActiveSheet.UsedRange
        maxRow = .Rows.Count
        maxCol = .Columns.Count
    End With
    
    If maxRow < 2 Then
       MsgBox "Data is Empty!", vbExclamation
       Exit Sub
    End If
    
    Dim fileIndex As Long: fileIndex = 1
 
    '==============================
    ' Prepare stream
    '==============================
    Dim outStream As ADODB.Stream
 
 
    '==============================
    ' Write data to stream
    '==============================
    Dim r As Long
    Dim c As Long
    Dim line As String
    Dim Data As Variant
    Dim shop As String
    Dim header As String
     
    Dim isFileOutput As Boolean: isFileOutput = False
    Dim isNewFile As Boolean: isNewFile = True
     
     
    With ActiveSheet
    Data = .Range("A1:" & Col_Letter(maxCol) & .Range("A1").End(xlDown).Row)
    End With
         
    '==============================
    ' Write header row
    '==============================
    header = """" & Data(1, 1) & """"
    For c = 2 To maxCol
        If Data(1, c) = "" Then
           header = header & "," & """"""
        Else
           header = header & ",""" & Replace(Trim(Data(1, c)), """", """""") & """"
        End If
    Next
    
    Dim baseName As String: baseName = GetBaseFileName()
    
    '==============================
    ' Write data rows
    '==============================
    For r = 2 To maxRow
        Application.StatusBar = String(Int(r / 1000), "*")
        If isNewFile Then
            ' create new out stream for next file
            Set outStream = New ADODB.Stream
        
            With outStream
                .Charset = "UTF-8"
                .Type = 2       'adTypeText
                .Open
            End With
            outStream.WriteText header, adWriteLine
            isNewFile = False
        End If
        
        ' Data line
        ' 1st column
        line = """" & Data(r, 1) & """"
        
        ' after 1st column
        For c = 2 To maxCol
            If Data(r, c) = "" Then
               line = line & "," & """"""
            Else
               line = line & ",""" & Replace(Trim(Data(r, c)), """", """""") & """"
            End If
        Next
        
        outStream.WriteText line, adWriteLine
        
        ' File output
        isFileOutput = (r Mod ROW_LIMIT = 0 Or r = maxRow)
        If isFileOutput Then
            ' Skip BOM
            With outStream
                .Position = 0
                .Type = 1       'adTypeBinary
                .Position = 3   'skip 3 byte in order to remive BOM
            End With
            
            ' stream for copy
            Dim csvStream As ADODB.Stream
            Set csvStream = New ADODB.Stream
         
            ' open binary mode
            csvStream.Type = adTypeBinary
            csvStream.Open
         
            ' copy data after BOM
            outStream.CopyTo csvStream
            csvStream.SaveToFile (OUTPUT_DIR & baseName & "_" & fileIndex & ".csv"), adSaveCreateOverWrite
            outStream.Close
            
            fileIndex = fileIndex + 1
            isNewFile = True
        End If
    Next
    Application.StatusBar = False
    MsgBox "Output " & fileIndex - 1 & " csv files on " & vbCrLf & OUTPUT_DIR & " from " & ActiveSheet.Name & " in " & Application.ActiveWorkbook.Name, vbInformation, "Csv Output"
    Exit Sub
ErrorHandler:
    Application.StatusBar = False
    MsgBox "Error Details:" & Err.Description, vbCritical, "Error"
End Sub

Private Function Col_Letter(lngCol As Long) As String
Dim vArr: vArr = Split(Cells(1, lngCol).Address(True, False), "$")
Col_Letter = vArr(0)
End Function

Private Function GetBaseFileName()
  Dim extIndex As Integer: extIndex = InStrRev(Application.ActiveWorkbook.Name, ".")
  If extIndex > 0 Then
    GetBaseFileName = Left(Application.ActiveWorkbook.Name, extIndex - 1)
  Else
    GetBaseFileName = Application.ActiveWorkbook.Name
  End If
End Function

Adding the Funtionality to Menu

If you would like to add this functionality to Excel, you need to create menu which executes the UTF-8 to csv macro and add it to Excel menu.

  • Create a standard Excel VBA module in VBA editor.
  • Copy and paste the above main VBA code (SaveAsCSV funciton etc.) to the module created in the previous step.
  • Also add the folliwng code which adds custom menu to Excel
  • Sub AddNewMenu()
    
        On Error GoTo ErrHand
    
        Dim cbrCmd As CommandBar
        Dim cbcMenu As CommandBarControl
    
        'Create menu
        Set cbrCmd = Application.CommandBars("Worksheet Menu Bar")
    
        'Delete menu if it already exists
        cbrCmd.Controls("UTF-8 CSV").Delete
    
        'Add menu
        Set cbcMenu = cbrCmd.Controls.Add(Type:=msoControlPopup)
        cbcMenu.Caption = "UTF-8 CSV"
        cbcMenu.Tag = "UTF-8 CSV"
    
        With cbcMenu.Controls.Add(Type:=msoControlButton)
            .Caption = " UTF-8 CSV"
            .OnAction = "SaveAsCSV"
            .FaceId = 3
        End With
    
        Set cbrCmd = Nothing
        Set cbcMenu = Nothing
    
        Exit Sub
    
    ErrHand:
    
        If Err.Number = 5 Then
           Resume Next
        Else
           MsgBox Err.Description
        End If
    
    End Sub
    
  • Open "ThisWorkbook" and copy and paste the fowlling code.
  • Option Explicit
    
    Private Sub Workbook_Open()
      Call AddNewMenu
    End Sub
    
  • Save the file as Excel Addin - xla file.
  • Load the xla file you saved to Excel. How to add addin to Excel is outside theme of this post :p Please google it yourself ;)
  • Congratulations! You can see the Excel Menu for saving excel sheet as UTF-8 CSV file. If you click "UTF-8 CSV", the macro should work.

コメント

Unknown さんの投稿…
Excellent, exactly what I'm looking for. Not working under win10 with Office Home an Student 2013

このブログの人気の投稿

Eclipseでコードカバレッジのハイライトを削除する方法

Eclipseには便利なコードカバレッジ表示機能が搭載されていますが、コード内に緑、赤、黄の色付けがされて煩く感じるときもあると思います。 1度カバレッジの色付けが出てしまった後に消す方法の紹介です(方法は簡単)。 下記のキャプチャの青いマーカーで示した「Remove All Sessions」のボタンを押せばすべて消えます。

「特定の文字から始まらない文字列」にマッチする正規表現

「特定の文字から始まらない文字列」 にマッチする正規表現の例です。  以下の例では、Aから始まらない文字列にマッチする正規表現を示しています。 ^(?!A).*$ 私も正規表現の組み方で四苦八苦することがあります。以下の書籍は実践的に様々な正規表現のパターンを例示してくれているので、重宝しています。

ダイソーで買った200円のドライバーセットでHDDを分解

HDDの処分 最近は個人情報の問題もあって、HDDを処分する前にちゃんとデータの消去を気にすることも多くなってきました。消去方法としては大きく分けて下記の3つがあります。 データ消去ソフトでフォーマット HDD内部のプラッタを物理破壊 データ消去を行ってくれる専門の業者や家電量販店(Sofmapやビックカメラで実施していると思います。費用発生。)に持ち込み。 データ消去ソフトでのフォーマットは簡単ですが、欠点として「フォーマットに時間がかかる」「セクタ破損などで中途半端に壊れたディスクのフォーマットができない」などがあります。 またHDD内部のプラッタの物理破壊については、HDDを分解するために、通常のプラスやマイナスドライバーではなく、星形ネジに対応したトルクスドライバーが必要とのこともあって、少し面倒です。 筆者は今回、今後もHDDの廃棄をするだろうなあと思い、思い切って自分で分解して廃棄することにチャレンジしてみました。(家電量販店に持って行くよりも安くできないかというどケチ丸出しですw) HDDの星形ネジ こんなやつです。ちなみに写真はSeagateのST2000DL003というHDDで撮影しました。 トルクスドライバー というわけで、分解のために Amazonでトルクスドライバー を探しました。 調べると T8のもだと使えそう とのことで、いろいろと物色。 セットのものとか T8一本で立派なやつとか 色々あったのですが、HDD壊すだけで800円かぁ(←どケチ)、と思って購入を躊躇。 ネット上で調べると100円ショップのダイソーでも、トルクスドライバーを販売しているとの情報をキャッチ!近所のダイソーに行って、探したところ星形のヘッド交換に対応した精密ドライバーセットがありました。 プラスが10種類、マイナスが8種類、六角が6種類、星形が6種類(今回ほしかったもの)のセットで、何とお値段税抜き200円!、税抜き200円!と安かったので、ダメもとで購入しました。 結論から言うと 買って大正解 でした。 ダイソーの精密ドライバーセット こんな商品です! 星形対応のヘッドを装着するとこんな感じ。ドライバーのグリップもゴムで滑らない様になっていて使いやす

SQLで特定の文字を組み合わせたランダムな文字列を生成

簡易的な方法として「指定した文字列からランダムに1文字選ぶ」を必要な文字の長さ分concat関数でつなげれば実現できます。 1文字ずつ文字を選ぶので、あまり性能もよくない上、セキュリティ的な観点からのランダム性も担保されていないので、あくまで開発中に必要になった時に使う程度が無難だと思います。 下記に英数字大文字小文字を含んだランダムな3文字の文字列を生成するクエリを示します。 # RAND関数で指定した文字列からランダムに1文字選択。 # 下記の例の62の部分はa~z、A~Z、1~9の文字数の合計値を入れた結果 SELECT CONCAT( SUBSTRING('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', FLOOR(RAND() * 62 + 1), 1), SUBSTRING('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', FLOOR(RAND() * 62 + 1), 1), SUBSTRING('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789', FLOOR(RAND() * 62 + 1), 1) ) AS random_string;

PHPの配列(array)のメモリ使用量の考察

はじめに 最近PHP上に大量のデータをメモリ上に展開していたのですが、配列(array)の形式(連想配列 or 単純な配列)や配列の要素のデータ構造(数字、配列、文字列など)で大きくメモリ使用量に差が出てくることに気づき、簡単なプログラムを組んで調べてみました。 あくまで筆者の環境での結果なので、細かい数値は参考程度に見てください。 測定環境と方法 OS: Windows 10 PHP 7.4.5 (php-7.4.5-nts-Win32-vc15-x64) 配列に要素を追加するプログラムを書いて、PHPのmemory_get_usage(true)関数を使って実メモリ使用量を計測しました。 計測結果 No. 方式 1MB当たり作成できる 要素数 プログラム 補足 1 キーも値も整数の配列 (整数IDを想定) 28571 // 2,000,000 / 70MB $row = []; for($i = 0; $i < 2000000; $i++) { $row[] = $i; } No.2~6でテストしたプログラム中の要素数は200,000。これだけ一桁多い! 2 キーが文字列、値が整数の連想配列 8333 // 200,000 / 24MB $row = []; for($i = 0; $i < 200000; $i++) { $row[$i.'_key_string'] = $i; } キーの文字列が長い方がメモリ使用量多くなる。 3 キーが整数、値が連想配列の配列 DBから取得してきたデータを想定 2325 // 200,000 / 86MB $row = []; for($i = 0; $i < 200000; $i++) { row[] = ['id' => $i]; } 4 キーが整数、値が連想配列の配列(配列に複数の値を保持) DBから取得してきたデータを想定 2127 // 200,000 /

ADODB.streamオブジェクトを使って文字列とByte配列を相互変換(Excel VBA)

ADODB.streamオブジェクトを使って文字列をByte配列に変換するコードのサンプルです。 ExcelVBAでADODB.streamを使う際には、 1. ExcelのMicrosoft Visual Basic エディタのメニューバーから「ツール->参照設定」とたどる。 2. 表示されたダイアログからMicrosoft ActiveX Data Objectsにチェックを入れる。 という手順が必要です。 文字列からByte配列へ Private Function ADOS_EncodeStringToByte(ByVal cset As String, ByRef strUni As String) As Byte() On Error GoTo e Dim objStm As ADODB.stream: Set objStm = New ADODB.stream objStm.Mode = adModeReadWrite objStm.Open objStm.Type = adTypeText objStm.Charset = cset objStm.WriteText strUni objStm.Position = 0 objStm.Type = adTypeBinary Select Case UCase(cset) Case "UNICODE", "UTF-16" objStm.Position = 2 Case "UTF-8" objStm.Position = 3 End Select ADOS_EncodeStringToByte = objStm.Read() objStm.Close Set objStm = Nothing Exit Function e: Debug.Print "Error occurred while encoding characters" & Err.Description If objStm Is No

MySQL: SELECTの結果をUNIONして ORDER BYする際の最適化方法

SELECTの結果をUNIONして ORDER BY する際には下記の点に注意する必要があります。 無駄なメモリ消費 ソートにINDEXが利かない (≒CPU負荷増大) 対応策 可能であればPush-down Condition (各サブクエリ内でORDER BY, LIMIT, OFFSETを適用してからUNION, ORDER BYを実行する)を利用することで、 パフォーマンスを改善できる場合があります。 下記に例を示します。 もともとのクエリ SELECT tmp.* FROM ( SELECT tableA.column1, tableA.column2 FROM tableA WHERE (条件) UNION ALL SELECT tableB.column1, tableB.column2 FROM tableB WHERE (条件) ) AS tmp ORDER BY tmp.column1, tmp.column2 LIMIT 100, 20 Push-down Conditionを用いて書き直したクエリ SELECT tmp.* FROM ( SELECT tableA.column1, tableA.column2 FROM tableA WHERE (条件) ORDER BY tableA.column1, tableA.column2 LIMIT 30 # <- 10 (offset) + 20 (limit) UNION ALL SELECT tableB.column1, tableB.column2 FROM tableB WHERE (条件) ORDER BY tableB.column1, tableB.column2 LIMIT 30 # <- 10 (offset) + 20 (limit) ) AS tmp ORDER BY tmp.column1, tmp.column2 LIMIT 10, 20 ただしこのPush-down Conditionの手法も下記の場合は、効果が半減しますので注意が必要です。 OFFSETの値が大きい場合は、結局全結果セットUNIONと変わらない サブクエリ内のソートで、INDEXが効かない場合

Visual Studio 2010 SP1のアンインストール

Visual Studio 2013に乗り換えるためにVisual Studio 2010をアンインストールしようとしたところで問題発生。。。 先にVisual Studio 2010本体をアンインストールした後、Visual Studio 2010 SP1をアンインストールできなくて困っていました。 Google先生で調べたところ、以下の情報が見つかり、書かれていた通り実施したところ無事Visual Studio 2010 SP1のアンインストールに成功しました。 How to uninstall/remove Visual Studio SP1 アンインストール手順は以下の通りです。 http://www.microsoft.com/en-gb/download/details.aspx?id=23691 からMicrosoft Visual Studio 2010 Service Pack 1 (Installer)をダウンロード VS10sp1-KB983509.exeというファイル名でダウンロードされる(はず)。 コマンドプロンプトから以下のコマンドを実行 (以下の例は、c:\tempにVS10sp1-KB983509.exeがある場合) c:\temp\VS10sp1-KB983509.exe /uninstall /force ダイアログが立ち上がるので、アンインストールを選択して次へ進めばOK!

MySQLのSQL_CALC_FOUND_ROWS使用上の注意

MySQLのSQL_CALC_FOUND_ROWSを使用する際の無駄なメモリ消費に注意 ページング機能の実装などのために、ヒットした全件数を取得する必要のある場合があるかと思います。 その場合、下記のようにSQL_CALC_FOUND_ROWSを使うと、検索結果に加えて、そのクエリの全ヒット件数が取得できます。 SELECT SQL_CALC_FOUND_ROWS table.column1, table.column2 ...(途中省略)... FROM table WHERE (条件) しかし、SQL_CALC_FOUND_ROWSを使うと、「絞り込んだ結果にヒットする全べての行の結果のセットを作成する」という大きな欠点があります。 これは、LIMIT, OFFSETを指定していても実行されてしまうので、 SELECTで指定するカラム数やデータ量が多い SELECTの結果返ってくる行数が多い 場合、無駄に領域(≒メモリ)を消費してしまうので注意が必要です。 例えば、100万行検索対象としてヒットするクエリの場合、仮にLIMIT 20と指定して最初の20行を取得するようにクエリを書いても、その100万行分の結果セットが作成されてしまうということになります。 対応策 根本的な対応策としては、SQL_CALC_FOUND_ROWSを使わない(結果行数の取得はCOUNTを用いた別クエリで取得する、結果行数をあらかじめサマリーテーブルに保持しておく)ことですが、 SQL_CALC_FOUND_ROWSをどうしても使う必要がある場合は、 SELECT SQL_CALC_FOUND_ROWS primary_id のように最低限のカラムを指定して結果行セットを取得 (LIMIT OFFSET指定前提) 取得したprimary_idを使って必要なデータを取得 して、SQL_CALC_FOUND_ROWSで使用する領域をできるだけ減らすことで、対応するしかないと思います。 世の中ではLIMIT, OFFSETは使わない方がよいとよく書かれていますが、 SQL_CALC_FOUND_ROWSは、書いてしまえばどんなときも検索にヒットする全結果行セットを作成するので、同じくらい使用する際には注意が必要です。

PHPでファイルを指定した行数ごとに分割

ファイルを指定した行数ごとに分割するためには、Linuxのsplitコマンドを使えば簡単に実現できます。 PHPではexec関数にsplitコマンドを渡して実行すればよいですが、下記の弱点があります。 Linuxのコマンドに依存 (PHPの場合はほとんどLinux環境で動作させることが普通なのでそこまで問題にならないかも知れません)。 exec関数は慎重に引数を渡さないと、OSコマンドインジェクション脆弱性を引き起こす可能性がある。 そこで、今回はPHPでファイルを指定した行数ごとに分割するプログラムを書いてみました。 <?php class FileSplitter { private $lines; private $fileCount; public function split($filePath, $linesPerFile, $outputDir) { $this->fileCount = 0; $this->lines = null; $file = new \SplFileObject($filePath); $lineCount = 0; try{ while (!$file->eof()) { if($lineCount % $linesPerFile === 0) { $this->writeToFile($this->generateOutputFilePath($outputDir, $file)); } $this->lines[] = $file->fgets(); $lineCount++; } $this->writeToFile($this->generateOutpu