Время прочтения: 7 мин.

Мы использовали в работе несколько способов обработки, но отобрали два, на наш взгляд, достаточно эффективных:

— выделение шаблонов из пакета;

— поиск паттернов в шаблонах.

Рассмотрим первый способ — выделение шаблонов из пакета.

Из названия понятно, что необходимо просмотреть весь пакет документов и выделить наиболее часто встречающиеся шаблоны, а потом уже работать с оставшимися документами, не подходящими к выделенным шаблонам. Отметим, что необходимо привести все документы к единому формату. Например, если вся нужная информация в документах находится в табличной форме, имеет смысл выделить таблицы и перевести их в формат xlsx. Далее для обработки каждого вида шаблона был написан отдельный код.

Рассмотрим в качестве примера анализ актов выполненных работ на ремонт IT оборудования. Ниже представлены названия соответствующих друг другу колонок из двух разных актов:

Очевидно, что данные будут одинаковы в обеих таблицах, но находятся они в колонках с разными названиями. При автоматическом распознавании эти таблицы будут выделены в шаблоны, отличные друг от друга, и для их объединения необходимо будет переструктурировать таблицы вручную. Для обработки данных из этого примера мы использовали следующий алгоритм действий:

— провели анализ пакета документов вручную;

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

— с помощью скрипта объединили нужные данные в одну общую таблицу.

Представляем код, в котором можно увидеть последовательность шагов:

— описание каждого шаблона, как набора колонок датафрейма;

— поочерёдное считывание файлов и сравнивание их с созданными шаблонами.

dd_eirs_1 = ['Номер обращения',
 'Наименование объектов обслуживания',
 'Стоимость обслуживания объекта в месяц за ед. без НДС, руб.'] #пример определенного шаблона, для каждого шаблона документа создается свой список

df_eirs = pd.DataFrame()
ost = []
factura = []
empty = []
find = []
for filename in tqdm(files): #итерируемся по всем файлам
    if not os.path.basename(filename).startswith('~$'):
        df = pd.read_excel(filename)
        if df.empty: # если пустой файл то записываем в пустые
            empty.append(filename)
            continue
        df.dropna(how='all', inplace=True)
        df.reset_index(drop=True, inplace=True)
        cols = list(df.columns) #выделяем колонки из таблицы в файле
        cols = clean(cols)

Далее удаляем ненужные поля (для примера допустим, что в шаблоне dd_eirs_2 присутствует столбец ‘Акт сдачи-приемки’):

         if 'Акт сдачи-приемки' in df.columns[0]:
            df.drop(df.columns[0], axis=1, inplace=True)
            df.columns = df.loc[0]
            df = df[1:]
            cols = list(df.columns)
            cols = clean(cols)
        if cols == dd_eirs_2: #смотрим подходят ли колонки каждому шаблону
            for i in df.index:
                if 'ИТОГО (' in str(df.loc[i, 'ИТ-услуга ТП']):
                    df = df[:i]
                    break
            df.columns = cols
            df['filename'] = os.path.basename(filename)
            df_eirs = pd.concat([df_eirs, df]) #если установили соответствие с шаблоном то записываем в общий файл
            find.append(filename)
        elif cols == dd_eirs_1: # если шаблону не подошло то сравниваем с другими
            for i in df.index:
                if 'ИТОГО (' in str(df.loc[i, 'ИТ-услуга ТП']):
                    df = df[:i]
                    break
            df.columns = cols
            df['filename'] = os.path.basename(filename)
            df_eirs = pd.concat([df_eirs, df])
            find.append(filename)
        else … #<обработка других шаблонов>

В результате получили сводную таблицу для проведения дальнейшего анализа:

Второй способ — поиск паттернов в шаблонах.

Данный способ позволяет выделять общие паттерны в каждом документе. Главное отличие от предыдущего способа заключается в том, что не надо обрабатывать каждый шаблон отдельно, а можно использовать набор функций, который поможет извлечь нужную информацию из значительного объёма документов.

