1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Excel差分をブラウザで比較する — SheetJSでシートを読み、CSV差分エンジンを再利用する

1
Last updated at Posted at 2026-08-23

3行まとめ

  • 2つのExcelファイルを行・列単位で比較するブラウザ完結ツールを作った。肝はExcelを比較用の中間表現(CSVテキスト)に正規化してから、既存のCSV差分エンジンにそのまま載せる設計
  • SheetJSで XLSX.readsheet_to_csv すれば、あとはCSV差分ツールと同じ computeCsvDiff を再利用できる。差分ロジックは1つで済む
  • 複数シートはBefore/Afterそれぞれプルダウンで選択。比較対象はセルの値のみで、色・罫線・フォントなどの書式は落ちる

Excelの新旧を比べたい——月次集計表の更新前後、複数人で編集した見積書、システムがエクスポートした台帳のバージョン差分。だが .xlsx は実体がZIPで固めたXMLの束なので、git diff にかけてもバイナリ扱いで中身は読めない。Excelの「変更履歴」機能は事前にブックへ設定しておかないと使えず、後から2ファイルを突き合わせるのには向かない。結局、目視でセルを追う羽目になる。

ぱんだツールズのExcel差分比較は、2つのExcelファイルをシート単位で読み込み、行の追加・削除・変更をセル単位でハイライトするツール。処理はすべてブラウザ内で完結し、Excelの内容はサーバーに送信されない。人事・給与・経理のような機密データも、オンラインの比較サービスにアップロードせず安全に比べられる。

この記事では、Excelの差分を「ゼロから実装しない」ための設計——SheetJSでExcelをCSVに正規化し、CSV用に作った差分エンジンをまるごと再利用する構成を、実装ベースで解説する。

設計の起点:差分エンジンを2つ持たない

先に結論を書くと、このツールはExcel専用の差分アルゴリズムを一切書いていない。すでに作ってあったCSV差分ツールの差分コア computeCsvDiff を、そのまま流用している。

import { parseCsv } from '@/lib/csv/editCsvColumns'
import { computeCsvDiff, buildCsvDiffReport } from '@/lib/csv/computeCsvDiff'

Excelの差分もCSVの差分も、本質は「行×列の表の突き合わせ」で同じ。だったら差分ロジックを2本持つ理由はない。違うのは入り口——Excelという形式を、差分コアが食える形(CSVテキスト)に変換するレイヤだけを足せばいい。

Excelファイル ──[SheetJS]──> CSVテキスト ──[computeCsvDiff]──> 差分結果
CSVファイル   ────────────> CSVテキスト ──[computeCsvDiff]──> 差分結果

CSV側とExcel側で「CSVテキストに正規化する」入り口だけが違い、そこから先の差分計算・レポート生成・ハイライト表示は完全に共通。差分アルゴリズム自体(キー列でのバッグマッチング、列構成が違う表の列ユニオン、セル単位の変更検出)はCSV差分の記事で詳しく書いたので、この記事はExcel→CSVの変換レイヤに集中する。

SheetJSでExcelを読み、シートをCSVに落とす

変換レイヤの主役はSheetJS(xlsx)。.xlsx も旧形式の .xls も同じAPIで読める。

ファイルを選んだら ArrayBuffer として読み込み、XLSX.read でワークブックにする。

async function handleFileLoad(side: 'left' | 'right', file: File) {
  const buffer = await file.arrayBuffer()
  const workbook = XLSX.read(buffer, { type: 'array' })
  const sheetNames = workbook.SheetNames
  const selectedSheet = sheetNames[0] ?? ''
  const worksheet = selectedSheet ? workbook.Sheets[selectedSheet] : undefined
  const csvText = worksheet ? XLSX.utils.sheet_to_csv(worksheet) : ''
  // ...state更新
}

ポイントは XLSX.utils.sheet_to_csv(worksheet)。ワークシートをCSVテキストに書き出すユーティリティで、これを通すと以降は「ただのCSV」として扱える。{ type: 'array' } は入力が ArrayBuffer(や Uint8Array)のバイト列であることをSheetJSに伝えるオプションで、file.arrayBuffer() の戻り値をそのまま渡せる。ブラウザでファイルを読むときはこれが基本になる。

sheet_to_csv が書き出すのはセルの表示テキスト。数式セルはファイルに保存済みの計算結果が入り、日付や通貨などはExcel上の表示形式に沿った文字列になる。逆に言えば、セルの色・罫線・フォント・結合といった書式情報はこの時点で全部そぎ落とされる。「見た目の差分」ではなく「値の差分」を取るツール、という性格はここで決まる。

複数シートは左右それぞれで選ばせる

Excelは1ファイルに複数シートを持てる。「Sheet1だけ比べたい」「集計タブ同士を比べたい」というケースがあるので、シートが2枚以上あるときだけプルダウンを出し、Before/Afterで独立に選べるようにした。

function handleSheetChange(side: 'left' | 'right', sheetName: string) {
  const current = side === 'left' ? left : right
  if (!current.workbook) return
  const worksheet = current.workbook.Sheets[sheetName]
  const csvText = worksheet ? XLSX.utils.sheet_to_csv(worksheet) : ''
  // 選択シートだけCSVを差し替え
}

XLSX.read の結果(workbook)は状態として保持しておき、シート切り替えのたびに sheet_to_csv をやり直すだけ。ファイルを読み直す必要はない。左右のシート選択が独立しているので、「AファイルのSheet1」と「BファイルのSheet2」のように、名前が違うシート同士も突き合わせられる。

