tech-book-labs
データユーティリティ · 最終検証 2026-09-05 · xlsx 0.20.3 · 初公開 2026-09-02

SheetJS で Excel / CSV を読み書きする

SheetJS(xlsx)0.20 で .xlsx / .xls / .csv / .ods をブラウザで読んで JSON にし、また書き出すまで。npm 版が 0.18.5 で止まっている理由と CDN tarball での導入、A1 キーのデータモデル(t / v / w / z)の図解、raw / cellDates / header / defval の効き方、日付の UTC ずれ、数式は計算されない罠、ExcelJS との使い分け。書き換えて試せる demo 2 つ付き。

sheetjs xlsx excel csv spreadsheet browser

検証日: 2026-09-02

使用バージョン: xlsx@0.20.3(SheetJS Community Edition、CDN tarball から導入)

対象: ユーザーがアップロードした Excel / CSV をブラウザで読んで JSON にしたい / 「npm の xlsx が古い・脆弱性警告が出る」で止まっている場面

SheetJS(パッケージ名 xlsx)で Excel / CSV をブラウザ内で読み込んで JS の配列にし、加工して書き出すまでの最小構成。「npm install xlsx」が正しくない導入経路になっている事情から、A1 キーのデータモデル、日付・数式の罠、ExcelJS との使い分けまで、書き換えて確認できる demo 2 つで押さえます。

触って試す(読む)

左の CSV を書き換えるか、手元の .xlsx をドロップすると、右に sheet_to_json の結果セルオブジェクトの中身が出る。raw を入れると "128,000" が数値にならず文字列のまま残り、cellDates を切ると日付が 46266 のような数値(Excel のシリアル値)に変わる — この 2 つのトグルが、SheetJS の「値の自動解釈」を理解する近道。ファイルはブラウザ内で処理され、どこにも送信されない。

npm の xlsx は使わない — CDN tarball で入れる

最初の関門が導入経路。npm レジストリの xlsx は 0.18.5(2022 年)で更新が止まっており、公式 docs は「レジストリのバグで古いまま。https://cdn.sheetjs.com/ が正式な配布元」と明記している。0.18.5 には Prototype Pollution の脆弱性(CVE-2023-30533、0.19.3 で修正)があり、npm audit にも出るので、tarball URL を直接指定して入れる:

npm rm xlsx
npm i https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz
# pnpm: pnpm add https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz

package.json には "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz" と URL がそのまま入り、import 名は従来どおり xlsx:

import { read, utils, writeFileXLSX } from "xlsx";
項目
検証バージョン0.20.3(cdn.sheetjs.com、2026-09 時点の最新)
npm レジストリ版0.18.5 で凍結 — 使わない
TypeScript 型本体同梱(types/index.d.ts)
ESM / CJS両方同梱(xlsx.mjs / xlsx.js)、ブラウザ用 standalone dist/xlsx.full.min.js
ファイルサイズxlsx.mjs 1.0 MB(gzip 254 KB)。ページ初期表示に載せず動的 import が現実的
ライセンスApache-2.0(Community Edition)

データモデル: ワークシートは「A1 → セルオブジェクト」の辞書

SheetJS の API は関数が多く見えるが、扱うデータ構造は 1 つだけ覚えればよい:

Excel で見えるもの A B C 1 品名 金額 日付 2 A ¥1,235 9/1/26 3 B ¥99 9/2/26 見た目は「¥1,235」— 中身は数値 1234.5 + 書式 read() SheetJS が持つもの(プレーンオブジェクト) ws = { "!ref": "A1:C3", ← 範囲。外のセルは無視される "A1": { t: "s", v: "品名" }, "B1": { t: "s", v: "金額" }, … "B2": { t: "n", 型: n=数値 s=文字 d=日付 b=真偽 e=エラー v: 1234.5, 生の値(JS の number) z: '"¥"#,##0', 書式コード(cellNF で保持) w: "¥1,235" }, 表示文字(z から生成) "C2": { t: "n", v: 46266, w: "9/1/26" }, "!cols": [{ wch: 10 }, …] … } 日付も既定では数値(シリアル値 46266 = 2026-09-01)。cellDates: true で t: "d" の Date になる
ワークシートは「A1 形式のアドレス → セルオブジェクト」の素朴な辞書。! で始まるキー(!ref / !cols)がメタ情報

ws["B2"] のように A1 形式のアドレスでセルオブジェクトを直接引けるプレーンオブジェクトで、クラスもメソッドも無い。セルオブジェクトの 4 つの鍵:

キー意味
t型(n=数値 / s=文字列 / d=日付 / b=真偽 / e=エラー / z=空スタブ)"n"
v生の値(JS の number / string / Date …)1234.5
w表示文字(書式を当てた結果。あれば)"¥1,235"
z書式コード(cellNF: true で読んだ時 / 書く時に指定)'"¥"#,##0'

範囲は ws["!ref"](例 "A1:C3")が持ち、範囲外のセルは存在しても無視される。列幅は ws["!cols"]、結合は ws["!merges"] と、! 始まりのキーがメタ情報。

読む: read()sheet_to_json()

