Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Скрейпинг сайтов с .Net и WatiN

25 комментариев

Рано или поздно у каждого разработчика появится соблазн «скачать» какой-нибудь сайт, либо для того чтобы получить или проанализировать определенный контент, либо просто доказать себе что это в его (её) силах. На самом деле, получить доступ к HTML определенного сайта просто, но проблемы начинаются тогда, когда сайт требует авторизации или содержит контент в виде картинки (например капчу или текст). В этом посте я расскажу какими методами я получаю контент с сайтов и что я с ним делаю.

Как получить текст?

Первый инструмент который я использую называется WatiN. Этот фреймворк используется для интерактивного тестирования веб-приложений. С помощью него, ваша .Net программа может открыть браузер, перейти на определенную страницу, нажать на кнопку или проверить что открылось ожидаемое окошко. WatiN предоставляет более-менее объектную модель, с помощью которой можно разбирать страницу на ее DOM составляющие и вытаскивать из них информацию.

Начать работать с WatiN просто – нужно всего лишь создать объект типа браузера (например IE или Firefox).

using (var browser = new FireFox(@"http://apps.facebook.com/desktopdefender/"))
{
  ...
}

Дальше, WatiN сам дождется пока страница загрузится, и потом объектная модель станет доступна. Для идентификации элемента очень удобно использовать Firebug. Конечно, если страница использует AJAX, скорее всего придется использовать Thread.Sleep() – то же самое относится например к ситуациям где вам нужно дождаться загрузки какой-нибудь flash вставки.

Получив объект browser, мы можем беспрепятственно найти определенный элемент. Например вот так:

var infoTable = browser.Tables.First(t => t.ClassName == "info");

Код выше находит первую таблицу, у которой определен класс info. Дальше действия могут быть разными, но мое предпочтение – по возможности загружать начинку элементов в System.Xml.Linq.XElement, чтобы в посделствии использовать LINQ:

var clean = Regex.Replace(infoTable.InnerHtml, @"<img [^>]+>", string.Empty);
var xe = XElement.Parse(clean);

В примере выше я корректирую тот факт что в HTML страница иногда использует незакрытый тэг <img>. В обычном сценарии бывает несколько подобных незакрытых элементов которые нужно либо удалить либо трансформировать.

После того, как вы загрузили XElement (и не произошло ошибки), его можно анализировать и вытаскивать начинку с помощью чисто .Net-ного API:

var rows = xe.Elements("tr");
string[] values = rows.Where(r => r.Elements("td").Any()).Select(r => r.Element("td").Value).ToArray();
foreach (string value in values)
  ...

Мое обычное действие в данном случае – создание сущности для вставления в базу.

Как получить картинку?

Иногда нужно автоматизировать не взаимодействие с HTML (что достаточно просто, т.к. элементы в WatiN обладают такими полезными методами как Click()), а например с Flash. В этом случае наша первая цель – это сделать «снимок» страницы дабы понять что где находится.

Мой подход к «фотографированию» страницы прост – я делаю скриншот всего монитора! Обычно графические элементы находятся на одном и том же месте, следовательно последующий поиск отрезка (например капчи) который нужно проанализировать особого труда не составляет.

Скриншот берется вот так:

private static Bitmap GetSnapshot()
{
  var bounds = Screen.GetBounds(Point.Empty);
  var bmp = new Bitmap(bounds.Width, bounds.Height);
  using (var g = Graphics.FromImage(bmp))
    g.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);
  return bmp;
}

Что делать с картинкой – решать вам, но учтите, что использовать managed структуру System.Drawing.Bitmap я не рекоммендую – это один из самых медленных аспектов в .Net. Работать с картинкой лучше в C++, о чем я уже писал.

Как двигать и кликать мышкой?

Казалось бы – простая вещь – двигать и кликать мышью. Это еще одна фича которая требуется если вы хотите взаимодействовать с графическим или RIA-контентом. Мне для этого понадобилась следующая инфраструктура:

enum Messages { WM_LBUTTONDOWN = 0x0201, WM_LBUTTONUP = 0x0202 };
 
