[Из песочницы] Извлечение данных из фотохостинга

Наткнулся однажды на этот пост и мне подумалось — раз у нас есть такая прекрасная, полностью открытая галерея частных данных (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);

if (imageUrl!= null) { var image = Parser.GetImageFromUrl (imageUrl); var 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().InsertOnSubmit (this); Context.SubmitChanges (); }

public static bool Processed (long imgCode) { return DataEngine.Data().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().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 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 съест всю память. Исходный код (проект для Visual Studio 2013) — скачать.

© Habrahabr.ru