モーリーのメモ

アプリ開発等(プログラミング、CG作成)、興味を持ったことを実践してまとめるブログです。

モーリーのメモ

同期・非同期処理で、フォルダ内のファイルを列挙する!:Node.js

 ↓こちらの記事で、Node.jsの環境を作成しました。
mmorley.hatenablog.com
 Node.jsの使用例を見ていたら、ファイル操作が出来ることを発見!ここでのファイル操作とは、ファイルの名前変更、削除、ファイル情報の取得等です。
 
 ブラウザ上で動く従来のJavaScriptでは出来なかったので盲点でした。個人的には、不慣れなunixコマンドよりいろいろ出来そうです。
 
 ファイル操作に関する関数群は、Node.jsの標準モジュールに含まれています。Node.jsには使いたいライブラリをnpmで組み込んで使用する仕組みがありますが、標準モジュールは最初から組み込まれています。
 標準モジュールに含まれる関数群の詳細は、公式のAPIリファレンスで確認出来ます。
 
 ということで、今回は試しにファイル内のフォルダを列挙するプログラムを作成します。
 またNode.jsのプログラムでは、同期処理、非同期処理に注意する必要があります。今回は試しにそれぞれ作ってみます。

使用環境

 私が今回使用した環境です。

同期処理と非同期処理について

 下記のリンクは、今回使用するFile System APIの関数群です。

 ↑の関数名の一覧を見ると『〜Sync』の有無で2通りあるものがあります。
 『〜Sync』が付いてるのが同期処理の関数で、付いてないのが非同期処理の関数です。
 同期処理と非同期処理のふるまいの違いを把握していないと、意図する結果が得られないので、理解は必須です。
 注意するのは処理の順序です。

    同期処理 <プログラムに記述した順序で処理される>
    関数の結果が返されるまで処理を待つ
    =結果が出るまで1つの処理に占有される(フリーズする)
    =複数を相手にするサーバー処理に適さない
    非同期処理 <プログラムに記述した順序で処理されない>
    関数の結果を待たずに次の関数を実行
    =1つの処理に占有されない(フリーズしない)
    =複数を相手にするサーバー処理向き

同期・非同期処理の例

 次の関数で例を示します。

  • fs.readdirSync(path[, options]):同期処理
  • fs.readdir(path[, options], callback): 非同期処理

 指定フォルダ内のファイル・サブフォルダ名を取得する関数です。
 Desktopのファイルとフォルダ名をターミナルに表示するプログラムを書きます。

同期処理(readdirSync)の場合

 下記のコードで、
 ①の関数の処理はディスクにアクセスするので時間がかかります。
 ①の処理の完了を待ってから②が実行されるので、ターミナルにDesktopの情報が表示されます。
 『var dirTarget = '/Users/ユーザ名/Desktop';』のユーザ名の部分は、使用中のユーザ名に書き換えて下さい。

    var fs = require('fs'); // File System(Node API):ファイル操作
    var files = []; // フォルダの情報を格納する配列
    
    // ①:readdirを実行
    files = fs.readdirSync('/Users/ユーザー名/Desktop');
    
    // ②:filesの内容をターミナルに表示
    files.forEach(function(file){ 
        console.log(file);
    });
    
    // 実行結果:デスクトップのファイルとフォルダ名が列挙される
    

非同期処理の場合(readdir):失敗例

 下記のコードで、
 ①は関数の呼び出しだけで処理の完了を待たずに、すぐに②を実行します。
 file[]は空の状態なので、何も表示されません。
 ①の処理が完了したら、③が実行されます。(時、既に遅しです。)

    var fs = require('fs'); // File System(Node API):ファイル操作
    var files = []; // フォルダの情報を格納する配列
    
    // ①:readdirを実行
    fs.readdir('/Users/ユーザー名/Desktop', 
        // ③:readdirのコールバック関数を登録
        function(err, tmpFiles){ 
            if(err) throw err; // エラーを上位の処理にスロー
            // tmpFilesの内容をfilesにコピー
            files = []; // 配列をクリア
            tmpFiles.forEach(function(tmpFile){
                files.push(tmpFile); // 処理対象に追加
            });
        }
    )
    
    // ②:filesの内容をターミナルに表示
    files.forEach(function(file){ 
        console.log(file);
    });
    
    // 実行結果:何も表示されない
    

 上記コードを直すには、②の処理を③のコールバック関数の中に移すだけですが、次の場合はもう少し複雑です。

サブフォルダ内も含めて、フォルダ内のファイルを列挙する

 サブフォルダ内も含めてデスクトップ内のファイル情報(ファイル名、サイズ、作成日)を取得し、デスクトップにresult.csvという名前で書き出します。

