본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 20. 12:10

Python으로 EPUB 파일 파싱 및 재구축하기: AI 번역 서비스를 구축하며 얻은 교훈

요약

LLM을 활용한 AI 전자책 번역 서비스 구축 과정에서 겪은 EPUB 파싱 및 재구축 기술적 과제와 해결 방법을 다룹니다. 기존 라이브러리의 한계를 극복하기 위해 lxml을 병행 사용하여 서식과 메타데이터를 보존하는 파이프라인을 구축하는 과정을 설명합니다.

핵심 포인트

  • 기존 ebooklib 라이브러리의 메타데이터 손실 및 메모리 문제 식별
  • lxml을 활용한 정밀한 XML 제어로 XHTML 구조 및 네임스페이스 보존
  • 텍스트 노드와 부모 요소 간 매핑을 통한 서식 유지 번역 전략
  • EPUB의 구조적 요소(OPF, Spine, TOC)를 온전하게 재구축하는 파이프라인

모든 세부 사항을 보존하면서 Python을 사용하여 전체 전자책을 추출, 번역 및 재구성하는 방법

LectuLibre에서 우리는 대규모 언어 모델 (LLM)을 사용하여 전체 도서를 번역하는 서비스를 구축했습니다. 사용자가 EPUB 파일을 업로드하면, 우리의 백엔드 파이프라인은 이를 파싱(parsing)하고, 텍스트를 추출하며, 번역을 위해 LLM으로 전송한 다음, 원래의 서식, 이미지 및 메타데이터를 모두 보존하면서 번역된 콘텐츠로 EPUB를 재구축합니다. 실제 EPUB 내부를 들여다보기 전까지는 이 과정이 간단해 보였습니다.

EPUB는 본질적으로 구조화된 XHTML, CSS 및 XML 파일 세트를 포함하는 ZIP 파일입니다. content.opf 파일은 읽기 순서(spine), 메타데이터 및 매니페스트(manifest)를 정의합니다. toc.ncx는 목차(table of contents)를 보유합니다. 실제 텍스트는 종종 장(chapter)별로 나뉜 XHTML 문서에 들어 있습니다. 책을 번역하기 위해서는 다음 작업이 필요했습니다: 1) EPUB를 안정적으로 파싱하기, 2) 번역 가능한 모든 텍스트를 찾기, 3) 이를 청크(chunk) 단위로 LLM에 전송하기, 4) 서식의 모든 바이트를 온전하게 유지하면서 번역된 텍스트로 EPUB를 재구축하기.

기성 라이브러리의 문제점

우리는 처음에 EPUB 조작을 위해 가장 인기 있는 Python 라이브러리인 ebooklib을 사용했습니다. 단순한 EPUB에는 잘 작동했지만, 수백 개의 실제 파일을 투입하자 곧 문제에 직면했습니다:

  • 메타데이터 손실: ebooklib은 OPF 내의 사용자 정의 메타데이터나 네임스페이스 접두사가 붙은 속성(namespace-prefixed properties)을 완전히 보존하지 못했습니다.
  • 네임스페이스 처리: XHTML을 수정할 때 xmlns 속성을 제거하거나 망가뜨려 일부 기기에서의 렌더링을 깨뜨릴 수 있었습니다.
  • TOC 및 spine 동기화: 재구축 후, 수동으로 복구하지 않으면 목차(TOC)와 spine이 어긋나는 경우가 빈번했습니다.
  • 대용량 파일: ebooklib은 모든 것을 한꺼번에 로드하기 때문에 200개 장으로 구성된 책을 처리할 때 놀라울 정도로 많은 메모리를 소비했습니다.

Calibre의 명령줄 인터페이스(command-line interface)와 같은 무거운 도구를 사용할 수도 있었지만, 이는 외부 의존성(external dependencies)을 유발하며 프로그래밍적으로 유연하지 않았습니다. 대신, 우리는 상위 수준의 도서 구조를 위해 ebooklib을 유지하면서, 정밀한 XML 제어를 위해 lxml을 추가하여 보완하기로 결정했습니다.

