node.jsでディレクトリ配下の全エクセルから特定のシートを探し出して、値を抽出した結果をCSV出力する。(Excel VBAを使いたくない)
はじめに
※一番最後にコードの全量があります。 読むの面倒だから結論頼むって方は最後のほうを見てみてください。
Excel VBAじゃなくて、node.jsを選択する理由は後述しますが
ざっくり説明すると
このプログラムの作成に時間をかけてしまうと本末転倒なので、30分でコードを書ききりたい。
だからnode.jsを選択するんだ! ということ。
あと仕事用のPCはスペックがそこまで高くないので、VBAで大量のエクセルを読み込もうとすると
重たい。 処理に時間がかかる。
やりたいこと
テスト結果の品質報告書を書くために
ポンポンディレクトリ内に追加されるテスト結果の状況を逐一最新化して、品質データを集計したい。
そのためにディレクトリ内のエクセル内の特定シートから値を抽出したい。
抽出したい内容
想定する抽出結果
なぜExcel VBAを使いたくないのか
※非常に抽象的な理由です。
タイトル通りなのですが、Excel VBAを使わずにディレクトリ内に配置された
全エクセルファイルから、特定のシートを探し出して、特定セルの値を抽出した結果をCSV出力したいと思っています。
Excel VBAを使いたくない理由としては
・重い
・デバッグがめんどう
・コンソールの仕様が複雑で非効率
・抽出対象のエクセルが改変された場合のメンテナンスがめんどう
・プログラム実行時の作業状況が可視化しづらい
・最近あまり使ってないのでプログラムの作成に1時間くらいかかりそう
正直言語は何でもいいんですが、node.jsを使いたい理由としては
・軽い
・デバッグが簡単
・コンソールの仕様がシンプルで効率的
・抽出対象のエクセルが改変された場合のメンテナンスが容易
・プログラム実行時の作業状況が可視化しやすい
・VS Code上で開発できる
・node.jsでならプログラムを30分で作り切れる自信がある
使いたく理由の反面や・・・
さっそくとりかかる
準備
コンソールで下記を実行して、必要なリソースをそろえる
#とりあえず作業用ディレクトリ作成
mkdir aggregate
#ベースアプリの作成
npm init -y
npm install xlsx fs csv-writer
#プログラム実行用ファイルの作成
type app.js >app.js
各ライブラリは以下の通りです
ライブラリ | 設営 |
---|---|
xlsx | エクセルの読み込みに使用 |
fs | ファイル探索に使用 |
csv-writer | データをcsvに書き出す際に使用 |
プログラムの作成
ディレクトリ配下のエクセルファイルをリストアップしてみる
app.js
// ファイル読み込みvarfs=require('fs');// ディレクトリ指定constdirPath='C:\\Users\\makiaaaaaa\\テスト結果';// ディレクトリ配下を探索して、xlsxファイルの一覧を取得するspecifiedDirectory=fs.readdirSync(dirPath);varbookNames=specifiedDirectory.filter(function(file){// 一時ファイルは除外したいvarpatternFalse='~$';// エクセルだけ出力したいvarpatternTrue='.xlsx';return(!file.startsWith(patternFalse)&&(file.lastIndexOf(patternTrue)+patternTrue.length===file.length)&&(patternTrue.length<=file.length))});// とりあえずコンソール出力console.log(bookNames);
実行
node app.js
実行結果
テストするよ.xlsx
テストするよ。その2.xlsx
ディレクトリ配下のブックがリストアップできていることを確認できました。
ディレクトリ配下のエクセルファイルを開いて、シート名を取得
app.js
// 全シートを解析して、条件を満たすシートを探すbookNames.forEach(bookName=>{// ブックを開くletworkbook=xlsx.readFile(dirPath+'\\'+bookName);// 全シート名取得letsheetNames=workbook.SheetNames;});
全シートを指定解析して、欲しい情報を持つシートからデータを抽出する
全シートに欲しい情報があるわけではないので
データ取得条件を満たすシートをどうやって選定するかは下記のように考えます。
データ抽出条件 と データ抽出可能か判定するファンクションを追加する
app.js
// データ抽出条件constexCondition=[{key:"P2",value:"テストケース名"},{key:"R2",value:"項目数"}];// 集計対象シートかどうかを調べるfunctionisTargetOfAggregation(worksheet){for(vari=0;i<exConditions.length;i++){letcondition=exConditions[i];letrange=condition.key;if(worksheet[range]===undefined||worksheet[range].v===undefined||worksheet[range].v!==condition.value){returnfalse;}}returntrue;}
どのセルから、何の情報を取得するか定義しておく
// データ抽出内容constexValues=[{key:"P3",value:"テストケース名"},{key:"S3",value:"項目数"},{key:"S4",value:"OK"},{key:"S5",value:"NG"}];
データを抽出する
出力用のオブジェクトを作成
// CSV出力用のオブジェクトvarresults=[];
データを抽出する
// 全シートを解析して、条件を満たすシートを探すsheetNames.forEach(sheet=>{letworksheet=workbook.Sheets[sheet];// シートを判定してデータ抽出可能か調べるif(isTargetOfAggregation(worksheet)){// 判定した結果、問題がなければデータを抽出する// ブック名とシート名varresult={'ブック名':bookName,'シート名':sheet}// 値を抽出するfor(vari=0;i<exValues.length;i++){letexValue=exValues[i];if(worksheet[exValue.range]!==undefined&&worksheet[exValue.range].v!==undefined){result[exValue.label]=worksheet[exValue.range].v;}}results.push(result);}});
データ出力準備
// CSV出力用const{createObjectCsvWriter}=require('csv-writer');// CSV出力ファイル名constcsvFileName='data.csv';
データ出力
// 抽出結果をCSVに出力するconstCW=createObjectCsvWriter({path:csvFileName,header:Object.keys(results[0]).map(v=>({id:v,title:v}))})CW.writeRecords(results).then(()=>{console.log('書き込み完了(既に同名ファイルが存在する場合は上書き)');});
プログラムを実行してみる
実行
node app.js
実行結果
{
'ブック名': 'テストするよ.xlsx',
'シート名': 'テストするよー',
'テストケース名': 'テストしたよん',
'項目数': 83,
'OK': 81,
'NG': 2
}
]
書き込み完了(既に同名ファイルが存在する場合は上書き)
1ファイルだけ読み込んでデータ抽出した結果
ブック名,シート名,テストケース名,項目数,OK,NGテストするよ.xlsx,テストするよー.テストしたよん,83,81,2
対象にファイルをコピーしてデータ抽出した結果
このCSVのデータを報告書の集計用シートに張り付けて、報告用集計データが自動生成されるといった仕組みです。
ブック名,シート名,テストケース名,項目数,OK,NGテストするよ.xlsx,テストするよー.テストしたよん,83,81,2テストするよ2.xlsx,テストするよー.テストしたよん,83,81,2テストするよ3.xlsx,テストするよー.テストしたよん,83,81,2テストするよ4.xlsx,テストするよー.テストしたよん,83,81,2テストするよ5.xlsx,テストするよー.テストしたよん,83,81,2テストするよ6.xlsx,テストするよー.テストしたよん,83,81,2テストするよ7.xlsx,テストするよー.テストしたよん,83,81,2テストするよ8.xlsx,テストするよー.テストしたよん,83,81,2テストするよ9.xlsx,テストするよー.テストしたよん,83,81,2テストするよ10.xlsx,テストするよー.テストしたよん,83,81,2テストするよ11.xlsx,テストするよー.テストしたよん,83,81,2
ソースコードの全量です
なんだかんだ1時間くらいかかった。
さぁ・・・ 報告書を書こう・・・。
用途・環境に合わせて、以下の値を書き換えれば
どの環境でも動く・・・はず。
dirPath ・・・ ファイル格納ディレクトリ
exConditions ・・・ 抽出条件
exValues ・・・ 抽出内容
// ファイル読み込みvarfs=require('fs');// エクセルファイルの読み込みconstxlsx=require('xlsx');// CSV出力用const{createObjectCsvWriter}=require('csv-writer');// CSV出力ファイル名constcsvFileName='data.csv';// ディレクトリ指定 探索するディレクトリを指定するconstdirPath='C:\\Users\\makiaaaaaa\\テスト結果';// データ抽出条件 どのセルにどういった情報を持つシートからデータを抽出するかの条件を書くconstexConditions=[{key:"P2",value:"テストケース名"},{key:"R2",value:"項目数"}];// データ抽出内容 どのセルからどういった情報を取得するか書くconstexValues=[{key:"P3",value:"テストケース名"},{key:"S3",value:"項目数"},{key:"S4",value:"OK"},{key:"S5",value:"NG"}];// CSV出力用のオブジェクトvarresults=[];// ディレクトリ配下を探索して、xlsxファイルの一覧を取得するspecifiedDirectory=fs.readdirSync(dirPath);varbookNames=specifiedDirectory.filter(function(file){// 一時ファイルは除外したいvarpatternFalse='~$';// エクセルだけ出力したいvarpatternTrue='.xlsx';return(!file.startsWith(patternFalse)&&(file.lastIndexOf(patternTrue)+patternTrue.length===file.length)&&(patternTrue.length<=file.length))});// 全シートを解析して、条件を満たすシートを探すbookNames.forEach(bookName=>{// ブックを開くletworkbook=xlsx.readFile(dirPath+'\\'+bookName);// 全シート名取得letsheetNames=workbook.SheetNames;// 全シートを解析して、条件を満たすシートを探すsheetNames.forEach(sheet=>{letworksheet=workbook.Sheets[sheet];// シートを判定してデータ抽出可能か調べるif(isTargetOfAggregation(worksheet)){// 判定した結果、問題がなければデータを抽出する// ブック名とシート名varresult={"ブック名":bookName,"シート名":sheet}// 値を抽出するfor(vari=0;i<exValues.length;i++){letexValue=exValues[i];if(worksheet[exValue.range]!==undefined&&worksheet[exValue.range].v!==undefined){result[exValue.label]=worksheet[exValue.range].v;}}results.push(result);}});});console.log(results);// 抽出結果をCSVに出力するconstCW=createObjectCsvWriter({path:csvFileName,header:Object.keys(results[0]).map(v=>({id:v,title:v}))})CW.writeRecords(results).then(()=>{console.log('書き込み完了(既に同名ファイルが存在する場合は上書き)');});// 集計対象シートかどうかを調べるfunctionisTargetOfAggregation(worksheet){for(vari=0;i<exConditions.length;i++){letcondition=exConditions[i];letrange=condition.key;if(worksheet[range]===undefined||worksheet[range].v===undefined||worksheet[range].v!==condition.value){returnfalse;}}returntrue;}