
Разберёмся что “под капотом” формата EPUB и как перевести текст, но не переводить код в книге. Познакомимся с библиотекой Ebook Lib, а также узнаем для чего нам понадобиться библиотека Beautiful Soup.
Занимаясь программированием, в русскоязычном сегменте интернета, столкнулся с тем, что много литературы на интересующие меня темы на английском языке. Либо есть перевод, но специфика отрасли такая, что все очень быстро меняется и если заграничные авторы книг исправно выпускают обновление, то перевод зачастую отстает на 2-3 года, что достаточно критично. Прекрасно понимаю, что такие книги и документацию необходимо уметь читать на английском языке, над чем я собственно усердно работаю. С другой стороны читая монументальную литературу на языке оригинала, все еще хочется открыть перевод в соседнем окне и свериться правильно ли ты уловил мысль автора.
Кажется, в чем проблема? Закинул PDF в любом переводчике, а то и в самом браузере перевод автоматический подтягивается, только такие переводчики в основном не распознают код в тексте. Тут возникает основная проблема, которая и сподвигла меня на поиск решения и автоматизации всего процесса. А для этого есть язык программирования Python.
Чем переводить
Для перевода текста я использовал библиотеку Googletrans и написал небольшую функцию, чтобы удобнее было пользоваться.
def translation_func(text):   
    translator = Translator()   
    result = translator.translate(text, dest='ru')   
    return result.textТак мы подходим к предмету нашего изучения, коим является один из самых популярных форматов электронных книг - EPUB. Все дело в том, что PDF не содержит никакой информации о параметрах текста. А вот EPUB включает в себя набор XHTML- или HTML-страниц, что существенно облегчает перевод текста по нужным нам параметрам.
Чтобы посмотреть структуру электронной книги я воспользовался программой Sigil - EPUB Editor.

Тут можно определить на какие части делится документ, его форматы (XHTML, HTML или PDF), а главное посмотреть разметку в каких тегах у нас содержится код и по каким признакам его можно будет исключить из перевода.
Вот пример таких тегов:
tag_exeption = ['code', 'a', 'strong', 'pre', 'span', 'html', 'div', 'body', 'head']Теперь, воспользуемся библиотекой Ebook Lib, примеры ее использования можно посмотреть здесь.
С помощью функции ebooklib.epub.read_epub() читаем файл и получаем экземпляр класса ebooklib.epub.EpubBook.
from ebooklib import epub 
book = epub.read_epub('book.epub')Все ресурсы в электронной книге (таблицы стилей, изображения, видео, звуки, скрипты и HTML-файлы) являются элементами. Их можно извлечь по типу с помощью функции ebooklib.epub.EpubBook.get_items_of_type().
Вот список элементов которые можно использовать:
- ITEM_UNKNOWN = 0
- ITEM_IMAGE = 1
- ITEM_STYLE = 2
- ITEM_SCRIPT = 3
- ITEM_NAVIGATION = 4
- ITEM_VECTOR = 5
- ITEM_FONT = 6
- ITEM_VIDEO = 7
- ITEM_AUDIO = 8
- ITEM_DOCUMENT = 9
- ITEM_COVER = 10
Мы воспользуемся методом book.get_items() который позволяет получать нам итератор по всем элементам книги - объекты ebooklib.epub.EpubItem. Для перевода нам нужны элементы навигации ITEM_NAVIGATION = 4 и главы книги которые содержатся в элементах ITEM_DOCUMENT = 9, чтобы их получить по типу используйте метод item.get_type().
for item in book.get_items():
    if item.get_type() == 4:
    …
    if item.get_type() == 9:
    …Также мы можем получить имя элемента item.get_name(), уникальный идентификатор для этого элемента item.get_id() и его содержимое item.get_content().
for item in book.get_items():
    if item.get_type() == ebooklib.ITEM_DOCUMENT:
        print('==================================')
        print('NAME : ', item.get_name())  
        print('----------------------------------')  
        print('ID : ', item.get_id())    
        print('----------------------------------')   
        print(item.get_content())    
        print('==================================')...