キー列の候補は「両シート共通のヘッダー」だけ

行の並び順が違っても正しく比較するには、会員IDや商品コードのようなキー列を指定する。ここで気をつけたのが、キー列のドロップダウンに出す候補を両方のCSVに共通して存在する列だけに絞ること。片方にしかない列をキーに選んでも突き合わせは成立しないからだ。

const leftHeaders = useMemo(
  () => (left.csvText.trim() ? parseCsv(left.csvText).headers : []),
  [left.csvText],
)
const rightHeaders = useMemo(
  () => (right.csvText.trim() ? parseCsv(right.csvText).headers : []),
  [right.csvText],
)
const commonHeaders = useMemo(
  () => leftHeaders.filter((h) => rightHeaders.includes(h)),
  [leftHeaders, rightHeaders],
)

ヘッダーを取り出すのに使っている parseCsv は、差分コアが内部で使うのと同じCSVパーサ。sheet_to_csv の出力をそのパーサで読み直してヘッダー行だけ取っている。Excelを一度CSVに落としたおかげで、UIのヘッダー解析までCSV用の道具で完結するのが、この正規化設計の副次的な旨味。

さらに、ファイルを差し替えて共通ヘッダーが変わったとき、いま選んでいるキー列がもう存在しないなら選択をリセットする。

useEffect(() => {
  if (keyColumn !== '' && !commonHeaders.includes(keyColumn)) setKeyColumn('')
}, [commonHeaders, keyColumn])

これをやらないと「存在しないキー列」が選ばれたまま比較ボタンを押せてしまい、差分コア側が キー列「◯◯」が左側のCSVに見つかりません と例外を投げる。UI側で候補を絞りつつ、消えた選択を自動で戻すことで、その例外を未然に潰している。

あとは共通の差分コアに丸投げする

ここまで来れば、左右のCSVテキストとキー列を差分コアに渡すだけ。

function handleCompare() {
  try {
    const diffResult = computeCsvDiff(
      left.csvText,
      right.csvText,
      keyColumn === '' ? null : keyColumn,
    )
    setResult(diffResult)
    setReportBlob(
      new Blob([buildCsvDiffReport(diffResult)], { type: 'text/csv;charset=utf-8;' }),
    )
  } catch (err) {
    setDiffError(err instanceof Error ? err.message : 'Excelの比較に失敗しました')
  }
}

computeCsvDiff の中身——キー列を指定したときの Map+FIFOキューによるバッグマッチング(同じキーが複数行あっても出現順に対応づける)、BeforeとAfterで列構成が違うときの列ユニオン、「列が無い(null)」と「値が空('')」の区別、セル単位の変更検出——は、CSV差分とまったく同じものが動く。Excel側で足したのは変換レイヤだけなので、CSVツールを改良すればExcelツールも同時に賢くなる。

差分結果は buildCsvDiffReport で「状態」列(変更なし/追加/削除/変更前/変更後)付きのCSVレポートに書き出せる。Excelで比較して、結果はCSVで受け取り、Excelに取り込み直して「状態」列でフィルタする、といった往復もできる。

割り切ったこと・注意点

この設計は「Excelを値の表として正規化する」ことで差分エンジンを共有できた反面、正規化で捨てたものがある。実務で使うときの前提として明示しておく。

  • 書式は比較しないsheet_to_csv を通した時点で色・罫線・フォント・セル結合は消える。「セルの塗りが変わった」は検出できない。あくまで値の比較
  • 数式は結果値で比較=SUM(...) のような数式そのものではなく、ファイルに保存された計算結果を比べる。ロジックの差分ではなく、出てきた数値の差分になる
  • 日付・数値は表示形式に依存sheet_to_csv はセルの表示テキストを出すため、同じ値でもBefore/Afterで表示形式(2026/01/012026-01-01 など)が違えば「変更」に見えることがある

書式まで含めた完全な差分がほしい場面には向かないが、「値がどこで変わったか」を機密データを外に出さずに素早く洗い出す、という用途にはこの割り切りがちょうどいい。SheetJSでExcelを扱うときのハマりどころ(型変換や日付シリアル値など)はSheetJSでExcel↔CSV変換する時の落とし穴にまとめてある。

まとめ

  • Excel差分は専用アルゴリズムを書かず、SheetJSでCSVテキストに正規化してから既存のCSV差分エンジンに載せる構成にした。差分ロジックは1本で、CSVツールとExcelツールが共有する
  • 変換レイヤは XLSX.read(buffer, { type: 'array' })XLSX.utils.sheet_to_csv(worksheet) の2ステップ。.xlsx.xls も同じAPIで読める
  • 複数シートは左右独立のプルダウンで選択し、ワークブックを保持したまま sheet_to_csv をやり直す。キー列候補は両シート共通のヘッダーに絞り、消えた選択は自動リセット
  • 正規化の代償として書式は比較対象外。数式は結果値、日付・数値は表示形式ベースで比べる、という割り切りがある

Excelを丸ごとサービスにアップロードせず、ブラウザの中だけで新旧を突き合わせたいときにどうぞ。

ぱんだツールズ では他にも CSV差分比較・CSV文字コード変換・銀行明細CSV変換・PDF処理など、日本語の実務ファイルに強いブラウザ完結ツールを多数公開中。全部無料・登録不要・ファイルはサーバーに送られない。
https://sakutto-panda.com


この記事は Zenn にも同じ内容を投稿しています。

1
0
3

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
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?