Blog
pysparksparkregextext-processingnlpdata-engineering

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

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

Data Dynamics2026년 6월 5일8 min read

웹 로그, 애플리케이션 로그, 사용자 리뷰, 비정형 텍스트 — 데이터의 상당수는 구조가 없습니다. 이걸 분석하려면 정규식으로 파싱하고, 토큰화하고, 정규화해야 합니다. 수십억 건 규모에서 이 텍스트 처리는 의외로 무거운 작업이고, 정규식 하나를 잘못 쓰면 잡 전체가 멈춥니다(catastrophic backtracking).

이 글은 PySpark 로 대규모 텍스트를 정규식으로 처리할 때의 성능 함정과, 내장 함수로 빠르게 처리하는 패턴, 그리고 토큰화·정규화·인코딩 문제 대응을 정리합니다.

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 액세스 로그 파싱
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

정규식의 특정 패턴은 입력에 따라 지수적 시간이 걸립니다. 한 워커가 한 행에서 영원히 멈춰, 잡이 끝나지 않습니다.

위험 패턴: 중첩된 수량자 (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 필터로 데이터를 먼저 줄이는 습관이 텍스트 파이프라인을 빠르고 안정적으로 만듭니다.


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

— Data Dynamics 엔지니어링 팀