[DllImport("User32.dll")]
static extern void mouse_event(MouseFlags dwFlags, int dx, int dy, int dwData, UIntPtr dwExtraInfo);
 
[Flags]
enum MouseFlags
{
  Move = 0x0001, LeftDown = 0x0002, LeftUp = 0x0004, RightDown = 0x0008,
  RightUp = 0x0010, Absolute = 0x8000
};

Ну а что касается самого метода «переместиться и кликнуть», то он выглядит вот так:

private static void ClickAt(Point p, int waitMsec)
{
  Console.WriteLine("Clicking at {0}, {1}", p.X, p.Y);
  Cursor.Position = p;
  mouse_event(MouseFlags.LeftDown, p.X, p.Y, 0, UIntPtr.Zero);
  mouse_event(MouseFlags.LeftUp, p.X, p.Y, 0, UIntPtr.Zero);
  Thread.Sleep(waitMsec);
}

Поверх перечисленной инфраструктуры собственно строится разбор – либо текстовый (что легко), либо графические, что потяжелее. Тут в ход идет вся тяжелая артиллерия – многопоточность, кластеры, unmanaged код, нейронные сети и прочие извращения в зависимости от задачи. ■

Реклама

Written by Dmitri

10 марта 2010 в 18:44

Опубликовано в Technology

Tagged with

комментариев 25

