PySpark 대규모 텍스트·정규식 처리 — 수십억 로그를 파싱하기
비정형 로그·텍스트 수십억 건을 정규식으로 파싱하고 정제할 때의 성능 함정. catastrophic backtracking, UDF 대신 내장 정규식 함수, 토큰화·정규화, 그리고 깨진 인코딩 처리까지 PySpark 패턴으로 정리합니다.
웹 로그, 애플리케이션 로그, 사용자 리뷰, 비정형 텍스트 — 데이터의 상당수는 구조가 없습니다. 이걸 분석하려면 정규식으로 파싱하고, 토큰화하고, 정규화해야 합니다. 수십억 건 규모에서 이 텍스트 처리는 의외로 무거운 작업이고, 정규식 하나를 잘못 쓰면 잡 전체가 멈춥니다(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-gram | NGram |
| 벡터화 | 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 엔지니어링 팀