Сортировка книг по тематикам скриптами Python

На момент написания этой заметки около половины из 16 тысяч книг в моей библиотеке — ИТшные, другая половина — медицинские. Две трети этих книг на английском, одна треть — на русском.

Примерно раз в месяц я с телеграм-каналов докачиваю еще 1–2 тысячи книг, из которых реально новых — не более 100–200, остальное у меня уже есть. Кроме того, попадаются сканированные книги с околонулевой пользой, если их не распознавать.

Всё это добро мне нужно регулярно дедуплицировать, раскладывать по тематическим папочкам, выкладывать в облако для коллег и при этом не тратить на это много времени. Готовых программ для таких задач я не нашел, поэтому, как мог, справлялся сам — писал скрипты на Python.

Дисклеймер.
Я гуманитарий (переводчик), поэтому вполне допускаю, что я ломлюсь в открытую дверь и где-то у кого-то есть гораздо более изящное решение. Как бы то ни было, я его не нашел, задачу решать как-то надо было, и я сконструировал небольшой конвейер, который работает почти без моего участия (т. е. я могу включить его на ночь, и к утру уже всё готово). Логику этого конвейера и этапов, из которых он состоит, я и решил описать, вдруг кому-то еще пригодится.

Всего 7 этапов:

  1. Разбираем архивы

  2. Чистим мелкие и левые файлы

  3. Дедуплицируем файлы

  4. Разбиваем все скачанные файлы на ИТ и медицину

  5. Проставка языковых префиксов

  6. Сортировка по подпапкам

  7. Разработка ключевых слов для папки

1. Разбираем архивы.

В телеграме я регулярно прохожу по книжным каналам, читаю все непрочитанные сообщения, все файлы из этих сообщений валятся в папку \YandexDisk\__unsorteds. Именно с этой папки начинают свой путь вообще все файлы.

Поскольку в библиотеку я должен отправить только файлы в формате PDF или EPUB, я с помощью библиотеки patoolib все архивы *.zip, *.rar, *.7z распаковываю в папку \YandexDisk\__unsorteds\unpacked, потом отбираю все файлы PDF и EPUB в этой подпапке и забрасываю их обратно в папку \YandexDisk\__unsorteds.

Все имена файлов и расширение во избежание будущих недоразумений привожу к нижнему регистру. Исходные архивы после распаковки удаляю, они больше не нужны. Папки, куда распаковывались файлы, тоже удаляю, нужные файлы оттуда я уже вытащил, всё остальное мне не нужно.

2. Чистим мелкие и нерелевантные файлы

Здесь у меня используются три рабочих папки.

Первую вы уже знаете — \YandexDisk\__unsorteds. Здесь лежат и скачанные файлы и файлы после распаковки и очистки.

В папку \YandexDisk\4OCR перемещаются все файлы с расширением *.djvu. Я в силу своей кодерской беспомощности, так и не придумал, как нормально работать с файлами с этим расширением (они то открываются через раз, то текстовый слой там кривой), поэтому я от греха подальше забрасываю все такие файлы в эту папку. Их далее оттуда подхватывает служба HotFolder из пакета FineReader, распознает и складывает в папку \YandexDisk\OCRed.

Вот из последней папки я на этом этапе подхватываю уже распознанные PDFки с хорошим текстовым слоем и переношу в папку \YandexDisk\__unsorteds, до кучи.

Таким образом перед началом чистки я пополняю исходную папку распознанными файлами и забрасываю на распознавание заведомо неудобные, но потенциально полезные DJVU-файлы. Дальше начинаю чистку PDF и EPUB-файлов.

У меня два критерия чистки на этом этапе:

3. Дедуплицируем файлы

Дедупликацию я делаю в два этапа:

  1. удаляю дубли внутри стартовой папки \YandexDisk\__unsorteds;

  2. удаляю дубли в стартовой папке, если находятся совпадения с файлами из папок и подпапок в \YandexDisk\IT и \YandexDisk\MED.

Как вы поняли, две последние папки — это корневые тематические папки для ИТшных и медицинских книг.

Мне показалось, что вычислять дубли по хешу первых 1000 знаков будет быстрее, чем по хешу всей книги (но я не замерял), поэтому настроил вычисление хеша только по этой тысяче.

4. Разбиваем все скачанные файлы на ИТ и медицину

На этом этапе файлы из стартовой папки \YandexDisk\__unsorteds разъезжаются по трем папкам:

Логика здесь получилась такая. Я собрал ключевые слова, которые с наибольшей вероятностью попадутся в медицинских книгах, и положил их в список keywords_med. Аналогично поступил с ИТшными ключами — keywords_it.

Я также подгружаю библиотеку spacy, а к ней две модельки — английского и русского языков…

Далее считываю из каждого файла по 100 тыс. знаков, вычисляю 35 наиболее часто используемых слов и сравниваю количество совпадений со списками keywords_med и keywords_it. По количеству совпадений скрипт принимает решение, куда переселять файл.

Если скрипт не может прочитать эти слова или говорит, что их количество одинаковое (скорее всего 0 и в том, и в другом) — значит текстовый слой в файле отсутствует или он битый. Следовательно, файл уезжает на распознавание, в папку \YandexDisk\4OCR.

