웹 테이블은 문자열입니다. 모든 것이 문자열입니다. 하지만 JSON이나 SQL로 내보낼 때는 이렇게 변환되어야 합니다:
-
"1,234.56"→1234.56(숫자) -
"2024-03-15"→ 날짜 타입 -
"Yes"→true(불리언) -
"N/A"→null
신뢰할 수 있는 타입 추론 시스템을 구축한다는 것은 실제 데이터의 혼란을 처리한다는 의미입니다. HTML Table Exporter를 개발하면서 이 문제에 접근한 방법을 공유합니다.
문제: 모호성의 바다
다음 값들을 살펴보세요:
| 값 | 해석 가능성 |
|---|---|
"1,234" |
숫자 1234 (미국식) 또는 1.234 (유럽식) |
"01/02/03" |
2003년 1월 2일 (미국) 또는 2003년 2월 1일 (유럽) 또는 2001-02-03 |
"1" |
정수 1 또는 불리언 true |
"N/A" |
문자열 또는 null |
"0" |
정수 0 또는 불리언 false |
타입 추론은 파싱이 아닙니다—모호한 증거에서 의도를 추측하는 것입니다.
아키텍처: 컬럼 레벨 추론
셀 단위 타입 추론은 신뢰할 수 없습니다. 단독 값 "1"은 무엇이든 될 수 있습니다. 하지만 ["1", "2", "3", "4"]를 포함하는 컬럼은 분명히 정수입니다.
제 접근법:
- 컬럼에서 값 샘플링 (최대 100개)
- null이 아닌 각 값의 타입 추론
- 집계하여 컬럼 타입 결정
- 90% 임계값 적용—90% 이상의 값이 타입과 일치하면 해당 타입 사용
const DATA_TYPES = {
STRING: "string",
INTEGER: "integer",
NUMBER: "number",
BOOLEAN: "boolean",
DATE: "date",
NULL: "null",
};
function inferColumnType(columnValues, maxSamples = 100) {
const nonNullValues = columnValues.filter(v => v != null && v !== "");
if (nonNullValues.length === 0) {
return { type: DATA_TYPES.STRING, confidence: 0 };
}
const sample = nonNullValues.slice(0, maxSamples);
// 각 타입의 출현 횟수 카운트
const typeCounts = {
[DATA_TYPES.INTEGER]: 0,
[DATA_TYPES.NUMBER]: 0,
[DATA_TYPES.BOOLEAN]: 0,
[DATA_TYPES.DATE]: 0,
[DATA_TYPES.STRING]: 0,
};
for (const val of sample) {
const type = inferValueType(val);
if (type === DATA_TYPES.INTEGER) {
// INTEGER는 NUMBER의 부분집합
typeCounts[DATA_TYPES.INTEGER]++;
typeCounts[DATA_TYPES.NUMBER]++;
} else if (type !== DATA_TYPES.NULL) {
typeCounts[type]++;
}
}
const total = sample.length;
const threshold = 0.9;
// 우선순위 순서로 타입 확인
if (typeCounts[DATA_TYPES.BOOLEAN] >= total * threshold) {
return { type: DATA_TYPES.BOOLEAN, confidence: typeCounts[DATA_TYPES.BOOLEAN] / total };
}
if (typeCounts[DATA_TYPES.DATE] >= total * threshold) {
return { type: DATA_TYPES.DATE, confidence: typeCounts[DATA_TYPES.DATE] / total };
}
if (typeCounts[DATA_TYPES.INTEGER] >= total * threshold) {
return { type: DATA_TYPES.INTEGER, confidence: typeCounts[DATA_TYPES.INTEGER] / total };
}
if (typeCounts[DATA_TYPES.NUMBER] >= total * threshold) {
return { type: DATA_TYPES.NUMBER, confidence: typeCounts[DATA_TYPES.NUMBER] / total };
}
return { type: DATA_TYPES.STRING, confidence: 1 };
}
왜 90%인가? 실제 데이터에는 노이즈가 있습니다. 100개 정수 컬럼에 "N/A" 하나가 있을 수 있습니다. 100% 일치를 요구하면 너무 엄격합니다.
값 레벨 타입 감지
inferValueType 함수는 개별 값을 처리합니다:
function inferValueType(value) {
if (value == null || value === "") {
return DATA_TYPES.NULL;
}
const str = String(value).trim();
if (str === "") return DATA_TYPES.NULL;
// 불리언 확인
const lowerStr = str.toLowerCase();
if (["true", "false", "yes", "no", "sí", "si"].includes(lowerStr)) {
return DATA_TYPES.BOOLEAN;
}
// 날짜 확인 (ISO 형식 우선)
if (/^\d{4}-\d{2}-\d{2}/.test(str)) {
return DATA_TYPES.DATE;
}
if (/^\d{1,2}[/\-\.]\d{1,2}[/\-\.]\d{2,4}$/.test(str)) {
return DATA_TYPES.DATE;
}
// 정수 확인 (엄격)
if (/^-?\d+$/.test(str)) {
return DATA_TYPES.INTEGER;
}
// 숫자 확인 (소수점 및 구분자 포함)
const cleanedForNumber = str
.replace(/[$€£¥%\s]/g, "")
.replace(/,/g, ".");
if (/^-?\d+(\.\d+)?$/.test(cleanedForNumber)) {
return DATA_TYPES.NUMBER;
}
return DATA_TYPES.STRING;
}
숫자 정규화의 난제
유럽식 vs 미국식 숫자 형식이 가장 까다로운 모호성입니다:
| 값 | 미국식 해석 | 유럽식 해석 |
|---|---|---|
"1,234" |
1234 | 1.234 |
"1.234" |
1.234 | 1234 |
"1,234.56" |
1234.56 | 유효하지 않음 |
"1.234,56" |
유효하지 않음 | 1234.56 |
형식 감지를 위한 휴리스틱:
function normalizeNumber(value) {
if (value == null) return value;
let str = String(value).trim();
// 통화 기호 및 공백 제거
str = str.replace(/[$€£¥\s]/g, "");
// 퍼센트 처리
const isPercent = str.endsWith("%");
if (isPercent) str = str.slice(0, -1);
// 구분자 분석으로 형식 감지
const commaCount = (str.match(/,/g) || []).length;
const dotCount = (str.match(/\./g) || []).length;
const lastComma = str.lastIndexOf(",");
const lastDot = str.lastIndexOf(".");
let normalized;
if (commaCount === 0 && dotCount === 0) {
// 순수 정수: "1234"
normalized = str;
} else if (commaCount === 0 && dotCount === 1) {
// "1.234" (유럽식 천 단위) 또는 "1.23" (소수)
// 휴리스틱: 점 뒤 3자리 = 천 단위 구분자
const afterDot = str.slice(lastDot + 1);
if (afterDot.length === 3 && /^\d+$/.test(afterDot)) {
normalized = str.replace(".", "");
} else {
normalized = str;
}
} else if (commaCount === 1 && dotCount === 0) {
// "1,234" (미국식 천 단위) 또는 "1,23" (유럽식 소수)
const afterComma = str.slice(lastComma + 1);
if (afterComma.length === 3 && /^\d+$/.test(afterComma)) {
normalized = str.replace(",", "");
} else {
normalized = str.replace(",", ".");
}
} else if (lastDot > lastComma) {
// "1,234.56" - 미국식
normalized = str.replace(/,/g, "");
} else if (lastComma > lastDot) {
// "1.234,56" - 유럽식
normalized = str.replace(/\./g, "").replace(",", ".");
} else {
return value;
}
const num = parseFloat(normalized);
if (Number.isNaN(num)) return value;
return isPercent ? num / 100 : num;
}
핵심 인사이트: 구분자의 위치와 개수가 대부분의 모호성을 해결합니다. 마지막 구분자가 보통 소수점 구분자입니다.
불리언 감지 (주의사항 포함)
명확한 불리언: "true", "false", "yes", "no"
하지만 "0"과 "1"은 어떨까요?
// 문제: 세율을 불리언으로 변환
// 컬럼: [0, 0.05, 0.1, 0.2] -> [false, 0.05, 0.1, 0.2]
제 해결책: 순수 숫자 값은 불리언으로 변환하지 않습니다.
function applyBooleanNormalization(rows, booleanConfig) {
const trueValues = new Set(booleanConfig.true.map(v => v.toLowerCase()));
const falseValues = new Set(booleanConfig.false.map(v => v.toLowerCase()));
return rows.map((row, rowIndex) => {
if (rowIndex === 0) return row; // 헤더 스킵
return row.map(cell => {
if (cell == null) return cell;
const cellStr = String(cell).toLowerCase().trim();
// 순수 숫자면 스킵 (숫자 컬럼에서 0 -> false 방지)
if (/^-?\d+(\.\d+)?$/.test(cellStr)) {
return cell;
}
if (trueValues.has(cellStr)) return "true";
if (falseValues.has(cellStr)) return "false";
return cell;
});
});
}
Null 값 처리
웹 데이터는 "값 없음"을 다양하게 표현합니다:
-
""(빈 문자열) -
"N/A","n/a","NA" -
"-","--" -
"null","NULL" -
"none","None" -
"."(정부 데이터에서 발견)
설정 가능한 null 감지:
function applyNullValues(rows, nullPatterns) {
const nullSet = new Set(nullPatterns.map(v => v.toLowerCase().trim()));
return rows.map((row, rowIndex) => {
if (rowIndex === 0) return row; // 헤더 스킵
return row.map(cell => {
if (cell == null) return null;
const cellStr = String(cell).toLowerCase().trim();
return nullSet.has(cellStr) ? null : cell;
});
});
}
// 사용법
applyNullValues(rows, ["N/A", "n/a", "-", "--", "null", "none", "."]);
파이프라인
전체 타입 추론 및 정제 파이프라인:
function cleanTable(tableInfo, config) {
let rows = cloneRows(tableInfo.rows);
// 1. 모든 문자열 트림
if (config.trimStrings) {
rows = applyTrimStrings(rows);
}
// 2. null 패턴을 실제 null로 변환
if (config.nullValues?.length) {
rows = applyNullValues(rows, config.nullValues);
}
// 3. 불리언 정규화 (숫자보다 먼저, 0->false 이슈 방지)
if (config.booleans) {
rows = applyBooleanNormalization(rows, config.booleans);
}
// 4. 숫자 정규화 (EU/US 형식 처리)
if (config.normalizeNumbers) {
rows = applyNumberNormalization(rows);
}
// 5. 날짜 정규화 (선택적, 형식별)
if (config.normalizeDates) {
rows = applyDateNormalization(rows, config.dateFormat);
}
return { ...tableInfo, rows };
}
엣지 케이스 테스트
타입 추론에는 많은 엣지 케이스가 있습니다. 테스트 스위트를 유지합니다:
// 숫자 정규화 테스트
assertEqual(normalizeNumber("1,234.56"), 1234.56); // 미국식
assertEqual(normalizeNumber("1.234,56"), 1234.56); // 유럽식
assertEqual(normalizeNumber("€1.234,56"), 1234.56); // 통화 포함
assertEqual(normalizeNumber("45,5%"), 0.455); // 퍼센트
assertEqual(normalizeNumber("1.234"), 1234); // 유럽식 천 단위
assertEqual(normalizeNumber("1.23"), 1.23); // 소수
// 불리언 테스트
assertEqual(inferValueType("Yes"), DATA_TYPES.BOOLEAN);
assertEqual(inferValueType("0"), DATA_TYPES.INTEGER); // 불리언 아님
assertEqual(inferValueType("0.5"), DATA_TYPES.NUMBER); // 불리언 아님
내보내기 통합
SQL로 내보낼 때 추론된 타입 사용:
function inferSqlColumnTypes(rows) {
const header = rows[0];
const dataRows = rows.slice(1);
return header.map((_, colIndex) => {
const columnValues = dataRows.map(row => row[colIndex]);
const { type } = inferColumnType(columnValues);
switch (type) {
case DATA_TYPES.INTEGER: return "INTEGER";
case DATA_TYPES.NUMBER: return "REAL";
case DATA_TYPES.BOOLEAN: return "BOOLEAN";
case DATA_TYPES.DATE: return "DATE";
default: return "TEXT";
}
});
}
배운 교훈
컬럼 컨텍스트가 셀 분석을 이깁니다. 단일
"1"은 모호하지만, 정수 컬럼은 아닙니다.변환에 보수적이 되세요. 잘못된 변환으로 데이터를 손상시키는 것보다 문자열로 두는 것이 낫습니다.
설정 가능하게 만드세요. 도메인마다 null 값, 불리언 표현, 숫자 형식이 다릅니다.
실제 데이터로 테스트하세요. 합성 테스트로는 실제 웹 테이블의 혼돈을 놓칩니다.
이 시스템이 HTML Table Exporter의 데이터 정제를 구동합니다. 이런 문제들의 실용적 예시는 웹사이트에서 테이블을 Excel로 복사하기에서 적절한 타입 추론이 깨진 서식을 어떻게 수정하는지 확인할 수 있습니다.
PRO 버전에서는 내보내기 프로필별로 이 규칙들을 설정할 수 있습니다. gauchogrid.com/ko/html-table-exporter에서 자세히 알아보거나 Chrome 웹 스토어에서 사용해보세요.
다른 도메인에서 타입 추론을 구축 중이신가요? 엣지 케이스를 들려주세요.
Top comments (0)