Blog
pysparksparkregextext-processingnlpdata-engineering

PySpark 대규모 텍스트·정규식 처리 — 수십억 로그를 파싱하기

비정형 로그·텍스트 수십억 건을 정규식으로 파싱하고 정제할 때의 성능 함정. catastrophic backtracking, UDF 대신 내장 정규식 함수, 토큰화·정규화, 그리고 깨진 인코딩 처리까지 PySpark 패턴으로 정리합니다.

Data Dynamics2026年6月5日11 min read
This post is not yet translated. The original Korean version is shown below.

수억 건의 웹 서버 로그가 쌓여 있고, 각 줄에서 IP, 상태 코드, 응답 시간을 뽑아내야 한다고 상상해 보세요. 정규식 몇 줄이면 될 것 같지만, 수십억 건 규모에서는 정규식 패턴 하나를 잘못 쓰면 잡 전체가 멈춥니다(catastrophic backtracking). 이 글은 그 함정을 피하면서 대규모 텍스트를 빠르고 안정적으로 처리하는 PySpark 패턴을 정리합니다.

이 글에서 배우는 것

  • Python UDF 대신 JVM 내 내장 정규식 함수를 써야 하는 이유
  • Apache 로그처럼 구조가 있는 텍스트를 그룹 패턴으로 파싱하는 방법
  • Catastrophic backtracking 이 왜 잡을 멈추는지, 어떻게 피하는지
  • NLP 전처리(토큰화·불용어 제거)를 MLlib 으로 처리하는 방법
  • 깨진 인코딩·제어문자 처리와 성능 최적화 패턴

1. 첫 원칙 — 정규식도 UDF 말고 내장 함수로

텍스트 처리를 Python UDF 로 하면 느립니다(JVM↔Python 직렬화 + 행 단위). Spark 는 JVM 내에서 동작하는 정규식 내장 함수를 제공합니다. 이걸 쓰세요.

from pyspark.sql import functions as F
 
# ❌ Python UDF (느림)
import re
@F.udf("string")
def extract_ip(line):
    m = re.search(r"\d+\.\d+\.\d+\.\d+", line)
    return m.group() if m else None
 
# ✅ 내장 정규식 함수 (JVM 내 실행, 빠름)
df = df.withColumn("ip", F.regexp_extract("line", r"(\d+\.\d+\.\d+\.\d+)", 1))
내장 함수용도
regexp_extract(col, pattern, group)그룹 추출
regexp_extract_all모든 매칭 추출(배열)
regexp_replace(col, pattern, repl)치환
rlike / regexp_like패턴 매칭 필터
split(col, pattern)분할

(UDF 가 느린 근본 이유는 별도 글 "PySpark UDF가 느린 이유와 Pandas UDF" 참고.)

2. 로그 파싱 — 한 패턴으로 여러 필드

Apache 액세스 로그처럼 포맷이 일정한 텍스트는 캡처 그룹을 활용하면 한 번의 패턴으로 여러 필드를 동시에 뽑아낼 수 있습니다.

# Apache 액세스 로그 파싱
pattern = r'(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) [^"]*" (\d{3}) (\d+)'
 
parsed = df.select(
    F.regexp_extract("line", pattern, 1).alias("ip"),
    F.regexp_extract("line", pattern, 2).alias("ts"),
    F.regexp_extract("line", pattern, 3).alias("method"),
    F.regexp_extract("line", pattern, 4).alias("path"),
    F.regexp_extract("line", pattern, 5).cast("int").alias("status"),
    F.regexp_extract("line", pattern, 6).cast("long").alias("bytes"))

팁: 같은 패턴을 regexp_extract 로 6번 호출하면 매번 매칭합니다. 성능이 중요하면 한 번 매칭해 구조체로 받는 방식(또는 from_csv/고정 구분자 split)을 고려하세요. 구분자가 명확하면 정규식보다 split 이 훨씬 빠릅니다.

3. 가장 위험한 함정 — Catastrophic Backtracking

정규식 엔진이 매칭을 시도할 때 실패하면 이전 단계로 돌아가 다른 경우를 시도합니다. 특정 패턴에서 이 되돌아가기가 폭발적으로 늘어나 한 행 처리에 수분이 걸리기도 하는데, 이것이 catastrophic backtracking 입니다.

위험 패턴: 중첩된 수량자 (a+)+,  (.*)*,  (\d+)+$
악성 입력: "aaaaaaaaaaaaaaaaaaaaaaaa!" 같은 거의-매칭
→ 백트래킹이 폭발 → 한 행 처리에 수초~수분 → 잡 멈춤
위험안전
(a+)+, (.*)*중첩 수량자 제거
.*foo.* 남발앵커(^, $)·구체적 문자클래스
욕심쟁이(.*)게으른(.*?) 또는 문자클래스 [^"]*
# ❌ 위험: 중첩 수량자
r"(\w+\s*)+"
 
# ✅ 안전: 구체적 문자클래스, 앵커
r"^\w+(\s\w+)*$"

진단: Spark UI 에서 한 태스크만 끝없이 RUNNING 이고 CPU 가 100% 인데 데이터 스큐가 아니라면, 악성 입력 + 취약 정규식의 backtracking 을 의심하세요. 패턴을 단순화하거나 입력 길이를 제한하세요.

한 문장으로: 중첩 수량자는 수십억 건 중 단 한 행만으로도 잡 전체를 멈출 수 있습니다.

4. 토큰화·정규화 (NLP 전처리)

검색·임베딩·분류를 위한 텍스트 전처리는 MLlib 의 텍스트 트랜스포머나 내장 함수로 합니다.

from pyspark.ml.feature import RegexTokenizer, StopWordsRemover
 
# 정규화: 소문자 + 특수문자 제거
df = df.withColumn("clean",
    F.regexp_replace(F.lower("text"), r"[^\w\s가-힣]", " "))
 
# 토큰화 (정규식 기반)
tokenizer = RegexTokenizer(inputCol="clean", outputCol="tokens",
                           pattern=r"\s+", minTokenLength=2)
df = tokenizer.transform(df)
 
# 불용어 제거
remover = StopWordsRemover(inputCol="tokens", outputCol="filtered")
df = remover.transform(df)
작업도구
소문자·정제lower, regexp_replace
토큰화RegexTokenizer, split
불용어StopWordsRemover
n-gramNGram
벡터화HashingTF, CountVectorizer, Word2Vec

(한국어 다국어 검색 전처리는 별도 글 "특허·법률·논문 검색을 위한 다국어 검색 엔진 설계"에서도 다룹니다.)

5. 인코딩·깨진 문자 처리

실제 현장 데이터를 다루다 보면 깨진 인코딩, 제어문자, 이모지가 예고 없이 섞여 있습니다. 대규모 텍스트에는 깨진 인코딩, 제어문자, 이모지가 섞입니다.

# 제어문자·비인쇄 문자 제거
df = df.withColumn("clean",
    F.regexp_replace("text", r"[\x00-\x1F\x7F]", ""))
 
# 읽기 시 인코딩 지정 (깨짐 방지)
df = spark.read.option("encoding", "UTF-8").text("path")
 
# 깨진 행이 많으면 분리·격리 (별도 글 "중첩 반정형 데이터"의 quarantine 패턴)

6. 성능 패턴

텍스트 처리에서 속도를 높이는 핵심은 비싼 작업을 최대한 늦게, 최소한의 데이터에만 적용하는 것입니다.

패턴효과
내장 정규식 함수UDF 대비 수배
split (구분자 명확 시)정규식보다 빠름
필터 먼저(rlike)처리량 축소 후 파싱
패턴 단순화backtracking 회피
컬럼 일찍 좁히기불필요 텍스트 제거
# 관심 있는 로그만 먼저 필터 → 그 다음 비싼 파싱
errors = df.filter(F.col("line").rlike(r"\bERROR\b"))
parsed = errors.select(F.regexp_extract(...))

먼저 rlike 로 대상을 줄이고 비싼 추출은 줄어든 데이터에만 적용하는 것이 핵심입니다.

7. 정리

영역핵심
함수 선택UDF 금지, 내장 정규식 함수
로그 파싱그룹 추출 또는 split(구분자 명확 시)
backtracking중첩 수량자·.* 남발 금지
NLP 전처리RegexTokenizer + StopWordsRemover
인코딩제어문자 제거, 깨진 행 격리
성능rlike 로 먼저 필터, 컬럼 일찍 좁히기

대규모 텍스트 처리의 핵심은 두 가지입니다. 첫째, JVM 내 내장 정규식 함수를 써서 UDF 직렬화 비용을 없애는 것. 둘째, catastrophic backtracking 을 일으키는 정규식을 피하는 것 — 수십억 건 중 단 몇 개의 악성 입력이 취약한 패턴을 만나면 잡 전체가 멈추기 때문입니다. 구분자가 명확하면 정규식보다 split 을, 비싼 파싱 전에는 rlike 필터로 데이터를 먼저 줄이는 습관이 텍스트 파이프라인을 빠르고 안정적으로 만듭니다.

마치며 — 핵심 요약

  • UDF 는 쓰지 마세요. 정규식 처리도 내장 함수(regexp_extract, regexp_replace, rlike)로 처리하면 JVM 직렬화 비용 없이 훨씬 빠릅니다.
  • 구분자가 명확하면 정규식보다 split 이 빠릅니다. 정규식이 꼭 필요한 곳에만 쓰세요.
  • 중첩 수량자는 재앙입니다. (a+)+ 같은 패턴 하나가 수십억 건 파이프라인을 통째로 멈출 수 있습니다.
  • rlike 로 먼저 걸러내세요. 비싼 추출 작업은 관심 대상이 된 적은 수의 행에만 적용합니다.
  • NLP 전처리는 MLlib 트랜스포머(RegexTokenizer, StopWordsRemover)가 분산 환경에 최적화되어 있습니다.
  • 인코딩 문제는 초반에 잡으세요. 읽기 시 인코딩 지정과 제어문자 제거를 파이프라인 첫 단계에 두면 이후 처리가 훨씬 안정적입니다.

텍스트는 지저분하고 정규식은 강력하지만 날카롭습니다. 올바른 도구와 올바른 패턴을 함께 쓰면, 수십억 건의 비정형 데이터도 충분히 다룰 수 있습니다.


이 글은 Spark 3.5 기준으로 작성되었습니다. 대규모 로그·텍스트 파싱 파이프라인 설계가 필요하시면 언제든 문의해 주세요.

— Data Dynamics 엔지니어링 팀