Наткнулся однажды на этот пост и мне подумалось — раз у нас есть такая прекрасная, полностью открытая галерея частных данных (Radikal.ru), не попытаться ли извлечь из нее эти данные в удобном для обработки виде? То есть:
- Скачать картинки;
- Распознать текст на них;
- Выделить из этого текста полезную информацию и классифицировать ее для дальнейшего анализа.
И в результате, после нескольких вечеров, работающий прототип был сделан. Много технических деталей:
Все делалось на C# в среде ASP MVC 5. Просто потому, что я там пишу постоянно и мне так удобнее.
Этап 1: Скачать картинку
Как следует посидев в исходном коде страниц галереи, я не нашел какой-то последовательности — значит придется скачивать каждую веб-страницу, и выдирать из кода ссылку на картинку. Хорошо хоть, что адрес страницы с картинкой поддается автоматическому формированию — это просто URL с порядковым номером картинки. Ок, берем HtmlAgilityPack, и пишем парсер, благо классов на странице с картинкой достаточно, и выдернуть нужный узел не сложно.
Вытаскиваем узел, смотрим — ссылки нет. Ссылка, оказывается генерируется посредством JavaScript, который у нас не был запущен. Это грустно, т.к. скрипты обфусцированы, и терпения разобраться в принципах их работы мне не хватило.
Ок, есть другой путь — открыть страницу в браузере, дождать выполнения скриптов, и получить ссылку из заполненной страницы. Благо для этого есть прекрасная связка в виде Selenium и PhantomJS (браузер без графической оболочки), потому как делать все через, к примеру, FireFox — и дольше по времени выполнения, и неудобнее. К сожалению, и это тоже очень медленно — вряд ли есть еще более медленный способ :( Примерно по 1 секунде на картинку.
Парсер:
public static string Parse_Radikal_ImagePage(IWebDriver wd, string Url)
{
wd.Url = Url;
wd.Navigate();
new WebDriverWait(wd, TimeSpan.FromSeconds(3));
HtmlDocument html = new HtmlDocument();
html.OptionOutputAsXml = true;
html.LoadHtml(wd.PageSource);
HtmlNodeCollection Blocks = html.DocumentNode.SelectNodes("//div[@class='show_pict']//div//a//img");
return Blocks[0].Attributes["src"].Value;
}
Контроллер — обработчик:
IWebDriver wd = new PhantomJSDriver("C:\PhantomJS");
for (var imageCode = data.imgCode; imageCode > data.imgCode - data.imgCount; imageCode--)
{
if (ParserResult.Processed(imageCode)) continue;
var Url = "http://radikal.ru/Img/ShowGallery#aid=" + imageCode.ToString() + "&sm=true";
var imageUrl = Parser.Parse_Radikal_ImagePage(wd, Url);
var Filename = (string)null;
if (imageUrl != null)
{
var image = Parser.GetImageFromUrl(imageUrl);
Filename = TempFilesRepository.TempFilesDirectory() + "Radikal_" + imageCode.ToString() + "." + Parser.GetImageFormat(image);
image.Save(Filename);
}
}
wd.Quit();
Все это над где-то хранить и обрабатывать. Логично выбрать уже развернутый MS SQL Server, создать на нем небольшую базу и сложить туда ссылки на картинки и путь к скачанному файлу. Пишем маленький класс для хранения и записи результата парсинга картинки. Почему не хранить картинки в базе? Об этом ниже, в разделе про распознавание.
[Table(Name = "ParserResults")]
public class ParserResult
{
[Key]
[Column(Name = "id", IsPrimaryKey = true, IsDbGenerated=true)]
public long id { get; set; }
[Column(Name = "Url")]
public string Url { get; set; }
[Column(Name = "Code")]
public long Code { get; set; }
[Column(Name = "Filename")]
public string Filename { get; set; }
[Column(Name = "Date")]
public DateTime Date { get; set; }
[Column(Name = "Text")]
public string Text { get; set; }
[Column(Name = "Extracted")]
public bool Extracted { get; set; }
public ParserResult() { }
public ParserResult(string Url, long Code, string Filename, string Text)
{
this.Url = Url;
this.Code = Code;
this.Filename = Filename;
this.Date = DateTime.Now;
this.Text = Text;
this.Extracted = false;
DataContext Context = DataEngine.Context();
Context.GetTable<ParserResult>().InsertOnSubmit(this);
Context.SubmitChanges();
}
public static bool Processed(long imgCode)
{
return DataEngine.Data<ParserResult>().Where(x => x.Code == imgCode).Count() > 0;
}
}
Этап 2: Распознать текст
Тоже, казалось бы, не самая сложная задача. Берем Tesseract (точнее, обертку для него под .NET), качаем данные для русского языка, и… облом! Как выяснилось, для нормальной работы Tesseract с русским языком, необходимы условия близкие к идеальным — отличного качества скан, а не фотка документа на дрянной мобильник. Процент распознавания — хорошо если приближается к 10.
Вообще, всё приемлемое распознавание кириллицы представлено всего тремя продуктами: CuneiForm, Tesseract, FineReader. Чтение форумов и блогов укрепило в мысли, что CuneiForm пробовать смысла нет (многие пишут, что по качеству распознавания он недалеко ушел от Tesseract), и я решил сразу пробовать FineReader. Основной его минус — он платный, очень платный. К тому же под рукой не было Finereader Engine (который предоставляет API для распознавания), и пришлось делать ужасный велосипед: запускать Abbyy Hotfolder, которая смотрит в указанную папку, распознает появляющиеся там картинки, и кладет рядом одноименные текстовые файлы. Таким образом, выждав немного после скачивания картинок, мы можем взять готовые результаты распознавания и положить их в базу данных. Очень медленно, очень костыльно — но качество распознавания, я надеюсь, окупает эти затраты.
var data = DataEngine.Data<ParserResult>().Where(x => x.Text == null & x.Filename != null).ToList();
foreach (var result in data)
{
var textFilename = result.Filename.Replace(Path.GetExtension(result.Filename), ".txt");
if (System.IO.File.Exists(textFilename))
{
result.Text = System.IO.File.ReadAllText(textFilename, Encoding.Default).Trim();
result.Update();
}
}
Кстати, именно по причине таких костылей картинки храним не в БД — Abbyy Hotfolder с БД, к сожалению, не работает.
Этап 3: Извлечь из текста информацию
На удивление, этот этап оказался самым простым. Наверное, потому что я знал, что искать — год назад я прошел курс Natural Language Processing на Coursera.org, и представлял, как решаются такие задачи и какая терминология используется. В том числе поэтому я решил не писать очередные велосипеды, а недолго погуглив, взял библиотеку PullEnti, которая:
- заточена на работу с русским языком;
- сразу обернута для работы с C#;
- бесплатна для некоммерческого использования.
Выделить с помощью нее сущности оказалось очень просто:
public static List<Referent> ExtractEntities(string source)
{
// создаём экземпляр процессора
Processor processor = new Processor();
// запускаем на тексте
AnalysisResult result = processor.Process(new SourceOfAnalysis(source));
return result.Entities;
}
Выделенные сущности надо хранить и анализировать, для этого пишем их в простенькую табличку в БД: ID картинки / тип сущности / значение сущности. После парсинга получается что-то такое:
DocID | EntityType | Value |
63 | Территориальное образование | город Уссурийск |
63 | Адрес | улица Дзер д.1; город Уссурийск |
63 | Дата | 17 ноября 2014 года |
PullEnti умеет выделять из текста (автоматически правя ошибки) довольно много таких сущностей: Банковские реквизиты, Территориальное образование, Улица, Адрес, URI, Дата, Период, Обозначение, Денежная сумма, Персона, Организация, etc… А дальше над полученными таблицами надо садиться и думать: выбирать документы по конкретному городу, искать конкретную организацию, и т.п. Главную задачу мы выполнили — данные извлекли и подготовили.
Результаты
Давайте посмотрим, что получилось на небольшой пробной выборке.
- Обработано страниц галереи — 2 263;
- Получено изображений — 1 972 (на остальных страницах изображения удалены либо закрыты настройками приватности);
- Выделен текст — 773 (на других изображениях FineReader не обнаружил ничего подоходящего для распознавания);
- Выделены сущности из текста — 293.
Правильные срабатывания — это последний показатель, т.к. довольно часто из картинки с насыщенной графикой выделяется текст в виде "^ЯА71 Г1/Г" и так далее. Получается, что годный для анализа текст мы находим, приблизительно, в каждом десятом изображении. Это неплохо для такого беспорядочного хранилища!
А вот, например, список извлеченных городов (довольно часто документы, из которых они извлечены — фотографии паспортов): Анкара, Бобруйск, Варшава, Златоуст, Казань, Киев, Красноярск, Минск, Москва, Омск, Санкт-Петербург, Сухум, Тверь, Уссурийск, Усть-Каменогорск, Челябинск, Шуя, Ярославль.
Итоги
- Задача решается; создан работающий прототип решения.
- Скорость работы этого прототипа пока что не выдерживает никакой критики :( Картинка в секунду — это очень медленно.
- И, конечно, есть ряд нерешенных проблем: например, аварийное завершение работы после того, как PhantomJS съест всю память.
Автор: vasyaabr