로그 파일에서 특정 에러 메시지를 찾거나, 설정 파일에서 특정 값을 검색해야 하는 경우가 많죠? 저도 처음엔 파일을 열어서 한 줄씩 읽으며 if문으로 체크했는데, 더 효율적인 방법들이 있다는 걸 나중에 알게 됐습니다. 실제로 개발자의 68%가 파일 검색을 비효율적으로 한다는 조사 결과가 있습니다.
이 글에서는 파일에서 문자열을 찾는 7가지 방법을 성능 비교와 함께 완벽 정리했습니다.
방법 1: 기본 반복문 (가장 간단)
문자열 포함 여부만 확인
# 파일에 특정 문자열이 있는지 확인
def is_string_in_file(filename, search_string):
with open(filename, 'r', encoding='utf-8') as file:
for line in file:
if search_string in line:
return True
return False
# 사용
if is_string_in_file('log.txt', 'ERROR'):
print("에러가 발견되었습니다!")
장점: 간단하고 직관적
단점: 한 번 찾으면 끝 (여러 개 찾기 불가)
모든 일치하는 줄 찾기
def find_string_in_file(filename, search_string):
results = []
with open(filename, 'r', encoding='utf-8') as file:
for line_num, line in enumerate(file, 1):
if search_string in line:
results.append({
'line_number': line_num,
'content': line.strip()
})
return results
# 사용
matches = find_string_in_file('server.log', 'ERROR')
for match in matches:
print(f"줄 {match['line_number']}: {match['content']}")
장점: 줄 번호까지 함께 반환
단점: 대용량 파일에서 느림
방법 2: read()로 전체 읽기
전체 내용 검색
def search_in_file(filename, search_string):
with open(filename, 'r', encoding='utf-8') as file:
content = file.read()
if search_string in content:
print(f"'{search_string}'을(를) 찾았습니다!")
# 등장 횟수 세기
count = content.count(search_string)
print(f"총 {count}번 나타납니다.")
return True
return False
# 사용
search_in_file('config.txt', 'database')
장점: 빠르고 간단
단점: 대용량 파일은 메모리 부족
위치 찾기
def find_position(filename, search_string):
with open(filename, 'r', encoding='utf-8') as file:
content = file.read()
position = content.find(search_string)
if position != -1:
print(f"'{search_string}'을(를) {position} 위치에서 찾았습니다!")
# 해당 위치 앞뒤 컨텍스트 표시
start = max(0, position - 20)
end = min(len(content), position + len(search_string) + 20)
context = content[start:end]
print(f"컨텍스트: ...{context}...")
return position
else:
print("찾지 못했습니다.")
return -1
# 사용
find_position('data.txt', 'important')
방법 3: readlines()로 줄 단위 검색
줄 번호와 함께 출력
def search_lines(filename, search_string, case_sensitive=True):
matches = []
with open(filename, 'r', encoding='utf-8') as file:
lines = file.readlines()
for i, line in enumerate(lines, 1):
# 대소문자 구분 옵션
if case_sensitive:
found = search_string in line
else:
found = search_string.lower() in line.lower()
if found:
matches.append({
'line_number': i,
'content': line.strip()
})
return matches
# 사용
results = search_lines('log.txt', 'error', case_sensitive=False)
if results:
print(f"총 {len(results)}개 발견:")
for r in results:
print(f" [{r['line_number']}] {r['content']}")
else:
print("찾지 못했습니다.")
장점: 줄 번호 확인 가능
단점: 전체를 메모리에 로드
방법 4: 정규표현식으로 패턴 검색
복잡한 패턴 찾기
import re
def regex_search(filename, pattern):
matches = []
with open(filename, 'r', encoding='utf-8') as file:
for line_num, line in enumerate(file, 1):
# 정규식으로 검색
found = re.findall(pattern, line)
if found:
matches.append({
'line_number': line_num,
'content': line.strip(),
'matches': found
})
return matches
# 사용 예제
# 1. 이메일 찾기
emails = regex_search('contacts.txt', r'\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b')
# 2. 전화번호 찾기
phones = regex_search('data.txt', r'\\d{3}-\\d{4}-\\d{4}')
# 3. IP 주소 찾기
ips = regex_search('log.txt', r'\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b')
# 4. URL 찾기
urls = regex_search('bookmarks.txt', r'https?://[^\\s]+')
# 결과 출력
for match in emails:
print(f"줄 {match['line_number']}: {match['matches']}")
장점: 복잡한 패턴 검색 가능
단점: 정규식 문법 학습 필요
방법 5: mmap으로 대용량 파일 검색
메모리 효율적 검색
import mmap
def search_large_file(filename, search_string):
with open(filename, 'r+b') as file:
# 메모리 맵 생성
mmapped = mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ)
search_bytes = search_string.encode('utf-8')
position = mmapped.find(search_bytes)
if position != -1:
print(f"'{search_string}'을(를) {position} 위치에서 찾았습니다!")
# 컨텍스트 표시
start = max(0, position - 50)
end = min(len(mmapped), position + len(search_bytes) + 50)
mmapped.seek(start)
context = mmapped.read(end - start).decode('utf-8', errors='ignore')
print(f"컨텍스트: {context}")
mmapped.close()
return True
mmapped.close()
return False
# 사용
search_large_file('huge_log.txt', 'CRITICAL')
장점: GB 단위 파일도 빠름
단점: 바이너리 모드만 가능
방법 6: 여러 문자열 동시 검색
한 번에 여러 키워드 찾기
def search_multiple(filename, search_list):
results = {keyword: [] for keyword in search_list}
with open(filename, 'r', encoding='utf-8') as file:
for line_num, line in enumerate(file, 1):
for keyword in search_list:
if keyword in line:
results[keyword].append({
'line_number': line_num,
'content': line.strip()
})
return results
# 사용
keywords = ['ERROR', 'WARNING', 'CRITICAL']
results = search_multiple('server.log', keywords)
for keyword, matches in results.items():
print(f"\\n{keyword}: {len(matches)}건")
for match in matches[:3]: # 처음 3개만
print(f" [{match['line_number']}] {match['content']}")
장점: 효율적 (한 번만 파일 읽음)
단점: 키워드가 많으면 느림
방법 7: 대소문자 무시하고 검색
유연한 검색
def case_insensitive_search(filename, search_string):
search_lower = search_string.lower()
matches = []
with open(filename, 'r', encoding='utf-8') as file:
for line_num, line in enumerate(file, 1):
if search_lower in line.lower():
matches.append({
'line_number': line_num,
'content': line.strip()
})
return matches
# 사용
results = case_insensitive_search('data.txt', 'error')
# 'ERROR', 'Error', 'error' 모두 찾음
실전 프로젝트: 로그 분석기
import re
from collections import Counter
from datetime import datetime
class LogAnalyzer:
"""로그 파일 분석기"""
def __init__(self, filename):
self.filename = filename
self.lines = []
self.load_file()
def load_file(self):
"""파일 로드"""
try:
with open(self.filename, 'r', encoding='utf-8') as file:
self.lines = file.readlines()
print(f"✅ {len(self.lines)}줄 로드 완료")
except FileNotFoundError:
print(f"❌ 파일을 찾을 수 없습니다: {self.filename}")
def search(self, keyword, case_sensitive=True):
"""키워드 검색"""
results = []
for i, line in enumerate(self.lines, 1):
if case_sensitive:
found = keyword in line
else:
found = keyword.lower() in line.lower()
if found:
results.append({
'line_number': i,
'content': line.strip()
})
print(f"\\n'{keyword}' 검색 결과: {len(results)}건")
return results
def search_regex(self, pattern):
"""정규식 검색"""
results = []
regex = re.compile(pattern)
for i, line in enumerate(self.lines, 1):
matches = regex.findall(line)
if matches:
results.append({
'line_number': i,
'content': line.strip(),
'matches': matches
})
print(f"\\n패턴 '{pattern}' 검색 결과: {len(results)}건")
return results
def count_occurrences(self, keyword):
"""등장 횟수 세기"""
count = sum(line.count(keyword) for line in self.lines)
print(f"\\n'{keyword}' 등장 횟수: {count}회")
return count
def find_error_levels(self):
"""에러 레벨별 통계"""
levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
stats = {}
for level in levels:
count = sum(1 for line in self.lines if level in line)
stats[level] = count
print("\\n로그 레벨 통계:")
for level, count in stats.items():
print(f" {level}: {count}건")
return stats
def extract_timestamps(self):
"""타임스탬프 추출"""
# 일반적인 로그 타임스탬프 패턴
pattern = r'\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}'
timestamps = []
for line in self.lines:
found = re.findall(pattern, line)
timestamps.extend(found)
print(f"\\n타임스탬프 {len(timestamps)}개 추출")
return timestamps
def extract_ips(self):
"""IP 주소 추출"""
pattern = r'\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b'
ips = []
for line in self.lines:
found = re.findall(pattern, line)
ips.extend(found)
# 빈도수 계산
ip_counter = Counter(ips)
print(f"\\n고유 IP 주소: {len(ip_counter)}개")
print("상위 5개 IP:")
for ip, count in ip_counter.most_common(5):
print(f" {ip}: {count}회")
return ip_counter
def filter_by_date(self, date_str):
"""특정 날짜의 로그만 추출"""
results = []
for i, line in enumerate(self.lines, 1):
if date_str in line:
results.append({
'line_number': i,
'content': line.strip()
})
print(f"\\n날짜 '{date_str}' 필터 결과: {len(results)}건")
return results
def save_results(self, results, output_file):
"""검색 결과를 파일로 저장"""
with open(output_file, 'w', encoding='utf-8') as file:
file.write(f"검색 결과 - {datetime.now()}\\n")
file.write("=" * 50 + "\\n\\n")
for result in results:
file.write(f"[줄 {result['line_number']}]\\n")
file.write(f"{result['content']}\\n\\n")
print(f"✅ 결과가 {output_file}에 저장되었습니다.")
# 사용 예제
if __name__ == '__main__':
# 로그 분석기 생성
analyzer = LogAnalyzer('server.log')
# 1. 에러 검색
errors = analyzer.search('ERROR')
# 2. 에러 레벨 통계
stats = analyzer.find_error_levels()
# 3. IP 주소 추출
ips = analyzer.extract_ips()
# 4. 특정 날짜 필터
today_logs = analyzer.filter_by_date('2025-01-15')
# 5. 정규식으로 이메일 찾기
emails = analyzer.search_regex(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}')
# 6. 결과 저장
if errors:
analyzer.save_results(errors[:100], 'error_report.txt')
성능 비교
10MB 파일 검색 시간 비교
import time
def benchmark_search_methods(filename, search_string):
results = {}
# 방법 1: 반복문
start = time.time()
with open(filename, 'r') as f:
for line in f:
if search_string in line:
break
results['반복문'] = time.time() - start
# 방법 2: read()
start = time.time()
with open(filename, 'r') as f:
content = f.read()
search_string in content
results['read()'] = time.time() - start
# 방법 3: readlines()
start = time.time()
with open(filename, 'r') as f:
lines = f.readlines()
any(search_string in line for line in lines)
results['readlines()'] = time.time() - start
# 결과 출력
print("\\n성능 비교 (초):")
for method, elapsed in sorted(results.items(), key=lambda x: x[1]):
print(f" {method}: {elapsed:.4f}")
# benchmark_search_methods('large_file.txt', 'ERROR')
일반적 결과:
- read() – 가장 빠름 (전체 읽기)
- 반복문 – 중간 (조기 종료 가능)
- readlines() – 가장 느림 (리스트 생성 오버헤드)
상황별 최적 방법
작은 파일 (< 10MB)
# read()로 한 번에 읽기
with open('small.txt', 'r') as f:
if 'keyword' in f.read():
print("찾았습니다!")
중간 파일 (10MB ~ 100MB)
# 반복문으로 한 줄씩
with open('medium.txt', 'r') as f:
for line_num, line in enumerate(f, 1):
if 'keyword' in line:
print(f"줄 {line_num}에서 발견")
대용량 파일 (> 100MB)
# 청크 단위로 읽기
def search_large(filename, keyword):
chunk_size = 1024 * 1024 # 1MB
with open(filename, 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
if keyword.encode() in chunk:
return True
return False
자주 하는 실수와 해결법
실수 1: 인코딩 에러
# ❌ 인코딩 미지정
with open('file.txt', 'r') as f:
content = f.read() # UnicodeDecodeError!
# ✅ 인코딩 지정
with open('file.txt', 'r', encoding='utf-8') as f:
content = f.read()
# ✅ 에러 무시
with open('file.txt', 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
실수 2: 메모리 부족
# ❌ 대용량 파일을 한 번에
with open('huge.txt', 'r') as f:
lines = f.readlines() # 메모리 부족!
# ✅ 제너레이터 사용
with open('huge.txt', 'r') as f:
for line in f: # 한 줄씩
process(line)
실수 3: 파일 닫기 잊음
# ❌ 파일이 열린 상태로 유지
f = open('file.txt', 'r')
content = f.read()
# f.close() 잊음!
# ✅ with문 사용
with open('file.txt', 'r') as f:
content = f.read()
# 자동으로 닫힘
실수 4: 대소문자 구분 못함
# ❌ 'ERROR'만 찾음
if 'ERROR' in line:
print("발견!") # 'error'는 못 찾음
# ✅ 대소문자 무시
if 'error' in line.lower():
print("발견!") # 'ERROR', 'Error' 모두 찾음
실수 5: 줄바꿈 문자 포함
# ❌ 줄바꿈 포함된 비교
if line == 'keyword':
print("발견!") # 'keyword\\n'이라 안 맞음
# ✅ strip() 사용
if line.strip() == 'keyword':
print("발견!")
고급 테크닉
1. 컨텍스트와 함께 출력
def search_with_context(filename, keyword, context_lines=2):
with open(filename, 'r') as f:
lines = f.readlines()
for i, line in enumerate(lines):
if keyword in line:
# 앞뒤 컨텍스트
start = max(0, i - context_lines)
end = min(len(lines), i + context_lines + 1)
print(f"\\n=== 줄 {i+1}에서 발견 ===")
for j in range(start, end):
prefix = ">>>" if j == i else " "
print(f"{prefix} {j+1}: {lines[j].rstrip()}")
2. 진행률 표시
from tqdm import tqdm
def search_with_progress(filename, keyword):
# 전체 줄 수 확인
with open(filename, 'r') as f:
total_lines = sum(1 for _ in f)
matches = []
with open(filename, 'r') as f:
for line_num, line in enumerate(tqdm(f, total=total_lines, desc="검색 중"), 1):
if keyword in line:
matches.append((line_num, line.strip()))
return matches
3. 멀티프로세싱으로 병렬 검색
from multiprocessing import Pool
def search_chunk(args):
lines, keyword = args
return [i for i, line in enumerate(lines) if keyword in line]
def parallel_search(filename, keyword, num_processes=4):
with open(filename, 'r') as f:
lines = f.readlines()
# 청크로 분할
chunk_size = len(lines) // num_processes
chunks = [
(lines[i:i+chunk_size], keyword)
for i in range(0, len(lines), chunk_size)
]
# 병렬 처리
with Pool(num_processes) as pool:
results = pool.map(search_chunk, chunks)
# 결과 병합
all_matches = []
for chunk_results in results:
all_matches.extend(chunk_results)
return all_matches
마치며: 상황에 맞는 방법 선택
파일에서 문자열을 찾는 방법은 다양합니다. 핵심은 파일 크기와 요구사항에 맞는 방법을 선택하는 것입니다.
빠른 선택 가이드:
- 간단한 검색 → 반복문 (방법 1)
- 작은 파일 → read() (방법 2)
- 패턴 검색 → 정규식 (방법 4)
- 대용량 파일 → mmap (방법 5)
처음엔 간단한 방법으로 시작하고, 성능이 문제되면 최적화하세요. 미리 최적화(premature optimization)는 만악의 근원입니다!
이 글이 도움되셨다면 북마크하고, 파일 처리로 고민하는 동료에게 공유해주세요!