はじめに
前回の設計編では、バラバラなCSVをn8nで統合するアーキテクチャを解説した。
今回はその続き。実際にn8nの画面上でどのようにノードを設定し、どんなJavaScriptを書けば動くのか、コードをそのままコピペして動かせるレベルで公開する。
動作確認環境:n8n v1.85.4(self-hosted)、Node.js 22.x
前回のおさらい:今回実装するパイプライン
使用するサンプルデータは前回と同じ3種類のCSVだ。
| 検査機 | キー列名 | 判定表記 |
|---|---|---|
| 検査機A(外径) | ロット番号 | OK / NG |
| 検査機B(硬度) | lot_id | PASS / FAIL |
| 検査機C(内径) | ロットID | 合格 / 不合格 |
実装:5ステップのノード設定
Step 1. Folder Trigger — ファイル検知
n8nのノード追加画面で「Local File Trigger」を選択する。
設定項目:
| 項目 | 設定値 |
|---|---|
| Watch Folder | /data/csv_input |
| Events | File Added |
| File Extension Filter | csv |
self-hostedの場合、n8nが起動しているサーバー上のパスを指定する。Docker環境ではボリュームマウントを忘れずに。
このノードが発火すると、後続ノードに {{ $json.path }} としてファイルパスが渡される。
Step 2. CSVパースと列名の正規化 — Codeノード
ここが最も重要なステップだ。Codeノード(JavaScript) を1つ用意し、以下の処理を一括で行う。
- CSVファイルを読み込む
- どの検査機のデータかをファイル名で判定する
- 列名を共通スキーマに正規化する
- 判定表記を
OK/NGに統一する
// Step2: CSVパース + 列名正規化
const fs = require('fs');
const path = require('path');
// 前ノードから受け取ったファイルパス
const filePath = $input.first().json.path;
const fileName = path.basename(filePath);
const raw = fs.readFileSync(filePath, 'utf-8');
// CSVをオブジェクト配列にパース
function parseCsv(text) {
const lines = text.trim().split('\n');
const headers = lines[0].split(',').map(h => h.trim());
return lines.slice(1).map(line => {
const values = line.split(',').map(v => v.trim());
return Object.fromEntries(headers.map((h, i) => [h, values[i]]));
});
}
// 検査機ごとの列マッピング定義
const SCHEMA_MAP = {
'machine_a': {
lotKey: 'ロット番号',
judgeKey: '判定',
judgeMap: { 'OK': 'OK', 'NG': 'NG' },
valueKey: '外径(mm)',
outputKey: '外径判定',
},
'machine_b': {
lotKey: 'lot_id',
judgeKey: 'result',
judgeMap: { 'PASS': 'OK', 'FAIL': 'NG' },
valueKey: 'hardness',
outputKey: '硬度判定',
},
'machine_c': {
lotKey: 'ロットID',
judgeKey: '合否',
judgeMap: { '合格': 'OK', '不合格': 'NG' },
valueKey: '内径(mm)',
outputKey: '内径判定',
},
};
// ファイル名から検査機を特定
function detectMachine(name) {
if (name.includes('machine_a')) return 'machine_a';
if (name.includes('machine_b')) return 'machine_b';
if (name.includes('machine_c')) return 'machine_c';
throw new Error(`未知のファイル: ${name}`);
}
const machineKey = detectMachine(fileName);
const schema = SCHEMA_MAP[machineKey];
const rows = parseCsv(raw);
// 共通スキーマへ正規化
const normalized = rows.map(row => ({
ロット番号: row[schema.lotKey],
[schema.outputKey]: schema.judgeMap[row[schema.judgeKey]] ?? 'NG',
生データ: JSON.stringify(row), // 原本保持(障害調査用)
検査機: machineKey,
}));
return normalized.map(item => ({ json: item }));
設計のポイント:
列マッピングを SCHEMA_MAP オブジェクトに外部化している。新しい検査機が増えても、このオブジェクトにエントリを追加するだけで対応できる。「コードではなくデータを変える」設計だ。
また 生データ として元のCSV行をJSON文字列で保持している。後から「なぜこのロットはNGになったのか」を追跡できるようにするためだ。
Step 3. Mergeノード — ロット番号で結合
3つの検査機からのデータをn8nの Mergeノード で結合する。
設定:
| 項目 | 設定値 |
|---|---|
| Mode | Combine |
| Combination Mode | Multiplex |
| Join Field(左) | ロット番号 |
| Join Field(右) | ロット番号 |
n8n v1.x以降、Mergeノードの設定UIが変更されている。「Combine」→「By Key」を選ぶと、指定したキーで行を突合できる。
このノードの出力は、1ロットあたり1行に集約されたJSONになる。
{
"ロット番号": "LOT-2026-001",
"外径判定": "OK",
"硬度判定": "OK",
"内径判定": "NG"
}
Step 4. 総合判定ロジック — Codeノード
// Step4: 総合判定
const items = $input.all();
return items.map(item => {
const d = item.json;
const allOk =
d['外径判定'] === 'OK' &&
d['硬度判定'] === 'OK' &&
d['内径判定'] === 'OK';
// どの検査でNGが出たかを記録(後工程でのトレーサビリティ確保)
const ngItems = ['外径判定', '硬度判定', '内径判定']
.filter(key => d[key] === 'NG');
return {
json: {
...d,
総合判定: allOk ? 'OK' : 'NG',
NG項目: ngItems.join(', ') || '-',
処理日時: new Date().toISOString(),
},
};
});
なぜNG項目を記録するのか:
「総合判定NG」だけでは、現場担当者が「どの検査機のどの項目で落ちたか」を再度確認しに行かなければならない。NG項目 カラムを持たせることで、Google Sheetsを見るだけで原因が特定できる。
Step 5. Google Sheetsノード — 書き込み
設定:
| 項目 | 設定値 |
|---|---|
| Operation | Append Row |
| Spreadsheet ID | (対象のシートID) |
| Sheet Name | 統合品質DB |
| Columns | ロット番号, 外径判定, 硬度判定, 内径判定, 総合判定, NG項目, 処理日時 |
Google Sheets APIの書き込みは1秒あたり約60リクエストの上限がある。大量バッチ処理時はSplit In Batchesノードで分割するか、Spreadsheets Append(配列一括書き込み)を使うこと。
完成後の動作確認
以下の3ファイルを取込フォルダに投入してテストする。
machine_a_20260320.csv
ロット番号,外径(mm),判定
LOT-001,12.05,OK
LOT-002,11.87,NG
LOT-003,12.01,OK
machine_b_20260320.csv
lot_id,hardness,result
LOT-001,58.2,PASS
LOT-002,57.9,PASS
LOT-003,61.0,PASS
machine_c_20260320.csv
ロットID,内径(mm),合否
LOT-001,8.01,合格
LOT-002,8.03,合格
LOT-003,7.94,不合格
期待される出力(Google Sheets):
| ロット番号 | 外径判定 | 硬度判定 | 内径判定 | 総合判定 | NG項目 |
|---|---|---|---|---|---|
| LOT-001 | OK | OK | OK | OK | - |
| LOT-002 | NG | OK | OK | NG | 外径判定 |
| LOT-003 | OK | OK | NG | NG | 内径判定 |
LOT-002は外径でNG、LOT-003は内径でNG。それぞれの原因が1カラムで特定できる。
本番運用で追加すべき3つの処理
上記の実装はハッピーパスのみだ。本番で安定稼働させるには以下を追加する。
1. ファイル二重取り込みの防止
同じファイルを2回投入した場合の処理を定義する。処理済みファイルをアーカイブフォルダに移動するか、ロット番号の重複チェックを入れる。
2. パースエラー時の通知
CSVの列が増減した、文字コードがShift-JISだったなど、予期しないフォーマット変更は現場では起きる。エラー時にSlack通知を飛ばすError Triggerノードを繋いでおく。
3. 処理ログの保持
どのファイルをいつ処理したかを別シートに記録する。障害調査と監査対応の両方に使える。
まとめ
今回実装したポイントを振り返る。
| ステップ | 実装のキモ |
|---|---|
| 列名正規化 | SCHEMA_MAPで検査機ごとのマッピングを外部化 |
| 結合 | Mergeノードをロット番号キーで突合 |
| 総合判定 | NG項目を明示してトレーサビリティを確保 |
| 書き込み | Google Sheetsにリアルタイム追記 |
設計編で「1年以内に回収可能」と書いたが、実際の回収期間は現場の手作業時間によって変わる。本記事のコードをそのまま動かして、まず自社データで検証してほしい。
次回は 「n8n × LangChainで、統合されたデータから異常検知アラートを自動生成する」 実装を予定している。
この記事を書いた人 ✏️ @YushiYamamoto
株式会社プロドウガ CEO / AIアーキテクト
製造業・EC・SaaSの業務自動化設計を専門としています。
n8n・Supabase・Claude Codeを活用した自律型アーキテクチャの構築事例を発信中。
技術的な相談やシステム設計の壁打ちは、記事下部のコメント欄か、プロフィールのリンクからどうぞ。