1. Поиск текста с помощью встроенных в класс String методов
Самый быстрый и понятный для новичков в программировании на С# способ.
Представим, что нам нужен только заголовок страницы, который лежит в теге <title>. HTML код будет выглядеть таким образом:
<title>
Искомый текст
</title>
Задача сводится к поиска позиции фраз "<title>" и "</title>", и вырезанию текста между ними.
var txtHTML = @"<title>Page title</title>";
var txtPrefix = @"<title>";
var txtSuffix = @"</title>";
var txtPrefixPosition = txtHTML.IndexOf(txtPrefix, StringComparison.OrdinalIgnoreCase);
var txtSuffixPosition = txtHTML.IndexOf(txtSuffix, txtPrefixPosition + txtPrefix.Length, StringComparison.OrdinalIgnoreCase);
var txtTitle = txtHTML.Substring(
txtPrefixPosition + txtPrefix.Length,
txtSuffixPosition - txtPrefixPosition - txtPrefix.Length
);
Такой код хорошо оформить в виде функции:
public static String FindText(string source, string prefix, string suffix)
{
var prefixPosition = source.IndexOf(prefix, StringComparison.OrdinalIgnoreCase);
var suffixPosition = source.IndexOf(suffix, prefixPosition + prefix.Length, StringComparison.OrdinalIgnoreCase);
if ((prefixPosition >= 0) && (suffixPosition >= 0) && (suffixPosition > prefixPosition) && ((prefixPosition + prefix.Length) <= suffixPosition))
{
return source.Substring(
prefixPosition + prefix.Length,
suffixPosition - prefixPosition - prefix.Length
);
}
else
{
return String.Empty;
}
}
Использовать такой код можно так:
var txtTitle = FindText(txtHTML,@"<title>",@"</title>");
Такой способ хорошо подходит для одиночного поиска и когда искомый текст окружен уникальными последовательностями (в данном случае - тегами <title> и </title>).
2. Поиск текста с помощью регулярных выражений
Предыдущий способ плохо работает, если prefix и suffix имеют сложный вид, или вообще неизвестны, но известна структура искомого текста.
Например, нам нужны все ссылки со страницы. Тогда поиск можно организовать так:
using System.Text.RegularExpressions;
using System.Linq;
//...
//Регулярное выражение для поиска A тега
var regexpATag = new Regex(@"<a[^<>]*>[^<]*<\/a>", RegexOptions.IgnoreCase | RegexOptions.Multiline);
//Регулярное выражение для поиска href свойства
var regexpHref = new Regex(@"href\s*=\s*[""'](.*?)[""']", RegexOptions.IgnoreCase | RegexOptions.Multiline);
var matches = regexpATag.Matches(txtHTML);
var links = new List<string>();
foreach (Match match in matches)
{
var link = regexpHref.Match(match.Value);
if (link.Success) links.Add(link.Groups[1].Value);
};
//LINQ запрос на сортировку и уникализацию
links = links.Distinct().OrderBy(el => el).ToList();
В результате выполнения кода в переменной links будет список уникальных ссылок, отсортированных в алфавитном порядке.
Подобный способ подразумевает умение составлять регулярные выражения. Протестировать ваши регулярные выражения удобно на сайте regex101.com.
3. Использование библиотеки HtmlAgilityPack (https://html-agility-pack.net/)
Следующая степень удобства - использование сторонних библиотек, например HtmlAgilityPack. Данная библиотека умеет строить DOM дерево по HTML коду. При этом сам код нужно получить заранее:
using System.Net;
using System.IO;
//...
/// <summary>
/// Возвращает текст страницы по адресу url
/// </summary>
/// <param name="url">Адрес страницы</param>
/// <returns>Возвращает текст страницы по адресу url</returns>
public static string GetPage(string url)
{
var result = String.Empty;
var request = (HttpWebRequest)WebRequest.Create(url);
var response = (HttpWebResponse)request.GetResponse();
if (response.StatusCode == HttpStatusCode.OK)
{
var responseStream = response.GetResponseStream();
if (responseStream != null)
{
StreamReader streamReader;
if (response.CharacterSet != null)
streamReader = new StreamReader(responseStream, Encoding.GetEncoding(response.CharacterSet));
else
streamReader = new StreamReader(responseStream);
result = streamReader.ReadToEnd();
streamReader.Close();
}
response.Close();
}
return result;
}
Тогда получить страницу можно так (для примера - каталог вакансий):
var txtHTML = GetPage(@"https://joblab.ru/search.php?r=vac&srregion=100&maxThread=100&submit=1");
var doc = new HtmlDocument(); // Создание документа
doc.LoadHtml(txtHTML); // Загрузка кода в документ
Анализируя код, нужно понять:
1) По какому принципу формируются URL страниц со списком вакансий (их обычно несколько).
https://joblab.ru/search.php?r=vac&srregion=100&maxThread=100&submit=1&page=2
https://joblab.ru/search.php?r=vac&srregion=100&maxThread=100&submit=1&page=3 ...
var baseURL = @"https://joblab.ru/search.php?r=vac&srregion=100&pred=30&maxThread=100&submit=1";
catalogPages.AddRange(
Enumerable
.Range(2, Convert.ToInt32(lastPageNumber) - 2)
.Select(el=> $"{baseURL}&page={el}")
);
2) Как узнать номер последней подобной страницы?
(Клик правой кнопкой по элементу в Chrome - просмотреть код) Все кнопки навигации имеют класс "pager", притом последняя из таких кнопок - содержит номер последней страницы
var lastPageNumber = doc.DocumentNode.SelectNodes("//*[@class='pager']").Last().InnerText;
3) Какого вида ссылки на страницы с данными?
Сылки начинаются с "/vac":
var vacancyPages = new List<string>();
foreach (var catalogPage in catalogPages) {
txtHTML = GetPage(catalogPage); // Получение кода страницы
doc.LoadHtml(txtHTML); // Загрузка кода в документ
vacancyPages.AddRange( //Добавить к vacancyPages список ссылок
doc.DocumentNode
//Выбрать все ссылки (тут используется XPath запрос)
.SelectNodes("//a")
//Преобразовать список ссылкок в список их href
.Select(el => el.Attributes["href"].Value)
//Фильтр - оставить только ссылки, содержащие "/vac"
.Where(el=>el.Contains("/vac"))
//Преобразовать относительные ссылки в абсолютные
.Select(el=> "https://joblab.ru"+el)
//Преобразование в список
.ToList()
);
}
4) Открыв страницу с вакансией, подобным образом ищем интересующие поля - название, телефон, email, описания и т.п.
Чтобы не делать множество запросов к серверу, можно получить код страницы однократно, и сохранить его. //Получение HTML кода страниц с товарами
Dictionary<string, string> pages = new Dictionary<string, string>();
foreach (var productPage in productPages)
{
txtHTML = GetPage(productPage); // Получение кода страницы
doc.LoadHtml(txtHTML); // Загрузка кода в документ
pages[productPage] = txtHTML;
}
Тогда код парсера будет такой:
//Парсинг данных
foreach (var page in pages) {
doc.LoadHtml(page.Value); // Загрузка кода в документ
//ссылка на профиль компании
var company = "https://joblab.ru" + doc.DocumentNode
.SelectNodes("//a") //Выбрать все ссылки из документа(тут используется XPath запрос)
.Select(el => el.Attributes["href"].Value)//Взять только href свойство
.Where(el => el.Contains("/e"))//Отфильтровать по признаку "содержит /e"
.FirstOrDefault();//Взять первый
//Название организации
var companyName = HttpUtility.HtmlDecode ( doc.DocumentNode
.SelectNodes("//a")//Выбрать все ссылки из документа
.Where(el => el.Attributes["href"].Value.Contains("/e"))//Отфильтровать по признаку "содержит /e"
.Select(el=>el.InnerText)//Взять только текст внутри тегов
.Aggregate("",(acc,val)=>acc+=val.ToString())//Выдать весь текст
);
//Название вакансии
var vacancy = FindText(page.Value, "<h1>", "</h1>");//Тут проще всего использовать поиск текста
//регион или город
var city = FindText(page.Value, @"graytext"">Город</p></td><td><p><b>", "</b>").ToString();//Тут проще всего использовать поиск текста
// и т.д.
}
После парсинга данные сохраняются любым удобным способом. Чаще всего это csv файл. Ниже представлен простейший код для формирования небольшого по размеру csv файла (для больших файлов желательно использовать StreamWriter)
в каждом значении кавычки заменяются на двойные кавычки, переводы строк заменяются на пробел и значение обрамляется двойными кавычками. Каждая запись отделяется от другой переводом строки.
Dictionary<string, Tuple<string, string, string, string, string, string, string>> data = new Dictionary<string, Tuple<string, string, string, string, string, string, string>>();
Здесь ключ словаря - url, значение словаря - кортеж из 7 string(хотя тут можно применить и List<string> и массив строк и т.п.)
var csvData = "";
foreach(var row in data)
{
csvData+= "\"" + row.Key.Replace("\"", "\"\"").Replace('\n', ' ').Replace('\r', ' ') + "\";" +
"\"" + row.Value.Item1.Replace("\"", "\"\"").Replace('\n', ' ').Replace('\r', ' ') + "\";" +
"\"" + row.Value.Item2.Replace("\"", "\"\"").Replace('\n', ' ').Replace('\r', ' ') + "\";" +
"\"" + row.Value.Item3.Replace("\"", "\"\"").Replace('\n', ' ').Replace('\r', ' ') + "\";" +
"\"" + row.Value.Item4.Replace("\"", "\"\"").Replace('\n', ' ').Replace('\r', ' ') + "\";" +
"\"" + row.Value.Item5.Replace("\"", "\"\"").Replace('\n', ' ').Replace('\r', ' ') + "\";" +
"\"" + row.Value.Item6.Replace("\"", "\"\"").Replace('\n', ' ').Replace('\r', ' ') + "\";" +
"\"" + row.Value.Item7.Replace("\"", "\"\"").Replace('\n', ' ').Replace('\r', ' ') + "\"\n";
}
System.IO.File.WriteAllText("out.csv",csvData,Encoding.UTF8);//запись в файл "out.csv" в кодировке UTF8
Все эти способы подойдут для парсинга большинства простых сайтов.
Напоследок, подскажу как улучшить ваш парсер. Для более сложных случаев, когда содержимое страницы формируется динамически, вышеописанные способы не подойдут, и нужно полноценно эмулировать браузер, используя, например, библиотеку Selenium.
Парсить сайт в один поток - довольно медленно. Поэтому распараллеливание парсинга ускорит процесс в разы. И интерфейс программы станет приятнее (не будет ощущения, что программа зависла, пока получает данные).
Многие сайты активно сопротивляются, если заподозрят вас в парсинге, поэтому есть смысл продумать использование прокси-серверов.
Автор материала Григорий Боев
Оставьте свой комментарий
Комментарии