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 つ付き。
検証日: 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 つだけ覚えればよい:
! で始まるキー(!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 オブジェクトを挟んで対称になっている:
ブラウザで <input type="file"> から読む最小コードは次の 4 行。File を ArrayBuffer にして 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() の raw と cellDates
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_sheet → book_append_sheet → writeFileXLSX の 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で読んだセルのvは2026-09-01T00:00:00.000Z(UTC 解釈)だが、sheet_to_jsonの出力は既定UTC: falseで2026-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 し直してもvはundefined(実測ではt: "e"の空セルになる)。sheet_to_csvは=SUM(B2:B4)という式文字列を出す。集計値を JSON で使いたいなら JS 側で計算して値も入れる - 空セルのキーが消える —
sheet_to_jsonの既定。defval: ""やdefval: nullで埋める。header: 1の時は逆に 空行が既定で残る(blankrows: falseで落とす) readFileはブラウザで動かない — Node 専用(fsを使う)。ブラウザはFile→arrayBuffer()→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 CE | ExcelJS | |
|---|---|---|
| 得意 | 読み込み(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.0 | MIT |
目安: ユーザーの Excel / CSV を取り込む入口は SheetJS、見た目を整えた帳票を出す出口は ExcelJS。両方入れるとバンドルが重くなるので、どちらも動的 import にする。
関連書籍
プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで | tech-book.net
TypeScriptは、JavaScriptに静的型付けの機能を加えたオープンソースのプログラミング言語です。本書では、根幹となるJavaScrip
Python と JavaScriptではじめるデータビジュアライゼーション | tech-book.net
Webからデータを取得して整理・分析し可視化するまで一連の流れがわかる! 本書はデータ可視化データフローを最初から終わりまでカバーし
関連記事 / 関連 Topic
- ExcelJS で .xlsx をブラウザで生成・ダウンロードする — 装飾付き帳票の書き出し側
- PapaParse で CSV をストリーミング処理する — 巨大 CSV を行単位で流す場合(SheetJS はメモリ上に全展開)
- DuckDB-Wasm でブラウザ SQL — 読み込んだ表を SQL で集計する構成
- Zod v4 のスキーマパターン —
sheet_to_jsonの出力を検証する - 体系的に学ぶなら: JavaScript の本 / TypeScript の本(tech-book.net)