Subscribe to comments with RSS.

  1. а координаты положения объекта на странице не определяете?

    al

    10 марта 2010 at 19:53

    • Нет, и не знаю как. Вроде как это невозможно методами WatiN.

      Dmitri

      22 марта 2010 at 23:25

  2. Спасибо, очень полезная статья. Месяц назад она бы сэкономила мне кучу времени.

    Bdfy

    10 марта 2010 at 21:09

  3. А как же Selenium, ведь он умеет то же самое и даже больше. Нужно ли изобретать велосипед?

    Sergey

    11 марта 2010 at 13:25

    • Сам ни разу Selenium не смотрел — знаю что упущение, займусь когда время будет. Взял WatiN т.к. он достаточно хорошо распиарен в .Net-комьюнити.

      Dmitri

      22 марта 2010 at 23:23

  4. Не так давно пробовал библиотеку WatiN в работе. Были очень положительные впечатления.

    На мой взгляд в этой библиотеке очень выгодно отличается API. Он очень domain specific и его удобно использовать.

    Так же очень логично использовать так называемый объектный подход, когда каждая страница в сценарии инкапсулирована в отдельный класс, унаследованный от класса WatiN.Core.Page. В этом классе описывается мэппинг контролов страницы на определенные свойста с помощью атрибутов плюс реализуется логика взаимодействия со страницей.

    После чего реализованные страницы могут быть использованы во множестве тестов с разными входными данными и с разными наборами страниц.
    Таким образом, при чтении теста QA или разработчик концентрируется не на полях ввода и кнопках, а на бизнес-логике и логике взаимодействия.

    Классический API можно увидеть на главной странице сайта WatiN, а вот ниже пример «Page-based API»:

    using (var ie = new IE("http://www.google.com"))
    {
    	var page = ie.Page();
    	page.SearchFor("WatiN");
    }
    ...
    [Page(UrlRegex="www.google.*")]
    public class GoogleSearchPage : Page
    {
    	[FindBy(Name="btnG")]
    	public Button SearchButton;
    	[FindBy(Name="q")]
    	public TextField SearchCriteria;
    	public void SearchFor(string searchCriteria)
    	{
    		this.SearchCriteria.TypeText(searchCriteria);
    		this.SearchButton.Click();
    	}
    }
    

    Вот как может выглядить реальный сценарий на WatiN:

    browser = new IE(fedExHomePageUrl);
    browser.Page().LogIn(login, password);
    browser.GoTo(fedExSearchPageUrl);
    browser.Page().Search(dateFrom, dateTo);
    browser.Page().SaveAs(reportPath);
    browser.Page().Download(browser, reportPath);
    ...
    

    Если нужно сделать скриншот именно страницы, а не всего рабочего стола либо всего браузера, то можно воспользоваться существующим API:

    // Делаем окно браузера видимым, но не активным. Если окно скрыто, то вместо скриншота у нас будет снимок черного экрана. Окно можно делать не активным, чтобы оно не мешало работе. После снятия скриншота окно можно будет сразу же спрятать:
    browser.ShowWindow(NativeMethods.WindowShowStyle.ShowNormalNoActivate);
    browser.CaptureWebPageToFile(debugImagePath);
    browser.ShowWindow(NativeMethods.WindowShowStyle.Hide);
    

    Если приходится парсить не well-formed HTML страницу, то лучше использовать связку SGMLReader / HtmlAgilityPack и Linq to XML:
    http://status-alexus.blogspot.com/2010/01/html.html
    http://www.codeplex.com/htmlagilitypack

    P.S. Мой твиттер на правах самопиара :) http://twitter.com/alexey_diyan

    Alexey Diyan

    11 марта 2010 at 13:33

    • Спасибо за комментарий. Сам лично WatiN глубоко не копал т.к. не было необходимости. А еще, насколько я помню CaptureWebPageToFile работает только в IE, так? На самом деле мне даже не весь документ нужен как картинка — мне скорее надо снэпшот конкретного элемента. Ну и знать его координаты и размер тоже полезно.

      Dmitri

      11 марта 2010 at 15:39

    • Алексей, Дмитрий, если вас не затруднит набросайте пожалуйста пример работы с Selectlist. Спасибо.

      Иван Пучков

      13 марта 2010 at 20:51

      • Вот пример из «живого» проекта — у меня есть банковское приложение которые спрашивает три буквы из пароля в SelectList’ах. Код выборки выглядит вот так:

                string pwd = "Abracadabra";
                for (int i = 0; i < 3; ++i)
                {
                  var list = browser.SelectLists[i];
                  Debug.Assert(list.Options.Count > 0);
                  list.Option(x => x.Value == pwd[values[i]-1].ToString()).Select();
                }
        

        Dmitri

        14 марта 2010 at 15:04

        • Ну конечно же! Спасибо!

          Пучков Иван

          14 марта 2010 at 15:54

  5. …к сожалению разметку кода в комментариях немного покоцало :(

    Alexey Diyan

    11 марта 2010 at 13:34

  6. Если б она еще php файлы с сервака качала, то цены б ей небыло.

    Tibald

    13 марта 2010 at 1:33

  7. Подскажите пожалуйста почему при выполнении скрипта по нажатию кнопок программа работает без как либо проблем
    private void buttonWork_Click(object sender, RoutedEventArgs e)
    {
    _browser.GoTo(«http://192.168.0.1»);
    _browser.RunScript(«Work();»);
    }
    но как только вызов метода произвожу из события таймера программа вываливается с ошибкой
    void _timer_Elapsed(object sender, ElapsedEventArgs e)
    {
    _browser.GoTo(«http://192.168.0.1»);
    _browser.RunScript(«Work();»);
    }

    BUTEK

    26 марта 2010 at 8:53

    • Если честно, понятия не имею. Попробуйте задать этот вопрос на StackOverflow, там иногда появляются эксперты по этому фреймворку.

      Dmitri

      26 марта 2010 at 9:11

    • А что за ошибка?

      Виталий

      7 июня 2010 at 8:43

  8. Бьюсь над проблемой. Необходимо загрузить файл на сайт. Вылезает диалог выбора файла, а дальше что делать не знаю. Был ли у вас опыт реализации загрузки файла через WatiN? Если был поделитесь.

    Mr. DuDuDu

    13 июня 2010 at 12:30

  9. @Mr. DuDuDu. Для решения подобной задачи нужно использовать один из DialogHanlder’ов.

    Насколько я знаю есть обработчики диалогов как минимум для для OpenFile, SaveAs, Alert и Confirm диалогов.

    Скопировал пример с официального сайта, в котором используется обработка SaveAs диалога:

    using(IE ie = new IE(someUrlToGoTo))
    {
    FileDownloadHandler fileDownloadHandler = new FileDownloadHandler(fullFileName);
    ie.AddDialogHandler(fileDownloadHandler);

    ie.Link(«startDownloadLinkId»).Click();

    fileDownloadHandler.WaitUntilFileDownloadDialogIsHandled(15);
    fileDownloadHandler.WaitUntilDownloadCompleted(200);
    }

    Alexey Diyan

    13 июня 2010 at 16:48

  10. IE ie = new IE("https://www.xxx.com/WFrmlogin.aspx&quot;);
    MyFileUploadDialogHandler uploadHandler = new MyFileUploadDialogHandler(@"D:65-6405_URGENT.xls");

    ie.WaitForComplete();
    ie.TextField(Find.ById("txtUser")).TypeText("login");
    ie.TextField(Find.ById("txtPassWord")).TypeText("***");

    ie.Button(Find.ById("btnok")).Click();
    ie.WaitForComplete();

    ie.GoTo("https://www.xxx.com/inq/WFrmUpOption.aspx&quot;);
    ie.WaitForComplete();

    ie.DialogWatcher.Clear();
    ie.AddDialogHandler(uploadHandler);
    ie.FileUpload(Find.ById("FilUpload")).ClickNoWait();

    //ie.DialogWatcher.Clear();

    ie.Button(Find.ById("butUpload")).Click();
    ie.WaitForComplete();

    Появляется диалог выбора файла. А мне надо что бы этот диалог автоматически выбирал нужный мне файл и закрывался.

    Mr. DuDuDu

    14 июня 2010 at 11:01

  11. Спасибо за труд. Отличная статья. Одно для меня осталось непонятным. Как скачивать оригиналы картинок со страницы?

    Tim

    1 марта 2011 at 11:50

    • Ну тут все просто — через XPath получить все тэги IMG а потом скачать их «обычным путем».

      Dmitri

      1 марта 2011 at 12:01

  12. Добрый день Дмитрий!
    Вы привели такой пример парсинга таблицы:
    var infoTable = browser.Tables.First(t => t.ClassName == «info»);
    var clean = Regex.Replace(infoTable.InnerHtml, @»]+>», string.Empty);
    var xe = XElement.Parse(clean);
    Попробовал так сделать, но при вызове XElement.Parse(clean) программа ругается так:
    ‘ctl00_ctl00_main_cphMain_grdEntities’ is an unexpected token. The expected token is ‘»‘ or »’. Line 2, position 11.

    ctl00_ctl00_main_cphMain_grdEntities это идентификатор таблицы, я так понимаю что эта ошибка возникает из-за того, что watin почему-то убирает кавычки, в которых этот идентификатор. Когда я в браузере смотрю код html, то он такой:
    <table cellspacing="0" cellpadding="0" border="0" id="ctl00_ctl00_main_cphMain_grdEntities" …
    а когда в отладчике смотрю clean, то такой:
    <TABLE id=ctl00_ctl00_main_cphMain_grdEntities…
    т.е. без кавычек.
    Как тогда распарсить?
    Заранее большое спасибо!

    onton

    16 июня 2011 at 9:41

    • Привет! Скажу сразу — WatiN очень чувствителен к качеству HTML который ему дается. Если вам нужно только парсить, советую пропустить весь текст через MindTouch.SgmlReader, он возможно подправит все неточности.

      Dmitri

      16 июня 2011 at 11:33

  13. Доброго времени суток Дмитрий. Возможно этот вопрос уже звучал — подскажите пожалуйста как работать с Watin чтобы браузер не загружался(т.е. был невидим для пользователя), а всё выполнялось в невидимом режиме.

    Вадим

    12 июня 2013 at 23:05

    • Вообще есть свойство, что-то вроде Visible, в купе с AutoClose вроде как браузер невидим. Другое дело что все равно это не Phantom, окно так и так создается, от чего идет масса проблем.

      Dmitri

      12 июня 2013 at 23:07


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: