Excelからのコピペをテキストエリアで受け取ってテーブルに取り込む方法

やりたいこと:Excelの範囲コピーをそのまま貼り付けて取り込む

業務アプリで「Excelで作った一覧を、この画面にそのまま貼り付けて登録したい」という要望はよくあります。CSVファイルをアップロードさせる方法もありますが、ユーザーからするとExcelでセル範囲を選んでコピーし、テキストエリアに貼り付けるだけのほうが手数が少なく喜ばれます。

これが実現できるのは、Excelでセル範囲をコピーすると、クリップボードにタブ区切り+改行のテキスト(TSV)が入るためです。つまり、行は改行で、列はタブ文字 \t で区切られた文字列がそのまま貼り付けられます。この仕様を使えば、特別なライブラリなしのバニラJSで取り込み機能が作れます。

ただし、素直に split('\n')split('\t') で分割すると、セル内改行を含むデータで確実に壊れます。この記事では、まず壊れる例を失敗デモで見せてから、正しく処理するパーサーを修正デモとして提供します。

まず失敗を見る:split('\n') だけで書くと壊れる

いちばん素朴な実装は、貼り付けられたテキストを改行で行に分け、各行をタブで列に分けるものです。多くの場合はこれで動いてしまうので、そのまま本番に乗ってしまいがちです。

// 素朴な実装(セル内改行があると壊れる)
function parseNaive(text) {
  return text
    .replace(/\r\n/g, '\n')      // 改行コードをそろえる
    .split('\n')                 // 改行で行に分割
    .filter(line => line !== '') // 末尾の空行を落とす
    .map(line => line.split('\t')); // タブで列に分割
}

下のデモで試せます。まず「セル内改行なしのサンプル」を入れて取り込むと、期待どおり2行のテーブルになります。次に「セル内改行ありのサンプル」を入れてから取り込んでください。住所のセルに改行が入っているだけで、行数も列数もずれて崩れます。

失敗デモ:split('\n') だけのパーサー

Excelでセル内改行(セル内で Alt+Enter した値)を含む範囲をコピーすると、その改行もそのまま貼り付けられます。素朴なパーサーはこれを「次の行」と誤認します。

まだ取り込んでいません。

セル内改行ありのサンプルでは、本来「氏名/住所/メモ」の3列2行のはずが、住所セルの改行で行が余計に増え、列数も合わなくなります。CSVでも同じ問題は起きますが、Excelからの直接コピペはユーザーが気軽にセル内改行を使っているぶん、この崩れに遭遇しやすいのが業務アプリの現実です。

原因:セル内改行はダブルクォートで囲まれて貼り付けられる

崩れる理由は、Excelがクリップボードに書き出すTSVのルールにあります。セルの中身に改行やタブが含まれるとき、Excelはそのセルをダブルクォート " で囲んで出力します。CSVでおなじみの、あのクォートのルールがTSVでも同じように適用されるということです。

セル内に改行やタブ、ダブルクォート自身が含まれる場合、そのセルは全体を " で囲まれる。セル内のダブルクォートは "" と2つ重ねてエスケープされる。だから改行の意味は「クォートの外にあるか、中にあるか」で変わる。

たとえば住所セルに「東京都千代田区(改行)1-2-3」という2行の値が入っていると、貼り付けテキストの該当箇所はこうなります(見やすくするため [TAB] と改行を明示します)。

氏名[TAB]住所[TAB]メモ
山田太郎[TAB]"東京都千代田区
1-2-3"[TAB]VIP顧客

このとき、住所セルを囲むクォートの中にある改行はセル内改行で、行の区切りではありません。split('\n') はクォートの内か外かを区別しないので、この改行を行の区切りと誤認して分割してしまいます。これが崩れの正体です。正しく取り込むには、CSVパーサーと同じく1文字ずつ状態を見ながら(クォートの中にいるかどうかを追いながら)分割する必要があります。

対策:クォートの状態を見ながら1文字ずつパースする

タブ区切り版の状態機械パーサーです。文字を先頭からなめて、「いまクォートの中か外か」を inQuotes で管理します。クォートの外にあるタブは列区切り、外にある改行は行区切り。クォートの中にあるタブ・改行はセルの中身としてそのまま積みます。"" はクォート1つとして復元します。