우리의 파싱 및 재구축 파이프라인 (Parsing and Rebuilding Pipeline)

우리가 최종적으로 결정한 핵심 접근 방식은 다음과 같습니다:

  1. EPUB 읽기: ebooklib을 사용하여 아이템(문서, 이미지, CSS) 목록을 가져옵니다.
  2. 번역 가능한 콘텐츠 식별: 주로 ITEM_DOCUMENT (XHTML)이며, 때로는 ITEM_NAVIGATION (제목을 위한 NCX)을 포함합니다.
  3. 각 XHTML 문서 파싱: lxml로 각 XHTML 문서를 파싱하여 텍스트를 추출하는 동시에, 각 텍스트 노드와 그 부모 요소 간의 매핑(map)을 유지합니다.
  4. 텍스트 블록 전송: 순서와 문맥을 보존하며 LLM에 텍스트 블록을 번역 요청합니다.
  5. XHTML 재구축: 저장된 매핑을 사용하여 원래의 텍스트 노드를 번역된 텍스트로 교체함으로써 XHTML을 재구축합니다.
  6. 새로운 EPUB 쓰기: ebooklib을 사용하여 새로운 EPUB을 작성하며, OPF와 스파인(spine)이 올바른지 수동으로 확인합니다.

코드를 자세히 살펴보겠습니다.

1단계: 아이템 읽기 및 필터링

import ebooklib
from ebooklib import epub

...

이미지, 폰트, CSS는 번역 가능한 텍스트를 포함하지 않으므로 무시합니다.

2단계: 문맥과 함께 텍스트 추출하기

텍스트가 정확히 어디에서 왔는지 기억하면서 추출해야 합니다. 우리는 lxml.etree를 사용하여 XHTML을 파싱하고 트리를 순회하며, 텍스트 노드와 그 XPath 위치를 수집합니다:

from lxml import etree

def extract_text_with_xpath(content):
...

tail 텍스트에 주의하십시오. 이는 닫는 태그 뒤에 오는 텍스트로, 마크업이 섞여 있는 경우 흔히 나타납니다. 이를 놓치면 문장이 유실될 수 있습니다.

3단계: 청크 단위 번역

우리는 수집된 텍스트 노드들을 LLM 토큰 제한을 준수하는 청크 (chunk) 단위로 배치 (batch) 합니다. 예를 들어, 동일한 XHTML 문서 내의 연속된 텍스트를 그룹화하여 배치당 약 3,000 토큰을 목표로 합니다. 그런 다음 각 청크를 번역 모델 (예: Claude 3.5 Sonnet)로 보내 번역된 텍스트 블록을 받습니다. 우리는 길이를 비교하여 번역된 블록을 다시 개별 문자열로 분할합니다 (심화 단계: 원문과 번역된 문장을 정렬하기 위해 diff 알고리즘을 사용합니다). 여기서는 간결함을 위해 단순화하여 설명했습니다.

4단계: 원본 XHTML 내 텍스트 교체

이제 번역된 내용을 다시 매핑합니다:

for (xpath, original, elem), translated_text in zip(text_mapping, translations):
    # xpath를 사용하여 요소를 다시 찾습니다 (원본에서 새로 파싱됨)
    # 하지만 요소 객체를 캐싱해 두었으므로, 바로 업데이트할 수 있습니다
...

수정된 XHTML을 문자열로 반환하며, 이는 EPUB 내 항목의 콘텐츠를 교체할 준비가 된 상태입니다.

5단계: EPUB 재구축

여기서 ebooklib의 진가가 발휘됩니다. 새로운 EpubBook을 생성하고, 동일한 메타데이터 (제목, 저자, 언어)를 설정한 뒤 항목들을 추가합니다:

new_book = epub.EpubBook()
new_book.set_identifier(original_book.get_metadata('DC', 'identifier')[0][0])
new_book.set_title(original_book.get_metadata('DC', 'title')[0][0])
...

