HTMLテーブルの最初の行はヘッダー行。
ただし、そうでない場合もあります。
Wikipediaテーブルでは実際のヘッダーの前に全列にまたがるタイトル行があることが多い。スポーツ統計サイトでは「出場時間」が「試合数」「先発」「出場分」といった複数のサブ列にまたがるグループヘッダーがある。財務テーブルにはヘッダーのように見えるが実際にはそうではない単位行(「百万ドル」)がある。
行0が常にヘッダーだと仮定すると、実世界のテーブルのかなりの割合でエクスポートが壊れます。
プログラムで実際のヘッダー行を検出する方法を解説します。
問題:3種類の「最初の行」
よくあるパターンを見てみましょう:
パターン1:タイトル行
<table>
<tr>
<th colspan="4">世界の国別人口</th> <!-- タイトルであり、ヘッダーではない -->
</tr>
<tr>
<th>順位</th>
<th>国名</th>
<th>人口</th>
<th>世界比率</th>
</tr>
<tr>
<td>1</td>
<td>インド</td>
<td>1,428,627,663</td>
<td>17.85%</td>
</tr>
</table>
行0はタイトル。行1がヘッダー。行2以降がデータ。
パターン2:グループヘッダー(2階層)
<table>
<tr>
<th></th>
<th></th>
<th colspan="3">出場時間</th>
<th colspan="2">パフォーマンス</th>
</tr>
<tr>
<th>選手</th>
<th>国籍</th>
<th>試合</th>
<th>先発</th>
<th>出場分</th>
<th>得点</th>
<th>アシスト</th>
</tr>
<tr>
<td>田中太郎</td>
<td>JPN</td>
<td>34</td>
<td>30</td>
<td>2700</td>
<td>12</td>
<td>8</td>
</tr>
</table>
行0はグループヘッダー。行1が実際の列ヘッダー。行2以降がデータ。
パターン3:Wikipediaのナビゲーション接頭辞
<tr>
<th colspan="3">v t e 世界遺産</th>
</tr>
「v t e」(表示/トーク/編集)リンクはWikipediaのテンプレートナビゲーション。除去する必要があり、その行自体もヘッダーではなくタイトルかもしれません。
ヒューリスティック1:タイトル行の検出
タイトル行の典型的な特徴:
- セルが1つ(または非常に少ない)
- ほとんど/すべての列にまたがる大きなcolspan
- 列名ではなくタイトルに見えるテキストコンテンツ
function isTitleRow(row, totalColumns) {
if (!row || row.length === 0) return false;
// 空でないセルを数える
const nonEmptyCells = row.filter(cell => cell && cell.trim()).length;
// タイトル行は通常1-2個の非空セル
if (nonEmptyCells > 2) return false;
// 最初のセルがほとんどの列にまたがっているか確認(colspanを示す)
// 正規化されたマトリクスでは、これは繰り返される値として表示される
const firstValue = row[0];
const repeatedCount = row.filter(cell => cell === firstValue).length;
// 最初の値が列の50%以上で繰り返されている場合、colspanタイトルの可能性が高い
if (repeatedCount > totalColumns * 0.5) {
return true;
}
return false;
}
ヒューリスティック2:「ヘッダーらしい」行の特徴
ヘッダー行にはデータ行と区別する特徴があります:
function rowLooksLikeHeaders(row) {
if (!row || row.length === 0) return false;
let numericCells = 0;
let textCells = 0;
let emptyCells = 0;
for (const cell of row) {
const value = (cell || "").trim();
if (!value) {
emptyCells++;
} else if (/^-?\d+([.,]\d+)?%?$/.test(value)) {
// 純粋な数値またはパーセント
numericCells++;
} else {
textCells++;
}
}
const totalNonEmpty = numericCells + textCells;
if (totalNonEmpty === 0) return false;
// ヘッダーは主にテキストで、数値ではない
// 非空セルの70%以上が数値ならデータの可能性が高い
if (numericCells / totalNonEmpty > 0.7) {
return false;
}
// ヘッダーはほとんど空であるべきではない
if (emptyCells / row.length > 0.7) {
return false;
}
return true;
}
ヒューリスティック3:「データらしい」行の特徴
逆のチェックで、正しい境界を見つけたことを確認します:
function rowLooksLikeData(row) {
if (!row || row.length === 0) return false;
let numericCells = 0;
let dateCells = 0;
let totalNonEmpty = 0;
for (const cell of row) {
const value = (cell || "").trim();
if (!value) continue;
totalNonEmpty++;
// 数値パターンのチェック
if (/^-?\d+([.,]\d+)?%?$/.test(value)) {
numericCells++;
}
// 日付パターンのチェック
if (/^\d{1,4}[-/\.]\d{1,2}[-/\.]\d{1,4}$/.test(value)) {
dateCells++;
}
}
if (totalNonEmpty === 0) return false;
// データ行は通常、数値または日付コンテンツを含む
const dataLikeCells = numericCells + dateCells;
return dataLikeCells / totalNonEmpty > 0.3;
}
ヒューリスティック4:グループ列ヘッダーの検出
FBREF形式のテーブルには、グループヘッダー行の後にサブヘッダー行が続きます。グループ行の特徴:
- 先頭に空セル(グループのない列)
- colspanの展開による繰り返し値
- 複数のユニークな非空値(タイトルのように1つだけではない)
function detectGroupHeaderRow(row, nextRow) {
if (!row || !nextRow || row.length < 4) return false;
// グループヘッダー行は先頭に空セルがなければならない
const firstCellEmpty = !(row[0] || "").trim();
if (!firstCellEmpty) return false;
// ユニークな非空値を数える
const uniqueValues = new Set(
row.filter(v => v && v.trim()).map(v => v.trim().toLowerCase())
);
// タイトル行はユニーク値が1つだけ
// グループヘッダー行は複数のユニーク値が必要
if (uniqueValues.size <= 1) return false;
// 連続する繰り返し値を数える(colspan展開を示す)
let consecutiveRepeats = 0;
for (let i = 1; i < row.length; i++) {
const curr = (row[i] || "").trim();
const prev = (row[i - 1] || "").trim();
if (curr === prev) consecutiveRepeats++;
}
const repeatRatio = consecutiveRepeats / (row.length - 1);
// 高い繰り返し率(>30%)はcolspan展開を示唆
// 次の行はより多くのユニーク値を持つべき(実際のサブヘッダー)
const nextUniqueValues = new Set(
nextRow.filter(v => v && v.trim()).map(v => v.trim().toLowerCase())
);
return repeatRatio > 0.3 && nextUniqueValues.size > uniqueValues.size;
}
ヒューリスティック5:Wikipediaナビゲーション接頭辞のクリーニング
Wikipediaのテンプレートはコンテンツの前に「v t e」(テンプレートの表示/トーク/編集へのリンク)を付加することがあります:
function cleanWikipediaNavPrefix(text) {
if (!text) return text;
// パターン1:"v t e " が先頭に(スペース区切り)
// パターン2:"v | t | e "(パイプ区切り)
// パターン3:"[v] [t] [e] "(ブラケット区切り)
return text
.replace(/^\s*v\s+t\s+e\s+/i, "")
.replace(/^\s*v\s*\|\s*t\s*\|\s*e\s+/i, "")
.replace(/^\s*\[v\]\s*\[t\]\s*\[e\]\s+/i, "")
.trim();
}
統合:検出アルゴリズム
function detectHeaderRowIndex(matrix) {
if (!matrix || matrix.length < 2) return 0;
const totalColumns = matrix[0]?.length || 0;
for (let i = 0; i < Math.min(matrix.length - 1, 5); i++) {
const currentRow = matrix[i];
const nextRow = matrix[i + 1];
// タイトル行をスキップ
if (isTitleRow(currentRow, totalColumns)) {
continue;
}
// グループヘッダー(2階層)をチェック
if (detectGroupHeaderRow(currentRow, nextRow)) {
// サブヘッダー行(i+1)が実際のヘッダー
return i + 1;
}
// この行がヘッダーに見え、次の行がデータに見えるかチェック
if (rowLooksLikeHeaders(currentRow) && rowLooksLikeData(nextRow)) {
return i;
}
}
// フォールバック:行0をヘッダーと仮定
return 0;
}
実世界でのテスト
これらのヒューリスティクスは以下のテストを経て開発されました:
- Wikipediaの国別/人口テーブル(タイトル行 + 「v t e」接頭辞)
- FBREFの選手統計(グループヘッダー)
- 単位行を持つ財務テーブル
- 複数ヘッダーレベルの政府データテーブル
完璧なヒューリスティクスはありません。目標はよくあるパターンを正しく処理し、珍しいテーブルでもグレースフルに失敗することです。
検出が失敗する場合
一般的なパターンに当てはまらないテーブルには、手動オーバーライドを提供します:
function extractTable(matrix, options = {}) {
const headerRowIndex = options.headerRowIndex ?? detectHeaderRowIndex(matrix);
const headerRow = matrix[headerRowIndex];
const dataRows = matrix.slice(headerRowIndex + 1);
return { headerRow, dataRows };
}
データを知っているユーザーはヘッダー行を明示的に指定できます。
まとめ
| パターン | 検出方法 |
|---|---|
| タイトル行 | 大きなcolspanを持つ単一セル |
| 標準ヘッダー | 主にテキストの行の後に数値の行 |
| グループヘッダー | 先頭の空セル + 繰り返し値 + 次の行により多くのユニーク値 |
| Wikipediaナビ | "v t e" 接頭辞パターン |
重要なインサイト:ヘッダーとデータには異なる特性があります。ヘッダーは説明的なラベルを持つテキスト中心。データは実際の値を持つ数値中心。その境界は通常検出可能です。
Wikipediaの特有のテーブル課題については、WikipediaテーブルをExcelにエクスポートする方法のガイドをご覧ください。
コードを書かずにヘッダー自動検出が必要ですか?gauchogrid.com/ja/html-table-exporterで詳細を確認するか、Chrome ウェブストアで無料でお試しください。
Top comments (0)