【VBA】サーバのファイルをローカルにコピー

Yahoo!知恵袋から、サーバにあるCSVファイルをローカルにコピーしたいというご要望がありましたので、コードを紹介します。

質問

https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q10268370805

Excel VBAを使い、
サーバー(\ホスト名\D$\work)上の「CSVファイル」を
更新日付の新しい順で「8つ」、
ローカルの指定の場所「C:\local」へコピーし、
1枚のシートに以下の情報を出力させたいです。
①ファイル名
②ファイルの更新時間
③コピーしてきたそれぞのファイルの最終行の値
(最終行のセルの位置はファイルによって異なる)

ざっくりとしたイメージは添付画像の感じです。
処理は繋げて書いていただいてもかまいませんが、
ファイルのコピーと、最終行取得は分けていただけると助かります。
難しくなるようであれば都合の良いようにしていただければと思います。

出力シートについては、マクロを実行したシートでも良いですし、
シートを新しく一枚追加しても良いです。
直近でスケジュール通り処理がなされている事と、
処理の結果(OK・NG)が最後の行に記載されるのでそこだけを一覧で見れると確認が早く終わるという理由です。

なお、Windowsの資格情報でサーバーの管理者権限を登録しているので、
ローカルからファイル共有でサーバーへアクセスすることは可能な状態です。


https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q10268370805


この質問文の中にある画像はこちら↓



https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q10268370805

回答

回答ではコードが長くなってしまいましたので、Googleドライブにテキストにしてアップしました。

標準モジュール↓

https://drive.google.com/file/d/1R15m7oV-JbXuPbr3H3_s__Bx7xIO4czg/view?usp=sharing

クラスモジュール↓

https://drive.google.com/file/d/1LNgAQZzfBwwJdi5QLiMncX6MNJZZ6l6J/view?usp=sharing

ブログでは幸いいくらでもコードを書き連ねてもよいので、ここに全部を載せようと思います。

Option Explicit

'OutputSheet Header
Enum en
    サーバー上のファイル名 = 1
    ファイル更新時間
    最終行の値
    theEnd = 最終行の値
End Enum

'サーバー(\\ホスト名\D$\work)上の「CSVファイル」を
'サーバのアドレス
Const cstSrv As String = "\\fs\public\20220928"

Public Sub ファイルコピー()
    Dim tmpSh As Worksheet
    Dim tmpBk As Workbook
    Dim Bk As Workbook
    Dim Sh As Worksheet
    Const clCnt As Long = 8 '更新日付の新しい順で「8つ」、
    Const cstLocal As String = "C:\local" 'ローカルの指定の場所「C:\local」へコピーし、

    'Create Temporary
    Call CreateTemporary(tmpBk, tmpSh, Bk, Sh)
    
    Dim lListCounts As Long
    'Get Server File List
    lListCounts = GetServerFileList(tmpSh, cstSrv)
    
    If lListCounts > 0 Then
        Dim lLocalListCnt As Long
        'Edit File List
        lLocalListCnt = EditFileList(Sh, tmpSh, clCnt, lListCounts)
        
        'Copy CSV From Server to LocalFolder
        Call CopyCSVFromServer2LocalFolder(Sh, cstSrv, cstLocal, lLocalListCnt)
        
    '1 枚のシートに以下の情報を出力させたいです。
    '①ファイル名
    '②ファイルの更新時間
    '③コピーしてきたそれぞのファイルの最終行の値
    '(最終行のセルの位置はファイルによって異なる)
        'Get data at each files' last row
        Call GetDataAtEachFilesLastRow
    End If
    
    
    'End Program
    Call EndProgram(tmpBk, tmpSh, Bk)
End Sub


'Create Temporary
Private Sub CreateTemporary(ByRef tmpBk As Workbook, ByRef tmpSh As Worksheet, ByRef Bk As Workbook, ByRef Sh As Worksheet)
    Set tmpBk = Workbooks.Add
    Set tmpSh = tmpBk.Worksheets(1)
    Set Bk = ThisWorkbook
    Set Sh = Bk.Worksheets(1)       '←ここでCSVを8件だけリストアップするシートを指定してください。
End Sub

'Get Server File List
Private Function GetServerFileList(ByRef tmpSh As Worksheet, ByRef cstSrv As String) As Long
    Dim CSVFiles As Collection
    Set CSVFiles = New Collection
    
    Dim stBuf As String
    Dim cl As Class1
    
    stBuf = Dir(cstSrv & "\" & "*.csv")
    Do While stBuf <> ""
        Set cl = New Class1
        cl.name = stBuf
        cl.FileDateTime = FileDateTime(cstSrv & "\" & stBuf)
        CSVFiles.Add cl, stBuf
        stBuf = Dir()
    Loop
    
    Dim lCnt As Long
    lCnt = CSVFiles.Count
    Dim Var() As Variant
    Dim ii As Long
    
    If lCnt > 0 Then
        ReDim Var(1 To 2, 1 To lCnt)
        For ii = 1 To lCnt
        
            Var(1, ii) = CSVFiles(ii).name
            Var(2, ii) = CSVFiles(ii).FileDateTime
            
        Next ii
        
        With tmpSh
            .Cells(1, 1).Resize(lCnt, 2).Value = WorksheetFunction.Transpose(Var)
        End With
    End If
    
    Set cl = Nothing
    Set CSVFiles = Nothing
    GetServerFileList = lCnt
End Function

'Edit File List
Private Function EditFileList(ByRef Sh As Worksheet, ByRef tmpSh As Worksheet, ByRef clCnt As Long, ByVal lListCounts As Long)
    Dim lRow As Long
    With tmpSh
        .Cells(1, 1).Resize(lListCounts, 2).Sort Key1:=.Cells(1, 2), Order1:=xlDescending
        lRow = .Cells(.Rows.Count, 1).End(xlUp).Row
        If lRow >= clCnt Then
            .Range(Cells(clCnt + 1, 1), .Cells(lRow, 1)).EntireRow.ClearContents
        End If
        lRow = .Cells(.Rows.Count, 1).End(xlUp).Row
        Sh.Range(Sh.Cells(5, en.サーバー上のファイル名), Sh.Cells(4 + lRow, en.ファイル更新時間)).Value = .Range(.Cells(1, 1), .Cells(lRow, 2)).Value
    End With
    EditFileList = lRow
End Function

'Copy CSV From Server to LocalFolder
Private Sub CopyCSVFromServer2LocalFolder(ByRef Sh As Worksheet, _
                                            ByRef cstSrv As String, ByRef cstLocal As String, _
                                            ByRef lLocalListCnt As Long)
    Dim ii As Long
    Dim stFileName As String
    With Sh
        For ii = 1 To lLocalListCnt
            stFileName = .Cells(ii + 4, en.サーバー上のファイル名)
            FileCopy cstSrv & "\" & stFileName, cstLocal & "\" & stFileName
        Next ii
    End With
End Sub

'Get data at each files' last row
Public Sub GetDataAtEachFilesLastRow()
    Dim Sh As Worksheet
    Dim FSO As Object
    Dim stBuf As String
    Dim stTarget As String
    Dim lRow As Long
    Dim lLast As Long
    Dim ii As Long
    
    Set Sh = ThisWorkbook.Worksheets(1)     '←リストのあるシート名を指定してください。
    Set FSO = CreateObject("Scripting.FileSystemObject")
    
    lRow = Sh.Cells(Sh.Rows.Count, 1).End(xlUp).Row

    For ii = 5 To lRow
        stTarget = cstSrv & "\" & Sh.Cells(ii, en.サーバー上のファイル名).Value
        With FSO.OpenTextFile(stTarget, 1)
            stBuf = .ReadAll
            .Close
        End With
        lLast = UBound(Split(stBuf, vbCrLf)) - 1
        Sh.Cells(ii, en.最終行の値).Value = Split(stBuf, vbCrLf)(lLast)
        '↑CSVの仕様が分からないので最終行をまるまるとってくるようにしましたが、
        '特定のカラムだけが必要なら、適切な文字列でSplit関数をかますなどしてください。
    Next ii
    
    Set Sh = Nothing
    Set FSO = Nothing
End Sub
'End Program
Private Sub EndProgram(ByRef tmpBk As Workbook, ByRef tmpSh As Worksheet, ByRef Bk As Workbook)
    Application.DisplayAlerts = False
    tmpBk.Close SaveChanges:=False
    Application.DisplayAlerts = True
    Set tmpSh = Nothing
    Set tmpBk = Nothing
    Set Bk = Nothing
End Sub

↑これがModule1です。

途中でコレクションを使っていまして、コレクションのアイテム用にクラスモジュールを使っています。

クラスモジュールのコードはこちらです↓

Option Explicit

Public name As String
Public FileDateTime As Variant

Javaなどちゃんとした言語の場合はGetとLetを定義するのでしょうが、VBAなんて所詮アマチュアな言語だと思うので、私はいつもこれです(つまり、さぼっています^^;)。

どうやって作ったか

今回このコードを作るときに、途中経過のスクリーンショットをとっておきました。どうやってコーディングしているか、説明しようと思います。

枠組みを作る

最初の状態


最初に枠組みだけ作りました。

だいたいこんな感じかなーーーという枠組みを作り、必要となりそうな変数を引数に適当に放り込んでいます。


コメントは質問者様の文章をそのままコピペして貼り付けています。

私は処理名や変数名を考えるのが苦手なので、質問文にある言葉をなるべく使うようにしています。

この段階では、全部Subです。戻り値なしです。

戻り値は処理の中身を作っていく途中で必要となってくることがありますので、そのときにFunctionに修正すればよいと思っています。


関数に展開する

関数に展開


次に、関数に展開しました。コンパイルエラーにならないようにさっさとやってしまいます。

これは機械的な作業となりますので、この辺の作業のときは私は何も考えていません。

ただ黙々と、自分がロボットになったつもりでてきぱき手を動かすだけです。

引数をByRef参照にするのは、メモリを節約できるからです。

共通で使うならモジュール変数にしてしまえばよいかもしれないのですが、そうするとメモリを喰うと思うので、最終的にはモジュール変数にしたり、Public変数にすることがあるとしても、最初からはしません。

こんな風にちまちまと引数を指定していきます。

型指定は必ず指定しています。


処理をコーディング


次に、関数の中身を細かくコーディングしていきます。

上から順に中身を埋めていく。ここではCreateTemporary関数を埋めました。


関数の中身を埋めていく作業が一番楽しいのかもしれません。

モノづくりというか、頭をフルに使うところです。

CreateTemporaryというのは、このプログラムの一時的なファイルを作る処理です。

確かこのとき、最後に解放する処理を忘れていたなと思い出し、EndProgramという関数を追加しています。

EndProgramを追加


VBAはきれいに正対称を描くプログラムなので、SETしたなら解放も先に書いてしまうと漏れがないです。

後のコードは見てもらえれば分かると思うので省略します。


コンパイルとデバッグ

コーディングが終わったらコンパイルし、デバッグします。

コンパイルエラーはさくっと終わらせ、デバッグに時間をかけます。

ここまでで1時間半経っていたので、今回はあまりデバッグしている暇がありませんでした。

テストデータを作るのが一番めんどくさいです。

今回はたまたま家計簿のCSVファイルがあったのでそれを使いました。

困ったのがサーバです。CentOSをサーバ代わりに使っているのですが、調子が悪く、熱暴走してしまうのですぐにシャットダウンしないといけず、十分なテストができませんでした。時間もなかったので30分でデバッグは切り上げました(本当はもっとやりたかった)。

デバッグで気を付けていることは、少ないデータ→多いデータの順にやっていくことと、できる限りイレギュラーなデータを試すことです。

まとめ

ファイルをコピーしてリストアップするのは、いろいろな問題を考えないといけないので結構難しいと思います。

今回ファイルをコピーする先のローカルフォルダに同名のファイルがあった場合の挙動とか、リストに以前のデータが残っていたときのクリアコンテンツする挙動とか、盛り込むことができませんでした。

プログラミングは大枠を作ってから詳細を埋めていくやり方は、忙しかったパートタイマー時代に見つけました。

このやり方が自分には一番しっくりきます。

Yahoo知恵袋でこんな質問が来るのは珍しいですね。いつももっとおおざっぱで、的を得ない、なんと答えたらよいのか分からないような質問の方が多いのですが。今回はだいぶしっかりと仕様が分かっている人が質問したので楽でした。

何かのお役に立てば幸いです。

ここまでお読みくださりありがとうございました。