티스토리 뷰

PDF 텍스트 검색 및 치환: PyMuPDF Pro로 간단하게 해결하기

PDF 편집은 오래전부터 개발자들에게 까다로운 작업이었지만, PyMuPDF Pro를 사용하면 훨씬 간단하게 처리할 수 있습니다.
회사 이름을 업데이트하거나, 오타를 수정하거나, 여러 문서에 걸쳐 오래된 정보를 교체해야 할 때 PyMuPDF Pro는 강력한 검색 및 치환 기능을 제공합니다.

PyMuPDF Pro란?

PyMuPDF Pro는 MuPDF의 파이썬 바인딩으로, 가볍고 빠른 PDF 툴킷입니다.
속도가 빠르고 메모리 효율이 뛰어나며, 텍스트 추출·렌더링·수정까지 다양한 기능을 제공합니다.
특히 일부 라이브러리처럼 새 문서를 생성하지 않고, 기존 PDF의 구조와 포맷을 유지한 채 직접 수정할 수 있다는 장점이 있습니다.


설치

아래와 같이 pip으로 손쉽게 설치할 수 있습니다:

pip install PyMuPDF

기본 텍스트 검색 및 치환

아래 예제는 PDF 안의 특정 텍스트를 찾아 다른 문자열로 교체하는 기본적인 방법을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import pymupdf
 
def search_and_replace_text(pdf_path, search_text, replace_text, output_path):
    # Open the PDF document
    doc = pymupdf.open(pdf_path)
    
    # Iterate through each page
    for page_num in range(len(doc)):
        page = doc[page_num]
        
        # Search for the text
        text_instances = page.search_for(search_text)
        
        # Replace each instance
        for inst in text_instances:
            # Get the rectangle containing the text
            rect = inst
            
            # Add a white rectangle to cover the old text
            page.draw_rect(rect, color=(111), fill=(111))
            
            # Insert the new text
            page.insert_text(rect.tl, replace_text, fontsize=12, color=(000))
    
    # Save the modified document
    doc.save(output_path)
    doc.close()
 
# Usage example
search_and_replace_text(
    "input.pdf"
    "Hello World",
    "Goodbye!",
    "output.pdf"
)
cs

고급 텍스트 검색 및 치환 (서식 유지)

앞의 기본 방법은 동작은 하지만 글꼴이나 크기 같은 원래 서식을 유지하지 못합니다. 아래 코드는 원래 텍스트의 속성을 추출해 최대한 비슷하게 치환하는 방식입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import fitz  # PyMuPDF
 
