У меня скопилась большая коллекция технических книг в PDF и DJVU. Хотелось читать их прямо на сайте и делиться с другими. Решил автоматизировать процесс: конвертировать документы в Markdown, обогащать метаданные через поиск и публиковать в новом разделе блога.

Записываю всё как runbook — чтобы в следующий раз не вспоминать и сразу делать.

🗺 Обзор: что получилось #

Раздел /reading на сайте с карточками книг: название, автор, статус чтения, рейтинг, теги. При открытии книги — полный текст с оглавлением. Скрипты для пакетного и одиночного импорта.

Стек: Hugo, Python, MarkItDown, Open Library API.


Часть 1. Структура Hugo-раздела #

Что создаётся #

content/reading/           # контент книг
  _index.md                # лендинг раздела
  pro-git.md               # каждая книга — отдельный .md

layouts/reading/
  list.html                # карточки книг с сеткой
  single.html              # страница одной книги

archetypes/reading.md      # шаблон для hugo new
assets/css/reading.css     # стили карточек

Front matter книги #

---
title: "Pro Git"
author: "Скотт Чакон, Бен Штрауб"
date: 2026-04-10T12:00:00+03:00
draft: false
summary: "Полное руководство по Git от авторов системы."
description: "Полное руководство по Git от авторов системы."
tags: ["git", "контроль версий", "разработка"]
toc: true
readTime: false
status: "finished"    # want-to-read | reading | finished
rating: 0             # 1-5, 0 = без оценки
source: "Pro Git.pdf" # исходный файл
---

Поля status и rating управляют отображением карточки. Раздел читает их через .Params.status и .Params.rating в шаблоне.

Добавить раздел в меню #

В hugo.toml:

[[params.menu]]
  name = "Чтение"
  url  = "/reading"

Часть 2. Установка MarkItDown #

MarkItDown — инструмент Microsoft для конвертации документов в Markdown. Поддерживает PDF, DOCX, EPUB, PPTX, XLSX, HTML и другие форматы.

Установка в изолированный venv #

Важно: не устанавливать глобально, чтобы не конфликтовать с системным Python macOS.

# Создаём venv рядом со скриптами
python3 -m venv scripts/.venv

# Устанавливаем с поддержкой PDF
scripts/.venv/bin/pip install markitdown unidecode
scripts/.venv/bin/pip install "markitdown[pdf]"

Скрипты автоматически подхватывают этот venv через bootstrap-строки в начале файла:

import sys, os
_venv = os.path.join(os.path.dirname(__file__), ".venv", "bin", "python3")
if os.path.exists(_venv) and sys.executable != _venv:
    os.execv(_venv, [_venv] + sys.argv)

Теперь python3 scripts/import-book.py работает без ручной активации venv.

.gitignore #

Books/           # не коммитим большие PDF
scripts/.venv/   # не коммитим virtualenv

Часть 3. Одиночный импорт: import-book.py #

Интерактивный скрипт для добавления одной книги.

Использование #

python3 scripts/import-book.py ~/путь/к/книге.pdf

Что происходит #

  1. Конвертирует файл через MarkItDown → получает Markdown
  2. Пытается найти в тексте заголовок (# ...) и автора (Автор: ...)
  3. Показывает найденное и предлагает подтвердить или исправить
  4. Запрашивает статус (читаю / прочитал / в очереди), рейтинг, теги, описание
  5. Генерирует slug через транслитерацию (unidecode)
  6. Создаёт content/reading/<slug>.md

Пример сессии #

⏳ Конвертирую Pro Git.pdf... ✅ 947,557 символов

📝 Введи метаданные книги:

  Название [Pro Git]: Pro Git
  Автор [Scott Chacon]: Скотт Чакон, Бен Штрауб
  Статус:
    1) 📖 Читаю
    2) ✅ Прочитал
    3) 🕐 В очереди
  Выбор [3]: 2
  Рейтинг (0–5) [0]: 5
  Теги: git, контроль версий, разработка
  Краткое описание: Официальная книга по Git, доступна бесплатно
  Черновик? (y/n) [n]: n

✅ Создан файл: content/reading/pro-git.md

Часть 4. Пакетный импорт: batch-import-books.py #

Для массового добавления папки с книгами.

Использование #

python3 scripts/batch-import-books.py Books/
# или с указанием статуса:
python3 scripts/batch-import-books.py Books/ --status finished
# проверить без записи:
python3 scripts/batch-import-books.py Books/ --dry-run

Что происходит для каждого файла #

  1. Конвертация через MarkItDown
  2. Поиск метаданных в Open Library API (бесплатно, без ключа):
    GET https://openlibrary.org/search.json?q=<название>&limit=1
  3. Из ответа берёт: автора, год, теги (subjects)
  4. Создаёт content/reading/<slug>.md

⚠️ Проблема: неверный заголовок #

MarkItDown конвертирует книгу как есть. Первый # заголовок в PDF — это часто не название книги, а команда или раздел из тела. Например:

  • OpenStack-книга → # yum -y update
  • PostgreSQL → # book_ref
  • Командная строка → # cd /usr/bin

Решение: не доверять автовыбору, всегда проверять. При пакетном импорте — запускать fix-reading-metadata.py с таблицей правильных данных, либо использовать интерактивный import-book.py для каждой книги.


Часть 5. Агентский поиск метаданных 🤖 #

Автоматический поиск через Open Library хорошо работает для известных книг. Для надёжности — дополнительно использовать AI-агента.

Промпт для агента (ИИ-ассистент с веб-поиском):