О применении данного способа расскажем на примере обработки 2-х заказ-нарядов на ремонт автомобиля, оформленных по разным шаблонам. Задача: узнать у каких машин, когда были совершены ремонтные работы, а также детальную информацию по работам (детали, их количество, вид работ, стоимость и т.д.).

Можно заметить, что в документах есть вся нужная информация, но представлена она в разных форматах (столбцы называются по-разному, номер машины расположен в разных местах листа, в одном случае отсутствует номер договора и т.д.). Соответственно, нельзя просто извлечь и соединить таблицы из этих документов.

При обработке данных вторым способом мы использовали другой алгоритм действий:

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

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

def get_info_from_pdf(row):
    try:
        f = fitz.open(row['FilePath'])
        text = ''
        for page in f:
            text += page.get_text()
        text = text.lower()
        text = re.sub(r'\n', ' ', text)
#выделяем текст из файла, в тексте ищем интересующую нас информацию с помощью регулярных выражений
        zakaz = re.findall(r'(заказ-наряд)|(наряд-заказ)|(заказ-наряда)|(наряд-заказа)|(наряд-заказу)|(заказ-наряду)', text)
        if len(zakaz)==0:
            if len(re.findall(r'пробег\D*\d+', text))!= 0:
                row['Пробег'] = re.findall(r'пробег\D*\d+', text)[0]
            if len(re.findall(r'sd\d+', text)) != 0:     
                row['Номер SD'] = re.findall(r'sd\d+', text)[0]
            return row
    except:
        return row

— с помощью Python в документах были найдены и извлечены все таблицы;

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

def extract_table(o):
    table = tables[o].df
    table.columns = [ str(x).lower().translate(str.maketrans('','',string.punctuation)) for x in table.columns] # выделяем названия колонок в таблице
    df = pd.DataFrame()
    for i in table.columns: #итерируемся по каждой колонке
        for j in range(len(table)):
            data = str(table[i][j]).lower().translate(str.maketrans('','',string.punctuation))
            data = [i] + data.split()
            for d in data:
		#смотрим встречается ли в данных колонки слова маркеры нужных нам данных
                if d in ['код']:
                    df["Код"] = table[i] # если нужный маркер найден, то данные из колонки записываются в соответствующую колонку общего файла
                if d in ['наименованиие', 'работ', 'работы', 'услуги', 'услуг', 'услуга']:
                    df["Работы"] = table[i]
                if d in ['время']:
                    df["Время"] = table[i]
                if d in ['нч']:
                    df["Н/ч"] = table[i]
                if d in ['колво','кол']:
                    df["Кол-во"] = table[i]
                if d in ['цена']:
                    df["Цена"] = table[i]
                if d in ['сумма']:
                    df["Сумма"] = table[i].drop([j])
    return df.dropna(how='any')

В результате была получена следующая таблица для проведения дальнейшего анализа:

Оба способа имеют свои плюсы и минусы.

Для первого способа характерна бОльшая точность выходных данных. Однако, он требует проведения ручного анализа.

Для второго способа использование регулярных выражений позволяет снизить трудозатраты. Поиск подходящего выражения или ключевого слова занимает не так много времени, является более универсальным и не требует доработки при добавлении новых вариантов документов. Однако, при применении данного инструмента, могут встречаться некоторые артефакты выходных данных (например, номер машины: ъ000уъ00, дата договора 20.07.2024 и т.д.), которые могут усложнить дальнейший анализ.

При анализе 100 пакетов документов, содержащих разнородные таблицы, я получил следующую статистику:

Как видно из сравнительной таблицы, ручная обработка даёт высокую точность обработки данных, но требует большего количества времени, чем поиск паттернов. Очевидно, чем более однородные таблицы приводятся к единому виду, тем выше точность данных и скорость обработки.

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