背景
ウェブページの表をコピーして表計算ソフトに貼ると、たいてい崩れます。
私は昔からこれが苦手で、そのたびに手で直していました…。
というわけで、HTML の table を CSV にするだけのツールを作りました。
作っている途中で、結合セル(colspan / rowspan)の扱いをどうするかで手が止まりました。
その判断の話です。
用語
念のため。
colspan は「このセルを横に何マスぶん広げるか」の指定です。
rowspan は「縦に何行ぶん広げるか」の指定。
CSV は行と列がきれいな長方形に並んでいる前提の形式なので、結合セルはそのままでは表現できません。
colspan は補える
横方向は、実は素直です。
colspan="3" のセルがあったら、値を1つ入れて、残り2つは空文字を入れておけばよい。
列の位置が保たれるので、後続の列がずれません。
セルを取り出す部分は下記です。
table は DOMParser で読み込んだあとの table 要素、cellText() はセルから見えている文字だけを取り出す関数です。
out に「1行 = 文字列の配列」を詰めていきます。
function tableToRows(table){
var out = [];
var trs = table.querySelectorAll('tr');
for(var i=0;i<trs.length;i++){
var cells = trs[i].querySelectorAll('th,td');
if(!cells.length){ continue; }
var row = [];
for(var j=0;j<cells.length;j++){
var span = parseInt(cells[j].getAttribute('colspan') || '1', 10);
if(!isFinite(span) || span < 1){ span = 1; }
row.push(cellText(cells[j]));
for(var k=1;k<span;k++){ row.push(''); }
}
out.push(row);
}
return out;
}
th と td を同じループで拾っているのは、見出し行も普通の行として1行目に置きたいからです。
CSV 側にヘッダー行という概念は無いので、区別する意味がありません。
rowspan は補えない
縦方向は、同じ手が使えません。
rowspan="2" のセルは1行目にしか存在しないので、2行目を処理している時点で「ここに1マス空ける」という情報が、その行の DOM のどこにも無いのです。
補うには、前の行で見た rowspan を覚えておいて、次の行の先頭に空セルを差し込む、という状態を持つ必要があります。
持てないわけではありません。
ただ、入れ子の table や不正な HTML が混ざると、この状態管理はすぐ壊れます。
壊れたときに何が起きるかというと、静かに1列ずれた CSV が出てきます。
「エラーにならず、間違った答えが出る」のがいちばん困る。
なので展開しないことにしました。
そのかわり、結果の横に行数と最大列数を出しています。
たとえば3行の表を入れて 最大列数: 3 と出れば、少なくとも列数は揃っています。
3 のはずが 2 と出たら、どこかで縦結合が効いている合図になります。
自動で直すのをやめて、ずれていることが目で分かる形にした、という判断です。
CSV の引用も少しだけ
ついでに。
セルにカンマが入っているときだけ、二重引用符で囲んでいます。
入力: <td>A4 コピー用紙, 500枚</td><td>480</td>
出力: "A4 コピー用紙, 500枚",480
囲む条件はカンマ・二重引用符・改行のどれかを含むときだけ。
全部囲んでも仕様上は正しいのですが、囲まないほうが目で読める CSV になるので、必要なときだけにしました。
TSV を選んだときは引用符を使わず、セル内のタブと改行を半角スペースに置き換えています。
タブ区切りは引用の仕組みを持たないので、区切り文字そのものを値から消すしかないためです。
置いてあるのはこちらです。
https://hashitosystem.com/tools/htmltablecsv/
自動で補正すると、補正できなかった場所が見えなくなる。
補えない側は、補わずに数字で見せるほうが結局速いと思います。
本記事はAI補助で執筆した、個人開発の紹介記事です。