読みと書きは、中央の workbook オブジェクトを挟んで対称になっている:

読む .xlsx / .xls / .ods CSV 文字列 / ArrayBuffer (File, fetch, Node の Buffer) read() workbook { SheetNames: [...], Sheets: { 名前: ws } } utils.sheet_to_json JS の配列 [{ 品名: "A", 金額: 1234.5 }] header: 1 なら配列の配列 同じ workbook オブジェクト。読んだものをそのまま書き出せる 書く JS の配列 json_to_sheet / aoa_to_sheet → ws(!cols / z を後付け) book_append_sheet workbook utils.book_new() シート名は 31 文字まで writeFile .xlsx / .csv / .ods writeFileXLSX(wb, name) write(wb, { type: "array" }) どの段階でも数式は「計算しない」。f に式を入れて書き出すと、Excel が開いた時に計算する
読むも書くも中央の workbook オブジェクトを経由する。入口・出口の関数名を変えるだけで xlsx / csv / ods を行き来できる

ブラウザで <input type="file"> から読む最小コードは次の 4 行。FileArrayBuffer にして read に渡し、先頭シートを JSON にする:

import { read, utils } from "xlsx";

async function parse(file: File) {
  const wb = read(await file.arrayBuffer(), { type: "array" });  // ArrayBuffer は type: "array"
  const ws = wb.Sheets[wb.SheetNames[0]];                        // 先頭シート
  return utils.sheet_to_json<Record<string, unknown>>(ws);       // 1 行目をキーにしたオブジェクト配列
}

CSV 文字列なら read(text, { type: "string" })拡張子で分岐しないread が中身を見て xlsx / xls / csv / ods を判別する(demo にどれをドロップしても同じコードで動く)。

read() の rawcellDates

read は CSV / テキスト系を読む時に 値を自動解釈する。demo の CSV で実測:

入力既定(raw: false)raw: true
"128,000"{ t: "n", v: 128000, w: "128,000" } — 数値に{ t: "s", v: "128,000" } — 文字列のまま
2026/09/01{ t: "n", v: 46266, w: "2026/09/01" } — 日付シリアル値に{ t: "s", v: "2026/09/01" }
2026/09/01 + cellDates: true{ t: "d", v: Date(2026-09-01) }(raw が優先され文字列)

郵便番号や商品コードのような「数字だが数値ではない」列がある CSV は raw: true で読んでから自分で変換する方が事故が少ない。Excel ファイルの場合は型がファイルに入っているので raw は効かず、cellDates だけが意味を持つ。

sheet_to_json() の header / defval / raw

JSON 化の形は sheet_to_json の第 2 引数で決まる。demo のチェックボックスと対応:

utils.sheet_to_json(ws);                       // [{ 商品: "ノートPC", 数量: 3, ... }]  1 行目がキー
utils.sheet_to_json(ws, { header: 1 });        // [["商品","数量",...], ["ノートPC", 3, ...]]  配列の配列
utils.sheet_to_json(ws, { defval: null });     // 空セルも null で埋める(既定はキー自体が無い)
utils.sheet_to_json(ws, { raw: false });       // v ではなく表示文字 w を返す("128,000" / "9/1/26")

押さえどころ:

  • 空セルのキーは出力されない(demo の「マウス」行に 数量 が無い)。テーブル表示や型付けで揃えたいなら defval 必須
  • 同名の列は foo, foo_1, foo_2 と自動でリネームされる。元の列名で引きたい場合は header: 1 で配列にしてから自分でマッピング
  • 型引数 sheet_to_json<T>()実行時には何も検証しない(公式 docs 明記)。外部ファイルは必ず Zod などで検証する

書く: json_to_sheet()writeFileXLSX()

JSON を書き換えると右の CSV プレビューが即時に変わり、.xlsx / .csv / .ods を同じ workbook から出せる。列幅・通貨書式・合計行(数式)のチェックを切り替えて、何がファイルに載るかを確認できる。

最小コードは json_to_sheetbook_append_sheetwriteFileXLSX の 3 手:

import { utils, writeFileXLSX } from "xlsx";

const ws = utils.json_to_sheet(rows, { cellDates: true }); // Date をそのまま日付セルに
ws["!cols"] = [{ wch: 12 }, { wch: 8 }, { wch: 12 }];       // 列幅(文字数)
ws["C2"].z = '"¥"#,##0';                                     // セル単位の数値書式

const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "売上");                     // シート名は 31 文字まで(超えると throw)
writeFileXLSX(wb, "report.xlsx", { compression: true }); // compression を付けないと ZIP 非圧縮で書かれる(下の実測)

writeFileXLSX は xlsx 専用の軽量版。csv / ods など他形式は writeFile(wb, "a.ods", { bookType: "ods" })。サーバーへ送る・自前で Blob を作る場合は write(wb, { type: "array", bookType: "xlsx" })ArrayBuffer を受け取る。

