파이썬 파일에서 문자열 찾기 완벽 가이드

로그 파일에서 특정 에러 메시지를 찾거나, 설정 파일에서 특정 값을 검색해야 하는 경우가 많죠? 저도 처음엔 파일을 열어서 한 줄씩 읽으며 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')

일반적 결과:

  1. read() – 가장 빠름 (전체 읽기)
  2. 반복문 – 중간 (조기 종료 가능)
  3. 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)는 만악의 근원입니다!

이 글이 도움되셨다면 북마크하고, 파일 처리로 고민하는 동료에게 공유해주세요!


참고 자료

댓글 남기기