1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

📁 拡張子別にファイルを一覧表示できるWebツールを作成しました

Last updated at Posted at 2025-07-29

フォルダ内に大量のファイルがあると、拡張子ごとに整理された一覧が欲しくなることがあります。

  • どんな拡張子が存在するか、拡張子の一覧が欲しい
  • 拡張子の内訳が知りたい
  • .txt だけ抜き出して一覧にしたい
  • .json.log などの数をざっくり把握したい
  • 中身を確認したいけど、いちいちフォルダをたどるのが面倒
  • フォルダ構成を csv にしたい

そんなときのために、拡張子単位で分類・プレビューできるシンプルなWebツールを作成しました。

👉 サイトはこちら


🧩 ツールの概要

File System Access API を使って、ローカルのフォルダをブラウザから読み込み、拡張子ごとに分類して表示します。

  • ✅ ブラウザのみで動作(Chrome系対応)
  • ✅ フォルダ以下のすべてのファイルを再帰的に取得
  • ✅ 拡張子ごとの件数をカウント
  • ✅ 「この拡張子を開く」ボタンで詳細表示
  • ✅ プレビュー機能(別タブで表示)付き
  • ✅ フォルダ構成を csv にする機能付き

特に拡張子の内訳が知りたい場合にとても便利です。


✅ 想定されるユースケース

  • サブフォルダが大量にあるプロジェクトの整理
  • バックアップフォルダの内容確認
  • どんな拡張子が存在するか、拡張子の一覧を確認
  • チームへの納品前チェック(ファイル構成の説明など)
  • フォルダ構成の csv を作成

🖱 操作方法

  1. 「📁 フォルダを選択」ボタンをクリック
  2. 拡張子ごとの一覧が表示されます(件数つき)
  3. 各行の「この拡張子を開く」ボタンを押すと、対象のファイルだけの一覧に切り替わります
  4. 「プレビュー」ボタンで、内容を新しいタブに表示できます
  5. 「← 戻る」で拡張子一覧に戻れます

💡 ファイルの中身は読み取り専用です。保存や変更処理は行いません。


🧪 デモ(コード全文)

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 =>
      ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' })[c]
    );
  }
</script>

</body>
</html>

📌 補足・注意点

  • このツールは File System Access API を使用しています
  • Chrome, Edge, Opera など Chromium系のブラウザのみ対応(Safari, Firefoxは非対応)
  • ファイルは読み取り専用であり、ローカル環境に変更は加えません
  • このサイト を開くか、ローカルに落としてから HTTPS または localhost が必要です。

📝 まとめ

フォルダの中身を「拡張子単位で分類・表示・確認できる」というのは、
エクスプローラーや検索ツールでは意外と実現しづらいニーズの一つです。

ちょっとした確認や作業効率化のために、ぜひ活用してみてください!

ご意見・改善アイデアなども歓迎です 🙏


追記

この記事は ChatGPT で添削しています。
生成 AI による添削が苦手な方は申し訳ありません。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?