フォルダ内に大量のファイルがあると、拡張子ごとに整理された一覧が欲しくなることがあります。
- どんな拡張子が存在するか、拡張子の一覧が欲しい
- 拡張子の内訳が知りたい
-
.txt
だけ抜き出して一覧にしたい -
.json
や.log
などの数をざっくり把握したい - 中身を確認したいけど、いちいちフォルダをたどるのが面倒
- フォルダ構成を csv にしたい
そんなときのために、拡張子単位で分類・プレビューできるシンプルなWebツールを作成しました。
👉 サイトはこちら
🧩 ツールの概要
File System Access API を使って、ローカルのフォルダをブラウザから読み込み、拡張子ごとに分類して表示します。
- ✅ ブラウザのみで動作(Chrome系対応)
- ✅ フォルダ以下のすべてのファイルを再帰的に取得
- ✅ 拡張子ごとの件数をカウント
- ✅ 「この拡張子を開く」ボタンで詳細表示
- ✅ プレビュー機能(別タブで表示)付き
- ✅ フォルダ構成を csv にする機能付き
特に拡張子の内訳が知りたい場合にとても便利です。
✅ 想定されるユースケース
- サブフォルダが大量にあるプロジェクトの整理
- バックアップフォルダの内容確認
- どんな拡張子が存在するか、拡張子の一覧を確認
- チームへの納品前チェック(ファイル構成の説明など)
- フォルダ構成の csv を作成
🖱 操作方法
- 「📁 フォルダを選択」ボタンをクリック
- 拡張子ごとの一覧が表示されます(件数つき)
- 各行の「この拡張子を開く」ボタンを押すと、対象のファイルだけの一覧に切り替わります
- 「プレビュー」ボタンで、内容を新しいタブに表示できます
- 「← 戻る」で拡張子一覧に戻れます
💡 ファイルの中身は読み取り専用です。保存や変更処理は行いません。
🧪 デモ(コード全文)
HTMLファイルとして保存 し、ブラウザ(Chromeなど)で開いて使えます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>拡張子で分類&CSV出力</title>
<style>
body { font-family: sans-serif; margin: 20px; }
table { border-collapse: collapse; width: 100%; margin-top: 10px; }
th, td { border: 1px solid #ccc; padding: 6px; }
th { background: #eee; cursor: pointer; }
button { margin: 5px 5px 5px 0; }
</style>
</head>
<body>
<h2>📂 拡張子で分類されたファイル一覧</h2>
<button id="pick">📁 フォルダを選択</button>
<button id="downloadCsv" disabled>📤 CSV出力</button>
<div id="view"></div>
<script>
const pickBtn = document.getElementById('pick');
const downloadBtn = document.getElementById('downloadCsv');
const viewDiv = document.getElementById('view');
let allFiles = [];
let extGroups = {};
let currentSort = { target: 'ext', ascending: true };
pickBtn.addEventListener('click', async () => {
try {
const dirHandle = await window.showDirectoryPicker();
allFiles = [];
extGroups = {};
await readAllFiles(dirHandle);
groupByExtension();
renderExtensionTable();
downloadBtn.disabled = false;
} catch (e) {
alert('キャンセルされました。');
}
});
downloadBtn.addEventListener('click', () => {
const csv = generateCsv();
const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
const blob = new Blob([bom, csv], { type: "text/tab-separated-values;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "file-list.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
function generateCsv() {
const rows = [["拡張子", "件数", "ファイル名", "パス", "サイズ", "更新日(適当な項目をダブルクリックして有効化して下さい)"]];
const sortedExts = Object.entries(extGroups).sort((a, b) => a[0].localeCompare(b[0]));
for (const [ext, files] of sortedExts) {
const sortedFiles = [...files].sort((a, b) => a.name.localeCompare(b.name));
rows.push([ext, sortedFiles.length, "", "", "", ""]);
for (const file of sortedFiles) {
const info = file.fileInfo;
const size = info?.size != null ? formatFileSize(info.size) : "";
const date = info?.lastModified
? new Date(info.lastModified).toLocaleString("ja-JP", {
year: "numeric", month: "2-digit", day: "2-digit",
hour: "2-digit", minute: "2-digit", second: "2-digit"
}).replace(/\//g, "-")
: "";
rows.push([
"", "", file.name,
file.path.replaceAll("/", "\\"),
size,
date
]);
}
}
return rows.map(row =>
row.map(cell => (cell ?? "").toString()).join(",")
).join("\n");
}
function formatFileSize(bytes) {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
}
async function readAllFiles(dirHandle, path = '') {
for await (const [name, handle] of dirHandle.entries()) {
const fullPath = `${path}${name}`;
if (handle.kind === 'file') {
const fileData = await handle.getFile();
allFiles.push({
name,
path: fullPath,
handle,
fileInfo: fileData // ← 追加:Fileオブジェクト保存
});
} else if (handle.kind === 'directory') {
await readAllFiles(handle, `${fullPath}/`);
}
}
}
function groupByExtension() {
extGroups = {};
for (const file of allFiles) {
const ext = getExt(file.name);
if (!extGroups[ext]) extGroups[ext] = [];
extGroups[ext].push(file);
}
}
function getExt(filename) {
const dot = filename.lastIndexOf('.');
return dot === -1 ? '(no ext)' : filename.slice(dot).toLowerCase();
}
function renderExtensionTable() {
viewDiv.innerHTML = '<h3>拡張子一覧</h3>';
const table = document.createElement('table');
const exts = Object.entries(extGroups);
if (currentSort.target === 'ext') {
exts.sort((a, b) => currentSort.ascending ? a[0].localeCompare(b[0]) : b[0].localeCompare(a[0]));
} else if (currentSort.target === 'count') {
exts.sort((a, b) => currentSort.ascending ? a[1].length - b[1].length : b[1].length - a[1].length);
}
table.innerHTML = `
<thead>
<tr>
<th onclick="sortExtTable('ext')">拡張子</th>
<th onclick="sortExtTable('count')">件数</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${exts.map(([ext, files]) => `
<tr>
<td>${ext}</td>
<td>${files.length}</td>
<td><button onclick="renderFileTable('${ext}')">この拡張子を開く</button></td>
</tr>
`).join('')}
</tbody>
`;
viewDiv.appendChild(table);
}
window.sortExtTable = function(target) {
if (currentSort.target === target) {
currentSort.ascending = !currentSort.ascending;
} else {
currentSort = { target, ascending: true };
}
renderExtensionTable();
};
window.renderFileTable = function(ext) {
const files = [...extGroups[ext]];
let fileSort = { key: 'name', ascending: true };
const render = () => {
viewDiv.innerHTML = `<h3>${ext} のファイル一覧</h3><button onclick="renderExtensionTable()">← 戻る</button>`;
const table = document.createElement('table');
table.innerHTML = `
<thead>
<tr>
<th onclick="sortFileTable('name')">ファイル名</th>
<th onclick="sortFileTable('path')">パス</th>
<th>操作</th>
</tr>
</thead>
<tbody>
${files.map((file) => `
<tr>
<td>${file.name}</td>
<td>${file.path}</td>
<td><button onclick="previewFile(${allFiles.indexOf(file)})">プレビュー</button></td>
</tr>
`).join('')}
</tbody>
`;
viewDiv.appendChild(table);
};
window.sortFileTable = (key) => {
if (fileSort.key === key) {
fileSort.ascending = !fileSort.ascending;
} else {
fileSort = { key, ascending: true };
}
files.sort((a, b) => {
const result = a[key].localeCompare(b[key]);
return fileSort.ascending ? result : -result;
});
render();
};
files.sort((a, b) => a.name.localeCompare(b.name));
render();
};
async function previewFile(index) {
const file = await allFiles[index].handle.getFile();
const content = await file.text();
const html = `
<html><head><meta charset="UTF-8"><title>${file.name}</title></head>
<body><pre style="white-space:pre-wrap;">${escapeHtml(content)}</pre></body>
</html>
`;
const newTab = window.open("", "_blank");
if (newTab) {
newTab.document.open();
newTab.document.write(html);
newTab.document.close();
} else {
alert("新しいタブを開けませんでした。");
}
}
function escapeHtml(str) {
return str.replace(/[&<>'"]/g, c =>
({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[c]
);
}
</script>
</body>
</html>
📌 補足・注意点
- このツールは File System Access API を使用しています
- Chrome, Edge, Opera など Chromium系のブラウザのみ対応(Safari, Firefoxは非対応)
- ファイルは読み取り専用であり、ローカル環境に変更は加えません
- このサイト を開くか、ローカルに落としてから HTTPS または localhost が必要です。
📝 まとめ
フォルダの中身を「拡張子単位で分類・表示・確認できる」というのは、
エクスプローラーや検索ツールでは意外と実現しづらいニーズの一つです。
ちょっとした確認や作業効率化のために、ぜひ活用してみてください!
ご意見・改善アイデアなども歓迎です 🙏
追記
この記事は ChatGPT で添削しています。
生成 AI による添削が苦手な方は申し訳ありません。