Найди метаданные для этих книг. Для каждой верни:
- Точное название
- Авторы
- Год первого издания
- 3-5 тегов на русском
- 1-2 предложения описания на русском

Список книг:
1. "Linux на практике" Бреснахэн/Блум (2017)
2. "BPF для мониторинга Linux"
...

Агент находит корректные данные даже для книг с плохими метаданными в PDF.


Часть 6. Постобработка: очистка HTML #

Конвертированные PDF содержат встроенный HTML (теги, JavaScript из интерактивных PDF). Hugo минификатор ломается на них.

Симптом:

ERROR: unexpected = in expression on line 4526
failed to render pages: failed to process "/reading/book/index.html"

Решение — очистка перед коммитом:

import re

def strip_html(text):
    text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL|re.IGNORECASE)
    text = re.sub(r'<style[^>]*>.*?</style>',  '', text, flags=re.DOTALL|re.IGNORECASE)
    # убираем теги кроме базовых markdown-safe
    safe = r'br|hr|em|strong|code|pre|b|i|u|s|sub|sup|table|thead|tbody|tr|td|th|ul|ol|li|p|h[1-6]|blockquote|a|img'
    text = re.sub(rf'<(?!(?:{safe})[/ >])[^>]+>', '', text)
    return text

Запускать после конвертации и перед hugo --minify. Для моих 11 книг убрало от 800 до 370 000 символов лишнего HTML.


Часть 7. Особые случаи #

Отсканированные PDF #

Если PDF создан сканером (картинки страниц), MarkItDown конвертирует в пустую строку — нет извлекаемого текста.

Определение: len(text_content) == 0 при ненулевом размере файла.

Варианты действий:

  • OCR через pytesseract (требует установки Tesseract):
    brew install tesseract tesseract-lang
    pip install pytesseract pillow pdf2image
  • Создать заглушку с описанием вручную и отметить в summary
  • Оставить без текста, только front matter с метаданными

DJVU #

MarkItDown не поддерживает DJVU. Варианты:

  • Конвертировать в PDF: brew install djvu2pdf
  • Или создать заглушку, указав в summary ссылку на онлайн-версию (если книга открытая)

Очень большие книги #

Pro Git — 947 000 символов, Командная строка — 943 000. Hugo собирает их без проблем. TOC автоматически генерируется из заголовков. Время сборки растёт, но незначительно.


Часть 8. Полный runbook: шаг за шагом #

Первый запуск (один раз) #

# 1. Создать venv
python3 -m venv scripts/.venv
scripts/.venv/bin/pip install markitdown unidecode "markitdown[pdf]"

# 2. Создать структуру раздела (уже в репозитории)
# content/reading/, layouts/reading/, assets/css/reading.css

# 3. Добавить в .gitignore
echo "Books/" >> .gitignore
echo "scripts/.venv/" >> .gitignore

Добавление новой книги (каждый раз) #

# 1. Положить файл в Books/
cp ~/Downloads/новая-книга.pdf Books/

# 2. Импортировать интерактивно
python3 scripts/import-book.py Books/новая-книга.pdf
# → ввести название, автора, статус, рейтинг, теги

# 3. Проверить результат
cat content/reading/<slug>.md | head -20
hugo server  # открыть /reading локально

# 4. Закоммитить и задеплоить
git add content/reading/<slug>.md
git commit -m "Добавил книгу: Название книги"
git push

Пакетное добавление папки книг #

# 1. Пробный прогон
python3 scripts/batch-import-books.py Books/ --dry-run

# 2. Реальный импорт
python3 scripts/batch-import-books.py Books/ --status finished

# 3. Очистка HTML в конвертированных файлах
python3 -c "
import re
from pathlib import Path
def strip_html(t):
    t = re.sub(r'<script.*?</script>', '', t, flags=re.DOTALL|re.I)
    t = re.sub(r'<style.*?</style>', '', t, flags=re.DOTALL|re.I)
    safe = r'br|hr|em|strong|code|pre|b|i|table|thead|tbody|tr|td|th|ul|ol|li|p|h[1-6]|a|img'
    t = re.sub(rf'<(?!(?:{safe})[/ >])[^>]+>', '', t)
    return t
for f in Path('content/reading').glob('*.md'):
    parts = f.read_text().split('---', 2)
    if len(parts)==3:
        clean = strip_html(parts[2])
        f.write_text('---' + parts[1] + '---' + clean)
        print(f.name)
"

# 4. Проверить сборку
hugo --minify

# 5. Вручную исправить метаданные (если нужно)
# открыть файлы с неверными заголовками и поправить title, author

# 6. Закоммитить
git add content/reading/
git commit -m "Добавил X книг в раздел Чтение"
git push

Итог и наблюдения #

За один прогон опубликовал 13 книг. Что работает хорошо:

  • MarkItDown отлично справляется с текстовыми PDF: извлекает не только текст, но и структуру (заголовки глав, списки, таблицы)
  • Open Library API находит метаданные для известных книг, но для русских переводов иногда возвращает англоязычный оригинал
  • Агентский поиск надёжнее — ИИ находит правильные данные даже по транслитерированным именам файлов

Что требует внимания:

  • Первый заголовок ≠ название книги — PDFs часто начинаются с команды или технического термина в теле
  • Отсканированные PDF не конвертируются без OCR
  • DJVU не поддерживается напрямую
  • Встроенный HTML в конвертированных файлах ломает Hugo minifier — нужна очистка

Раздел «Чтение» живёт по адресу kushnaren.co/reading. Буду пополнять по мере чтения.