Разберёмся что “под капотом” формата 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)


  1. pharo
    17.12.2022 18:21
    +1

    Кажется, в чем проблема? Закинул PDF в любом переводчике, а то и в самом браузере перевод автоматический подтягивается,

    У транслятора от гугеля огрaнаничение на 300 страниц и 10Мб размер файла и, если он больше этих ограничений, тогда это не такое приемлемое решение для непосредственного применения.
    А, с другими сервисами перевода ещё больше ограничений и это, если формат PDF понравится транслятору, что бывает, в силу специфики самого форматa, не всегда.


  1. Jury_78
    17.12.2022 18:49

    один из самых популярных форматов электронных книг - EPUB

    Мне нравится FictionBook (fb) - он чем то хуже?


    1. dleshko
      17.12.2022 18:55

      Не хуже, это просто факт.


    1. vassabi
      18.12.2022 00:32

      мне fb не зашел, потому что мне хотелось иметь отдельные готовые странички для глав. Т.е. чтобы это были готовые html файлы.

      А так - нормальный формат для текста, не хуже других.


    1. AlexanderAstafiev
      18.12.2022 06:27
      +4

      Морально устарел. Представляет собой один гигантский xml-документ, и если книга реально большая, читалка может надолго повиснуть, читая весь документ целиком. В ePub же, если по уму, то книга делится на главы (по одной на файл) или еще как-то. Нет поддержки списков (нумерованных и ненумерованных), кода (pre/code), математических формул (если не картинками). ePub в теории поддерживает непосредственно mathml-код, хотя я не нашел читалки (бесплатной), умеющей этот код корректно отображать. В fb2 любые картинки кодируются в текст через base64, а в ePub они спокойно лежат себе как есть в контейнере (файл ePub это обычный zip-архив).

      Вообще, fb2 это просто xml с определенным набором тегов, которых не всегда хватает для всех элементов книги. В ePub с этим гораздо лучше, если говорить о 3 версии стандарта. Другой вопрос — поддержка читалками ePub3.

      Плюс fb2 — специальные теги для семантической разметки стихов (<stanza> и еще какие-то там).


    1. Akr0n
      18.12.2022 09:13
      +2

      В дополнение к вышесказанному, благодаря тому, что формат представляет из себя zip-архив, файлы книг занимают гораздо меньше места по сравнению с аналогичными fb2.


    1. russarr
      18.12.2022 10:28
      +1

      Я когда свой парсер электронных книг писал, тоже на epub остановился. Если у тебя есть html страница с текстом, там просто добавляешь ее к архиву книги. А на fb2 пришлось бы теги конвертировать.

      Правда я тогда Ebook Lib не нашел, просто через jinja сделал шаблоны и заархивировал, вот тебе и epub книга.


      1. vassabi
        18.12.2022 16:57

        а мне Ebook Lib не понравился

        будете в следующий раз пробовать - посмотрите в сторону mkepub: https://pypi.org/project/mkepub/

        он ИМХО проще для понимания и использования