==================================
NAME :  Text/Chapter_6.xhtml
----------------------------------
ID :  Chapter_6
----------------------------------
b'<?xml version="1.0" encoding="utf-8"?>\r\n<ncx version="2005-1" xmlns="http://www.daisy.org/z3986/2005/ncx/">\r\n<head>\r\n
==================================
...Получив содержимое главы в формате XHTML, осталось отделить мух от котлет для этого нам поможет библиотека Beautiful Soup. Получаем объект soup:
soup = BeautifulSoup(item.get_content(), features="xml")Теперь нам нужно пробежаться по всем элементам внутри этого объекта, для этого будем использовать атрибут .descendants. Он хорош тем, что в отличие от атрибутов .contents и .children которые учитывают только прямых потомков, позволяет рекурсивно перебирать все дочерние элементы прямых дочерних элементов. Что из себя представляют такие элементы можно посмотреть используя атрибуты: .name - имя тега, .attrs - атрибуты тега (class, id) в формате словаря.
for child in soup.descendants:
   if child.name and child.string:
       print(child.name, '->', child.attrs)***
h1 -> {'class': 'chapterNumber'}
h1 -> {'class': 'chapterTitle', 'id': '_idParaDest-65'}
p -> {'class': 'normal'}
li -> {'class': 'bulletList'}
a -> {'href': 'https://github.com/example/tree/main/Chapter02'}
***Атрибут .descendants перебирает все отдельные элементы, которые содержит soup, в том числе и строки между тегов и пустые теги. Через условие отбираем нам нужные элементы, исключая tag_exeption, голый текст (child.name) и теги которые напрямую не содержащие текст (child.string). Полученный атрибутом .string текст переводим функцией translation_func() и потом присваиваем переведенный текст нашему дочернему элементу тем же атрибутом .string .
for child in soup.descendants:
    if child.name not in tag_exeption and child.name and child.string:
    	child.string = translation_func(child.string)Теги, которые не содержат на прямую текст отдельно прогоняем через атрибут .contents, исключая имена тегов (not content.name), пробелы и переносы ['\n', ' '].
elif not child.name in tag_exeption and child.name: #and count < 10:
    for content in child.contents:
        new_contents = []
        if content.string and content.string not in ['\n', ' '] and not content.name:
            translation_text = translation_func(content.string)
            content = NavigableString(translation_text)
            new_contents.append(content)
            new_contents.append(" ")
    child.clear()
    child.extend(new_contents)Beautiful Soup использует для хранения фрагментов текста класс NavigableString, переведенный текст делаем объектами этого класса, очищаем содержимое нашего потомка child.clear(), добавляем эти объекты в содержимое потомка используя child.extend(new_contents).
Осталось элементу book присвоить новый контент в виде нашего объекта soup, используя метод .set_content(), не забывая перекодировать.
item.set_content(soup.encode())Дополнительно, мне понравилось использовать просмотр контента элементов book в браузере с помощью метода .open_in_browser(contents) библиотеки lxml, для этого нужно предварительно перекодировать наш контент воспользовавшись утилитой из библиотеки Ebook Lib - utils.parse_string(item.get_content()).
from ebooklib import epub, utils
…
contents = utils.parse_string(item.get_content())
html.open_in_browser(contents)И последнее, что нам нужно - это сохранить переведенную книгу.
epub.write_epub('new_book.epub', book, {})Весь код выглядит вот так:
from googletrans import Translator
from ebooklib import epub, utils
from bs4 import BeautifulSoup, NavigableString
import lxml.html as html
def open_epub():
    tag_exeption = ["code", 'a', 'strong', 'pre', 'span', 'html',
                    'div', 'body', "head"]
    book = epub.read_epub('Django 4 By Example 2022.epub')
    for item in book.get_items():
        if item.get_id() == "Chapter_7":
            print('NAME : ', item.get_name())
            print('----------------------------------')
            print('ID : ', item.get_id())
            print('----------------------------------')
            print('ITEM : ', item.get_type())
            soup = BeautifulSoup(item.get_content(), features="xml")
            for child in soup.descendants:
                if child.name not in tag_exeption and child.name and child.string:
                    tag_text_before = child.string
                    translation_text = translation_func(tag_text_before)
                    child.string = translation_text
                elif not child.name in tag_exeption and child.name:
                    new_contents = []
                    class_attr = child.attrs.get('class')
                    for content in child.contents:
                        if content.string and content.string not in ['\n', ' '] and not content.name:
                            content = NavigableString(translation_func(content.string))
                            new_contents.append(content)
                            new_contents.append(" ")
                    child.clear()
                    child.extend(new_contents)
            item.set_content(soup.encode())
            contents = utils.parse_string(item.get_content())
            html.open_in_browser(contents)
            print('==================================')
    epub.write_epub('new_book.epub', book, {})