하지만 잠깐—이러한 단순한 접근 방식은 OPF를 손상시킬 수 있습니다. 우리는 원본에 복잡한 중첩 구조가 있는 경우 ebooklib이 때때로 스파인 (spine) 순서를 잘못 재작성한다는 것을 발견했습니다. 이를 해결하기 위해, lxml을 사용하여 작성된 EPUB의 content.opf를 수동으로 후처리합니다:

import zipfile
from lxml import etree

...

네, 투박한 방식이지만, 이 방법 덕분에 수많은 유효성 검사 (validation) 오류를 피할 수 있었습니다.

성능 및 실제 수치

우리는 일반적인 소설을 대상으로 벤치마크를 수행했습니다: 50개 장, 압축 전 350KB 기준. 텍스트 파싱 및 추출: 약 0.2초. 번역 후 재구축: 약 0.3초. LLM 번역 단계가 전체 시간을 지배합니다 (책 한 권 전체에 약 45초 소요). 따라서 우리는 대신 그 부분에 대한 병렬 처리 (parallelism) 작업에 집중했습니다.

하지만 수백 개의 이미지와 복잡한 표를 포함하는 더 큰 교육용 텍스트의 경우, 메모리 사용량이 500MB 이상으로 급증했습니다. 우리는 문서를 하나씩 처리하고 즉시 메모리에서 해제함으로써 이 문제를 완화했습니다.

주요 교훈 (Key Lessons Learned)

  1. 네임스페이스 (Namespaces)는 악마와 같습니다: <html> 태그의 xmlns="http://www.w3.org/1999/xhtml" 및 모든 사용자 정의 네임스페이스를 항상 보존해야 합니다. Lxml의 etree.tostring()method='html'로 사용할 경우, 명시적으로 다시 추가하지 않으면 이를 누락할 수 있습니다.
  2. 검증하고, 검증하고, 또 검증하세요: 재구축 후에는 문제를 포착하기 위해 (Python subprocess를 통해) epubcheck를 실행합니다. 사용자 정의 메타데이터로 인한 오탐(False positives)이 발생하나요? 수동 검토 후 화이트리스트(whitelist)에 추가합니다.
  3. 모든 것을 라이브러리에만 의존하지 마세요: ebooklib은 읽기에는 훌륭하지만, 쓰기 작업의 경우 규격 준수를 보장하기 위해 우리가 직접 OPF 및 NCX 조작을 많이 수행해야 했습니다.
  4. 인코딩을 사전에 처리하세요: 일부 오래된 EPUB는 Latin-1을 사용합니다. 나중에 발생할 수 있는 충돌을 방지하기 위해 파이프라인 초기 단계에서 모든 것을 UTF-8로 트랜스코드 (transcode) 합니다.
  5. DRM는 막다른 길입니다: META-INF/encryption.xml 내의 <encryption> 요소를 확인하여 암호화된 도서를 감지하고, 이를 우아하게 거부(reject)합니다.

커뮤니티를 위한 열린 질문

다른 분들은 운영 환경에서 복잡한 EPUB 조작을 어떻게 관리하고 계신지 알고 싶습니다. ebooklib보다 더 견고한 라이브러리를 찾으셨나요? 번역할 때 상호작용형 EPUB3 요소(Javascript, 양식 필드)는 어떻게 처리하시나요? 저희는 여전히 파이프라인을 반복 개선 중이며, 여러분의 실전 경험담(battle stories)을 환영합니다.

유사한 문제를 해결하고 있거나 직접 eBook 번역을 시도해보고 싶다면, LectuLibre에서 이 작업의 결과물을 확인하실 수 있습니다. 하지만 무엇보다도, 이 심층 분석이 다음에 여러분이 EPUB 내부 구조를 다뤄야 할 때 몇 번의 밤샘 작업을 줄여줄 수 있기를 바랍니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0