Pull to refresh

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

Reading time 6 min
Views 22K
Наткнулся однажды на этот пост и мне подумалось — раз у нас есть такая прекрасная, полностью открытая галерея частных данных (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<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) — скачать.
Tags:
Hubs:
+19
Comments 6
Comments Comments 6

Articles