Files
script_dev/csv_maker_vba.txt

490 lines
17 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Option Explicit
' ============================================
' CSV取り込み + ヘッダ/ドロップダウン設定(列挙メタは改行不問で検知)
' 実行時に新規シートを作成して結果を出力
' ============================================
' ===== 設定定数 =====
Private Const DV_USE_CODE_ONLY As Boolean = True ' True: ドロップダウンをコードのみ(例: 0,1にする
Private Const APPLY_DV_TO_ROW As Long = 50 ' データがない場合にDVを適用する最終行
Private Const CSV_ENCODING As String = "auto" ' "auto"|"utf8"|"cp932"
Private Const SHEET_NAME_PREFIX As String = "Import_" ' 作成シート名の接頭辞
'==== ボタンに割り当てるエントリーポイント ====
Public Sub Run_ImportCsv_CreateNewSheet()
Dim ctrlSheet As Worksheet
Set ctrlSheet = ActiveSheet ' ボタンを置いたシート(操作用)
' CSVパスを選択
Dim fpath As String
fpath = PickCsvFilePath()
If Len(fpath) = 0 Then
MsgBox "CSVの選択がキャンセルされました。", vbInformation
Exit Sub
End If
' 出力シート名
Dim outSheetName As String
outSheetName = GetUniqueSheetName(BuildOutputSheetName(fpath))
' 新規シート作成(操作用シートの後ろ)
Dim wsTarget As Worksheet
Set wsTarget = ThisWorkbook.Worksheets.Add(After:=ctrlSheet)
wsTarget.Name = outSheetName
' 取り込み実行(結果は wsTarget に出力)
ImportCsvToTargetSheet fpath, wsTarget
' 完了メッセージ
MsgBox "取り込み完了: " & wsTarget.Name, vbInformation
End Sub
'==== メイン処理(指定のターゲットシートに出力) ====
Private Sub ImportCsvToTargetSheet(ByVal fpath As String, ByVal wsTarget As Worksheet)
Dim wsTemp As Worksheet
Set wsTemp = ThisWorkbook.Worksheets.Add(After:=ThisWorkbook.Sheets(ThisWorkbook.Sheets.count))
wsTemp.Name = GetUniqueTempSheetName("CSV_RAW")
On Error GoTo CLEANUP_ON_ERROR
' CSVを一時シートに読み込みPower Query
If Not ImportCsvToTempSheet(fpath, CSV_ENCODING, wsTemp) Then
Err.Raise vbObjectError + 101, , "CSVの取り込みに失敗しました。"
End If
' 使用範囲特定
Dim lastRow As Long, lastCol As Long
lastRow = GetLastRow(wsTemp)
lastCol = GetLastCol(wsTemp)
If lastCol = 0 Or lastRow = 0 Then
Err.Raise vbObjectError + 102, , "CSVが空のようです。"
End If
Application.ScreenUpdating = False
Application.EnableEvents = False
' ターゲットシートをクリア
wsTarget.Cells.Clear
' ------- 1) ヘッダ処理(列挙メタはセル内に含まれていれば検知) -------
Dim colHasEnum() As Boolean
Dim colEnumList() As String
ReDim colHasEnum(1 To lastCol)
ReDim colEnumList(1 To lastCol)
Dim c As Long
For c = 1 To lastCol
Dim headerRaw As String
headerRaw = Nz(wsTemp.Cells(1, c).Value, "")
' ヘッダはそのまま出力(列挙メタ文字列も残す)
wsTarget.Cells(1, c).Value = headerRaw
' ヘッダに列挙メタ [n:ラベル] が1つでも含まれていれば検知
If ContainsEnumeration(headerRaw) Then
colHasEnum(c) = True
' ヘッダ全体から [n:ラベル] を抽出し、DV用のCSV文字列を構築
colEnumList(c) = BuildValidationListFromEnum(headerRaw, DV_USE_CODE_ONLY)
Else
colHasEnum(c) = False
colEnumList(c) = ""
End If
Next c
' ------- 2) データの貼付CSV 2行目→Excel 2行目 -------
Dim tgtDataStartRow As Long: tgtDataStartRow = 2
Dim tgtLastRow As Long: tgtLastRow = 1
If lastRow >= 2 Then
Dim dataRowCount As Long
dataRowCount = lastRow - 1 ' 1行目はヘッダ
wsTemp.Range(wsTemp.Cells(2, 1), wsTemp.Cells(lastRow, lastCol)).Copy
wsTarget.Cells(tgtDataStartRow, 1).PasteSpecial xlPasteValues
tgtLastRow = tgtDataStartRow + dataRowCount - 1
Else
tgtLastRow = 1 ' データ無し
End If
' ------- 3) ドロップダウン(入力規則)設定 -------
Dim applyToLast As Long
applyToLast = IIf(tgtLastRow >= 2, tgtLastRow, APPLY_DV_TO_ROW)
For c = 1 To lastCol
If colHasEnum(c) And Len(colEnumList(c)) > 0 Then
Dim dvRange As Range
Set dvRange = wsTarget.Range(wsTarget.Cells(2, c), wsTarget.Cells(applyToLast, c))
ApplyValidationList dvRange, colEnumList(c)
End If
Next c
' ------- 4) 調整(見やすく) -------
wsTarget.Columns("A:" & ColLetter(lastCol)).AutoFit
Application.CutCopyMode = False
Application.ScreenUpdating = True
Application.EnableEvents = True
' 一時シート削除
Application.DisplayAlerts = False
wsTemp.Delete
Application.DisplayAlerts = True
Exit Sub
CLEANUP_ON_ERROR:
Application.CutCopyMode = False
Application.ScreenUpdating = True
Application.EnableEvents = True
On Error Resume Next
Application.DisplayAlerts = False
If Not wsTemp Is Nothing Then wsTemp.Delete
Application.DisplayAlerts = True
On Error GoTo 0
MsgBox "エラー: " & Err.Description, vbExclamation
End Sub
'========================================================
' 出力シート名をファイル名から生成(長すぎる・使用不可文字は除去)
'========================================================
Private Function BuildOutputSheetName(ByVal fpath As String) As String
Dim fname As String
fname = fpath
Dim i As Long
' パスからファイル名抽出
For i = Len(fpath) To 1 Step -1
If Mid$(fpath, i, 1) = "\" Or Mid$(fpath, i, 1) = "/" Then
fname = Mid$(fpath, i + 1)
Exit For
End If
Next i
' 拡張子除去
Dim dotPos As Long
dotPos = InStrRev(fname, ".")
If dotPos > 1 Then fname = Left$(fname, dotPos - 1)
' シート名禁止文字の除去 / の代替
fname = Replace(fname, ":", " ")
fname = Replace(fname, "/", " ")
fname = Replace(fname, "\", " ")
fname = Replace(fname, "?", " ")
fname = Replace(fname, "*", " ")
fname = Replace(fname, "[", " ")
fname = Replace(fname, "]", " ")
' タイムスタンプ付与
BuildOutputSheetName = SHEET_NAME_PREFIX & fname & "_" & Format(Now, "yymmdd_hhnnss")
' 31文字制限対応必要なら切り詰め
If Len(BuildOutputSheetName) > 31 Then
BuildOutputSheetName = Left$(BuildOutputSheetName, 31)
End If
End Function
'========================================================
' シート名の一意化
'========================================================
Private Function GetUniqueSheetName(ByVal baseName As String) As String
Dim nameTry As String, n As Long
nameTry = baseName
n = 1
Do While SheetExists(nameTry)
n = n + 1
nameTry = Left$(baseName, 28) & "_" & CStr(n) ' 31文字制限に配慮
Loop
GetUniqueSheetName = nameTry
End Function
'========================================================
' ファイルダイアログでCSVファイルを取得
'========================================================
Private Function PickCsvFilePath() As String
Dim fd As FileDialog
Set fd = Application.FileDialog(msoFileDialogFilePicker)
With fd
.Title = "CSVファイルを選択"
.Filters.Clear
.Filters.Add "CSV ファイル", "*.csv", 1
.AllowMultiSelect = False
If .Show = -1 Then
PickCsvFilePath = .SelectedItems(1)
Else
PickCsvFilePath = ""
End If
End With
End Function
'========================================================
' 一時シート名をユニーク化
'========================================================
Private Function GetUniqueTempSheetName(ByVal baseName As String) As String
Dim n As Long
Dim nameTry As String
n = 0
Do
n = n + 1
nameTry = baseName & "_" & Format(Now, "yymmdd_hhnnss") & "_" & n
If SheetExists(nameTry) = False Then
GetUniqueTempSheetName = nameTry
Exit Do
End If
Loop
End Function
Private Function SheetExists(ByVal sName As String) As Boolean
Dim ws As Worksheet
On Error Resume Next
Set ws = ThisWorkbook.Worksheets(sName)
SheetExists = Not ws Is Nothing
On Error GoTo 0
End Function
'========================================================
' CSVを一時シートに読み込みPower Query エンジン版)
' encoding: "auto" | "utf8" | "cp932"
'========================================================
Private Function ImportCsvToTempSheet(ByVal fpath As String, ByVal encoding As String, ByVal wsTemp As Worksheet) As Boolean
On Error GoTo ERRH
wsTemp.Cells.Clear
Dim encCode As Long
Select Case LCase$(encoding)
Case "utf8": encCode = 65001
Case "cp932": encCode = 932
Case Else: encCode = 65001 ' autoはまずUTF-8で試行→失敗時に932で再試行
End Select
If ImportCsvViaPowerQuery(fpath, encCode, wsTemp) Then
ImportCsvToTempSheet = True
Exit Function
End If
' autoのときはフォールバックUTF-8失敗→CP932
If LCase$(encoding) = "auto" Then
If ImportCsvViaPowerQuery(fpath, 932, wsTemp) Then
ImportCsvToTempSheet = True
Exit Function
End If
End If
ImportCsvToTempSheet = False
Exit Function
ERRH:
ImportCsvToTempSheet = False
End Function
Private Function ImportCsvViaPowerQuery(ByVal fpath As String, ByVal encCode As Long, ByVal wsTemp As Worksheet) As Boolean
On Error GoTo ERRH
' クエリ名をユニークに
Randomize
Dim pqName As String
pqName = "PQ_CSV_" & Format(Now, "yymmdd_hhnnss_") & CStr(Int(Rnd() * 100000))
' --- Power Query (M) 式を定義 ---
Dim m As String
m = _
"let" & vbCrLf & _
" Source = Csv.Document(File.Contents(""" & Replace(fpath, """", """""") & """)," & _
" [Delimiter="","", Columns=null, Encoding=" & CStr(encCode) & ", QuoteStyle=QuoteStyle.Csv])," & vbCrLf & _
" Promote = Table.PromoteHeaders(Source, [PromoteAllScalars=true])," & vbCrLf & _
" AsText = Table.TransformColumnTypes(Promote, List.Transform(Table.ColumnNames(Promote), each {_, type text}))," & vbCrLf & _
" AllCols = Table.ColumnNames(AsText)," & vbCrLf & _
" // ""Column1"" のような自動列名 かつ 列内の全値が Null または 空白のみ なら削除対象にする" & vbCrLf & _
" Trim = (v as any) as text => Text.Trim(Text.From(v, Culture=null))," & vbCrLf & _
" IsEmptyCol = (tbl as table, col as text) as logical =>" & vbCrLf & _
" List.AllTrue(List.Transform(Table.Column(tbl, col), (v) => v = null or Trim(v) = """" ))," & vbCrLf & _
" ToRemove = List.Select(AllCols, (cn) => Text.StartsWith(cn, ""Column"") and IsEmptyCol(AsText, cn))," & vbCrLf & _
" Cleaned = if List.Count(ToRemove) > 0 then Table.RemoveColumns(AsText, ToRemove) else AsText" & vbCrLf & _
"in" & vbCrLf & _
" Cleaned"
' 既存の内容を一旦クリア
wsTemp.Cells.Clear
' --- クエリの追加(ブック内に定義) ---
Dim q As WorkbookQuery
Set q = ThisWorkbook.Queries.Add(Name:=pqName, Formula:=m)
' --- OLEDB 接続文字列Power Query への橋渡し) ---
Dim connStr As String
connStr = "OLEDB;Provider=Microsoft.Mashup.OleDb.1;Data Source=$Workbook$;Location=" & pqName & ";Extended Properties="""""
' --- QueryTable を使ってシートにロードSQL 明示)---
Dim qt As QueryTable
Set qt = wsTemp.QueryTables.Add(Connection:=connStr, Destination:=wsTemp.Range("A1"))
With qt
.CommandType = xlCmdSql
.CommandText = "SELECT * FROM [" & pqName & "]"
.PreserveFormatting = True
.AdjustColumnWidth = False
.RefreshStyle = xlOverwriteCells
.BackgroundQuery = False
.Refresh ' ← ここで実行
End With
' 値として固定(再解釈を防ぐ)
wsTemp.UsedRange.Value = wsTemp.UsedRange.Value
' --- 後片付け(接続・クエリの削除) ---
On Error Resume Next
Dim cn As WorkbookConnection
For Each cn In ThisWorkbook.Connections
If InStr(1, cn.Name, pqName, vbTextCompare) > 0 Then cn.Delete
If Not cn.OLEDBConnection Is Nothing Then
If InStr(1, cn.OLEDBConnection.Connection, "Location=" & pqName, vbTextCompare) > 0 Then cn.Delete
End If
Next
ThisWorkbook.Queries(pqName).Delete
On Error GoTo 0
ImportCsvViaPowerQuery = True
Exit Function
ERRH:
' クリーンアップ
On Error Resume Next
Dim cn2 As WorkbookConnection
For Each cn2 In ThisWorkbook.Connections
If InStr(1, cn2.Name, pqName, vbTextCompare) > 0 Then cn2.Delete
If Not cn2.OLEDBConnection Is Nothing Then
If InStr(1, cn2.OLEDBConnection.Connection, "Location=" & pqName, vbTextCompare) > 0 Then cn2.Delete
End If
Next
ThisWorkbook.Queries(pqName).Delete
On Error GoTo 0
ImportCsvViaPowerQuery = False
End Function
'========================================================
' 行・列の最終位置
'========================================================
Private Function GetLastRow(ByVal ws As Worksheet) As Long
Dim f As Range
On Error Resume Next
Set f = ws.Cells.Find(What:="*", After:=ws.Range("A1"), LookAt:=xlPart, _
LookIn:=xlFormulas, SearchOrder:=xlByRows, _
SearchDirection:=xlPrevious, MatchCase:=False)
On Error GoTo 0
If f Is Nothing Then
GetLastRow = 0
Else
GetLastRow = f.Row
End If
End Function
Private Function GetLastCol(ByVal ws As Worksheet) As Long
Dim f As Range
On Error Resume Next
Set f = ws.Cells.Find(What:="*", After:=ws.Range("A1"), LookAt:=xlPart, _
LookIn:=xlFormulas, SearchOrder:=xlByColumns, _
SearchDirection:=xlPrevious, MatchCase:=False)
On Error GoTo 0
If f Is Nothing Then
GetLastCol = 0
Else
GetLastCol = f.Column
End If
End Function
'========================================================
' ヘッダ文字列の中に [n:ラベル] パターンが含まれるか判定(改行不問)
'========================================================
Private Function ContainsEnumeration(ByVal s As String) As Boolean
Dim re As Object
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "\[\d+:[^\]]+\]"
re.IgnoreCase = True
re.Global = True
ContainsEnumeration = re.Test(s)
End Function
'========================================================
' ヘッダ全体から [n:ラベル] を抽出して、入力規則のリスト文字列を生成
' DV_USE_CODE_ONLY=False: "0:無効,1:有効"
' DV_USE_CODE_ONLY=True : "0,1"
'========================================================
Private Function BuildValidationListFromEnum(ByVal s As String, ByVal codeOnly As Boolean) As String
Dim re As Object, mc As Object, m As Object
Set re = CreateObject("VBScript.RegExp")
re.Pattern = "\[(\d+):([^\]]+)\]"
re.IgnoreCase = True
re.Global = True
Dim parts As Collection
Set parts = New Collection
If re.Test(s) Then
Set mc = re.Execute(s)
Dim code As String, label As String
For Each m In mc
code = m.SubMatches(0)
label = m.SubMatches(1)
If codeOnly Then
parts.Add code
Else
parts.Add code & ":" & label
End If
Next m
End If
Dim i As Long, buf As String
For i = 1 To parts.count
buf = buf & IIf(Len(buf) > 0, ",", "") & CStr(parts(i))
Next i
BuildValidationListFromEnum = buf
End Function
'========================================================
' 入力規則(リスト)を範囲に適用
'========================================================
Private Sub ApplyValidationList(ByVal rng As Range, ByVal listCsv As String)
On Error Resume Next
rng.Validation.Delete
On Error GoTo 0
If Len(listCsv) = 0 Then Exit Sub
With rng.Validation
.Add Type:=xlValidateList, AlertStyle:=xlValidAlertStop, Operator:=xlBetween, _
Formula1:=listCsv
.IgnoreBlank = True
.InCellDropdown = True
.ErrorTitle = "入力値エラー"
.ErrorMessage = "リストから選択してください。"
.ShowError = True
End With
End Sub
'========================================================
' Null/Emptyを空文字にする補助
'========================================================
Private Function Nz(ByVal v As Variant, Optional ByVal defaultValue As String = "") As String
If IsError(v) Then
Nz = defaultValue
ElseIf IsNull(v) Or IsEmpty(v) Then
Nz = defaultValue
Else
Nz = CStr(v)
End If
End Function
'========================================================
' 列番号 → 列記号
'========================================================
Private Function ColLetter(ByVal colNum As Long) As String
Dim s As String
Do While colNum > 0
s = Chr(((colNum - 1) Mod 26) + 65) & s
colNum = (colNum - 1) \ 26
Loop
ColLetter = s
End Function