def translation_func(text):
    translator = Translator()
    result = translator.translate(text, dest='ru')
    return result.text
def main():
    open_epub()
if __name__ == "__main__":
    main()В дополнение еще нужно отметить про файлы CSS, в книги их можно прочитать в файлах типа ITEM_STYLE = 2 или посмотреть в программе Sigil - EPUB Editor в заголовках элементов книги,
а находятся они в папке Styles.
<head>
  <title>Example book</title>
  <link href="../Styles/epub.css" rel="stylesheet" type="text/css"/>
  <link href="../Styles/syntax-highlighting.css" rel="stylesheet" type="text/css"/>
</head>После перезаписи элементов книги ссылки на CSS в заголовке пропадают их можно вернуть с помощью программы Sigil - EPUB Editor, нужно выбрать все элементы книги в папке text и правой кнопкой в контекстном меню выбрать "Связать с таблицей стилей…”.
Это все! Наша книга готова!
В заключении хочется сказать, что автоматизировать процессы - интересно, повышает общую эрудицию, учит работать с разными библиотеками, что называется залезть под “под капот”, да и просто разнообразить рутину.
Комментарии (8)
 - Jury_7817.12.2022 18:49- один из самых популярных форматов электронных книг - EPUB - Мне нравится FictionBook (fb) - он чем то хуже?  - vassabi18.12.2022 00:32- мне fb не зашел, потому что мне хотелось иметь отдельные готовые странички для глав. Т.е. чтобы это были готовые html файлы. - А так - нормальный формат для текста, не хуже других. 
  - AlexanderAstafiev18.12.2022 06:27+4- Морально устарел. Представляет собой один гигантский xml-документ, и если книга реально большая, читалка может надолго повиснуть, читая весь документ целиком. В ePub же, если по уму, то книга делится на главы (по одной на файл) или еще как-то. Нет поддержки списков (нумерованных и ненумерованных), кода (pre/code), математических формул (если не картинками). ePub в теории поддерживает непосредственно mathml-код, хотя я не нашел читалки (бесплатной), умеющей этот код корректно отображать. В fb2 любые картинки кодируются в текст через base64, а в ePub они спокойно лежат себе как есть в контейнере (файл ePub это обычный zip-архив). 
 Вообще, fb2 это просто xml с определенным набором тегов, которых не всегда хватает для всех элементов книги. В ePub с этим гораздо лучше, если говорить о 3 версии стандарта. Другой вопрос — поддержка читалками ePub3.
 Плюс fb2 — специальные теги для семантической разметки стихов (<stanza> и еще какие-то там).
  - Akr0n18.12.2022 09:13+2- В дополнение к вышесказанному, благодаря тому, что формат представляет из себя zip-архив, файлы книг занимают гораздо меньше места по сравнению с аналогичными fb2. 
  - russarr18.12.2022 10:28+1- Я когда свой парсер электронных книг писал, тоже на epub остановился. Если у тебя есть html страница с текстом, там просто добавляешь ее к архиву книги. А на fb2 пришлось бы теги конвертировать. - Правда я тогда Ebook Lib не нашел, просто через jinja сделал шаблоны и заархивировал, вот тебе и epub книга.  - vassabi18.12.2022 16:57- а мне Ebook Lib не понравился - будете в следующий раз пробовать - посмотрите в сторону mkepub: https://pypi.org/project/mkepub/ - он ИМХО проще для понимания и использования 
 
 
 
           
 
pharo
У транслятора от гугеля огрaнаничение на 300 страниц и 10Мб размер файла и, если он больше этих ограничений, тогда это не такое приемлемое решение для непосредственного применения.
А, с другими сервисами перевода ещё больше ограничений и это, если формат PDF понравится транслятору, что бывает, в силу специфики самого форматa, не всегда.