HTML 테이블의 첫 번째 행이 헤더 행입니다.
그런데 그렇지 않을 때가 있습니다.
Wikipedia 테이블에는 실제 헤더 앞에 모든 열에 걸친 제목 행이 있는 경우가 많습니다. 스포츠 통계 사이트에는 "출전 시간"이 "경기", "선발", "분" 같은 여러 하위 열에 걸치는 그룹 헤더가 있습니다. 재무 테이블에는 헤더처럼 보이지만 헤더가 아닌 단위 행("백만 USD 단위")이 있습니다.
0행이 항상 헤더라고 가정하면, 상당수의 실제 테이블에서 내보내기가 깨집니다.
프로그래밍 방식으로 실제 헤더 행을 감지하는 방법을 알아보겠습니다.
문제: 세 가지 유형의 "첫 번째 행"
다음과 같은 일반적인 패턴을 생각해 보세요:
패턴 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>KOR</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: 제목 행 감지
제목 행의 일반적인 특징:
- 단일 셀 (또는 매우 적은 수의 셀)
- 대부분/전체 열에 걸치는 큰 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 확장으로 인한 반복 값
- 제목처럼 하나가 아닌 여러 개의 고유한 비어 있지 않은 값
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())
);
// 제목 행은 정확히 하나의 고유 값을 가짐
// 그룹 헤더 행은 여러 개의 고유 값이 있어야 함
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/ko/html-table-exporter에서 자세히 알아보거나 Chrome 웹 스토어에서 무료로 사용해 보세요.
Top comments (0)