Сортировка книг по тематикам скриптами Python
На момент написания этой заметки около половины из 16 тысяч книг в моей библиотеке — ИТшные, другая половина — медицинские. Две трети этих книг на английском, одна треть — на русском.
Примерно раз в месяц я с телеграм-каналов докачиваю еще 1–2 тысячи книг, из которых реально новых — не более 100–200, остальное у меня уже есть. Кроме того, попадаются сканированные книги с околонулевой пользой, если их не распознавать.
Всё это добро мне нужно регулярно дедуплицировать, раскладывать по тематическим папочкам, выкладывать в облако для коллег и при этом не тратить на это много времени. Готовых программ для таких задач я не нашел, поэтому, как мог, справлялся сам — писал скрипты на Python.
Дисклеймер.
Я гуманитарий (переводчик), поэтому вполне допускаю, что я ломлюсь в открытую дверь и где-то у кого-то есть гораздо более изящное решение. Как бы то ни было, я его не нашел, задачу решать как-то надо было, и я сконструировал небольшой конвейер, который работает почти без моего участия (т. е. я могу включить его на ночь, и к утру уже всё готово). Логику этого конвейера и этапов, из которых он состоит, я и решил описать, вдруг кому-то еще пригодится.
Всего 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. Дедуплицируем файлы
Дедупликацию я делаю в два этапа:
удаляю дубли внутри стартовой папки
\YandexDisk\__unsorteds
;удаляю дубли в стартовой папке, если находятся совпадения с файлами из папок и подпапок в
\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
Как видите, все эти слова не годятся, т. к. они могут встретиться почти в любой ИТ-шной книге по разработке. Большая часть нужных слов находится в нижней половине списка (будет много малочастотных слов).
Заключение
Таким образом, сейчас алгоритм более или менее корректно раскидывает книги, практически не требуя моего внимания. Папки с отсортированными книгами выглядят примерно так:
Вот и все. Вроде бы пояснил, надеюсь, было полезно.
Если кому нужен код, пишите в телеграм (@bartov_e), скину.
P.S. Сейчас бьюсь над задачей, чтобы нормально извлекать имена книг из их содержимого и унифицировать имена файлов. Если вдруг кто-то уже решал такую задачу, не сочтите за труд, поделитесь соображениями в комментариях. ISBN не подходит, далеко не у всех книг он есть.