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

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

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

Воспользуюсь python и библиотеками regex и Natasha. С помощью регулярных выражений локализирую нужные предложения, а разбивать текст на предложения и вытаскивать необходимые поля буду с Natasha. Многие знакомы с отличными способностями библиотеки Natasha к распознаванию именованных сущностей (имена, города, названия компаний и т.д.). Но, помимо этого, она умеет находить даты, суммы денег и даже адреса!

Импортирую нужные библиотеки:

import os
import pandas as pd
from natasha import (Doc, Segmenter, NewsEmbedding, NewsMorphTagger,
                     NewsSyntaxParser, MorphVocab, NewsNERTagger,
                     DatesExtractor, MoneyExtractor)
import re
from collections import Counter
import openpyxl
import datetime
pd.options.display.max_columns = 100
pd.options.display.max_rows = 100
import warnings 
warnings.filterwarnings('ignore')

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

def get_info(text, sents):
  try:
    match = re.finditer('4\d{19}', text) 
    marks = [m.start() for m in match]
    bill = None
    money = None
    date1 = None
    date2 = None 
    for ind, i in enumerate(sents):
      for mark in marks:
   if i.start <= mark and i.stop > mark: 
      mini_sents = [sents[ind-1], i ,sents[ind+1]] # берем 3 предложения
      bill_text = ' '.join([x.text for x in mini_sents]) 

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

patterns = ['не более', 'в пределах', 'разблокир\w+', 'не может превышать', 'превыша\w+', 'самостоятельно', 'имеет право', 'распоряж\w+', '[Дд]еньги снимаются']              
      matches = []
      [matches.extend(re.findall(pattern, bill_text)) for pattern in patterns]
     if matches:
       matches = money_extractor(bill_text) # распознавание денег 
       facts = [i.fact.as_json for i in matches]
       facts = [f.get('amount') for f in facts]
       money = facts
       bill = re.search('4\d{19}',bill_text) # забираем счет 
  try:
    start = [m.start() for m in 
re.finditer('\s+с\s+\d{2}', bill_text)][0]
	   # находим упоминание периода
         dates = dates_extractor(bill_text[start:]) # распознавание дат 
         dates = [datetime.date(d.fact.as_json.get('year'), 
                                d.fact.as_json.get('month'), 
                                d.fact.as_json.get('day')) 
                 for d in dates]
         date1 = dates[0]
         date2 = dates[1]                
       except:
         pass
                            
       if money:
         money = [0]
            
       return bill, money, date1, date2
    except:
       return None, None, None, None

Создаю датафрейм:

data = pd.DataFrame({'ЗНО':[], 'Номер счета' : [], 'Дата 1' : [], 
                      'Дата 2' : [], 'Сумма' : []})

В цикле открываю и считаю информацию из файлов, убираю символы переноса и табуляции. Затем передаю текст в Natasha для токенизации и распознавания сущностей. Вызываю написанную мной функцию get_info и дописываю в датафрейм найденную информацию.

segmenter = Segmenter()
emb = NewsEmbedding()
morph_tagger = NewsMorphTagger(emb)
syntax_parser = NewsSyntaxParser(emb)
morph_vocab = MorphVocab()
ner_tagger = NewsNERTagger(emb)

path = 'docs/'
filenames = os.listdir(path)

for filename in filenames:
  with open(path + filename, 'r') as file:
    text += file.read()
  text = text.replace('\n', ' ')
  text = text.replace('\t', ' ')
  doc = Doc(text)
  dates_extractor = DatesExtractor(morph_vocab)
  money_extractor = MoneyExtractor(morph_vocab)
  doc.segment(segmenter)
  doc.tag_morph(morph_tagger)
  doc.parse_syntax(syntax_parser)
  doc.tag_ner(ner_tagger)
    num, money, date1, date2 = get_info(doc.text, doc.sents)
  data = pd.concat([pd.DataFrame({
    'ЗНО':[filename.split('.')[0].split('_')[0]], 
	# ЗНО в нашем случае являлось название документа
               'Номер счета' : [num], 'Дата 1' : [date1], 
               'Дата 2' : [date2], 'Сумма' : [money]}), data])

На выходе получаю желаемые данные в виде таблицы:

Таким образом, с помощью специальных эвристик и уже существующих инструментов мне удалось автоматизировано и качественно получить желаемую информацию из нескольких сотен тысяч документов и, при этом, не изобретать колесо. На этом всё, надеюсь была полезна, всем успехов!