同期処理

 『(function getFiles(dir){...})(dirTarget);』は、関数を定義して、即実行する書き方です。
 ①のgetFiles関数は、フォルダが見つかるたびにgetFiles自身を呼び出しています。
 何回getFilesが呼び出されるかは、フォルダの階層や数次第です。
 しかし同期処理なので、①の処理は、必ず②の前までに完了しています。

    var fs = require('fs'); // File System(Node API):ファイル操作
    var path = require('path'); // Path(Node API):パスの文字列操作
    
    var dirTarget = '/Users/ユーザー名/Desktop'; // 対象フォルダ
    var text = ''; // ファイルに書き出す情報
    
    // ①:getFiles(ファイル情報取得関数)の定義と実行
    (function getFiles(dir){
        var files = fs.readdirSync(dir); // 指定フォルダ内のファイル、サブフォルダを取得
        files.forEach(function(file){ 
            var fullPath = path.join(dir, file); // フルパスを取得
            var stats = fs.statSync(fullPath) // ファイル(またはフォルダ)の情報を取得
            if(stats.isDirectory()){ // フォルダの場合
                getFiles(fullPath); // getFilesを再帰的に呼び出し
            }else{ // ファイルの場合
                // ファイル情報を取得
                text += file // ファイル名
                    + ',' + stats.size // ファイルサイズ:単位バイト
                    + ',' + stats.birthtime.getFullYear() + '/' // ファイル作成日:年
                    + ('0' + (stats.birthtime.getMonth() + 1)).slice(-2) + '/' // ファイル作成日:月
                    + ('0' + stats.birthtime.getDate()).slice(-2) // ファイル作成日;日
                    + '\n'; 
            }
        });
    })(dirTarget);
    
    // ②:ファイル書き出し
    fs.writeFileSync( // テキストファイルに書き込み
        path.join(dirTarget, 'result.csv'), // 対象フォルダに結果ファイルを出力
        text // 書き込む文字列
    ); 
    

非同期処理

 今回は、

  • 同期処理の関数を非同期処理の関数に変更する
  • コールバック関数で、ファイル書き出しを行う

 という修正だけでは上手く行きません。
 
 全てのファイル情報の読み込みが完了したのを判定して、それからファイルに書き出す必要があります。
 これを解決するためにsetTimeout(callback, delay)を使用しました。
 delayミリ秒後に、callbackで指定した関数を1度だけ実行する関数です。
 
 ①でdirに含まれるファイル(またはフォルダ)をfileに読み込みます。
 ②でfileの中身をチェックし、フォルダが見つかるごとにdirに追加します。
 未チェックのフォルダがあれば①に戻り、無ければファイルに書き出します。

    var fs = require('fs'); // File System(Node API):ファイル操作
    var path = require('path'); // Path(Node API):パスの文字列操作
    
    var dirs = ['/Users/ユーザー名/Desktop']; // 処理対象のフォルダ(サブフォルダ)を格納
    
    // ①:フォルダ内のファイル名を取得
    function getFiles(dirs, dirIndex, text){
        fs.readdir(dirs[dirIndex], function(err, files){ 
            if(err) throw err; // エラーを上位メソッドにスロー
            setTimeout(function(){
                writeFileList(dirs, dirIndex, files, 0, text); // writeFileList()を呼び出す
            }, 0); // 0ミリ秒後に実行
        });
    }
    
    // ②:ファイルかフォルダかを判定し、ファイルならテキストに書き出す
    function writeFileList(dirs, dirIndex, files, fileIndex, text){
        // フォルダのフルパス
        var fullPath = path.join(dirs[dirIndex], files[fileIndex]);
        fs.stat(fullPath, function(err, stats){
            if(err) throw err; // エラーを上位メソッドにスロー
            if(stats.isDirectory()){ // フォルダの場合
                dirs.push(fullPath); // 処理対象フォルダに追加
            }else{
                text += files[fileIndex] // ファイル名
                    + ',' + stats.size // ファイルサイズ:単位バイト
                    + ',' + stats.birthtime.getFullYear() + '/' // ファイル作成日:年
                    + ('0' + (stats.birthtime.getMonth() + 1)).slice(-2) + '/' // ファイル作成日:月
                    + ('0' + stats.birthtime.getDate()).slice(-2) // ファイル作成日;日
                    + '\n'; 
            }
            if(++ fileIndex >= files.length){ // 次がfile[]の最後の場合
                if(++ dirIndex < dirs.length){ // 次がdir[]の最後でない場合
                    setTimeout(function(){
                        getFiles(dirs, dirIndex, text); // getFiles()を呼び出す
                    }, 0); // 0ミリ秒後に実行
                }else{
                    // ファイルに書き出し
                    fs.writeFile(
                        path.join(dirs[0], 'result.csv'), // 対象フォルダに結果ファイルを出力
                        text, // 書き込む文字列
                        function(err){
                        if (err) throw err; // エラーを上位メソッドにスロー
                    });
                }
            }else{ // file[]がまだ処理中の場合
                setTimeout(function(){
                    writeFileList(dirs, dirIndex, files, fileIndex, text); // writeFileList()を呼び出す
                }, 0); // 0ミリ秒後に実行
            }
        });
    }
    
    // 処理を開始
    getFiles(dirs, 0, "");
    

 非同期処理を行う関数が同時に複数実行されないようにしています。
 非同期処理が終わるごとに、コールバック関数で、setTimeout()によって次の関数を呼び出しています。

あとがき

 最初は、同期・非同期の関数をよく理解せずに使ったため、実行順序が無茶苦茶になってました。
 実行順序を理解していないと、バグが見つけにくいです。
 
 非同期処理は、サーバー処理のように複数を相手に処理するような場合のためのものです。
 今回書いた処理は単独で実行するものなので、本来は全部同期処理で書けば良いと思いますが、練習のため非同期処理で書きました。
 複数を相手にする場合、グローバル変数を使わない方が良いのかなと考えて、引数で渡すようにしてみました。