5. Проставка языковых префиксов

Это простой этап. Скрипт обходит все файлы из \YandexDisk\IT\#UNSORTED и \YandexDisk\MED\#UNSORTED, считывает из каждого первые 15 страниц, приводит считанные строки к нижнему регистру и считает из какой строки у него знаков больше:

    cyrillic_alphabet = "абвгдеёжзийклмнопрстуфхцчшщъыьэюя"
    english_alphabet = "abcdefghijklmnopqrstuvwxyz"

Если из cyrillic_alphabet, значит файлу делается префикс RU_, если из english_alphabet — префикс EN_.

6. Сортировка по папкам

На этом этапе файлы из папок \YandexDisk\MED\#UNSORTED и \YandexDisk\IT\#UNSORTED уже разъезжаются по своим подпапкам. ИТшные книги уезжают в папку для книг по разработке (а там далее по Python, Java, C#), DevOps, Data Science, СУБД и т. д.

Медицинские книги, соответственно, уезжают в тематические папки на тему кардиологии, неврологии, онкологии и т.д.

Логика тут получилась такая: считываю первые 30 000 знаков из каждой книги, привожу их в нижний регистр, в зависимости от префикса RU_ или EN_ обрабатываю их русской или английской моделью Spacy, причем слежу за тем, чтобы:

  • слова были не короче 4 знаков,

  • приводились к лемме (исходной форме),

  • в лемме не было цифр и небуквенных знаков (за исключением дефиса).

Получившийся текст прогоняю через алгоритм TF-IDF (TfidfVectorizer), чтобы получить список из ТОП-30 слов. Отмечу, что в ранних своих попытках решить задачу с сортировкой я считал просто часто используемые слова — в итоге в качестве ключевых выдавались наиболее употребимые слова, а это не то, что мне нужно.

Далее скрипт сопоставляет список полученных слов со списками заранее подготовленных ключевых слов (о том, как их делать — ниже). Каждому такому списку соответствует тематическая папка, и если как минимум 3 слова из списка совпало, то скрипт проверяет, с каким списком совпадений больше. Победивший список определяет целевую папку, и файл уходит в нее. Если совпадений меньше 3, значит, файл остается в исходной папке #UNSORTED, пока я не настрою под него свой список ключевых слов (тут я еще в процессе).

7. Разработка ключевых слов для папки

На этом этапе я долго ломал голову, как сделать ключевые слова так, чтобы, например, книга про Python попадала именно в папку для Python, а не для Java. Списки наиболее часто встречающихся слов не помогали, более того, они еще сильнее усложняли работу.

Например, слово function может встречаться в книгах и по разработке, и по DevOps, и по матану, и по линалу. В какой список поставить это слово? Постепенно допер, что это слово вообще нельзя использовать. Нужно использовать только те слова, которые ни в какой другой тематике больше не используются (или как минимум в ТОП-100 ключевых слов по TF-IDF не попадут).

Таким образом, в список слов по Python попали такие слова, как matplotlib, finally, jupyter, ipython, pandas, а в список слов по, например, неврологии — myelin, postsynaptic, таламус, неврологический, тройничный.

Для того чтобы сформировать этот список слов, я создавал папку, складывал туда вручную отобранные 20–30 книг по нужной тематике на двух языках — желательно такие, где нужного тематического текста побольше, — получалось что-то вроде тренировочного датасета. Далее извлекал из каждой 200 топовых слов по TF-IDF и складывал их в список, причем каждое слово должно было соответствовать следующим критериям:

  • не входит в список стоп-слов (из моделей spacy, то есть не является артиклем, предлогом и т. п.);

  • состоит только из букв;

  • не менее 4 букв;

  • не встречается в других тематических списках (как я уже говорил, нельзя, чтобы слова в списках пересекались).

После этого мне скрипт выдает список из нескольких сотен ключевых слов, в котором сообщает, какое ключевое слово в скольких книгах встретилось. Как правило, первую половину этих топовых слов можно смело проматывать, там не будет уникальных тематических, например, такой вывод получился из книг по PHP:

'function', :14
'datum', :13
'example', :13
'time', :13
'follow', :13
'work', :13
'need', :13
'chapter', :13
'true', :13

Как видите, все эти слова не годятся, т. к. они могут встретиться почти в любой ИТ-шной книге по разработке. Большая часть нужных слов находится в нижней половине списка (будет много малочастотных слов).

Заключение

Таким образом, сейчас алгоритм более или менее корректно раскидывает книги, практически не требуя моего внимания. Папки с отсортированными книгами выглядят примерно так:

k0bgh9y8incybolxkmfmwxgz1eg.png

Вот и все. Вроде бы пояснил, надеюсь, было полезно.
Если кому нужен код, пишите в телеграм (@bartov_e), скину.

P.S. Сейчас бьюсь над задачей, чтобы нормально извлекать имена книг из их содержимого и унифицировать имена файлов. Если вдруг кто-то уже решал такую задачу, не сочтите за труд, поделитесь соображениями в комментариях. ISBN не подходит, далеко не у всех книг он есть.

© Habrahabr.ru