[ 初めての方へ | 一覧(最新更新順) | 全文検索 | 過去ログ ]
『VBA教えてください』(あちゃこ)
下記のコードは、デスクトップにあるmというcsvファイル(3万行)から必要な項目だけを 集計ファイル、Sheet1のABC列に貼り付け、F列以降で集計をします。 ABC列は3万行あり私の環境では処理時間は2.2秒です。(Excel2003でXP)計算式は は先生方の指導をいただき大幅に短縮できるようになりましたが,まだ他でまだ短縮の 余地はありませんか?
質問1 CSVファイルを開く時に時間がかかるのが気になりますが、短縮の方法はありませんか?
質問2 宣言の位置宣言の仕方は間違っていませんか?(まだはっきり理解できていません)
質問3 所々に自動記録が入っていますが、コードを簡素化できませんか?
質問4 その他ご気付きの点、ご意見ご指導お願いいたします。
集計ファイルSheet1
A B C D E F G H I ...........Z 1 型式 日付け 個数 AA AB AC 2 AA 11/2 20 11/1 3 AC 11/4 10 11/2 4 AC 11/4 40 11/3 5 AA 11/1 30 11/4 6 AC 11/6 20 11/5 7 AB 11/6 10 11/6
Sub test1()
myTime = Timer ChDir "C:\Documents and Settings\xxx\デスクトップ" Workbooks.Open Filename:="C:\Documents and Settings\xxx\デスク トップ\m.csv"
Dim shtA As Worksheet, shtB As Worksheet Set shtA = Workbooks("m.CSV").Worksheets("m") Set shtB = Workbooks("集計.xls").Worksheets("Sheet1") Dim tbl As Variant Dim Dat() As Double Dim buf As String Dim i As Long Dim j As Long
Windows("集計.xls").Activate Columns("a:c").Select Selection.ClearContents
With shtA .Columns("r").Copy shtB.Columns(1) .Columns("o").Copy shtB.Columns(2) .Columns("n").Copy shtB.Columns(3)
Windows("m.csv").Activate ActiveWindow.Close
Columns("B:B").Select Selection.TextToColumns Destination:=Range("B1"), DataType:=xlDelimited, _ TextQualifier:=xlDoubleQuote, ConsecutiveDelimiter:=False, Tab:=True, _ Semicolon:=False, Comma:=False, Space:=False, Other:=False, FieldInfo _ :=Array(1, 5), TrailingMinusNumbers:=True
'******************:
tbl = Worksheets("Sheet1").Range("A1").CurrentRegion.Value With CreateObject("Scripting.Dictionary")
For i = 2 To UBound(tbl, 1) buf = Format(tbl(i, 2), "yyyymmdd") & vbTab & tbl(i, 1) .Item(buf) = .Item(buf) + tbl(i, 3) Next i With Worksheets("Sheet1").Range("F1").CurrentRegion tbl = .Value
ReDim Dat(1 To .Rows.Count - 1, 1 To .Columns.Count - 1) End With For i = 2 To UBound(tbl, 1) For j = 2 To UBound(tbl, 2) Dat(i - 1, j - 1) = .Item(Format(tbl(i, 1), "yyyymmdd") & vbTab & tbl(1, j)) Next j Next i End With Worksheets("Sheet1").Range("G2").Resize(UBound(Dat, 1), UBound(Dat, 2)).Value = Dat MsgBox Format(Timer - myTime, "#,##0.00") & "秒かかりました。" End With End Sub
要件そのものとコードの内容はまだ読んでいない。 コーディングという観点でのみ、アップされたコードを少しお化粧直し。
With/End With は大変に有効な記述だけど ・あまり、間隔をあけず、必要なスコープのみに。 ・別途、変数規定をしているなら、それを使うということも考慮。
シート修飾をしたり、しなかったり。これはあまり感心しないね。 コーディングは統一しておこう。
変数は、それを使う前に記述すれば構文的にはOKだえど、プロシジャの先頭で 全てを規定したほうがわかりやすい。
Sub test1() Dim shtA As Worksheet, shtB As Worksheet Dim tbl As Variant Dim Dat() As Double Dim buf As String Dim i As Long Dim j As Long Dim myTime As Double Dim dic As Object
myTime = Timer
With CreateObject("WScript.Shell") Workbooks.Open .SpecialFolders("Desktop") & "\m.csv" End With
Set shtA = Workbooks("m.CSV").Worksheets("m") Set shtB = Workbooks("集計.xls").Worksheets("Sheet1") Set dic = CreateObject("Scripting.Dictionary")
shtB.Columns("a:c").ClearContents
With shtA .Columns("r").Copy shtB.Columns(1) .Columns("o").Copy shtB.Columns(2) .Columns("n").Copy shtB.Columns(3) End With
shtA.Parent.Close savechanges:=False
shtB.Parent.Activate shtB.Select (Selectは好きじゃないけど既存の後続のコードをそのまま使うために)
Columns("B:B").TextToColumns Destination:=Range("B1"), DataType:=xlDelimited, _ TextQualifier:=xlDoubleQuote, ConsecutiveDelimiter:=False, Tab:=True, _ Semicolon:=False, Comma:=False, Space:=False, Other:=False, FieldInfo _ :=Array(1, 5), TrailingMinusNumbers:=True '******************:
tbl = Range("A1").CurrentRegion.Value
For i = 2 To UBound(tbl, 1) buf = Format(tbl(i, 2), "yyyymmdd") & vbTab & tbl(i, 1) dic.Item(buf) = dic.Item(buf) + tbl(i, 3) Next i
With Range("F1").CurrentRegion tbl = .Value ReDim Dat(1 To .Rows.Count - 1, 1 To .Columns.Count - 1) End With
For i = 2 To UBound(tbl, 1) For j = 2 To UBound(tbl, 2) Dat(i - 1, j - 1) = dic.Item(Format(tbl(i, 1), "yyyymmdd") & vbTab & tbl(1, j)) Next j Next i
Range("G2").Resize(UBound(Dat, 1), UBound(Dat, 2)).Value = Dat
Set shtA = Nothing Set shtB = Nothing Set dic = Nothing
MsgBox Format(Timer - myTime, "#,##0.00") & "秒かかりました。"
End Sub
ちょっと質問
・集計ファイルSheet1 の F列 および G1以降の1行目は、あらかじめ日付や型式がセットされている? ・Columns("B:B").TextToColumns ・・・・ これは、具体的には何をしようとしている?
追記) こちらのcsvファイルのレイアウトがおかしいのかもしれないんだけど アップされたコードで実行したとき、Dictionaryのキーマッチングはうまくいってる?
(ぶらっと)
ぶらっとさんありがとうございます。 With CreateObject("WScript.Shell") Workbooks.Open .SpecialFolders("Desktop") & "\m.csv" ファイルの開き方、閉じ方がよくわかりました。 宣言がよくわからず、学習したいとおもいます。
ご質問の 集計ファイルSheet1 の F列 および G1以降の1行目は、あらかじめ日付や型式 があらかじめ日付や型式がセットされている?
F列、G列ともにあらかじめセットしています。
・Columns("B:B").TextToColumns ・・・・ これは、具体的には何をしようとしてい る?
日付けが文字列で表記されているのでデータの区切り位置でシリアル値に変えるために 行いました。 こちらのcsvファイルのレイアウトがおかしいのかもしれないんだけど アップされたコードで実行したとき、Dictionaryのキーマッチングはうまくい ってる?
実行しても異常はありません。うまくいきました。 (あちゃこ)
TextToColumns については了解。 Dictionaryキーのマッチングについては、こちらの理解が間違っているんだろうね。
で、本題の処理効率。 csvから抽出した膨大なデータは、どうしたって処理しなきゃいけない訳なので ・もし、csv側のデータに、過去日付というか、今回分析しようとするF列の日付範囲以外のものが数多く存在するとすれば 抽出する日付のFrom/Toを与えてフィルターオプションで抜き出し、処理するデータを、少しでも少なくするということが 効果ありかも。From日付はF2の値、To日付はF列最終行の値 とか。 ・もう1つ、現在は 取り込んだデータを日付+型式でDictionaryに集約 作表するG2起点の領域の各要素それぞれにDictionaryから値を抽出
こんな構成だね。
これを取り込んだデータを、直接、G2起点の領域の各要素に足し込めば、取り込んだデータの処理が終わった時点で 配列も完成しているということになり、少しは処理時間が短縮されるかな?
(ぶらっと)
csvから抽出は不要な日付けがたくさん入っています。 「抽出する日付のFrom/Toを与えてフィルターオプション」やり方を教えていただけ ませんか? (あちゃこ)
>csvから抽出は不要な日付けがたくさん入っています。
これは、 [[20111101090418]]
ここで提示したADOを使えば、可能です。 いえ、Sqlの指定によっては、最終的な結果まで表示することもできますよ!!
再度、以下に投稿しますから検討してください。
新規ブックにて、
標準モジュール(Module1)にサンプルCSV作成プロシジャー群
Option Explicit Sub mk_csv_file() Dim flno As Long Dim dat As String Dim g0 As Long flno = FreeFile Open ThisWorkbook.path & "\file.csv" For Output As #flno dat = mk_header(95) Print #flno, dat For g0 = 1 To 30000 dat = mk_data(95) Print #flno, dat Next Close #flno End Sub Function mk_header(num As Long) As Variant Dim g0 As Long ReDim myarray(1 To num) For g0 = LBound(myarray) To UBound(myarray) myarray(g0) = "項目" & g0 Next mk_header = Join(myarray, ",")
End Function Function mk_data(num As Long) As Variant Dim g0 As Long ReDim myarray(1 To num) For g0 = LBound(myarray) To UBound(myarray) If g0 = 18 Then myarray(g0) = Chr(Int(Rnd() * 26) + 65) ElseIf g0 = 15 Then myarray(g0) = Format(CDate(#11/1/2011# + Int(Rnd * 60)), "yyyy/m/d") ElseIf g0 = 14 Then myarray(g0) = Int(Rnd * 50) + 1 Else myarray(g0) = Int(Rnd * 1000) + 1 End If Next mk_data = Join(myarray, ",") End Function
項目1から項目95列までデータがあり、30000件あるファイルです。
尚、この項目18が、型式 項目15が日付 項目14が個数にあたるデータが入っています。
上記のmk_csv_fileを実行すると、日付(項目15)が2011/11/1〜2011/12/30の データがランダムに作成されます。形式は、A〜Z、個数は、適当な数値が あてられます。その他の列は、ダミーの数値が作成されます。 作成されたら、メモ帳辺りで中身を確認してください。
上記のプロシジャーにて、サンプルCSVファイルが、マクロのがあるブックと同じフォルダ上に作成されます(file.csv)。
次に上記で作成されたCSVファイルの抽出マクロ。
別の標準モジュール(Module2)に ADO関連プロシジャー群
'=============================================================== Option Explicit '============================================================= Private cn As Object '============================================================= Function open_ado_text(path As String) As Long 'adoでテキストにアクセス On Error Resume Next Dim link_opt As String Set cn = CreateObject("adodb.connection") link_opt = "Driver={Microsoft Text Driver (*.txt; *.csv)};" & _ "DBQ=" & path & ";" & "ReadOnly=0"
cn.Open link_opt open_ado_text = Err.Number On Error GoTo 0 End Function '============================================================= Sub close_ado() 'クローズ On Error Resume Next cn.Close On Error GoTo 0 End Sub '============================================================= Function exec_sql(sql_str, rs As Object) As Long 'Sqlの実行 On Error Resume Next Set rs = cn.Execute(sql_str) exec_sql = Err.Number If Err.Number <> 0 Then MsgBox Err.Description On Error GoTo 0 End Function '========================================================================== Function mk_schema_ini(path As String, dat() As String) As Long 'schema.iniの作成 On Error GoTo err_mk_schema_ini Dim fno As Long Dim didx As Long mk_schema_ini = 0 fno = FreeFile() Open path & "\schema.ini" For Output As #fno For didx = LBound(dat()) To UBound(dat()) Print #fno, dat(didx) Next Close #fno ret_mk_schema_ini: On Error GoTo 0 Exit Function err_mk_schema_ini: MsgBox Err.Description mk_schema_ini = Err.Number Resume ret_mk_schema_ini End Function '============================================================= Function del_schema_ini(path As String) 'schema_iniの削除 On Error Resume Next Kill path & "\schema.ini" On Error GoTo 0 End Function
更に別の標準のジュール(Module3)にCSVファイル読み込みコード
'===================================================================== Option Explicit Sub main() Dim ret As Long Dim g0 As Long Dim dat(1 To 99) As String Dim rs As Object Dim ans As Variant Columns("a:cm").Clear dat(1) = "[file.csv]" dat(2) = "ColNameHeader = true" dat(3) = "CharacterSet = oem" dat(4) = "Format = CSVDelimited" dat(5) = "Col1 = 項目18 char width 255" dat(6) = "Col1 = 項目15 char width 255" For g0 = 5 To 95 If g0 = 22 Then dat(g0) = "Col" & (g0 - 4) & "=項目" & (g0 - 4) & " Char Width 255" ElseIf g0 = 19 Then dat(g0) = "Col" & (g0 - 4) & "=項目" & (g0 - 4) & " DateTime" Else dat(g0) = "Col" & (g0 - 4) & "=項目" & (g0 - 4) & " LONG" End If Next Call mk_schema_ini(ThisWorkbook.path, dat()) ret = open_ado_text(ThisWorkbook.path) If ret = 0 Then ret = exec_sql("select 項目18,項目15,項目14 from file.csv where 項目15 between #2011/11/1# and #2011/11/30#;", rs) If ret = 0 Then With ActiveSheet .Range("a:c").Clear .Range("a1:c1").Value = Array("型式", "日付", "個数") .Range("a2").CopyFromRecordset rs End With rs.Close Else MsgBox Error(ret) End If close_ado End If Call del_schema_ini(ThisWorkbook.path) Erase dat() End Sub
これで日付が2011/11/1から2011/11/30のデータが読み込まれます。
SQLを使えば、いきなり結果を表示することも出来ますが、今回は CSVファイルの抽出までに留めました。
前回は、ご返事が頂けませんでしたので、これでADOに関する投稿は最後にしますが、 ここでは、使えなくても覚えておくと便利ですよ!!
ichinose
やっぱりADO使った方が速いんだろうな?
Splitで分割するので、CSVがダブルクォーツで括られて居る様な物では無くて 日付がDateValueでシリアル値に成るなら CSVをBookで開かずOpenステートメントを使って読んでいます
Option Explicit
Public Sub Sample_2()
Dim i As Long Dim j As Long Dim lngRows As Long Dim lngColumns As Long Dim dicIndex As Object Dim shtB As Worksheet Dim rngList As Range Dim rngResult As Range Dim strPath As String Dim vntData() As Variant Dim vntResult() As Variant Dim vntColKeys As Variant Dim vntRowKeys As Variant Dim strKey As String Dim strPrompt As String
Dim myTime As Single
myTime = Timer
Set shtB = Workbooks("集計.xls").Worksheets("Sheet1")
Set rngList = shtB.Range("A1") Set rngResult = shtB.Range("F1")
' strPath = CreateObject("WScript.Shell").SpecialFolders("Desktop") & "\m.csv" strPath = "C:\Documents and Settings\xxx\デスクトップ" & "\m.csv"
'Dictionaryオブジェクトを取得 Set dicIndex = CreateObject("Scripting.Dictionary")
With rngResult '行数の取得 lngRows = .Offset(Rows.Count - .Row).End(xlUp).Row - .Row '列数の取得 lngColumns = .Offset(, Columns.Count - .Column).End(xlToLeft).Column - .Column 'クロス集計リストの列見出し、行見出しを取得 vntRowKeys = .Offset(1).Resize(lngRows).Value vntColKeys = .Offset(, 1).Resize(, lngColumns).Value End With
'行、列の見出しをKey、位置をItemとしてDictionaryに登録 With dicIndex strKey = Space(8) & vbTab For i = 1 To lngRows Mid(strKey, 1, 8) = Format(vntRowKeys(i, 1), "yyyymmdd") .Item(strKey) = i Next i For i = 1 To lngColumns .Item(vntColKeys(1, i)) = i Next i End With
'結果出力用配列を確保 ReDim vntResult(1 To lngRows, 1 To lngColumns)
'データをCsvファイルから取得 GetList strPath, vntData, dicIndex, vntResult
Application.ScreenUpdating = False
'データをシートに転記 With rngList .Resize(, UBound(vntData, 2)).EntireColumn.ClearContents .Resize(UBound(vntData, 1), UBound(vntData, 2)).Value = vntData End With
'結果を出力 With rngResult.Offset(1, 1).Resize(lngRows, lngColumns) .ClearContents .Value = vntResult End With
strPrompt = "処理が完了しました"
Wayout:
Application.ScreenUpdating = True
Set dicIndex = Nothing Set rngList = Nothing Set rngResult = Nothing Set shtB = Nothing
MsgBox strPrompt & vbLf & Format(Timer - myTime, "#,##0.00") _ & "秒かかりました。", vbInformation
End Sub
Private Sub GetList(strFileName As String, vntData() As Variant, _ dicIndex As Object, vntResult() As Variant)
Dim i As Long Dim dfn As Integer Dim bytBuff() As Byte Dim vntList As Variant Dim vntFields As Variant Dim strKey As String Dim lngRow As Long Dim lngColumn As Long
'読み込むファイルをOpen dfn = FreeFile Open strFileName For Binary As dfn
'全行変数に読み込み ReDim bytBuff(1 To LOF(dfn)) Get #dfn, , bytBuff
Close #dfn
'読み込んだファイルを改行コードで分割 vntList = Split(StrConv(bytBuff, vbUnicode), vbCrLf, , vbBinaryCompare) '分割したファイルを調整 For i = UBound(vntList, 1) To 0 Step -1 If vntList(i) <> "" Then Exit For End If Next i '配列の空白の部分迄を削除 If i > 0 Then ReDim Preserve vntList(i) End If
'必要なデータを配列と結果配列に取得 ReDim vntData(1 To UBound(vntList, 1) + 1, 1 To 3) strKey = Space(8) & vbTab For i = 0 To UBound(vntList, 1) 'データをカンマで分割 vntFields = Split(vntList(i), ",") 'R列に当たるフィールドを配列に転記 vntData(i + 1, 1) = vntFields(17) 'O列に当たるフィールドを配列に転記 vntData(i + 1, 2) = vntFields(14) '日付と認められる値なら If IsDate(vntData(i + 1, 2)) Then 'シリアル値に変更 vntData(i + 1, 2) = DateValue(vntData(i + 1, 2)) End If 'Nに当たるフィールドを配列に転記 vntData(i + 1, 3) = Val(vntFields(13)) With dicIndex 'Dictionaryから転記する行位置と列位置を取得 Mid(strKey, 1, 8) = Format(vntData(i + 1, 2), "yyyymmdd") & vbTab '結果配列に個数を加算 If .Exists(strKey) And .Exists(vntData(i + 1, 1)) Then lngRow = .Item(strKey) lngColumn = .Item(vntData(i + 1, 1)) vntResult(lngRow, lngColumn) _ = vntResult(lngRow, lngColumn) + vntData(i + 1, 3) End If End With Next i
End Sub
(Bun)
こちらで、3万行のCSVファイルを作成して、あちゃこさんの環境より、やや性能の劣る環境でテスト。 結果は、あちゃこさんのオリジナルのコードで5秒前後。以下にアップする、抽出を限定した方法で3.5秒前後。 確実に処理時間は減少しているけど、思ったより時間がかかっている。 やはり、もともとの3万行の負荷は大きいね。 なので、処理時間短縮というテーマであればichinoseさんからアドバイスがある方式かな?
Sub test3() Dim shtA As Worksheet Dim shtB As Worksheet Dim dicX As Object Dim dicY As Object Dim tbl As Variant Dim i As Long Dim j As Long Dim x As Long Dim y As Long Dim c As Range Dim myTime As Double
myTime = Timer
Application.ScreenUpdating = False
With CreateObject("WScript.Shell") Workbooks.Open .SpecialFolders("Desktop") & "\m.csv" End With
Set shtA = Workbooks("m.CSV").Worksheets("m") Set shtB = Workbooks("集計.xls").Worksheets("Sheet1") Set dicX = CreateObject("Scripting.Dictionary") Set dicY = CreateObject("Scripting.Dictionary")
With shtB
With .Range("F1").CurrentRegion x = .Columns.Count - 1 y = .Rows.Count - 1 End With For Each c In .Range("G1").Resize(, x) dicX(c.Value) = c.Column - 6 Next For Each c In .Range("F2").Resize(y) dicY(c.Value) = c.Row - 1 Next
End With
With shtA
.Columns("A:F").ClearContents '作業域 .Range("A1").Value = .Range("R1").Value '型式 抽出域タイトル .Range("B1").Value = .Range("O1").Value '日付 抽出域タイトル .Range("C1").Value = .Range("N1").Value '個数 抽出域タイトル .Range("E1").Value = .Range("O1").Value '日付 抽出条件タイトル .Range("E2").Resize(y).Value = shtB.Range("F2").Resize(y).Value .Columns("O").TextToColumns Destination:=.Range("O1"), DataType:=xlDelimited, _ TextQualifier:=xlDoubleQuote, ConsecutiveDelimiter:=False, Tab:=True, _ Semicolon:=False, Comma:=False, Space:=False, Other:=False, FieldInfo _ :=Array(1, 5), TrailingMinusNumbers:=True .Columns("N:R").AdvancedFilter Action:=xlFilterCopy, _ CriteriaRange:=.Range("E1").CurrentRegion, CopyToRange:=.Range("A1:C1"), Unique:=False
ReDim tbl(1 To y, 1 To x)
For Each c In .Range("A2", .Range("A" & .Rows.Count).End(xlUp)) i = dicY(c.Offset(, 1).Value) j = dicX(c.Value) If j > 0 Then tbl(i, j) = tbl(i, j) + c.Offset(, 2).Value Next
.Columns("A:C").Copy shtB.Columns(1) shtB.Range("G2").Resize(UBound(tbl, 1), UBound(tbl, 2)).Value = tbl
End With
shtA.Parent.Close savechanges:=False
Application.ScreenUpdating = True
Set shtA = Nothing Set shtB = Nothing Set dicX = Nothing Set dicY = Nothing
MsgBox Format(Timer - myTime, "#,##0.00") & "秒かかりました。"
End Sub
(ぶらっと)
ichinoseさん、前回はご丁寧な回答をいただきながら返信もぜず、申し訳ありませんで した。csvファイルの抽出うまくいきました。今のところ理解はしきれていませんが今後の 課題とし、学習したとおもいます。ありがとうがざいました。
Bunさん、解説もいれていただきありがとうございます。未熟者にとって大変分かり わかりやすいです。ありがとうございました。
ぶらっとさん、いつも対応ありがとうございます。毎回勉強をさせていただき活用 させていただいています。 (あちゃこ)
[ 一覧(最新更新順) ]
YukiWiki 1.6.7 Copyright (C) 2000,2001 by Hiroshi Yuki.
Modified by kazu.