На момент написания этой заметки около половины из 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-файлов.
У меня два критерия чистки на этом этапе:
если файл меньше 1 МБ;
если файл меньше 100 страниц.
3. Дедуплицируем файлы
Дедупликацию я делаю в два этапа:
удаляю дубли внутри стартовой папки
\YandexDisk\__unsorteds
;удаляю дубли в стартовой папке, если находятся совпадения с файлами из папок и подпапок в
\YandexDisk\IT
и\YandexDisk\MED
.
Как вы поняли, две последние папки — это корневые тематические папки для ИТшных и медицинских книг.
Мне показалось, что вычислять дубли по хешу первых 1000 знаков будет быстрее, чем по хешу всей книги (но я не замерял), поэтому настроил вычисление хеша только по этой тысяче.
4. Разбиваем все скачанные файлы на ИТ и медицину
На этом этапе файлы из стартовой папки \YandexDisk\__unsorteds
разъезжаются по трем папкам:
\YandexDisk\IT\#UNSORTED
\YandexDisk\MED\#UNSORTED
\YandexDisk\4OCR
Логика здесь получилась такая. Я собрал ключевые слова, которые с наибольшей вероятностью попадутся в медицинских книгах, и положил их в список 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
Как видите, все эти слова не годятся, т. к. они могут встретиться почти в любой ИТ-шной книге по разработке. Большая часть нужных слов находится в нижней половине списка (будет много малочастотных слов).
Заключение
Таким образом, сейчас алгоритм более или менее корректно раскидывает книги, практически не требуя моего внимания. Папки с отсортированными книгами выглядят примерно так:
Вот и все. Вроде бы пояснил, надеюсь, было полезно.
Если кому нужен код, пишите в личку или телеграм, скину.
P.S. Сейчас бьюсь над задачей, чтобы нормально извлекать имена книг из их содержимого и унифицировать имена файлов. Если вдруг кто-то уже решал такую задачу, не сочтите за труд, поделитесь соображениями в комментариях. ISBN не подходит, далеко не у всех книг он есть.
Комментарии (12)
Briksins
17.12.2024 19:49Идеальный кейс для LLM. подключив какую llama 3 модель локально можно много чего упростить (например определять тематику и получать более точные ответы при сортировке, или для де-дупликации, не уверен что хеш из первых 1000 знаков идеально подходит для этой задачи. А если все книги ещё и векторезировать уммммм....
Ещё вы опустили тему относительно хранения метаданных, в чем и как вы ее храните? например список уже имеющейся литературы? хеши всех книг, списки ключевых слов и т.д.
Тоже не отказался бы получить доступ в вашу библиотеку, особенно ИТшную.
bartov-e Автор
17.12.2024 19:49Публичные LLM, насколько я знаю, денег хотят.. Где же я их столько возьму? Хеши не храню. Они на лету считаются, скрипт принимает решение и видимо после обработки про хеши забывает.
Что не так с хешем из 1000 знаков? Коллизий при 16 тыс. файлов я точно не ожидаю при таком размере хешируемого текста.
Ключевые слова, которые я отобрал для сортировки, прямо в коде гвоздями и прибиты, зачем мудрить, они более или менее статичны.
По поводу доступа напишите в личку, здесь я точно доступы вешать не буду :)Briksins
17.12.2024 19:49LLM можно ранить локально на вашем компе, абсолютно бесплатно, по гуглите "LM Studio" и "HuggingFace", просто поднимаешь как локальный сервер на localhost. правда желательно иметь видео карту, хоть какой маломальский GPU.
Хеш из 1000 знаков однозначно найдет совпадения, но не знаю все ли книги совпадают, например та же книга но 2nd edition? или я не знаю, лишняя страница с каким текстом в самом начале а дальше все тоже самое? может конечно это мизерный процент...
Сравнивать хеш каждый раз на лету время затратная процедура, и чем больше библиотека будет расти тем дольше, думаю оптимальнее бы было высчитать его 1 раз и хранить как метаданные об уже рассортированной библиотеке, и тогда высчитывать хеш только новой книги и сравнивать его с базой всех хешей который был уже сохранен, и если книга не дубликат и попадает в общую библиотеку то добавить ее хеш к общему списку.
ну а про гвоздями прибито - думаю можно выдрать содержание (ToC) и скормить той же ЛЛМ, она по содержанию куда качественнее определит подраздел
ну и как вишенка на торте все книги можно скормить ЛЛМ а потом спрашивать/чатиться о контенте на любую тему и получать обобщённый ответ с ссылкой на референс на конкретную книгу и параграф в ней если нужен конкретно первоисточник.
Таким образом пользы от вашей библиотеки будет куда больше...
Хотите помогу, так то я Software Architect, тем более у меня есть другой проект очень схожий по функционалу, так что можно убить сразу 2х зайцев.
zoldaten
17.12.2024 19:49задача из области "размять пальцы". полезно, но не практично.
аргументы:
- если книг настолько много, что их приходится сортировать, то, возможно, их не перечитать.
- многие книги дублируют друг друга, особенно в начальных главах. "Самый сок" начинается к концу или "размазан" в середине. Поэтому в 30000 первых слов можно насобирать из глав "как установить" python.
- некоторые книги не переводятся корректно в текст.
- в отношении информации все быстро сейчас устаревает, поэтому "обновление кода" в книгах старше 3-4 лет может быть нетривиальной задачей (помним про Keras v2, TF и т.д.).
- концентрируясь только на python и медицине, можно пропустить интересное в смежных языках.
Поэтому, самое простое:
- качаем "свежее" за последние 1-2 года.
- раскладываем по языкам программирования.
*есть конечно хорошие книги или справочники, которые старше 1-2 лет, но их вы и так найдете. они либо в ссылках в новых книгах либо упоминаются в них.
**вот еще еще человек сизифовым трудом занимается - www.youtube.com/@stakap/videosbartov-e Автор
17.12.2024 19:49У меня нет задачи их читать. У меня задача иметь корпус актуальных текстов и формулировок на заданную тему. Чтения мне на работе хватает, пока перевожу и редачу тексты :)
janvarev
По возможности рекомендую взять первую часть текста книги и/или имя файла и отправить с API запросом в какую-нибудь LLM, например Google Flash 1.5 с запросом типа: "Перед тобой содержимое части книги и имя файла. Ответь JSON в формате: {"title": "название книги", "book_type": "медицина|it"}"
Должно получиться довольно эффективно.
Про подборку ЛЛМ-ок под задачи и API писал на Хабре здесь
bartov-e Автор
Премного! Буду разбираться :)
avshkol
Спасибо, отличная идея. У меня тоже задача отсортировать библиотеку из примерно 1,5 тыс.сканов книг по энергетике по разделам, попробую.
wyfinger
не поделитесь библиотекой в личку?
bartov-e Автор
вы мне или @avshkol?