Скрейпинг сайтов с .Net и WatiN
Рано или поздно у каждого разработчика появится соблазн «скачать» какой-нибудь сайт, либо для того чтобы получить или проанализировать определенный контент, либо просто доказать себе что это в его (её) силах. На самом деле, получить доступ к 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 код, нейронные сети и прочие извращения в зависимости от задачи. ■

а координаты положения объекта на странице не определяете?
al
10 Март 2010 в 19:53
Нет, и не знаю как. Вроде как это невозможно методами WatiN.
Dmitri
22 Март 2010 в 23:25
Спасибо, очень полезная статья. Месяц назад она бы сэкономила мне кучу времени.
Bdfy
10 Март 2010 в 21:09
А как же Selenium, ведь он умеет то же самое и даже больше. Нужно ли изобретать велосипед?
Sergey
11 Март 2010 в 13:25
Сам ни разу Selenium не смотрел – знаю что упущение, займусь когда время будет. Взял WatiN т.к. он достаточно хорошо распиарен в .Net-комьюнити.
Dmitri
22 Март 2010 в 23:23
Не так давно пробовал библиотеку 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:
Если нужно сделать скриншот именно страницы, а не всего рабочего стола либо всего браузера, то можно воспользоваться существующим API:
Если приходится парсить не 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 в 13:33
Спасибо за комментарий. Сам лично WatiN глубоко не копал т.к. не было необходимости. А еще, насколько я помню CaptureWebPageToFile работает только в IE, так? На самом деле мне даже не весь документ нужен как картинка – мне скорее надо снэпшот конкретного элемента. Ну и знать его координаты и размер тоже полезно.
Dmitri
11 Март 2010 в 15:39
Алексей, Дмитрий, если вас не затруднит набросайте пожалуйста пример работы с Selectlist. Спасибо.
Иван Пучков
13 Март 2010 в 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 в 15:04
Ну конечно же! Спасибо!
Пучков Иван
14 Март 2010 в 15:54
…к сожалению разметку кода в комментариях немного покоцало :(
Alexey Diyan
11 Март 2010 в 13:34
Если б она еще php файлы с сервака качала, то цены б ей небыло.
Tibald
13 Март 2010 в 1:33
Ну, некоторые даже .svn качают :)
Dmitri
14 Март 2010 в 15:06
Подскажите пожалуйста почему при выполнении скрипта по нажатию кнопок программа работает без как либо проблем
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 в 8:53
Если честно, понятия не имею. Попробуйте задать этот вопрос на StackOverflow, там иногда появляются эксперты по этому фреймворку.
Dmitri
26 Март 2010 в 9:11
А что за ошибка?
Виталий
7 Июнь 2010 в 8:43
Бьюсь над проблемой. Необходимо загрузить файл на сайт. Вылезает диалог выбора файла, а дальше что делать не знаю. Был ли у вас опыт реализации загрузки файла через WatiN? Если был поделитесь.
Mr. DuDuDu
13 Июнь 2010 в 12:30
@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 в 16:48
IE ie = new IE("https://www.xxx.com/WFrmlogin.aspx");
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");
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 в 11:01
Спасибо за труд. Отличная статья. Одно для меня осталось непонятным. Как скачивать оригиналы картинок со страницы?
Tim
1 Март 2011 в 11:50
Ну тут все просто – через XPath получить все тэги IMG а потом скачать их “обычным путем”.
Dmitri
1 Март 2011 в 12:01
Добрый день Дмитрий!
Вы привели такой пример парсинга таблицы:
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 в 9:41
Привет! Скажу сразу – WatiN очень чувствителен к качеству HTML который ему дается. Если вам нужно только парсить, советую пропустить весь текст через MindTouch.SgmlReader, он возможно подправит все неточности.
Dmitri
16 Июнь 2011 в 11:33