[Из песочницы] Извлечение данных из фотохостинга
Наткнулся однажды на этот пост и мне подумалось — раз у нас есть такая прекрасная, полностью открытая галерея частных данных (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
public static bool Processed (long imgCode)
{
return DataEngine.Data
var data = DataEngine.Data
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
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) — скачать.