레지덴셜 프록시를 통해 여러 국가에서 가격을 수집하면, 같은 숫자가 십여 가지 방식으로 표기된 것을 마주치게 됩니다. 독일 사이트는 1.234,56 €로 표시합니다. 프랑스 사이트는 1 234,56 €로 표시하는데, 이 공백은 눈에 잘 보이지 않을 수 있습니다. 스위스 사이트는 CHF 1’234.50으로 표시합니다. 인도 사이트는 12,34,567처럼 자릿수를 묶습니다. 그리고 03/04/2026 같은 날짜는 미국에서는 3월 4일을 뜻하지만, 그 외 거의 모든 지역에서는 4월 3일을 뜻합니다.
이 중 하나라도 잘못 해석하면 오류가 눈에 잘 띄지 않는 경우가 많습니다. 가격이 천 배 차이가 나거나, 날짜가 한 달 어긋나거나, 같은 기호를 공유하는 다른 통화와 혼동될 수 있습니다. 이 가이드는 마주치게 될 형식들, 이를 결정하는 규칙들, 그리고 값이 맞지 않을 때 추측하지 않고 거부하는 파싱 코드를 다룹니다.
핵심 요약
- 모든 값은 해당 페이지의 로케일을 기준으로 파싱하십시오. 같은 문자라도 로케일에 따라 다른 숫자를 의미합니다.
- 구분 기호를 제거하고 운에 맡기지 마십시오. 로케일을 기준으로 어떤 기호가 소수점 구분자인지 결정하고, 맞지 않는 값은 거부하십시오.
- 통화 기호는 모호한 것으로 취급하십시오. “$10.00”은 캐나다 페이지에서는 캐나다 달러이고, 멕시코 페이지에서는 페소이며, 호주 페이지에서는 호주 달러입니다. ISO 4217 코드를 저장하십시오.
- 금액은 올바른 정밀도로 최소 단위로 저장하십시오. 엔화는 소수점 이하 자릿수가 없고, 바레인 디나르와 쿠웨이트 디나르는 소수점 이하 세 자리입니다.
- 숫자로 표기된 날짜에는 로케일이 필요하며, 가능하다면 기계가 읽을 수 있는 출처를 사용하는 것이 낫습니다. 페이지가 구조화된 데이터에서 ISO 8601을 제공한다면 이를 우선하십시오.
규칙은 어디서 오는가
로케일 관례는 민간전승이 아닙니다. 이는 운영체제, 브라우저, 그리고 대부분의 국제화 라이브러리가 사용하는 유니코드 공통 로케일 데이터 저장소, CLDR에 공표되어 있습니다. 아래 예시는 CLDR 데이터에서 직접 가져온 것으로, 파이썬 라이브러리 Babel을 통해 숫자 1234567.891과 1,234.50유로 금액을 포맷한 결과입니다.
| 로케일 | 소수점 | 그룹 구분자 | 1234567.891 | €1,234.50 |
|---|---|---|---|---|
| 영어, 미국 | . | , | 1,234,567.891 | €1,234.50 |
| 독일어, 독일 | , | . | 1.234.567,891 | 1.234,50 € |
| 프랑스어, 프랑스 | , | 좁은 줄바꿈 없는 공백 | 1 234 567,891 | 1 234,50 € |
| 독일어, 스위스 | . | ’ | 1’234’567.891 | EUR 1’234.50 |
| 영어, 인도 | . | , | 12,34,567.891 | €1,234.50 |
| 포르투갈어, 브라질 | , | . | 1.234.567,891 | € 1.234,50 |
세 가지 세부사항이 특히 사람을 헷갈리게 합니다. 프랑스어는 천 단위를 좁은 줄바꿈 없는 공백으로 구분하는데, 이는 공백처럼 보이지만 실제로는 공백이 아닌 문자입니다. 스위스 독일어는 아포스트로피와 비슷한 문자를 사용합니다. 그리고 인도 영어는 처음 세 자리를 묶은 후 두 자리씩 묶기 때문에, 1,234,567은 12,34,567로 표기됩니다.
로케일로 파싱하고, 맞지 않는 것은 거부하기
유혹적인 접근법은 숫자와 구분자 하나를 제외한 모든 것을 제거한 다음, 어떤 구분자가 소수점인지 추측하는 것입니다. 이는 “1.234”를 만나기 전까지는 통합니다. 이 값은 독일에서는 1234를 뜻하지만 미국에서는 1.234를 뜻합니다. 아무리 영리한 방법을 써도 문자열만으로는 정답을 복구할 수 없습니다. 로케일만이 정답을 알려줍니다.
import re
from babel.dates import parse_date
from babel.numbers import NumberFormatError, get_currency_precision, get_group_symbol, parse_decimal
# Keep digits, separators and the space variants sites use between thousands.
NUMBER_CHARS = re.compile("[^0-9.,'\u2019 \u00a0\u2009\u202f-]")
SPACES = re.compile("[ \u00a0\u2009\u202f]")
def parse_amount(text, locale):
"""Parse a displayed amount using the page's locale. Refuses input that does not fit it."""
number = NUMBER_CHARS.sub("", text).strip()
group = get_group_symbol(locale)
if SPACES.fullmatch(group):
# Sites mix ordinary, no-break and narrow no-break spaces; use the locale's own.
number = SPACES.sub(group, number)
try:
return parse_decimal(number, locale=locale, strict=True)
except NumberFormatError as e:
raise ValueError(f"{text!r} does not parse as a {locale} number: {e}") from None
def to_minor_units(amount, currency):
"""Store money as an integer in the currency's smallest unit (cents, yen, fils)."""
return int((amount * 10 ** get_currency_precision(currency)).to_integral_value())
def parse_display_date(text, locale):
"""Numeric dates are ambiguous without a locale: 03/04/2026 is March or April."""
return parse_date(text.strip(), locale=locale)
실제 형식으로 테스트한 결과입니다.
| 표시된 값 | 로케일 | 파싱 결과 |
|---|---|---|
| $1,234.56 | 영어, 미국 | 1234.56 |
| 1.234,56 € | 독일어, 독일 | 1234.56 |
| 1 234,56 € (세 가지 공백 문자 중 하나) | 프랑스어, 프랑스 | 1234.56 |
| CHF 1’234.50 | 독일어, 스위스 | 1234.50 |
| ₹12,34,567.00 | 영어, 인도 | 1234567.00 |
| R$ 1.234,56 | 포르투갈어, 브라질 | 1234.56 |
| 1.234,56 € | 영어, 미국 | 거부됨 |
| 9,99 | 영어, 미국 | 거부됨 |
마지막 두 행이 핵심입니다. 미국 페이지라고 믿었던 곳에서 독일식으로 포맷된 가격이 나온다면, 이는 무언가 잘못되었다는 신호입니다. 잘못된 로케일 변형이 서빙되었거나, 페이지가 바뀌었거나, 설정이 틀렸을 수 있습니다. 눈에 보이는 오류가 조용히 천 배 차이로 어긋난 가격보다 낫습니다.
공백 처리는 테스트 과정에서 그 가치를 입증했습니다. 초기 버전은 일반 공백으로 표기된 프랑스어 가격을 거부했는데, CLDR의 프랑스어 구분자가 좁은 줄바꿈 없는 공백이기 때문이었습니다. 실제 사이트들은 세 가지 공백 문자를 모두 섞어 사용하므로, 코드는 이제 파싱 전에 이들 중 어느 것이든 해당 로케일 고유의 문자로 매핑합니다.
통화: 기호만 믿지 말 것
많은 통화가 기호를 공유합니다. CLDR은 캐나다, 멕시코, 호주 모두에서 현지 통화 10단위를 “$10.00”으로 포맷하며, 캐나다 페이지에서는 미국 달러를 “US$10.00”으로 표시합니다. ”$“를 USD로 매핑하는 스크레이퍼는 전 세계 사이트 중 상당수에서 틀리게 됩니다.
통화는 모호하지 않은 것에서 가져오십시오. 페이지의 구조화된 데이터에 있는 ISO 4217 코드, 통화 선택기의 값, 또는 수집한 사이트와 시장의 알려진 기본 통화 등입니다. 모든 가격에 세 글자 코드를 함께 저장하고, 다른 증거 없이 기호만 있는 가격은 추측하지 말고 불완전한 것으로 취급하십시오.
최소 단위와 정밀도
파싱한 후에는 금액을 해당 통화의 최소 단위로 정수로 저장하십시오. 부동소수점 산술은 금액 계산에서 반올림 오차가 누적되기 때문입니다. 소수점 이하 자릿수는 항상 2가 아닙니다.
| 통화 | 소수점 자릿수 | 1,234로 표시된 값은 다음이 됨 |
|---|---|---|
| 유로, 미국 달러 | 2 | 123400 |
| 일본 엔, 한국 원, 칠레 페소 | 0 | 1234 |
| 바레인 디나르, 쿠웨이트 디나르 | 3 | 1234000 |
값과 함께 단위를 기록하십시오. 스크레이핑된 데이터의 스키마 드리프트에서 설명한 것처럼, $100짜리 제품의 가격이 10000으로 들어오는 것과 같이 메이저 단위와 마이너 단위가 섞인 파이프라인은 정확히 100배 차이가 나는, 완전히 그럴듯해 보이는 오류를 만들어냅니다.
날짜: 기계가 읽을 수 있는 출처를 우선할 것
같은 숫자 날짜라도 로케일에 따라 다르게 파싱됩니다.
| 표시된 값 | 로케일 | 파싱 결과 |
|---|---|---|
| 03/04/2026 | 영어, 미국 | 2026년 3월 4일 |
| 03/04/2026 | 영어, 영국 | 2026년 4월 3일 |
| 03/04/2026 | 독일어, 독일 | 2026년 4월 3일 |
가능한 경우, 표시된 날짜를 아예 파싱하지 마십시오. 많은 페이지가 구조화된 데이터나 <time> 요소의 datetime 속성에 기계가 읽을 수 있는 날짜를 게시하며, 보통 모호하지 않은 ISO 8601 형식을 사용합니다. 이를 추출하는 방법은 HTML 파싱을 멈추기에서 다룹니다. 표시된 날짜를 파싱해야 한다면, 페이지의 로케일을 명시적으로 사용하고, 결과를 시간대와 함께 ISO 8601로 저장하며, 로케일을 확인하지 않은 사이트에서 일과 월이 모두 12 이하인 값은 표시해 두십시오.
로케일을 애초에 올바르게 파악하기
위의 모든 규칙은 페이지가 어떤 로케일을 사용했는지 아는 것에 달려 있으며, 이는 부분적으로 수집 방식의 문제이기도 합니다. 같은 제품 페이지도 방문자의 국가에 따라 다른 언어, 통화, 숫자 형식으로 렌더링될 수 있으며, 이것이 독일 출구를 통해 수집한 가격과 미국 출구를 통해 수집한 가격이 둘 다 정규화되기 전까지는 직접 비교할 수 없는 이유입니다. 각 값이 수집된 시장을 기록하고, 레지덴셜 프록시의 지역, 시간대, 로케일 맞추기에서 다룬 것처럼 세션의 언어와 지역을 일관되게 유지하십시오.
결론
숫자, 가격, 날짜는 보편적으로 보이지만 그렇지 않습니다. 같은 문자라도 로케일에 따라 다른 값을 의미하고, 기호는 여러 통화에 매핑되며, 정밀도는 통화마다 다르고, 숫자 날짜는 설계상 모호합니다.
모든 값을 작성된 로케일로 파싱하고, 추측하는 대신 맞지 않는 것은 거부하며, 통화는 ISO 코드로, 금액은 올바른 정밀도의 최소 단위로 저장하고, 페이지가 제공하는 경우 기계가 읽을 수 있는 날짜를 우선하십시오. 이 규칙들 각각은 조용하고 그럴듯한 오류를 눈에 보이는 오류로 바꿔줍니다.
출처 및 참고자료
- Unicode Consortium, Common Locale Data Repository (CLDR).
- Babel, Python internationalisation library, version 2.18.0, used for the examples above.
- ISO 4217 currency codes and ISO 8601 date and time format.