つまずいたポイント

  • 書き出した xlsx が異様に大きいcompression: true を付けていない。既定は ZIP 非圧縮(100,000 行で 20MB、実測)
  • npm i xlsx で 0.18.5 が入る / npm audit が Prototype Pollution を報告 — 前述のとおりレジストリが凍結されている。CDN tarball の 0.20.3 に入れ替える。overrides で依存の依存も差し替える
  • 日付の ISO 文字列が 9 時間ずれる — CSV を cellDates: true で読んだセルの v2026-09-01T00:00:00.000Z(UTC 解釈)だが、sheet_to_json の出力は既定 UTC: false2026-08-31T15:00:00.000Z(JST の 9/1 0:00)になる(実測)。同じ日付が場所によって別の ISO 文字列で見える。toISOString() で保存・比較しない。ローカル解釈なら getFullYear / getMonth / getDate、UTC で統一したいなら sheet_to_json(ws, { UTC: true }) + getUTC* と、どちらかに揃える。demo の UTC: true トグルで両方の値を見比べられる
  • 数式セルを書いて読み直すと値が無い — SheetJS は 数式を計算しない{ t: "n", f: "SUM(B2:B4)" } を書き出すと Excel は開いた時に計算するが、SheetJS で read し直しても vundefined(実測では t: "e" の空セルになる)。sheet_to_csv=SUM(B2:B4) という式文字列を出す。集計値を JSON で使いたいなら JS 側で計算して値も入れる
  • 空セルのキーが消えるsheet_to_json の既定。defval: ""defval: null で埋める。header: 1 の時は逆に 空行が既定で残る(blankrows: false で落とす)
  • readFile はブラウザで動かない — Node 専用(fs を使う)。ブラウザは FilearrayBuffer()read(buf, { type: "array" })
  • Workbook is empty で write が落ちるbook_append_sheet を忘れて空の workbook を書こうとした時のエラー。シート名の 31 文字超過も book_append_sheet で throw する
  • .numbers の書き出しは追加オプションが要るbookType: "numbers" はテンプレートを numbers オプションで渡す必要があり、そのままだと Must pass a numbers option で失敗する。読み込みはそのまま可
  • demo は client:only="react" で描画 — ファイル API とダウンロードはブラウザ前提のため SSR では描画しない

100,000 行で実測: サイズと速度(SheetJS vs ExcelJS)

5 列(id / 書名 / 分野 / 価格 / 発売日)× 100,000 行を同じデータで書き出し、読み戻した(Node 22.17、xlsx@0.20.3 / exceljs@4.4.0、3 回の中央値、2026-09-05)。

要点は 3 つ:

  • compression: true は必ず付けるwrite / writeFile / writeFileXLSX の既定は非圧縮で、100,000 行なら 20MB になる(本記事の demo も付け忘れていたので直した)
  • 付けても SheetJS 内蔵の deflate は圧縮率が低い(62%)。配布サイズが問題なら、書き出しは ExcelJS に任せる(同じ XML を zip -9 で圧縮し直すと 2.6MB になるので、ZIP を作り直す手もある)
  • 速度は SheetJSサイズは ExcelJS。読み込みは差が無いので、入口を SheetJS にする判断は変わらない

ExcelJS との使い分け

同じ「ブラウザで xlsx」でも得意分野が違う。ExcelJS の記事と合わせて:

SheetJS CEExcelJS
得意読み込み(xlsx / xls / csv / ods / numbers など多形式を同じ API で)書き出し(装飾・数式・画像入りの帳票)
セルの装飾(色・太字・罫線)✗(Pro のみ)
.xls(Excel 97-2003)読み込み✗(xlsx / csv のみ)
CSV の値解釈("128,000" → 数値)raw で制御独自パーサ(fast-csv)
データモデルプレーンオブジェクト(A1 キー)クラス(Workbook / Worksheet / Row / Cell)
導入CDN tarball(npm 版は凍結)npm i exceljs
ライセンスApache-2.0MIT

目安: ユーザーの Excel / CSV を取り込む入口は SheetJS、見た目を整えた帳票を出す出口は ExcelJS。両方入れるとバンドルが重くなるので、どちらも動的 import にする。

関連書籍

tech-book.net /books/9784297127473

プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで | tech-book.net

鈴木 僚太 · 技術評論社

TypeScriptは、JavaScriptに静的型付けの機能を加えたオープンソースのプログラミング言語です。本書では、根幹となるJavaScrip

この本が役立つ理由 — sheet_to_json<T>() の型引数は実行時に何も保証しない。外部ファイルの値を安全な型に落とし込む型ガード・narrowing の考え方をここで固めておくと、取り込み処理が堅くなる
詳細を tech-book.net で見る
tech-book.net /books/9784873118086

Python と JavaScriptではじめるデータビジュアライゼーション | tech-book.net

Kyran Dale/嶋田 健志/木下 哲也 · オライリー・ジャパン

Webからデータを取得して整理・分析し可視化するまで一連の流れがわかる! 本書はデータ可視化データフローを最初から終わりまでカバーし

この本が役立つ理由 — Excel / CSV から取り込んだデータを整形して可視化するまでの一連の流れを扱う本。本記事の read → JSON の先、「取り込んだ後に何をするか」の参照先に
詳細を tech-book.net で見る

関連記事 / 関連 Topic