Представление документа в виде простого текста понадобится для анализа его содержимого: индексирования и поиска, классификации, предварительной проверки.

В нашем случае, стояла задача предварительного анализа (скоринга) документов по их содержимому. Верхнеуровневый процесс обработки документов построен с использованием MS Power Automate, поэтому конвертор нужно было реализовать в виде некоего облачного сервиса, доступного через HTTP.

В результате получился очень компактный сервис экстракции текста из офисных файлов, который успешно работает у нас уже несколько месяцев. Под катом - краткое описание сервиса, ссылка на репозиторий и другие полезные статьи по теме.

Преобразование файлов

Выделение текстового содержимого из разнообразных офисных файлов разного формата - задача не новая. На Хабре уже опубликовано несколько хороших статей по теме конверсии PDF, RTF, например:

  • "Cага о пакетном конвертировании pdf в text" - тут хороша и статья, и обсуждения. Это была начальная точка входа в проблему конвертации файлов.

  • Серия статей "Текст любой ценой" (PDF, RTF) - это про то, как сделать вообще все своими руками. Статья сильно утвердила в понимании, что надо пользоваться готовыми библиотеками.

После некоторого исследования и тестов различных пакетов (PyPdf2, striprtf, doc2text), я остановился на textract. Пакет хорошо документирован в части использования и инсталляции необходимый базовых компонент.

Textract очень прост и компактен в использовании. И самое главное - он позволяет вытаскивать текст из различных форматов стандартных офисных файлов: .doc, .docx, .rtf, .pdf, .xlsx, .ppt

import textract

file_name = "<path to the file>"
encoding = "utf-8"

text = textract.process(file_name).decode(encoding)

В процессе эксплуатации встречались случаи, когда файл с расширением .doc на самом деле содержал rtf. В этом случае конвертор antiword выдавал соответствующую ошибку и все, что нужно было сделать - переименовать файл и подать его на обработку снова.

Сервис конвертации

Сервис конвертирования файлов реализован на базе FastAPI. Он упаковывается и запускается в Docker контейнере.

Сервис принимает исходный файл в виде multipart/form-data и возвращает ответ в JSON формате, содержащем текст, статус конвертации и информацию об ошибках.

class ConvertResult(BaseModel):
    result: str
    text: str
    text_length: int
    file_name: str
    messages: str

...

@app.post('/convert', response_model=ConvertResult)
async def convert_file(file: UploadFile = File("file_to_convert"),
                       encoding: str = "utf-8",
                       user_name: str = Depends(get_current_username)):
...

Авторизация

Компонент HTTP MS Power Automate, который мы использовали для вызова сервиса, умеет авторизоваться с использованием многих схем: Basic Authenticate, Client Certificate, OAuth, Raw.

Поскольку сервис ничего не хранит на своей стороне и реализует не критичный сервис, авторизация запросов была реализована на базовом уровне с использованием Basic Authentication. Совместно с протоколом https такой подход видится достаточным. А на стороне MS Power Automate прамеры аутентификации можно сохранить в raw виде.

При развертывании контейнера компоненты авторизации (логин / пароль) можно установить через параметры окружения контейнера.

Установка зависимостей textract

При сборке контейнера, как описано в документации по textract, нужно поставить утилиты, на которые пакет рассчитывает при работе. Чтобы не раздувать образ ставить можно только то, что реально планируется использовать.

Поскольку в нашем случае необходима была только конвертация из документов, из инсталляции удалены компоненты отвечающие за OCR и преобразование звуковых файлов.

Фрагмент Dockerfile выглядит так:

...

RUN apt-get update && \
    apt-get install -y antiword unrtf poppler-utils && \
    rm -rf /var/lib/apt/lists/* && \
    apt clean

...

Использование временных файлов

Определенной спецификой пакета textract является то, что на вход он принимает только файл, а не байтовый поток. Это из-за того, что по расширению файла определяется необходимый конвертер. А конверторы, зачастую - внешние CLI утилиты.

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

Вопрос производительности в данном сервисе не является очень критичным, однако несколько улучшить ситуацию можно размещая область временных файлов в памяти, используя tmpfs. Как это сделать для Docker описано в документации.

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

Вызов сервиса из MS Power Automate

Основное назначение сервиса - выделять текст из документа в рамках одного из шагов процесса. В этом случае сервис вызывается через компонент "HTTP".

Поскольку сервис принимает на вход файл в составе multipart\form-data, отправка запроса должна быть описана в определенном формате в разделе Body как показано на картинке.

Вызов сервиса из MS Power Automate
Вызов сервиса из MS Power Automate

При завершении вызова текст можно получить из поля ответа:

body('get_text')['text']

API сервиса

Для того, чтобы использовать сервис из различных приложений, вместе с сервисом разработан API пакет.

Пакет предоставляет классы синхронного и асинхронного клиентов для сервиса. Использование сервиса в этом случае выглядит примерно так:

from file_to_text import ConverterClient

client = ConverterClient('<url to your service>', '<username>', '<password>')
text = client.convert(filepath='<path to file to be converted>')

Репозиторий

Готовая к использованию реализация сервиса и API доступны в репозитории на GitHub.

Буду рад комментариям и улучшениям.

Комментарии (2)


  1. dmitryvolochaev
    05.05.2022 10:45

    Документы Офиса 2007 и более поздних - это XML в зипе. Для них не надо специальных библиотек. Нужна только распаковка zip и парсинг XML. Дальше просто берем для корневого элемента innerText.

    Вот так это выглядит на C#:

    using System;
    using System.IO;
    using System.IO.Compression;
    using System.Xml;
    
    namespace DocxToText
    {
        class Program
        {
            static void Main(string[] args)
            {
                string docxFilePath = args[0];
              
                XmlDocument doc = new XmlDocument();
              
                using (ZipArchive arch = ZipFile.OpenRead(docxFilePath))
                {
                    ZipArchiveEntry ent = arch.GetEntry("word/document.xml");
    
                    using (Stream readStream = ent.Open())
                    {
                        doc.Load(readStream);
                    }
                }
              
                string text = doc.DocumentElement.InnerText;
                Console.WriteLine(text);
            }
        }
    }

    Есть, конечно, у такого подхода недостатки: InnerText не содержит пробелов на месте границ тегов. Т.е. абзацы склеены не просто в одну строку, а еще и без пробела.

    Кроме того, грузить весь документ в память абсолютно незачем.

    Обе проблемы решаются SAX-парсером.

    А как у вашего конвертера потребление памяти зависит от размера документа?


    1. serhit Автор
      05.05.2022 11:58

      Да, все верно, поздние версии файлов MS Office, как и форматы Open Office, более открыты к извлечению текста. Однако, такие документы в нашем потоке составляют ~7% - остальное это pdf (~70%), и rtf с doc.

      По поводу памяти и производительности. Как видно из постановки задачи (часть no/low-code процесса) и выбора средств (универсальный парсер-аггрегатор, основанный на внешних утилитах) - серьезная работа не проводилась.

      Могу сказать, что при локальном запуске контейнера в Docker Desktop он при старте забирает ~25-26 Мб и дрейфует около этого значения (с учетом, что используется tmfs).

      Тот же локальный экземпляр сервиса проводит конвертацию фалов (приведено к вреднему времени за 1000 символов выходного текста):
      - docx - 5,3 ms
      - pdf - 9.8 ms
      - rtf - ~ 10 ms (но очень большой разброс в зависимости от файлов)
      - doc - 5 ms