Наткнулся однажды на этот пост и мне подумалось — раз у нас есть такая прекрасная, полностью открытая галерея частных данных (Radikal.ru), не попытаться ли извлечь из нее эти данные в удобном для обработки виде? То есть:
И в результате, после нескольких вечеров, работающий прототип был сделан. Много технических деталей:
Все делалось на C# в среде ASP MVC 5. Просто потому, что я там пишу постоянно и мне так удобнее.
Как следует посидев в исходном коде страниц галереи, я не нашел какой-то последовательности — значит придется скачивать каждую веб-страницу, и выдирать из кода ссылку на картинку. Хорошо хоть, что адрес страницы с картинкой поддается автоматическому формированию — это просто URL с порядковым номером картинки. Ок, берем HtmlAgilityPack, и пишем парсер, благо классов на странице с картинкой достаточно, и выдернуть нужный узел не сложно.
Вытаскиваем узел, смотрим — ссылки нет. Ссылка, оказывается генерируется посредством JavaScript, который у нас не был запущен. Это грустно, т.к. скрипты обфусцированы, и терпения разобраться в принципах их работы мне не хватило.
Ок, есть другой путь — открыть страницу в браузере, дождать выполнения скриптов, и получить ссылку из заполненной страницы. Благо для этого есть прекрасная связка в виде Selenium и PhantomJS (браузер без графической оболочки), потому как делать все через, к примеру, FireFox — и дольше по времени выполнения, и неудобнее. К сожалению, и это тоже очень медленно — вряд ли есть еще более медленный способ :( Примерно по 1 секунде на картинку.
Парсер:
* Весь код сильно упрощен, убраны некритические детали. Подробнее в исходниках
Контроллер — обработчик:
Все это над где-то хранить и обрабатывать. Логично выбрать уже развернутый MS SQL Server, создать на нем небольшую базу и сложить туда ссылки на картинки и путь к скачанному файлу. Пишем маленький класс для хранения и записи результата парсинга картинки. Почему не хранить картинки в базе? Об этом ниже, в разделе про распознавание.
Тоже, казалось бы, не самая сложная задача. Берем Tesseract (точнее, обертку для него под .NET), качаем данные для русского языка, и… облом! Как выяснилось, для нормальной работы Tesseract с русским языком, необходимы условия близкие к идеальным — отличного качества скан, а не фотка документа на дрянной мобильник. Процент распознавания — хорошо если приближается к 10.
Вообще, всё приемлемое распознавание кириллицы представлено всего тремя продуктами: CuneiForm, Tesseract, FineReader. Чтение форумов и блогов укрепило в мысли, что CuneiForm пробовать смысла нет (многие пишут, что по качеству распознавания он недалеко ушел от Tesseract), и я решил сразу пробовать FineReader. Основной его минус — он платный, очень платный. К тому же под рукой не было Finereader Engine (который предоставляет API для распознавания), и пришлось делать ужасный велосипед: запускать Abbyy Hotfolder, которая смотрит в указанную папку, распознает появляющиеся там картинки, и кладет рядом одноименные текстовые файлы. Таким образом, выждав немного после скачивания картинок, мы можем взять готовые результаты распознавания и положить их в базу данных. Очень медленно, очень костыльно — но качество распознавания, я надеюсь, окупает эти затраты.
Кстати, именно по причине таких костылей картинки храним не в БД — Abbyy Hotfolder с БД, к сожалению, не работает.
На удивление, этот этап оказался самым простым. Наверное, потому что я знал, что искать — год назад я прошел курс Natural Language Processing на Coursera.org, и представлял, как решаются такие задачи и какая терминология используется. В том числе поэтому я решил не писать очередные велосипеды, а недолго погуглив, взял библиотеку PullEnti, которая:
Выделить с помощью нее сущности оказалось очень просто:
Выделенные сущности надо хранить и анализировать, для этого пишем их в простенькую табличку в БД: ID картинки / тип сущности / значение сущности. После парсинга получается что-то такое:
PullEnti умеет выделять из текста (автоматически правя ошибки) довольно много таких сущностей: Банковские реквизиты, Территориальное образование, Улица, Адрес, URI, Дата, Период, Обозначение, Денежная сумма, Персона, Организация, etc… А дальше над полученными таблицами надо садиться и думать: выбирать документы по конкретному городу, искать конкретную организацию, и т.п. Главную задачу мы выполнили — данные извлекли и подготовили.
Давайте посмотрим, что получилось на небольшой пробной выборке.
Правильные срабатывания — это последний показатель, т.к. довольно часто из картинки с насыщенной графикой выделяется текст в виде "^ЯА71 Г1/Г" и так далее. Получается, что годный для анализа текст мы находим, приблизительно, в каждом десятом изображении. Это неплохо для такого беспорядочного хранилища!
А вот, например, список извлеченных городов (довольно часто документы, из которых они извлечены — фотографии паспортов): Анкара, Бобруйск, Варшава, Златоуст, Казань, Киев, Красноярск, Минск, Москва, Омск, Санкт-Петербург, Сухум, Тверь, Уссурийск, Усть-Каменогорск, Челябинск, Шуя, Ярославль.
Исходный код (проект для Visual Studio 2013) — скачать.
- Скачать картинки;
- Распознать текст на них;
- Выделить из этого текста полезную информацию и классифицировать ее для дальнейшего анализа.
И в результате, после нескольких вечеров, работающий прототип был сделан. Много технических деталей:
Все делалось на 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<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 съест всю память.
Исходный код (проект для Visual Studio 2013) — скачать.