[가상생명연구팀 김석겸] 이 글에서 소개 드릴 프로젝트의 주제는 “파일 번역” 입니다.
번역 모델을 개발하기 앞서 기존에 서비스 중인 번역 서비스들을 살펴 보았습니다. 그 중에 눈에 띈 것이 “파일 번역” 입니다.
파일 번역은 어떤 서비스인가?
파일 번역은 문서화된 워드 파일, 엑셀 파일 혹은 ppt 파일을 입력으로 받아서 목표로 하는 언어로 번역하고 이미지나 표, 글의 스타일 등은 유지하는 서비스입니다. (배치로 처리하기 위한 덤프용 파일이 아닙니다.)
어떤 서비스인지는 보여 드리는 게 가장 직관적이라 아래 영상을 참조해 주시기 바랍니다.
위 사이트는 NLP 관련 API 들을 사용해 볼 수 있는 데모 모음 사이트 내에서 파일 번역 페이지입니다.
현재는 한 <-> 영 언어를 지원하고, 파일 포맷은 워드와 엑셀 파일을 지원합니다.
용어집 기능은 placeholding 토큰을 학습 문장에 반영해서 필요에 따라 특정 용어 매핑을 학습에 반영하지 않고도 추론 시 활용할 수 있도록 해줍니다.
스마일게이트와 같이 게임 업계는 캐릭터 혹은 스킬처럼 새로운 단어들이 자주 나타나다 보니 이를 매번 학습하는 것보다는 용어집 기능을 활용하는 것이 더 효율적입니다.
다음은 이번 개발을 하면서 어려웠던 점들 위주로 어떻게 해결했는지 풀어보도록 하겠습니다.
무엇이 어려웠나?
1. 단연 스타일 복제
파일 번역의 핵심은 스타일을 복제한다는 것입니다. 문제는 워드나 엑셀을 완벽하게 파싱하고 다시 파일을 구성할 수 있는 라이브러리가 없습니다.
워드 파일을 위해서는 python-docx 를, 엑셀 파일을 위해서는 openpyxl 을 사용하고 있습니다만 둘 다 각 파일을 처리하는데 제한이 있습니다.
파일의 서브 컴포넌트를 하나씩 복제해서 스타일을 붙이는 것이 되는 것도 있고 아닌 것도 있습니다.
바꿀 부분만 바꾸자
리액트를 배울 때 보니 virtual DOM 이라는 개념을 접하였습니다. 가상의 DOM 을 띄우고 바뀐 부분만 변경하여 전체를 리로딩하는 수고를 줄여준다는 것입니다.
그 목적이 다르긴 하지만 바뀌지 않는 부분에 대해서는 바꾸지 않기 위해서 입력한 파일을 기준으로 입력 파일 변수와 출력 파일 변수를 미리 생성해두었습니다.
입력 파일 인스턴스에서 번역할 텍스트를 찾아 출력 파일 인스턴스에 변경할 부분만 교체하고 나머지는 그대로 두는 방식입니다.
이 방법이 잘 먹히는 부분이 있고 안 되는 부분도 있지만 기본적으로는 개발 부담을 줄일 수 있습니다.
다음은 엑셀 파일 예시입니다. (워드 파일도 동일하게 진행했습니다.)
from openpyxl import load_workbook
in_wb = load_workbook(input_file)
out_wb = load_workbook(input_file)
또 다른 스타일 복제 문제는 같은 문단에서 다른 스타일을 적용한 것이 있다는 것입니다. 예를 들면 “안녕하세요 저는 김석겸 입니다.” 에서 “김석겸”에만 볼드체가 적용되어 있습니다.
볼드체 부분을 구분할 수 있습니다. 다만 번역을 문장 전체로 하지 않으면 전혀 다른 의미의 번역문이 나올 수 있어서 번역은 문장 혹은 문단 단위로 진행하는 것이 좋습니다. 문제는 번역은 전체로 진행하는데 스타일 복제만 부분으로 하는 것이 꽤나 까다로운 작업이라는 것 입니다. 위의 예제는 그나마 명사로 되어 있어서 번역 시에 찾을 수도 있겠지만 그 외의 품사는 번역문으로 정확히 구분하기 어려울 수 있습니다.
이 문제에 대해서 해결보다는 절충안을 선택했습니다. 예를 들어, 워드 파일의 paragraph 안의 run 단위로 번역을 하는 것이 아니라 paragraph 단위로 번역을 하고 스타일은 첫 번째 run의 스타일을 해당 paragraph에 적용하는 것입니다.
(paragraph 의 하위 컴포넌트는 run 이고, run은 동일한 스타일을 가진다.)
2. 형태별 순서 지키기
이 애로사항은 워드 파일에 대한 부분입니다. Document 인스턴스의 주요 컴포넌트는 문단(Document.paragraphs)과 테이블(Document.tables) 입니다.
문단 컴포넌트와 테이블 컴포넌트는 한 번에 순서대로 내용을 구별할 수 있는 메서드가 없습니다.
문제는 문단과 테이블이 섞인 워드 파일을 앞 부분엔 문단만, 뒷 부분엔 테이블만 임의로 배치하면 파일 번역의 취지에 벗어나는 것입니다.
다 합쳐서 id로 정렬하기
from docx import Document
from docx.table import Table
from docx.text.paragraph import Paragraph
input_doc = Document(input_file)
# 리스트로 합치기
in_elements = input_doc.paragraphs + input_doc.tables
# elements를 문서에서의 순서대로 정렬
in_elements.sort(key=lambda x: x._element.getparent().index(x._element))
# 순서대로 확인
for in_element in in_elements:
if isinstance(in_element, Paragraph):
(코드)
elif isinstance(in_element, Table):
(코드)
3. 처리 속도
순서대로 처리하는 프로세스다 보니 번역에 소요되는 시간이 많이 들어갔습니다. 그나마 워드 파일은 paragraph 단위로 번역하여 추론 시간이 짧은데 비해 엑셀 파일은 처음에 셀 단위로 번역을 하니 추론 시간이 오래 걸렸습니다.
행 단위 배치 처리
제가 만든 번역 모델은 배치로 입력을 처리할 수 있기 때문에 엑셀의 행 단위로 데이터를 배치로 받아서 처리하였습니다.
4. 하이퍼링크
워드에서 하이퍼링크 추출은 python-docx가 공식적으로 지원하는 부분은 아닙니다. 다만 xml 을 가져오는 부분을 통해 링크를 추출할 수 있습니다.
다음은 다양한 상황에서 쓰일 수 있게 여러 함수를 소개하겠습니다.
1. paragraph에 하이퍼링크가 있는지 확인하는 함수
import xml.etree.ElementTree as ET
def contains_hyperlink(paragraph):
xml = ET.fromstring(paragraph._p.xml)
return bool(xml.find('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}hyperlink'))
2. 하이퍼링크가 포함된 paragraph에 있는 text와 url 을 추출하는 함수
def extract_text_and_links(paragraph):
result = []
hyperlink = []
# 모든 자식 요소를 순환한다
for element in paragraph._element.getchildren():
# hyperlink가 있는지 확인하고 있으면 url과 text를 리스트에 추가한다.
if "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}hyperlink" in element.tag:
hyperlink_text = "".join([e.text for e in element.iter() if "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}t" in e.tag])
r_id = element.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id")
hyperlink.append(paragraph.part.rels[r_id]._target)
result.append(hyperlink_text)
# 텍스트만 있는 경우 텍스트만 추가한다.
elif "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}r" in element.tag:
for sub_element in element:
if "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}t" in sub_element.tag:
result.append(sub_element.text)
# 텍스트와 URL 반환
# 텍스트 리스트는 하나의 텍스트 시퀀스로 반환
return "".join(result), hyperlink
3. paragraph에 하이퍼링크가 포함된 run 추가하는 함수
def add_hyperlink(paragraph, url, text, color="blue", underline=True):
# This gets access to the document.xml.rels file and gets a new relation id value
part = paragraph.part
r_id = part.relate_to(url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True)
# Create the w:hyperlink tag and add needed values
hyperlink = docx.oxml.shared.OxmlElement('w:hyperlink')
hyperlink.set(docx.oxml.shared.qn('r:id'), r_id, )
# Create a w:r element
new_run = docx.oxml.shared.OxmlElement('w:r')
# Create a new w:rPr element
rPr = docx.oxml.shared.OxmlElement('w:rPr')
# Add color if it is given
if color:
c = docx.oxml.shared.OxmlElement('w:color')
c.set(docx.oxml.shared.qn('w:val'), color)
rPr.append(c)
# Remove underlining if it is requested
if underline:
u = docx.oxml.shared.OxmlElement('w:u')
u.set(docx.oxml.shared.qn('w:val'), 'single')
rPr.append(u)
# Join all the xml elements together add add the required text to the w:r element
new_run.append(rPr)
new_run.text = text
hyperlink.append(new_run)
paragraph._p.append(hyperlink)
return hyperlink
디테일하게는 다른 문제들도 많았지만 중요하다고 생각하는 부분은 위와 같았습니다.
마침
모델링 위주로 작업하다가 이런 실용적인 서비스를 개발하면서 엔드 유저의 편의성 증대는 모델 성능만이 다가 아니라는 점을 느꼈습니다. 위 작업을 하면서 제대로 된 레퍼런스가 없어서 난감한 적이 여러 번 있었는데 번역이 아니더라도 워드 파일 등을 다루면서 어려움을 느끼시는 분들께 도움이 되는 글이었으면 합니다.
감사합니다.
References
- https://python-docx.readthedocs.io/en/latest/
- https://openpyxl.readthedocs.io/en/stable/
- https://wikidocs.net/91661