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