Привет, Хабр! Хочу поделиться опытом анализа текста. Возьму рабочий пример документов в отношении граждан, проходящих процедуру банкротства. Задача заключается в автоматизированном сборе информации из текста 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])
На выходе получаю желаемые данные в виде таблицы:
Таким образом, с помощью специальных эвристик и уже существующих инструментов мне удалось автоматизировано и качественно получить желаемую информацию из нескольких сотен тысяч документов и, при этом, не изобретать колесо. На этом всё, надеюсь была полезна, всем успехов!