// Excel/TSVのクォートを正しく解釈するパーサー
function parseExcelPaste(text) {
  const rows = [];
  let row = [];
  let cell = '';
  let inQuotes = false;
  const s = text;

  for (let i = 0; i < s.length; i++) {
    const ch = s[i];

    if (inQuotes) {
      if (ch === '"') {
        if (s[i + 1] === '"') { cell += '"'; i++; } // "" → " に復元
        else { inQuotes = false; }                  // 閉じクォート
      } else {
        cell += ch; // クォート内の改行・タブはセルの中身
      }
      continue;
    }

    if (ch === '"') {
      inQuotes = true;              // セルの開始クォート
    } else if (ch === '\t') {
      row.push(cell); cell = '';    // 列区切り
    } else if (ch === '\n') {
      row.push(cell); cell = '';    // 行区切り
      rows.push(row); row = [];
    } else if (ch === '\r') {
      // \r\n の \r は無視(\n 側で行を確定)
    } else {
      cell += ch;
    }
  }

  // 最後のセル・行を取りこぼさない
  if (cell !== '' || row.length > 0) {
    row.push(cell);
    rows.push(row);
  }
  return rows;
}

下の修正デモは、この parseExcelPaste で取り込んでいます。失敗デモで崩れた「セル内改行ありのサンプル」を入れて取り込むと、住所セルの改行がセルの中身として保持され、正しく3列2行になります。セル内タブを含むサンプルも用意したので、あわせて確認してください。

修正デモ:クォート対応パーサー parseExcelPaste

Excelから実際にコピーした範囲を貼り付けても動きます。手元にExcelがなくても、下のサンプルボタンで貼り付け内容を再現できます。

まだ取り込んでいません。

実務では、テキストエリアへの手動貼り付けだけでなく、テキストエリアに paste イベントを付けて event.clipboardData.getData('text') から直接取り込むこともできます。取得できる文字列は同じTSVなので、パーサー本体はそのまま使い回せます。1行目をヘッダーとして扱うかどうかは、取り込んだ rows[0] を見出しにするだけです。

エッジケース一覧:この場合はどうなるか

Excelコピペ取り込みで実際に問い合わせになりやすいパターンを、素朴な split 版と parseExcelPaste 版で対比します。ここが取り込み品質の差になる部分です。

入力パターン Excelが出力する形 split版の結果 parseExcelPaste版の結果
セル内改行(Alt+Enter) そのセルを " で囲み、中に生の改行を含む 改行を行区切りと誤認し、行数・列数がずれる クォート内の改行としてセルの中身に保持。行は崩れない
セル内タブ そのセルを " で囲み、中に生のタブを含む タブを列区切りと誤認し、列が1つ増える クォート内のタブとしてセルの中身に保持。列数は正しい
セル内のダブルクォート(例:"特価"品 セルを囲み、内側の """ にエスケープ "" がそのまま残り、値が汚れる """ に復元して正しい値になる
空セル タブが連続する(a[TAB][TAB]c 空文字の列として取れる(ここは両者とも正しい) 空文字の列として取れる
結合セル(Excel上でセル結合) 結合先の左上セルにだけ値が入り、他は空セルとして出力 値のないセルが空文字になる(元の結合情報は失われる) 同じく空文字。結合はTSVに情報が残らないため両者とも復元不可
末尾の空行・行末タブ 範囲選択によっては末尾に改行やタブが付く 空行・空列が混入しやすい 末尾処理で最終行を確定。必要なら空行フィルタを追加する

注意したいのは結合セルです。Excelで見た目上は1つに結合されていても、コピーされるTSVでは左上のセルにだけ値が入り、残りは空セルになります。これはパーサーの問題ではなく、コピー時点で結合情報が落ちているためで、どんなパーサーでも復元できません。結合セルを含む表を貼り付ける運用なら、取り込み後に「空セルは直前の値で埋める」といった補完ルールを別途決めておく必要があります。

もう一つ、行ごとの列数がそろわないケースもあります。ヘッダーが3列なのにデータ行が2列しかない、といった入力です。取り込み後に「ヘッダーの列数を基準にそろえる(足りない列は空文字、多い列は切るか警告する)」という正規化を1段挟むと、後続のテーブル描画やバリデーションが安定します。

まとめ

Excelの範囲コピーはタブ区切り+改行のテキストとして貼り付けられるので、テキストエリアで受け取ればライブラリなしで取り込めます。ただしセル内改行・セル内タブ・セル内クォートはダブルクォートで囲まれて出力されるため、split('\n')split('\t') だけの実装はこれらのデータで崩れます。

正しく取り込むには、CSVと同じく「いまクォートの中か外か」を1文字ずつ追う状態機械パーサーで分割する。改行・タブがクォートの中なら中身、外なら区切り、という判定がすべて。

結合セルだけはコピー時点で情報が失われるため、パーサーでは復元できません。ここは取り込み後の補完ルールで対応する、という切り分けを覚えておけば、Excelコピペ取り込みで想定外の崩れに悩むことはほぼなくなります。

関連するUI事例

ファイルからの取り込みやデータテーブル表示を、実際に動くコードで確認できる事例です。

あわせて読みたい

Excel連携では、文字化けや日付・0落ちといった別のハマりどころもあります。あわせて読むとExcelとのデータやり取りをまとめて押さえられます。