Разберёмся что “под капотом” формата 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_78
17.12.2022 18:49один из самых популярных форматов электронных книг - EPUB
Мне нравится FictionBook (fb) - он чем то хуже?
vassabi
18.12.2022 00:32мне fb не зашел, потому что мне хотелось иметь отдельные готовые странички для глав. Т.е. чтобы это были готовые html файлы.
А так - нормальный формат для текста, не хуже других.
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> и еще какие-то там).
Akr0n
18.12.2022 09:13+2В дополнение к вышесказанному, благодаря тому, что формат представляет из себя zip-архив, файлы книг занимают гораздо меньше места по сравнению с аналогичными fb2.
russarr
18.12.2022 10:28+1Я когда свой парсер электронных книг писал, тоже на epub остановился. Если у тебя есть html страница с текстом, там просто добавляешь ее к архиву книги. А на fb2 пришлось бы теги конвертировать.
Правда я тогда Ebook Lib не нашел, просто через jinja сделал шаблоны и заархивировал, вот тебе и epub книга.
vassabi
18.12.2022 16:57а мне Ebook Lib не понравился
будете в следующий раз пробовать - посмотрите в сторону mkepub: https://pypi.org/project/mkepub/
он ИМХО проще для понимания и использования
pharo
У транслятора от гугеля огрaнаничение на 300 страниц и 10Мб размер файла и, если он больше этих ограничений, тогда это не такое приемлемое решение для непосредственного применения.
А, с другими сервисами перевода ещё больше ограничений и это, если формат PDF понравится транслятору, что бывает, в силу специфики самого форматa, не всегда.