HTML ํ ์ด๋ธ ํ์ฑ์ ์ค์ ๋ฐ์ดํฐ๋ฅผ ๋ง๋๊ธฐ ์ ๊น์ง๋ ๊ฐ๋จํด ๋ณด์ ๋๋ค. ์ํคํผ๋์ ํ ์ด๋ธ์๋ ๋ค๋น๊ฒ์ด์ ํ์ด ์์ต๋๋ค. ๊ธ์ต ์ฌ์ดํธ๋ ๋ณต์กํ rowspan์ ์ฌ์ฉํฉ๋๋ค. ์คํฌ์ธ ํต๊ณ ์ฌ์ดํธ๋ ํค๋๋ฅผ ๋ ๋จ๊ณ๋ก ์ค์ฒฉํฉ๋๋ค.
์์ฒ ๊ฐ์ ๋ค๋ฅธ ์ฌ์ดํธ์์ ์ฌ์ฉ๋๋ ํ ์ด๋ธ ์ถ์ถ ๋๊ตฌ HTML Table Exporter๋ฅผ ๋ง๋ ํ, ๋๋ถ๋ถ์ ํ์๋ฅผ ๊นจ๋จ๋ฆฌ๋ ์ฃ์ง ์ผ์ด์ค๋ฅผ ์ ๋ฆฌํ์ต๋๋ค. ๊ฐ๊ฐ์ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
๋ฌธ์ 1: Rowspan ํ์ฅ
rowspan="3"์ธ ์
์ ํ์ฌ ํ๊ณผ ๋ค์ ๋ ํ์์ ์์ง ๊ณต๊ฐ์ ์ฐจ์งํฉ๋๋ค. row.cells๋ฅผ ๋จ์ํ๊ฒ ๋ฐ๋ณตํ๋ฉด ์ด์ด ์ด๊ธ๋ฉ๋๋ค.
๊นจ์ง ์ถ๋ ฅ:
| ๊ตญ๊ฐ | 2020 | 2021 | 2022 | <- ํค๋
| ๋ฏธ๊ตญ | 100 | 200 | 300 | <- ์์
| 150 | 250 | 350 | <- "๋ฏธ๊ตญ" ๋๋ฝ (rowspan ๊ณ์)
ํด๊ฒฐ ๋ฐฉ๋ฒ: ๊ฐ์ ๊ทธ๋ฆฌ๋์์ ์ฐจ์ง๋ ์์น๋ฅผ ์ถ์ ํฉ๋๋ค.
function expandRowspans(table) {
const rows = Array.from(table.rows);
const grid = [];
rows.forEach((rowEl, rowIndex) => {
if (!grid[rowIndex]) grid[rowIndex] = [];
let colIndex = 0;
Array.from(rowEl.cells).forEach(cell => {
// ๋ค์ ๋น์ด์๋ ์ด ์ฐพ๊ธฐ
while (grid[rowIndex][colIndex] !== undefined) {
colIndex++;
}
const text = cell.textContent.trim();
const rowSpan = parseInt(cell.rowSpan, 10) || 1;
const colSpan = parseInt(cell.colSpan, 10) || 1;
// ์ด ์์๊ฐ ๊ฑธ์น๋ ๋ชจ๋ ์
ํ์
for (let r = 0; r < rowSpan; r++) {
const targetRow = rowIndex + r;
if (!grid[targetRow]) grid[targetRow] = [];
for (let c = 0; c < colSpan; c++) {
grid[targetRow][colIndex + c] = text;
}
}
colIndex += colSpan;
});
});
// ํ ๊ธธ์ด ์ ๊ทํ
const maxCols = Math.max(...grid.map(r => r.length));
return grid.map(row => {
const normalized = new Array(maxCols).fill("");
row.forEach((val, i) => normalized[i] = val ?? "");
return normalized;
});
}
ํต์ฌ ์ธ์ฌ์ดํธ: ๊ฐ์ ๊ทธ๋ฆฌ๋๊ฐ ์ง์ค์ ์์ฒ์ ๋๋ค. DOM ์ ์ ๊ทธ๊ฒ์ ์ฑ์ฐ๊ธฐ ์ํ ์ง์์ฌํญ์ผ ๋ฟ์ ๋๋ค.
๋ฌธ์ 2: ์ค์ฒฉ ํ ์ด๋ธ
์ํคํผ๋์ ์ธํฌ๋ฐ์ค์๋ ์ข
์ข
ํ
์ด๋ธ ์
์์ ํ
์ด๋ธ์ด ์์ต๋๋ค. ์ฌ๊ท์ ์ ๊ทผ๋ฒ์ ์ธ๋ชจ์๋ ๊ฒฐ๊ณผ๋ฅผ ์ถ์ถํฉ๋๋ค:
<table>
<tr>
<td>๊ตญ๊ฐ</td>
<td>
<table> <!-- ์ค์ฒฉ! -->
<tr><td>์ธ๊ตฌ</td><td>3.3์ต</td></tr>
</table>
</td>
</tr>
</table>
๊ฐ์ง ์ ๋ต: ํ
์ด๋ธ์ ์์ ์์๊ฐ ํ
์ด๋ธ์ธ์ง ํ์ธํฉ๋๋ค.
function isNestedTable(table) {
let parent = table.parentElement;
while (parent) {
if (parent.tagName === "TABLE") {
return true;
}
parent = parent.parentElement;
}
return false;
}
// ํ์ด์ง ์ค์บ ์
function getTopLevelTables() {
const all = document.querySelectorAll("table");
return Array.from(all).filter(t => !isNestedTable(t));
}
๊ทธ๋ฐ๋ฐ ์ค์ฒฉ ํ ์ด๋ธ์ ๋ด์ฉ์?
์ธ๋ถ ํ
์ด๋ธ์ ๊ฒฝ์ฐ, ์ค์ฒฉ ํ
์ด๋ธ์ ํ
์คํธ ์ฝํ
์ธ ๋ก ํํํํฉ๋๋ค:
function extractCellText(cell) {
const clone = cell.cloneNode(true);
// ์ค์ฒฉ ํ
์ด๋ธ ์ ๊ฑฐ (ํ
์คํธ๋ textContent๋ฅผ ํตํด ์ด๋ฏธ ํฌํจ๋จ)
clone.querySelectorAll("table").forEach(t => t.remove());
// ๋ณด์ด์ง ์๋ ์์ ์ ๊ฑฐ
clone.querySelectorAll("style, script").forEach(el => el.remove());
return (clone.textContent || "").replace(/\s+/g, " ").trim();
}
๋ฌธ์ 3: ์ํคํผ๋์ ๋ค๋น๊ฒ์ด์ ํ
์ํคํผ๋์ ํ
์ด๋ธ์ ์ข
์ข
๋ค๋น๊ฒ์ด์
ํ์ผ๋ก ์์ํฉ๋๋ค:
| v t e ๊ตญ๊ฐ๋ณ ์ธ๊ตฌ ๋ชฉ๋ก |
| ์์ | ๊ตญ๊ฐ | ์ธ๊ตฌ |
| 1 | ์ค๊ตญ | 14์ต |
"v t e" ํ(๋ณด๊ธฐ/๋ํ/ํธ์ง ๋งํฌ)์ ๋ฐ์ดํฐ๊ฐ ์๋๋ผ UI์ ๋๋ค. ์ด๊ฒ์ ํค๋ ํ์ผ๋ก ์ฒ๋ฆฌํ๋ ํ์๋ ์ธ๋ชจ์๋ ๊ฒฐ๊ณผ๋ฅผ ๋ง๋ญ๋๋ค.
์ํคํผ๋์ ํ ์ด๋ธ ์ฒ๋ฆฌ์ ๋ํ ์ค์ฉ ๊ฐ์ด๋๋ ํ ์ด๋ธ ๋ด๋ณด๋ด๊ธฐ๋ฅผ ์ํ Chrome ์ต๊ณ ์ ํ์ฅ ํ๋ก๊ทธ๋จ 5์ ์ ์ฐธ์กฐํ์ธ์.
๊ฐ์ง:
function isWikipediaNavRow(row) {
const firstCell = row[0] || "";
// ๋ค๋น๊ฒ์ด์
ํ์ ์ผ๋ฐ์ ์ธ ํจํด
const patterns = [
/^v\s+t\s+e\s/i, // "v t e "
/^\s*v\s*\|\s*t\s*\|\s*e/i, // "v | t | e"
/^\[v\]\s*\[t\]\s*\[e\]/i // "[v] [t] [e]"
];
return patterns.some(p => p.test(firstCell));
}
function detectHeaderRowIndex(matrix) {
for (let i = 0; i < Math.min(3, matrix.length - 1); i++) {
if (isWikipediaNavRow(matrix[i])) {
return i + 1; // ํค๋๋ ๋ค์ ํ
}
}
return 0; // ๊ธฐ๋ณธ๊ฐ: ์ฒซ ๋ฒ์งธ ํ์ด ํค๋
}
๋ฌธ์ 4: ํ์ดํ ํ (์ ์ฒด ์ด์ ๊ฑธ์นจ)
์ผ๋ถ ํ
์ด๋ธ์๋ ์ ์ฒด ๋๋น์ ๊ฑธ์น๋ ํ์ดํ ํ์ด ์์ต๋๋ค:
<table>
<tr><td colspan="4">๋ถ๊ธฐ๋ณ ๋งค์ถ (๋ฐฑ๋ง ๋ฌ๋ฌ)</td></tr>
<tr><td>Q1</td><td>Q2</td><td>Q3</td><td>Q4</td></tr>
<tr><td>100</td><td>120</td><td>115</td><td>130</td></tr>
</table>
rowspan ํ์ฅ ํ ์ฒซ ๋ฒ์งธ ํ์ ["๋ถ๊ธฐ๋ณ ๋งค์ถ...", "๋ถ๊ธฐ๋ณ ๋งค์ถ...", ...]โ๊ฐ์ ๊ฐ์ด ๋ฐ๋ณต๋ฉ๋๋ค.
๊ฐ์ง:
function isTitleRow(row, nextRow) {
if (!row || !nextRow) return false;
const uniqueValues = new Set(row.filter(v => v.trim()));
const nextUniqueValues = new Set(nextRow.filter(v => v.trim()));
// ํ์ดํ ํ์ ํน์ฑ:
// 1. ๊ณ ์ ๊ฐ์ด ํ๋๋ง ์์ (colspan์ผ๋ก ๋ฐ๋ณต)
// 2. ๋ค์ ํ์๋ ์ฌ๋ฌ ๊ณ ์ ๊ฐ์ด ์์ (์ค์ ํค๋)
// 3. ๋จ์ผ ๊ฐ์ด ๊ธด ํ
์คํธ (๋ณดํต 30์ ์ด์)
return (
uniqueValues.size === 1 &&
nextUniqueValues.size > 2 &&
row[0] && row[0].length > 30
);
}
๋ฌธ์ 5: ๊ทธ๋ฃนํ๋ ์ด ํค๋ (FBREF ์คํ์ผ)
FBREF ๊ฐ์ ์คํฌ์ธ ํต๊ณ ์ฌ์ดํธ๋ 2๋จ๊ณ ํค๋๋ฅผ ์ฌ์ฉํฉ๋๋ค:
| | | ์ถ์ ์๊ฐ | ์ฑ์ |
| ์ ์ | ๊ตญ์ | MP | ์ ๋ฐ | ๋ถ | ๊ณจ | ์ด์์คํธ | xG |
| ํ๋๋ | ๋
ธ๋ฅด์จ์ด | 35 | 33 | 2950| 36 | 8 | 32 |
์ฒซ ๋ฒ์งธ ํ์๋ ๊ทธ๋ฃน ์ด๋ฆ์ด ์์ต๋๋ค. ๋ ๋ฒ์งธ ํ์๋ ์ค์ ์ด ์ด๋ฆ์ด ์์ต๋๋ค. ๋ ๋ค "ํค๋"์ ๋๋ค.
๊ณผ์ : colspan ํ์ฅ ํ 0๋ฒ์งธ ํ์ ์ด๋ ๊ฒ ๋ฉ๋๋ค:
["", "", "์ถ์ ์๊ฐ", "์ถ์ ์๊ฐ", "์ถ์ ์๊ฐ", "์ฑ์ ", "์ฑ์ ", "์ฑ์ "]
๊ฐ์ง ํด๋ฆฌ์คํฑ:
function isGroupHeaderRow(row, nextRow) {
if (!row || !nextRow || row.length !== nextRow.length) return false;
// ์ด์๊ณผ ๊ฐ์ ๊ฐ์ ๊ฐ์ง ์
์ ์ธ๊ธฐ
let repeatCount = 0;
for (let i = 1; i < row.length; i++) {
if (row[i] && row[i] === row[i-1]) repeatCount++;
}
const repeatRatio = repeatCount / (row.length - 1);
// ๊ทธ๋ฃน ํค๋ ํ์ ๋ณดํต 40% ์ด์ ๋ฐ๋ณต ๊ฐ์ ๊ฐ์ง
// ๊ทธ๋ฆฌ๊ณ ๋ค์ ํ์ด ๋ ๋ง์ ๊ณ ์ ๊ฐ์ ๊ฐ์ง
const uniqueInRow = new Set(row.filter(v => v.trim())).size;
const uniqueInNext = new Set(nextRow.filter(v => v.trim())).size;
return repeatRatio > 0.4 && uniqueInNext > uniqueInRow;
}
๊ทธ๋ฃน + ํ์ ํค๋ ๋ณํฉ:
function mergeGroupAndSubHeaders(groupRow, subHeaderRow) {
return subHeaderRow.map((subHeader, idx) => {
const group = (groupRow[idx] || "").trim();
const sub = (subHeader || "").trim();
if (!group) return sub;
if (!sub) return group;
if (sub.toLowerCase() === group.toLowerCase()) return sub;
return `${group} - ${sub}`;
});
}
// ๊ฒฐ๊ณผ: ["์ ์", "๊ตญ์ ", "์ถ์ ์๊ฐ - MP", "์ถ์ ์๊ฐ - ์ ๋ฐ", ...]
๋ฌธ์ 6: ์ํ์ผ๋ก ๋ณต์ ๋ ํ ์ด๋ธ
์ํคํผ๋์ ์ธ๊ตฌ ํ
์ด๋ธ์๋ ์ข
์ข
์ด๋ฐ ๊ตฌ์กฐ๊ฐ ์์ต๋๋ค:
| ์์ | ์ด๋ฆ | ์ธ๊ตฌ | ์์ | ์ด๋ฆ | ์ธ๊ตฌ |
| 1 | ๋์ฟ | 3700๋ง | 11 | ํ๋ฆฌ | 1100๋ง |
| 2 | ๋ธ๋ฆฌ | 3200๋ง | 12 | ์นด์ด๋ก | 1000๋ง |
์ด๊ฒ์ ์์ง ๊ณต๊ฐ์ ์ ์ฝํ๊ธฐ ์ํด ๋ ์ด๋ก ํ์๋ ํ๋์ ๋ ผ๋ฆฌ์ ํ ์ด๋ธ์ ๋๋ค.
๊ฐ์ง:
function detectHorizontalDuplication(headers) {
const half = Math.floor(headers.length / 2);
if (half < 2) return null;
const firstHalf = headers.slice(0, half);
const secondHalf = headers.slice(half, half * 2);
// ๋ ๋ฒ์งธ ์ ๋ฐ์ด ์ฒซ ๋ฒ์งธ ์ ๋ฐ๊ณผ ์ผ์นํ๋์ง ํ์ธ
const matches = firstHalf.every((h, i) =>
h.toLowerCase() === secondHalf[i]?.toLowerCase()
);
if (matches) {
return { detected: true, repeatCount: 2, baseColumns: half };
}
return null;
}
์ ๊ทํ: ๊ฐ ํ์ ๋ถํ ํ๊ณ ์์ง์ผ๋ก ์๊ธฐ:
function normalizeHorizontallyDuplicatedTable(matrix, baseColumns) {
const header = matrix[0].slice(0, baseColumns);
const normalizedRows = [header];
for (let i = 1; i < matrix.length; i++) {
const row = matrix[i];
// ์ฒซ ๋ฒ์งธ ์ ๋ฐ
normalizedRows.push(row.slice(0, baseColumns));
// ๋ ๋ฒ์งธ ์ ๋ฐ (๋น์ด์์ง ์์ ๊ฒฝ์ฐ)
const secondHalf = row.slice(baseColumns, baseColumns * 2);
if (secondHalf.some(cell => cell.trim())) {
normalizedRows.push(secondHalf);
}
}
return normalizedRows;
}
ํตํฉ ์๊ณ ๋ฆฌ์ฆ
์ค์ ํ์ฑ์์๋ ์ด ๋ชจ๋ ์ผ์ด์ค๋ฅผ ์์๋๋ก ํ์ธํด์ผ ํฉ๋๋ค:
function parseTable(table) {
// 1. rowspan/colspan์ ๊ฐ์ ๊ทธ๋ฆฌ๋๋ก ํ์ฅ
let matrix = expandRowspans(table);
// 2. ๋ค๋น๊ฒ์ด์
/ํ์ดํ ํ ๊ฐ์ง ๋ฐ ๊ฑด๋๋ฐ๊ธฐ
const headerIndex = detectHeaderRowIndex(matrix);
if (headerIndex > 0) {
matrix = matrix.slice(headerIndex);
}
// 3. ๊ทธ๋ฃนํ๋ ํค๋ ์ฒ๋ฆฌ (FBREF ์คํ์ผ)
const groupedHeaders = detectGroupedColumnHeaders(matrix);
if (groupedHeaders) {
const mergedHeaders = mergeGroupAndSubHeaders(matrix[0], matrix[1]);
matrix = [mergedHeaders, ...matrix.slice(2)];
}
// 4. ์ํ ๋ณต์ ์ฒ๋ฆฌ
const duplication = detectHorizontalDuplication(matrix[0]);
if (duplication) {
matrix = normalizeHorizontallyDuplicatedTable(matrix, duplication.baseColumns);
}
return matrix;
}
์ด ์ฃ์ง ์ผ์ด์ค ํ ์คํธ
์์ ๋ชจ๋ ํจํด์ ์ค์ ๋ฒ๊ทธ ๋ฆฌํฌํธ์์ ๋์์ต๋๋ค. ๊ฐ๊ฐ์ ๋ํ HTML ํฝ์ค์ฒ๊ฐ ํฌํจ๋ ํ
์คํธ ์ค์ํธ๋ฅผ ์ ์งํ๊ณ ์์ต๋๋ค:
// ํ
์คํธ: ์ํคํผ๋์ ์คํ์ผ ๋ค๋น๊ฒ์ด์
ํ
const navRowHtml = `
<table>
<tr><td colspan="3">v t e ๊ตญ๊ฐ</td></tr>
<tr><td>์์</td><td>๊ตญ๊ฐ</td><td>์ธ๊ตฌ</td></tr>
<tr><td>1</td><td>์ค๊ตญ</td><td>14์ต</td></tr>
</table>
`;
const result = parseTable(parseHtml(navRowHtml));
assert(result[0][0] === "์์"); // ํค๋ ์ฌ๋ฐ๋ฅด๊ฒ ์๋ณ
assert(result[1][1] === "์ค๊ตญ"); // ๋ฐ์ดํฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ ๋ ฌ
ํ ์คํธ ์ค์ํธ์๋ ์ด ํจํด๋ค์ ์กฐํฉ์ ๋ค๋ฃจ๋ 24๊ฐ์ ์ผ์ด์ค๊ฐ ์์ต๋๋ค. ์๋ก์ด ๋ฒ๊ทธ ๋ฆฌํฌํธ๊ฐ ์๋ก์ด ํ ์คํธ ์ผ์ด์ค๊ฐ ๋ฉ๋๋ค.
์ง์ ์ฌ์ฉํด ๋ณด์ธ์
ํ ์ด๋ธ ์ถ์ถ์ ๋ง๋ค๊ณ ์๋ค๋ฉด, ์ด ๊ธ์ด ๋๋ฒ๊น ์๊ฐ์ ์ ์ฝํด์ฃผ๊ธฐ๋ฅผ ๋ฐ๋๋๋ค. ์ฝ๋๋ฅผ ์์ฑํ์ง ์๊ณ ํ ์ด๋ธ๋ง ๋ด๋ณด๋ด๊ณ ์ถ๋ค๋ฉด, HTML Table Exporter๊ฐ ์ด ๋ชจ๋ ์ผ์ด์ค๋ฅผ ์๋์ผ๋ก ์ฒ๋ฆฌํฉ๋๋ค.
gauchogrid.com/ko/html-table-exporter์์ ์์ธํ ์์๋ณด๊ฑฐ๋ Chrome ์น ์คํ ์ด์์ ๋ฌด๋ฃ๋ก ์ฌ์ฉํด ๋ณด์ธ์.
ํ์๋ฅผ ๊นจ๋จ๋ฆฌ๋ ํ ์ด๋ธ์ ์ฐพ์ผ์ จ๋์? URL์ ๊ณต์ ํด ์ฃผ์ธ์. ์ ๋ ์ด๋ฐ ์ฃ์ง ์ผ์ด์ค๋ฅผ ์์งํ๊ณ ์์ต๋๋ค.