def advanced_search_and_replace(pdf_path, output_path, search_str, replace_str):
    doc = fitz.open(pdf_path)
 
    for page in doc:
        # 검색된 텍스트와 위치 정보 가져오기
        text_instances = page.search_for(search_str, quads=True)
 
        for inst in text_instances:
            # 글꼴 및 크기 정보 가져오기
            words = page.get_text("words")  # 단어 단위 텍스트 정보
            for w in words:
                if w[4== search_str:  # (x0, y0, x1, y1, word, block_no, line_no, word_no)
                    bbox = fitz.Rect(w[:4])
                    fontsize = w[3- w[1]  # 높이 기반으로 글자 크기 추정
                    break
 
            # 기존 텍스트 영역 흰색으로 덮기
            page.add_redact_annot(inst.rect, fill=(111))
        page.apply_redactions()
 
        for inst in text_instances:
            # 추출한 서식 기반으로 새 텍스트 추가
            page.insert_text(
                inst.rect.tl,  # 좌상단 좌표
                replace_str,
                fontsize=fontsize if 'fontsize' in locals() else 12,
                color=(000)
            )
 
    doc.save(output_path)
    doc.close()
 
# 사용 예시
advanced_search_and_replace("input.pdf""output_formatted.pdf""2023""2024")
cs
  • 참고사항
    여기서는 글꼴 이름(font name) 치환까지는 다루지 않았습니다.
    글꼴 이름을 유지하려면 추출된 글꼴 이름을 해당 PDF 내부의 참조 ID와 매칭하는 작업이 필요하며, 이는 조금 더 복잡한 절차가 요구됩니다. 자세한 내용은 PyMuPDF Pro 문서의 fontname 항목(영어)을 참고하세요.

여러 문자열 교체 처리

여러 개의 문자열을 한 번에 바꿔야 할 때는, 검색어와 교체어를 딕셔너리 형태로 받아 처리하는 방식이 효율적입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import pymupdf
 
def bulk_search_replace(pdf_path, replacements, output_path):
    """
    Replace multiple text strings in a PDF.
 
    Args:
        pdf_path: Path to input PDF
        replacements: Dictionary with search terms as keys and replacements as values
        output_path: Path for output PDF
    """
    doc = pymupdf.open(pdf_path)
 
    for page_num in range(len(doc)):
        page = doc[page_num]
 
        for search_text, replace_text in replacements.items():
            text_instances = page.search_for(search_text)
 
            for inst in text_instances:
                rect = inst
                page.draw_rect(rect, color=(111), fill=(111))
                page.insert_text(rect.tl, replace_text, fontsize=12)
 
    doc.save(output_path)
    doc.close()
 
# Usage example
replacements = {
    "Acme Corp""Super Corp",
    "2023""2024",
    "john@acme.com""john@supercorp.com"
}
 
bulk_search_replace("input.pdf", replacements, "output.pdf")
cs

대소문자 구분 없는 검색

PyMuPDF Pro의 search_for()는 기본적으로 대소문자를 구분합니다.
대소문자를 무시하고 검색하려면, 검색 범위를 직접 잡고 텍스트를 소문자(또는 대문자)로 변환해 비교하는 방식이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import pymupdf
 
def case_insensitive_replace(pdf_path, search_text, replace_text, output_path):
    doc = pymupdf.open(pdf_path)
 
    for page_num in range(len(doc)):
        page = doc[page_num]
 
        # Get all text on the page
        text_dict = page.get_text("dict")
 
        for block in text_dict["blocks"]:
            if "lines" in block:
                for line in block["lines"]:
                    for span in line["spans"]:
                        original_text = span["text"]
 
                        # Case-insensitive search
                        if search_text.lower() in original_text.lower():
                            # Find all occurrences (case-insensitive)
                            import re
                            pattern = re.compile(re.escape(search_text), re.IGNORECASE)
                            new_text = pattern.sub(replace_text, original_text)
 
                            if new_text != original_text:
                                bbox = span["bbox"]
                                rect = pymupdf.Rect(bbox)
 
                                # Replace text
                                page.draw_rect(rect, color=(111), fill=(111))
                                page.insert_text(
                                    rect.tl,
                                    new_text,
                                    fontsize=span["size"],
                                    color=span["color"]
                                )
 
    doc.save(output_path)
    doc.close()
 
# Usage example
case_insensitive_replace(
    "input.pdf",
    "HeLlo WoRlD",
    "Goodbye!",
    "output.pdf"
)
cs

정규식 지원

더 복잡한 패턴 매칭이 필요하다면 정규식을 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import pymupdf
import re
 
def regex_replace(pdf_path, pattern, replacement, output_path):
    """
    Replace text using regular expressions.
 
    Args:
        pattern: Regular expression pattern to search for
        replacement: Replacement string (can include group references like \1, \2)
    """
    doc = pymupdf.open(pdf_path)
    compiled_pattern = re.compile(pattern)
 
    for page_num in range(len(doc)):
        page = doc[page_num]
        text_dict = page.get_text("dict")
 
        for block in text_dict["blocks"]:
            if "lines" in block:
                for line in block["lines"]:
                    for span in line["spans"]:
                        original_text = span["text"]
                        new_text = compiled_pattern.sub(replacement, original_text)
 
                        if new_text != original_text:
                            bbox = span["bbox"]
                            rect = pymupdf.Rect(bbox)
 
                            page.draw_rect(rect, color=(111), fill=(111))
                            page.insert_text(
                                rect.tl,
                                new_text,
                                fontsize=span["size"]
                            )
 
    doc.save(output_path)
    doc.close()
 
# Example: Replace all email addresses
regex_replace(
    "input.pdf",
    r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
    "email@hidden.com",
    "output.pdf"
)
cs
  • 참고
    위 예시는 여러 줄에 걸쳐 있는 이메일 주소는 바꾸지 못합니다. 이런 특수한 경우가 없는지 반드시 결과를 확인하세요.

에러 처리와 모범 사례

실제 운영 환경에서는 반드시 적절한 에러 처리를 포함해야 합니다. 아래 코드는 PDF가 암호를 요구하는지 확인하고, 오류가 발생하면 예외를 발생시킵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import pymupdf
 
def safe_search_replace(pdf_path, search_text, replace_text, output_path):
    try:
        doc = pymupdf.open(pdf_path)
 
        if doc.is_encrypted:
            print("PDF is password protected")
            return False
 
        changes_made = False
 
        for page_num in range(len(doc)):
            page = doc[page_num]
            text_instances = page.search_for(search_text)
 
            if text_instances:
                changes_made = True
                for inst in text_instances:
                    rect = inst
                    page.draw_rect(rect, color=(111), fill=(111))
                    page.insert_text(rect.tl, replace_text, fontsize=12)
                    print("Text replacement made")
 
        if changes_made:
            doc.save(output_path)
            print(f"Successfully saved modified PDF to {output_path}")
        else:
            print(f"No instances of '{search_text}' found")
 
        doc.close()
        return True
 
    except Exception as e:
        print(f"Error processing PDF: {str(e)}")
        return False
 
 
# Usage example
safe_search_replace(
    "input.pdf",
    "Hello World",
    "Goodbye!",
    "output.pdf"
)
cs

그런데, 교체된 텍스트는 어떻게 될까요?

앞서 본 예제들은 찾은 텍스트 영역을 흰색 사각형으로 덮어서 가리는 방식이었습니다. 하지만 이렇게 하면 실제로는 텍스트가 여전히 문서 안에 남아 있어서, PDF 텍스트 추출 기능을 사용하면 원래 내용이 드러날 수 있습니다. 만약 교체 전에 텍스트를 완전히 삭제하고 싶다면 텍스트 삭제(Redaction) 기능을 사용해야 합니다.


교체 전에 텍스트 삭제하기

아래 예제는 기존 텍스트를 문서에서 완전히 제거한 뒤, 새로운 데이터로 교체하는 방법을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import pymupdf
 
def search_redact_and_replace_text(pdf_path, search_text, replace_text, output_path, fill_color=(111), text_color=(000), fontname="tiro", fontsize=14):
    # Open the PDF document
    doc = pymupdf.open(pdf_path)
 
    # Iterate through each page
    for page_num in range(len(doc)):
        page = doc[page_num]
 
        # Search for text instances
        text_instances = page.search_for(search_text)
 
        # Replace each instance
        for rect in text_instances:
 
            # Create redaction annotation
            redact_area = page.add_redact_annot(rect, text=replace_text,
                                              fill=fill_color, text_color=text_color, fontname=fontname, fontsize=fontsize)
 
            # Set additional properties
            redact_area.set_info(content=f"Redacted sensitive information")
            redact_area.update()
 
        page.apply_redactions()
 
    # Save the modified document
    doc.save(output_path)
    doc.close()
 
# Usage example
search_redact_and_replace_text(
    "input.pdf",
    "Hello World",
    "Goodbye!",
    "output.pdf"
)
cs

이 방법은 PyMuPDF Pro의 add_redaction 메서드를 활용해 기본 텍스트 옵션을 지정하는 방식입니다. 다만 실제 PDF의 스타일에 맞추려면 폰트 속성과 배경색을 직접 조정해야 합니다. 앞서 살펴본 예시들이 그 방법을 이해하는 데 도움이 될 수 있습니다.

텍스트 완전 삭제(Redaction)를 활용하면 단순히 텍스트를 가리는 것이 아니라 실제로 제거 후 교체하기 때문에 훨씬 안전하며, 검색·치환 작업 시 가장 권장되는 방법입니다.

  • 참고
    문서를 저장하면 텍스트 완전 삭제(Redaction) 대상으로 지정된 원본 텍스트는 완전히 삭제됩니다. 따라서 중요한 문서라면 반드시 원본 파일을 복사해 두고 작업하세요.

 

제한 사항과 고려해야 할 점

  • 폰트 매칭(Font Matching): 임베디드 글꼴이나 커스텀 폰트는 완벽히 매칭되지 않을 수 있습니다. 결과물을 반드시 확인하세요.
  • 레이아웃 유지(Layout Preservation): PDF는 워드 문서처럼 글자가 밀리면서 자동으로 줄바꿈·페이지 분할이 되지 않습니다. 교체된 텍스트가 원본보다 길면 바로 옆 단어를 침범할 수 있습니다.
  • 텍스트 인식(Text Recognition): PyMuPDF Pro는 실제 텍스트 객체만 다룰 수 있습니다. 이미지 기반 텍스트(스캔 PDF 등)는 교체할 수 없습니다.
  • 성능(Performance): 대용량 PDF나 다수 문서를 처리할 때는 페이지 단위로 나누거나 멀티프로세싱을 활용하는 것이 좋습니다.

결론

PyMuPDF Pro는 PDF 문서 내 텍스트 검색과 교체를 강력하고 효율적으로 지원합니다. 기본 기능은 간단하지만, 완벽한 서식 보존을 위해서는 폰트 속성과 텍스트 위치에 신경 써야 합니다.

레닥션 기반 교체는 보안상 안전한 방법이지만, 반드시 샘플 문서로 먼저 테스트하고, 중요한 문서는 원본 백업 후 진행하는 것이 좋습니다.