Многопоточный парсинг на C# и работа с базой данных

freeman

Client
Регистрация
31.07.2010
Сообщения
130
Благодарностей
138
Баллы
43
Jumping-spider.jpg


В этой статье я хочу продемонстрировать вам, как можно распараллелить работу даже на однопоточной версии зеннопостера. Благо, зенка позволяет выполнять произвольный C# код, чем мы и воспользуемся. В качестве демонстрационного примера, с помощью многопоточного паука будем решать задачу по парсингу одного интернет магазина с сохранением полученных данных в базу. Поэтому, базовые навыки работы с базой данных вы тоже освоите.

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

Принцип работы паука довольно прост:
  • Задачи, которые являют собой пару из ссылки и привязанной к ней функции, добавляются в очередь
  • Паук берет задачи из очереди и запускает потоки, которые GET запросом получают ответ сервера по ссылке
  • Паук контролирует количество запущенных потоков согласно заданному лимиту
  • После получения данных из сети по определенной ссылке, запускается функция, привязанная к этой ссылке, для обработки полученных данных. При этом, функция принимает в качестве аргумента html документ, сформированный из ответа сервера
  • В процессе обработки можно спарсить из документа новые ссылки и добавить их в очередь
  • Паук работает до тех пор, пока очередь не будет исчерпана
Паук реализован в виде библиотеки для удобства и возможности использовать в других проектах. Вы его найдете в архиве, прикрепленном к статье, вместе с другими необходимыми библиотеками. А тех, кто хочет изучить исходники и модифицировать паука для своих задач, прошу под кат.
1. Создаем в Visual Studio проект библиотечного типа, именуем его как-нибудь (я назвал ZennoSpider) и выбираем рантайм .net framework 4 версии.
vs_create_project.png


2. Подключаем в проекте ссылки на библиотеки HtmlAgilityPack.dll и xNet.dll. Они есть в архиве, прикрепленном к статье.
vs_add_reference.png


3. Копируем исходники паука в проект.
Код:
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using xNet;

namespace ZennoSpider
{
    public class Spider
    {
        private readonly Queue<Tuple<string, Action<HtmlDocument>>> _urlQueue = new Queue<Tuple<string, Action<HtmlDocument>>>();
        private readonly List<Tuple<Task<string>, Action<HtmlDocument>>> _tasks = new List<Tuple<Task<string>, Action<HtmlDocument>>>();
        private readonly int _threadsCount;
        private int _counter = 0;
        private Action<string> _log;
        private Encoding _encoding;

        public Spider(int threadsCount = 10)
        {
            _threadsCount = threadsCount;
        }

        public Encoding Encoding
        {
            set
            {
                _encoding = value;
            }
        }

        public Action<string> OnLog
        {
            set
            {
                _log = value;
            }
        }

        protected void AddTask(string url, Action<HtmlDocument> callbackFunc)
        {
            if (_tasks.Count < _threadsCount)
                _tasks.Add(new Tuple<Task<string>, Action<HtmlDocument>>(Task.Factory.StartNew<string>(Worker, url, TaskCreationOptions.LongRunning), callbackFunc));
            else
                _urlQueue.Enqueue(new Tuple<string, Action<HtmlDocument>>(url, callbackFunc));
        }

        protected virtual void Initialize() { }

        public void Start()
        {
            this.Initialize();

            while (_urlQueue.Count > 0 || _tasks.Count > 0)
            {
                if (_tasks.Count > 0)
                {
                    Tuple<Task<string>, Action<HtmlDocument>>[] temp = _tasks.ToArray();

                    foreach (Tuple<Task<string>, Action<HtmlDocument>> task in temp)
                    {
                        if (task.Item1.IsCompleted)
                        {
                            if (!task.Item1.IsFaulted)
                            {
                                if (_log != null)
                                    _log(string.Format("[{0}] {1} | OK", _counter++, task.Item1.AsyncState));
                                HtmlDocument doc = new HtmlDocument();
                                doc.LoadHtml(Html.ReplaceEntities(task.Item1.Result));
                                task.Item2(doc);
                            }
                            else
                            {
                                HttpException exc = (HttpException)task.Item1.Exception.InnerException;

                                if (_log != null)
                                {
                                    if (exc != null)
                                    {
                                        switch (exc.Status)
                                        {
                                            case HttpExceptionStatus.ProtocolError:
                                                _log(string.Format("[{0}] {1} | Код состояния: {2}", _counter++, task.Item1.AsyncState, (int)exc.HttpStatusCode));
                                                break;

                                            case HttpExceptionStatus.ConnectFailure:
                                                _log(string.Format("[{0}] {1} | Не удалось соединиться с HTTP-сервером.", _counter++, task.Item1.AsyncState));
                                                break;

                                            case HttpExceptionStatus.SendFailure:
                                                _log(string.Format("[{0}] {1} | Не удалось отправить запрос HTTP-серверу.", _counter++, task.Item1.AsyncState));
                                                break;

                                            case HttpExceptionStatus.ReceiveFailure:
                                                _log(string.Format("[{0}] {1} | Не удалось загрузить ответ от HTTP-сервера.", _counter++, task.Item1.AsyncState));
                                                break;

                                            case HttpExceptionStatus.Other:
                                                _log(string.Format("[{0}] {1} | Неизвестная ошибка.", _counter++, task.Item1.AsyncState));
                                                break;
                                        }
                                    }
                                    else
                                    {
                                        _log(string.Format("[{0}] {1} | {2}", _counter++, task.Item1.AsyncState, task.Item1.Exception.InnerException.Message));
                                    }
                                }
                            }

                            task.Item1.Dispose();
                            _tasks.Remove(task);
                        }
                    }

                    Array.Clear(temp, 0, temp.Length);
                }

                for (int i = 0; i < _threadsCount - _tasks.Count; i++)
                {
                    try
                    {
                        Tuple<string, Action<HtmlDocument>> t = _urlQueue.Dequeue();
                        _tasks.Add(new Tuple<Task<string>, Action<HtmlDocument>>(Task.Factory.StartNew<string>(Worker, t.Item1, TaskCreationOptions.LongRunning), t.Item2));
                    }
                    catch (InvalidOperationException)
                    {
                        break;
                    }
                }

                Thread.Sleep(500);
            }
        }

        private string Worker(object url)
        {
            using (HttpRequest request = new HttpRequest())
            {
                request.MaximumAutomaticRedirections = 10;
                //request.Cookies = new CookieDictionary();
                request.ConnectTimeout = 10 * 1000;
                request.ReadWriteTimeout = 30 * 1000;
                //request.IgnoreProtocolErrors = true;

                request["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
                request["Accept-Language"] = "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3";
                request["User-Agent"] = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:53.0) Gecko/20100101 Firefox/53.0";

                HttpResponse r = request.Get((string)url);

                byte[] bytes = r.ToBytes();

                if (_encoding != null)
                    return _encoding.GetString(bytes);
                return Encoding.UTF8.GetString(bytes);
            }
        }
    }
}

4. Собираем, выбрав в панели инструментов Сборка ==> Собрать решение или просто нажав F6 на клавиатуре.

mongodb.png

Мне не очень нравятся реляционные базы данных, поэтому я предпочитаю использовать решения из семейства NoSQL баз. Ярким представителем этого семейства является база данных MongoDB. MongoDB - это объектно-документарная база данных, хранящая данные в формате JSON. Именно ее мы и будем использовать для хранения наших данных.
1. Качаем бинарные файлы по ссылке http://downloads.mongodb.org/win32/mongodb-win32-x86_64-2008plus-3.0.1.zip
2. Разархивируем в любое удобное место
3. Запускаем из командной строки демон базы mongod.exe с параметром --dbpath, указывающего, в какой папке будут храниться непосредственно сами базы с данными. В моем случае эта команда выглядит так:
Код:
e:\Mongo\mongodb-win32-x86_64-2008plus-3.0.1\bin\mongod.exe --dbpath e:\Mongo\data
4. Корректно остановить работу демона базы данных можно комбинацией Ctrl + C
Код:
public class Contact
{
    public string email { get; set; }
    public string phone { get; set; }
}

public class User
{
    public ObjectId Id { get; set; }
    public string name { get; set; }
    public string surname { get; set; }
    public int age { get; set; }
    public Contact contactInfo { get; set; }
}

// Устанавливаем соединение с сервером базы данных
var server = new MongoClient().GetServer();
      
// Так можно получить названия всех баз данных
var databaseNames = server.GetDatabaseNames();
      
// Подключаемся к базе данных. Даже если БД с таким названием нет,
// она автоматически будет создана при записи данных в нее
var database = server.GetDatabase("users_database");

// Таким образом можно получить названия всех коллекций в этой БД
var collectionNames = database.GetCollectionNames();

// Выбираем коллекцию, с которой будем работать
// т.к. коллекции с таким названием пока в БД нет, она будет создана
var users = database.GetCollection<User>("users");

var user = new User { name = "John", surname = "Smith", age = 31, contactInfo = new Contact { email = "[email protected]", phone = "+7 (987) 654-32-10" } };

// Записываем пользователя в нашу коллекцию users
users.Insert(user);

// Так можно получить всех юзеров в коллекции
var cursor = users.FindAll();
foreach (var userData in cursor)
{
    // И проитерироваться по ним, получая доступ к интересующим данным
}

// А так можно выбрать всех людей, с фамилией Smith
cursor = users.Find(Query.EQ("surname", "Smith"));

// Выбираем всех юзеров от 18 лет и старше
cursor = users.Find(Query.GTE("age", 18));

// Условия можно комбинировать
cursor = users.Find(Query.And(Query.EQ("surname", "Smith"), Query.GTE("age", 18)));
      
// Удалить коллекцию с таким именем, если она существует в этой базе
database.DropCollection("users");

// Удалить базу данных, если база с таким именем существует
server.DropDatabase("users_database");
 
Тема статьи
Парсинг
Номер конкурса статей
Седьмой конкурс статей

Вложения

Для запуска проектов требуется программа ZennoPoster или ZennoDroid.
Это основное приложение, предназначенное для выполнения автоматизированных шаблонов действий (ботов).
Подробнее...

Для того чтобы запустить шаблон, откройте нужную программу. Нажмите кнопку «Добавить», и выберите файл проекта, который хотите запустить.
Подробнее о том, где и как выполняется проект.

Последнее редактирование:

freeman

Client
Регистрация
31.07.2010
Сообщения
130
Благодарностей
138
Баллы
43
И так, настало время воспользоваться полученными навыками. Но прежде чем создать проект в ProjectMaker'е, необходимо сначала скопировать все библиотеки из архива в папку Progs\ExternalAssemblies. Кроме того, файлы HtmlAgilityPack.dll и MongoDB.Bson.dll нужно также скопировать в корень папки Progs, так как от них зависит работа других библиотек. Теперь создаем проект в ProjectMaker'е и открываем Расширенный редактор.

1. Добавляем в шаблон инструмент Ссылки из GAC и в него по очереди добавляем все наши ссылки из архива.
  • HtmlAgilityPack.dll - отличный инструмент для разбора html кода, в т.ч. и невалидного, с помощью xPath выражений
  • xNet.dll - замечательная библиотека для работы с GET/POST запросами
  • ZennoSpider.dll - это, собственно, наш паук
  • MongoDB.Bson.dll
  • MongoDB.Driver.dll - это, как понятно из названия, драйвер для работы с базой MongoDB
  • Также добавляем ссылку на сборку System.Xml.dll, воспользовавшись фильтрацией. Она необходима для работы HtmlAgilityPack
Итоговый список ссылок должен выглядеть примерно так:
pm_references.png


2. Добавляем в шаблон инструмент Директивы using и общий код. Открываем для редактирования и переходим на вкладку Общий код. Первым делом, подключаем пространства имен, которые нам понадобятся.
Код:
using HtmlAgilityPack;
using MongoDB.Bson;
using MongoDB.Driver;
using ZennoSpider;
Теперь нам необходимо создать новый класс паука, унаследовав все возможности базового паука и снабдить его логикой, указав ему, где и что парсить. Я назвал его RegardSpider, согласно названию интернет магазина, который мы будем парсить. Конструктор класса принимает целое число, указывающее на максимальное количество потоков, которые будут задействованы при парсинге.
Код:
public class RegardSpider : Spider
{
    public RegardSpider(int threadsCount)
        : base(threadsCount) { }
}
Для того, чтобы наш паук заработал, ему нужно указать отправную точку, с которой начнется его ползание по всемирной паутине. Сделать это можно переопределив метод Initialize, в котором появляется возможность вызовом функции AddTask добавлять задания в очередь на парсинг. Метод AddTask принимает 2 агрумента - ссылку на веб-страницу и функцию для обработки HTML документа, полученного от сервера в ответ на GET запрос. В функциях-обработчиках, помимо разбора HTML документа с помощью xPath выражений или регулярок, можно как сохранять обработанные данные, так и добавлять новые задания для следующих этапов парсинга. И таких этапов может быть сколько угодно.

Откроем в браузере адрес сайта, который будем парсить. В данном случае, в качестве жертвы для нашего паука, был выбран интернет-магазин http://www.regard.ru. Чтобы случайно не убить сайт большим количеством запросов, парсить будем только информацию о товарах из раздела МОБИЛЬНЫЕ ТЕЛЕФОНЫ. Переходим в соответствующий раздел и копируем ссылку из адресной строки браузера. Она и будет отправной точкой для паука.
Код:
protected override void Initialize()
{
    this.AddTask("http://www.regard.ru/catalog/group52000.htm", this.CategoryParser);
}
Первую функцию-обработчик, которая будет парсить главную страницу раздела, я назвал CategoryParser. Название функциям-обработчикам можно давать произвольное. Теперь эту функцию надо создать. А какую информацию она будет парсить? Дело в том, что при парсинге количество страниц в разделе может быть любым, в зависимости от количества товаров. В нижней части страницы есть навигация по страницам. Прокликав парочку страниц легко понять, что ссылки формируются путем добавления слова page и числа, соответствующего номеру страницы. Последний элемент пагинации содержит число, указывающее на количество страниц в разделе. Спарсив его, мы сможем в цикле сформировать ссылки на все страницы с первой до последней и добавить их в очередь. Кликаем по нему правой клавишей мыши и выбираем пункт Исследовать элемент в файрфоксе или Просмотреть код в хроме и опере.
rg_last_page.png

Выбрать этот элемент можно с помощью простого xPath выражения:
Код:
//div[@class='pagination']/a[last()]
Вы легко можете протестировать составленное xPath выражение в консоли браузера, используя такую конструкцию. Работает во всех современных браузерах.
Код:
$x("//div[@class='pagination']/a[last()]")
Убедившись, что выражение составлено корректно и находит нужный нам элемент, пишем функцию-обработчик, который в цикле подготавливает ссылки для следующего этапа парсинга.
Код:
private void CategoryParser(HtmlDocument doc)
{
    var node = doc.DocumentNode.SelectSingleNode("//div[@class='pagination']/a[last()]");

    int count = int.Parse(node.InnerText);

    for (int i = 1; i <= count; i++)
        this.AddTask(string.Format("http://www.regard.ru/catalog/group52000/page{0}.htm", i), this.CategoryPageParser);
}
На втором этапе нам необходимо спарсить все ссылки, ведущие на страницы товаров. Проинспектировав в браузере элемент, ссылающийся на страницу товара, обнаруживаем, что ссылки относительные.
rg_item.png

Напишем обработчик, который извлечет все ссылки на страницы товаров, приведет их к абсолютным значениям и добавит в очередь на парсинг. Не забудем указать функцию, которая выполнит обработку данных на следующем этапе.
Код:
private void CategoryPageParser(HtmlDocument doc)
{
    var nodes = doc.DocumentNode.SelectNodes("//div[@id='hits']//a[@class='header' and contains(@href, '/catalog/tovar')]");

    foreach (var node in nodes)
        this.AddTask("http://www.regard.ru" + node.Attributes["href"].Value, this.ItemParser);
}
Третий этап парсинга, в данном конкретном случае, является заключительным. Теперь мы можем собрать интересующую нас информацию на странице товара. Парсить будем название товара, цену, изображение и характеристики.
rg_item_info.png

Нужно также учесть тот факт, что к некоторым товарам картинки отсутствуют, а на их месте стоит заглушка. Поэтому в обработчике необходимо добавить условие, чтобы данные сохранялись только в случае наличия изображения к товару. Но прежде чем написать функцию-обработчик, необходимо решить, как мы будем хранить полученные данные. Вы можете сохранять в файлы, но я предпочитаю для этих целей использовать базу данных MongoDB, так как тысячи мелких файлов явно не лучший способ хранения данных. MongoDB является объектно-документарной базой данных и хранит данные целыми объектами. Поэтому, для ее использования необходимо создать класс, описывающий поля и структуру хранимого объекта.
Код:
public class Item
{
    public ObjectId Id { get; set; }
    public string itemHeader { get; set; }
    public string itemPrice { get; set; }
    public string itemImage { get; set; }
    public Dictionary<string, string> itemProps { get; set; }
}
Теперь данные в базе будут храниться вот в таком структурированном виде:
Код:
{
        "_id" : ObjectId("5925c245c1d2a210d4ed3459"),
        "itemHeader" : "Телефон Acer Liquid Zest Z525 8Gb Black",
        "itemPrice" : "6340.00",
        "itemImage" : "http://www.regard.ru/cpreview280/shop/223008.jpg",
        "itemProps" : {
                "Производитель" : "Acer",
                "Код производителя" : "HM.HU6EU.001",
                "Тип" : "смартфон/коммуникатор",
                "Операционная система" : "Android 6.0",
                "Тип корпуса" : "классический",
                "Материал корпуса" : "пластик",
                "Тип SIM-карты" : "Micro-SIM",
                "Режим работы нескольких SIM-карт" : "попеременный",
                "Размеры" : "71х146х8.4 мм",
                "Поддержка двух SIM-карт" : "да",
                "Тип сенсорного экрана" : "емкостный",
                "Диагональ" : "5\"",
                "Размер изображения" : "1280x720",
                "Фотокамера" : "основная - 8 МП; фронтальная - 5 МП",
                "Интерфейсы" : "Bluetooth, USB, Wi-Fi",
                "Спутниковая навигация" : "GPS",
                "Процессор" : "MediaTek MT6580 1300 МГц",
                "Количество ядер процессора" : "4",
                "Объем встроенной памяти" : "8 Гб",
                "Объем оперативной памяти" : "1 Гб",
                "Поддержка карт памяти" : "microSD",
                "Емкость аккумулятора" : "2000 мАч",
                "Цвет" : "чёрный",
                "Вес" : "0.125 кг",
                "Гарантия" : "официальная гарантия производителя",
                "Сайт производителя" : "www.acer.ru"
        }
}
А вот и код последнего обработчика подъехал. Он парсит интересующие нас данные со страницы товара и сохраняет их в базу. Задания на парсинг больше не добавляются.
Код:
private void ItemParser(HtmlDocument doc)
{
    string itemHeader = doc.DocumentNode.SelectSingleNode("//h1[@id='goods_head']").InnerText;
    string itemPrice = doc.DocumentNode.SelectSingleNode("//div[@class='price_block']//meta[@itemprop='price']").Attributes["content"].Value;

    var imgNode = doc.DocumentNode.SelectSingleNode("//img[@id='big_preview_1']");
    if (imgNode != null)
    {
        string imgUrl = "http://www.regard.ru" + imgNode.Attributes["src"].Value;

        var itemProps = new Dictionary<string, string>();

        var nodes = doc.DocumentNode.SelectNodes("//div[@id='tabs-1']//tr[not(@class)]");
        foreach (var node in nodes)
        {
            string propName = node.SelectSingleNode("./td").InnerText.Trim();
            string propValue = node.SelectSingleNode("./td[2]").InnerText.Trim();

            itemProps[propName] = propValue;
        }

        Item itemData = new Item
        {
            itemHeader = itemHeader,
            itemPrice = itemPrice,
            itemImage = imgUrl,
            itemProps = itemProps
        };

        this.itemsCollection.Insert(itemData);
    }
}

Таким образом, мы написали паука, который доберется до нужной нам информации в 3 этапа. По итогу, в общем коде мы должны получить примерно следующее. Код сопровождается комментариями, которые помогут вам разобраться в происходящем.
Код:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.IO;
using System.Text.RegularExpressions;
using ZennoLab.CommandCenter;
using ZennoLab.InterfacesLibrary;
using ZennoLab.InterfacesLibrary.ProjectModel;
using ZennoLab.InterfacesLibrary.ProjectModel.Collections;
using ZennoLab.InterfacesLibrary.ProjectModel.Enums;
using ZennoLab.Macros;
using Global.ZennoExtensions;
using ZennoLab.Emulation;

// Подключаем пространства имен используемых библиотек
using HtmlAgilityPack;
using MongoDB.Bson;
using MongoDB.Driver;
using ZennoSpider;

namespace ZennoLab.OwnCode
{
    /// <summary>
    /// A simple class of the common code
    /// </summary>
    public class CommonCode
    {
        /// <summary>
        /// Lock this object to mark part of code for single thread execution
        /// </summary>
        public static object SyncObject = new object();

        // Insert your code here
    }
 
    public class Item
    {
        public ObjectId Id { get; set; }
        public string itemHeader { get; set; }
        public string itemPrice { get; set; }
        public string itemImage { get; set; }
        public Dictionary<string, string> itemProps { get; set; }
    }

    // Создаем новый класс, наследуя его от базового класса паука,
    // т.о. получаем весь функционал паука и возможность расширения под свои нужды
    public class RegardSpider : Spider
    {
        // Объект для работы с базой данных.
        // В данном конкретном случае это объект для работы с коллецией типа Item
        private readonly MongoCollection<Item> itemsCollection = new MongoClient("mongodb://localhost:27017").GetServer().GetDatabase("regard").GetCollection<Item>("items");

        // Конструктор класса паука, который принимает в качестве аргумента целое число,
        // отвечающее за максимальное количество потоков, работающих одновременно
        public RegardSpider(int threadsCount)
            : base(threadsCount) { }

        // Переопределяем метод Initialize, который вызывается сразу после запуска паука в методе Start.
        // Именно здесь появляется первая возможность добавить задачи в очередь
        protected override void Initialize()
        {
            // Метод AddTask принимает 2 аргумента, ссылку для GET запроса и ссылку на функцию,
            // в которую передается документ, сформированный из html кода для его дальнейшего разбора
            // и парсинга необходимых данных
            this.AddTask("http://www.regard.ru/catalog/group52000.htm", this.CategoryParser);
        }

        private void CategoryParser(HtmlDocument doc)
        {
            // Парсим последний элемент пагинации, который содержит номер последней страницы
            // т.о. определяем, сколько страниц в категории
            var node = doc.DocumentNode.SelectSingleNode("//div[@class='pagination']/a[last()]");

            if (node != null)
            {
                int count = int.Parse(node.InnerText);

                // Запускаем цикл, добавляя задания в очередь в зависимости от количества страниц
                for (int i = 1; i <= count; i++)
                    this.AddTask(string.Format("http://www.regard.ru/catalog/group52000/page{0}.htm", i), this.CategoryPageParser);
            }
        }

        private void CategoryPageParser(HtmlDocument doc)
        {
            // Парсим ссылки на страницы с товаром
            var nodes = doc.DocumentNode.SelectNodes("//div[@id='hits']//a[@class='header' and contains(@href, '/catalog/tovar')]");
            foreach (var node in nodes)
                // Т.к. ссылки относительные, добавляем вначале домен, чтобы получить абсолютную
                this.AddTask("http://www.regard.ru" + node.Attributes["href"].Value, this.ItemParser);
        }

        private void ItemParser(HtmlDocument doc)
        {
            // Парсим название товара
            string itemHeader = doc.DocumentNode.SelectSingleNode("//h1[@id='goods_head']").InnerText;
            // Парсим цену товара
            string itemPrice = doc.DocumentNode.SelectSingleNode("//div[@class='price_block']//meta[@itemprop='price']").Attributes["content"].Value;

            // Парсим картинку. Если картинки нет, то данные об этом товаре в базу не вносим
            var imgNode = doc.DocumentNode.SelectSingleNode("//img[@id='big_preview_1']");
            if (imgNode != null)
            {
                string imgUrl = "http://www.regard.ru" + imgNode.Attributes["src"].Value;

                // Список свойств товара в виде словаря
                var itemProps = new Dictionary<string, string>();

                // Парсим описание свойств товара
                var nodes = doc.DocumentNode.SelectNodes("//div[@id='tabs-1']//tr[not(@class)]");
                foreach (var node in nodes)
                {
                    string propName = node.SelectSingleNode("./td").InnerText.Trim();
                    string propValue = node.SelectSingleNode("./td[2]").InnerText.Trim();

                    // Добавляем каждое свойство товара в список
                    itemProps[propName] = propValue;
                }

                Item itemData = new Item { itemHeader = itemHeader, itemPrice = itemPrice, itemImage = imgUrl, itemProps = itemProps };

                // Сохраняем данные о товаре в базу
                this.itemsCollection.Insert(itemData);
            }
        }
    }
}
3. Добавляем в шаблон входные настройки, чтобы можно было удобно задавать количество потоков нашему пауку через переменную.

4. Добавляем в шаблон экшн C#, в котором создаем паука, указываем ему кодировку сайта, если она отличается от utf-8, задаем функцию для вывода информации в лог и можно запускать нашего паучка.
Код:
int maxThreads = int.Parse(project.Variables["maxThreads"].Value);

RegardSpider spider = new RegardSpider(maxThreads);
spider.Encoding = Encoding.GetEncoding(1251);
// Функция для вывода информации в лог
spider.OnLog = (string logEntry) => project.SendInfoToLog(logEntry);
spider.Start();
Количество потоков для работы паука можно указать любое, десятки, сотни или даже тысячи. Это зависит от пропускной способности вашего интернет канала, мощности процессора, количества оперативной памяти. Все это справедливо и для сервера, хостящего сайт, который вы собираетесь парсить. Также зависит и от настроек сервера. Некоторые админы ограничивают количество запросов в единицу времени с одного IP адреса. Оптимальное количество потоков подбирается экспериментальным путем. Сам же шаблон, начинающий работу паука, необходимо запускать в 1 поток.

Если статья вам понравилась, проголосуйте за нее, пожалуйста :ah:
 
Последнее редактирование:

Valiksim

Client
Регистрация
14.04.2012
Сообщения
1 340
Благодарностей
298
Баллы
83

arhip1985

Client
Регистрация
31.10.2011
Сообщения
2 952
Благодарностей
780
Баллы
113
достойно конкурса, побольше бы такого. в основном было интересно посмотреть вашу реализацию многопотока. спасибо
 

AZANIR

Client
Регистрация
09.06.2014
Сообщения
405
Благодарностей
196
Баллы
43
Отлично молодчага!!!
 

baimkin

Client
Регистрация
04.08.2015
Сообщения
283
Благодарностей
111
Баллы
43
Еще не читал но уже лайк, как раз самое то, на мой проект распараллелить задачи локальной и веб обработки.
 

stanar

Client
Регистрация
19.12.2015
Сообщения
314
Благодарностей
157
Баллы
43
Да. Наконец-то эта статья)
 

Geograph

Client
Регистрация
16.02.2014
Сообщения
209
Благодарностей
113
Баллы
43
Хороший пример асинхронных запросов в C#
 
Последнее редактирование модератором:
  • Спасибо
Реакции: kagorec

Nord

Client
Регистрация
22.03.2012
Сообщения
2 358
Благодарностей
1 420
Баллы
113
ZennoSpider.dll - а что в средине?
Извиняюсь за паранойю, но тут разные товарищи то ботнет в шаб всунут, то логгер пытаются, то рефками скрытыми заспамят, то свой кей капчи прикрутят =))

PS
Sorry, сначала не заметил
"Паук реализован в виде библиотеки для удобства и возможности использовать в других проектах. Вы его найдете в архиве, прикрепленном к статье, вместе с другими необходимыми библиотеками. А тех, кто хочет изучить исходники и модифицировать паука для своих задач, прошу под кат."
 
Последнее редактирование:

DenisK

Client
Регистрация
28.06.2016
Сообщения
591
Благодарностей
288
Баллы
63

freeman

Client
Регистрация
31.07.2010
Сообщения
130
Благодарностей
138
Баллы
43
Всем спасибо за лестные отзывы, парни. Всегда рад помочь по C# коду, даже вне конкурса. Обращайтесь!

То, что ты задумал, очень интересно. Но, то, что выставил,- малопонятно, к сожалению
Что именно не понятно? Я попробую объяснить.
 

Nick

Client
Регистрация
22.07.2014
Сообщения
1 961
Благодарностей
796
Баллы
113
Хороший почин, кому-то может оказаться полезной эта наработка. Было бы интереснее увидеть сам прикладной пример использования - спарсили такие-то сайты, контакты выглядят так-то, мы их используем так-то. А то так смотрю на это - ну, прикольно, а пригодится мне? Не знаю...
 
  • Спасибо
Реакции: SHoro

Valiksim

Client
Регистрация
14.04.2012
Сообщения
1 340
Благодарностей
298
Баллы
83
Что именно не понятно? Я попробую объяснить.
Как бы это сказать, не знаю? Когда тема раскрыта, есть о чём спросить, а когда нет... сложно

А то так смотрю на это - ну, прикольно, а пригодится мне? Не знаю...
Вот, Nick, ответил, тоже сформулировать вопрос (как я понимаю), затрудняется.
 
  • Спасибо
Реакции: SHoro

freeman

Client
Регистрация
31.07.2010
Сообщения
130
Благодарностей
138
Баллы
43
Дополню сегодня статью, попробую объяснить момент с парсингом магазина поэтапно.
 
  • Спасибо
Реакции: Koqpe

samsonnn

Client
Регистрация
02.06.2015
Сообщения
1 630
Благодарностей
1 303
Баллы
113
Я тоже не понял как работает паук, там все в общем коде, и ссылка на сайт и парсинг данных через xpath, Фриман это вам не кликем, помогите нам всем понять как работает ваш паук.
 
  • Спасибо
Реакции: Valiksim и Koqpe

Valiksim

Client
Регистрация
14.04.2012
Сообщения
1 340
Благодарностей
298
Баллы
83
Последнее редактирование:

baimkin

Client
Регистрация
04.08.2015
Сообщения
283
Благодарностей
111
Баллы
43
Не знаю насколько это правильно, но я попробовал запускать долгий процесс в новом потоке вот таким образом:
Код:
//Запуск действий в новом потоке
    Thread t = new Thread(delegate(){            //Создаем новый поток ( t - название нашего потока )
        // С этой строки начинается код который мы хотим выполнить в новом потоке
        // Наш код
        // Наш код
        // Наш код
        // Последняя строка с кодом который должен работать в новом потоке       
    });                //Закрываем действия в новом потоке
    t.Start();        //Запускаем поток
Вроде работает.
 

freeman

Client
Регистрация
31.07.2010
Сообщения
130
Благодарностей
138
Баллы
43
Дополнил статью. Описал подробно процесс поэтапного создания паука. Читайте второй пост.
 

Борат Сагдиев

Пользователь
Регистрация
09.05.2017
Сообщения
61
Благодарностей
36
Баллы
8

Обращаем Ваше внимание на то, что данный пользователь заблокирован.
Не рекомендуем проводить с Борат Сагдиев какие-либо сделки.

Автор если пишешь шабы на заказ сбрось скайп в личку.
Хочу шаб на основе этой темы ))
 

Moadip

Client
Регистрация
26.09.2015
Сообщения
509
Благодарностей
823
Баллы
93
Было бы интереснее увидеть сам прикладной пример использования - спарсили такие-то сайты, контакты выглядят так-то, мы их используем так-то. А то так смотрю на это - ну, прикольно, а пригодится мне?
Так это инструмент, как его юзать это уже дело каждого. Лопатой можно картошку копать, а можно и по голове долбануть.:D
В плане полезности, ну если нужен парсер, то пригодится.

Я тоже не понял как работает паук, там все в общем коде, и ссылка на сайт и парсинг данных через xpath, Фриман это вам не кликем, помогите нам всем понять как работает ваш паук.
надо сначала словами объяснить (именно работу), потом кодом
Т.е. другими словами объяснить каждую строчку кода? От этого толку не будет. Тут надо скилл по C# подтягивать. Как работает и в чем идея вроде в описании есть.

Посмотрел код. Код чистый, code style соблюдается, переменный внятные, без какого то запутанного непонятного кода. Карочь читать приятно и понимается норм. Можно бы было в некоторых местах комменты воткнуть, но по логике и так понятно.

Ну а так конечно, делегаты, наследование, переопределение методов, для многих это будет как высшая математика. Да еще работа с mongoDB.:D
Собственно поэтому и вопросы, что нихрена непонятно. Но объяснить каждую строчку когда "на пальцах" навряд ли получится.
 
  • Спасибо
Реакции: freeman

amyboose

Client
Регистрация
21.04.2016
Сообщения
2 312
Благодарностей
1 190
Баллы
113
Чтобы не дергать каждый раз MongDB с подключением и отключением можно установить 1 общее статическое соединение и на базе него получать таблицы.
 
  • Спасибо
Реакции: Nike59

freeman

Client
Регистрация
31.07.2010
Сообщения
130
Благодарностей
138
Баллы
43
Чтобы не дергать каждый раз MongDB с подключением и отключением можно установить 1 общее статическое соединение и на базе него получать таблицы.
Ну вообще-то в моем примере именно так и сделано. Соединение с базой устанавливается 1 раз и обработчики получают доступ через поле класса.
 

Valiksim

Client
Регистрация
14.04.2012
Сообщения
1 340
Благодарностей
298
Баллы
83
Т.е. другими словами объяснить каждую строчку кода?
Я говорил о том, что надо было ... что-то типа введения сделать, словами объяснить, что к чему. Речь шла не о коде
 
  • Спасибо
Реакции: Nick

zhekan3

Client
Регистрация
27.12.2015
Сообщения
32
Благодарностей
4
Баллы
8
Отличный пример! Но не понятно как потом использовать то что спарсили. Как извлечь данные из базы не покажите пример? Заранее благодарен. Может не мне одному это будет интересно.
 

lzlmrf

Client
Регистрация
14.08.2015
Сообщения
487
Благодарностей
148
Баллы
43
Уверен что мне это нужно. Но перечитываю уже третий раз )) И видимо еще не раз предстоит. Большое спасибо за труды.
Я так понял для вас зенка - как костыль, если в вижуал студии работаете. Врядли ваша статья победит - местному большинству ближе "натыкал шаб записью за час и работает" . И удивился вначале , как это разработчики разрешили эту статью выложить, ведь отпадает нужда в Про версии..Прочитав понял почему )) Имея такой скил и лайт не нужен
 
  • Спасибо
Реакции: freeman

freeman

Client
Регистрация
31.07.2010
Сообщения
130
Благодарностей
138
Баллы
43
Отличный пример! Но не понятно как потом использовать то что спарсили. Как извлечь данные из базы не покажите пример? Заранее благодарен. Может не мне одному это будет интересно.
В первом посте есть несколько примеров, как извлекать данные из базы. Конечно, это самые азы, но раскрыть даже малую часть всего функционала в статье невозможно. Для этого пишут целые книги. Если структура спаршенных данных простая, то можно обойтись без базы. Вот еще примерчик, надо добавить в шаблон новый экшн C# и в него вставить код.
Код:
var client = new MongoDB.Driver.MongoClient("mongodb://localhost:27017");
var database = client.GetServer().GetDatabase("regard");
var itemsCollection = database.GetCollection<Item>("items");

var items = itemsCollection.FindAll();
foreach (Item item in items)
{
    string itemName = item.itemHeader;
    string itemPrice = item.itemPrice;
    var itemProps = item.itemProps;
 
    var props = new StringBuilder();
    foreach (var prop in itemProps)
    {
        props.Append(string.Format("{0}: {1} ", prop.Key, prop.Value));
    }
 
    project.SendInfoToLog(string.Format("Наименование: {0}, Цена: {1} руб. Характеристики: {2}", itemName, itemPrice, props.ToString()));
}
Уверен что мне это нужно. Но перечитываю уже третий раз )) И видимо еще не раз предстоит. Большое спасибо за труды.
Я так понял для вас зенка - как костыль, если в вижуал студии работаете. Врядли ваша статья победит - местному большинству ближе "натыкал шаб записью за час и работает" . И удивился вначале , как это разработчики разрешили эту статью выложить, ведь отпадает нужда в Про версии..Прочитав понял почему )) Имея такой скил и лайт не нужен
Спасибо. На самом деле, тут ничего сложного нет. Может чуточку сложнее, чем использовать сниппеты.
 
  • Спасибо
Реакции: lupo, Nike59 и zhekan3

Valery_Sh

Новичок
Регистрация
07.04.2019
Сообщения
3
Благодарностей
0
Баллы
1
Здравствуйте, нужна ваша помощь в написании сниппета
 

bhairava7

Client
Регистрация
18.08.2015
Сообщения
154
Благодарностей
15
Баллы
18
Некропост, но может кому-то пригодится, код паука немного отредактировал, теперь он сам определяет кодировку сайта и конвертирует её в UTF-8 , что удобно, причём не по заголовку Content-Type, а по содержимому страницы, это надёжней, так как бывают сайты, которые на отдают кодировку в заголовке из-за неправильной настройки сервера.

В пауке изменено следующее:
1. Библиотека xNet заменена на более свежую Leaf.xNet, установить её можно через Nuget
2. Добавлена библиотека UtfUnknown, которая собственно и определяет кодировку, определять может из потока, файла или байтов, устанавливается так же через Nuget, на гитхабе есть документация
3. Выпилен метод "spider.Encoding", так как теперь он не нужен
4. User-Agent маскируется под основного поискового бота Яндекса

C#:
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Leaf.xNet;
using UtfUnknown;

namespace ZennoSpider
{
    public class Spider
    {
        private readonly Queue<Tuple<string, Action<HtmlDocument>>> _urlQueue = new Queue<Tuple<string, Action<HtmlDocument>>>();
        private readonly List<Tuple<Task<string>, Action<HtmlDocument>>> _tasks = new List<Tuple<Task<string>, Action<HtmlDocument>>>();
        private readonly int _threadsCount;
        private int _counter = 0;
        private Action<string> _log;

        public Spider(int threadsCount = 10)
        {
            _threadsCount = threadsCount;
        }

        public Action<string> OnLog
        {
            set
            {
                _log = value;
            }
        }

        protected void AddTask(string url, Action<HtmlDocument> callbackFunc)
        {
            if (_tasks.Count < _threadsCount)
                _tasks.Add(new Tuple<Task<string>, Action<HtmlDocument>>(Task.Factory.StartNew<string>(Worker, url, TaskCreationOptions.LongRunning), callbackFunc));
            else
                _urlQueue.Enqueue(new Tuple<string, Action<HtmlDocument>>(url, callbackFunc));
        }

        protected virtual void Initialize() { }

        public void Start()
        {
            this.Initialize();

            while (_urlQueue.Count > 0 || _tasks.Count > 0)
            {
                if (_tasks.Count > 0)
                {
                    Tuple<Task<string>, Action<HtmlDocument>>[] temp = _tasks.ToArray();

                    foreach (Tuple<Task<string>, Action<HtmlDocument>> task in temp)
                    {
                        if (task.Item1.IsCompleted)
                        {
                            if (!task.Item1.IsFaulted)
                            {
                                if (_log != null)
                                    _log(string.Format("[{0}] {1} | OK", _counter++, task.Item1.AsyncState));
                                HtmlDocument doc = new HtmlDocument();
                                //doc.LoadHtml(Html.ReplaceEntities(task.Item1.Result));
                                doc.LoadHtml(task.Item1.Result);
                                task.Item2(doc);
                            }
                            else
                            {
                                HttpException exc = (HttpException)task.Item1.Exception.InnerException;

                                if (_log != null)
                                {
                                    if (exc != null)
                                    {
                                        switch (exc.Status)
                                        {
                                            case HttpExceptionStatus.ProtocolError:
                                                _log(string.Format("[{0}] {1} | Код состояния: {2}", _counter++, task.Item1.AsyncState, (int)exc.HttpStatusCode));
                                                break;

                                            case HttpExceptionStatus.ConnectFailure:
                                                _log(string.Format("[{0}] {1} | Не удалось соединиться с HTTP-сервером.", _counter++, task.Item1.AsyncState));
                                                break;

                                            case HttpExceptionStatus.SendFailure:
                                                _log(string.Format("[{0}] {1} | Не удалось отправить запрос HTTP-серверу.", _counter++, task.Item1.AsyncState));
                                                break;

                                            case HttpExceptionStatus.ReceiveFailure:
                                                _log(string.Format("[{0}] {1} | Не удалось загрузить ответ от HTTP-сервера.", _counter++, task.Item1.AsyncState));
                                                break;

                                            case HttpExceptionStatus.Other:
                                                _log(string.Format("[{0}] {1} | Неизвестная ошибка.", _counter++, task.Item1.AsyncState));
                                                break;
                                        }
                                    }
                                    else
                                    {
                                        _log(string.Format("[{0}] {1} | {2}", _counter++, task.Item1.AsyncState, task.Item1.Exception.InnerException.Message));
                                    }
                                }
                            }

                            task.Item1.Dispose();
                            _tasks.Remove(task);
                        }
                    }

                    Array.Clear(temp, 0, temp.Length);
                }

                for (int i = 0; i < _threadsCount - _tasks.Count; i++)
                {
                    try
                    {
                        Tuple<string, Action<HtmlDocument>> t = _urlQueue.Dequeue();
                        _tasks.Add(new Tuple<Task<string>, Action<HtmlDocument>>(Task.Factory.StartNew<string>(Worker, t.Item1, TaskCreationOptions.LongRunning), t.Item2));
                    }
                    catch (InvalidOperationException)
                    {
                        break;
                    }
                }

                Thread.Sleep(500);
            }
        }

        private string Worker(object url)
        {
            using (HttpRequest request = new HttpRequest())
            {
                request.MaximumAutomaticRedirections = 10;
                //request.Cookies = new CookieDictionary();
                request.ConnectTimeout = 10 * 1000;
                request.ReadWriteTimeout = 30 * 1000;
                request.IgnoreProtocolErrors = true;
                request.CharacterSet = Encoding.UTF8;

                request["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8";
                request["Accept-Language"] = "ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3";
                request["User-Agent"] = "Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)";

                HttpResponse r = request.Get((string)url);

                /*
                string responseHeader = r["Content-Type"];
                _log(responseHeader);
                */

                byte[] bytes = r.ToBytes();

                DetectionResult result = CharsetDetector.DetectFromBytes(bytes);       
                DetectionDetail resultDetected = result.Detected;

                if (resultDetected != null)
                {
                    string encodingName = resultDetected.EncodingName;
                    byte[] convertedBytes = Encoding.Convert(Encoding.GetEncoding(encodingName), Encoding.UTF8, bytes);

                    return Encoding.UTF8.GetString(convertedBytes);
                }

                return Encoding.UTF8.GetString(bytes);
            }
        }
    }
}

В кубике C# вызов теперь такой:

C#:
int maxThreads = int.Parse(project.Variables["maxThreads"].Value);

RegardSpider spider = new RegardSpider(project, maxThreads);

// Функция для вывода информации в лог
spider.OnLog = (string logEntry) => project.SendInfoToLog(logEntry);
spider.Start();
Т.е. без spider.Encoding = Encoding.GetEncoding(1251);
Всё остальное как было ранее.
 
  • Спасибо
Реакции: IH4w6UuEMt, bizzon и Koqpe

radv

Client
Регистрация
11.05.2015
Сообщения
3 673
Благодарностей
1 855
Баллы
113
2. Добавлена библиотека UtfUnknown, которая собственно и определяет кодировку, определять может из потока, файла или байтов, устанавливается так же через Nuget, на гитхабе есть документация
ссылка на гитхаб кому лень искать https://github.com/CharsetDetector/UTF-unknown там есть и несколько примеров по использованию этой либы
 
  • Спасибо
Реакции: bizzon

Кто просматривает тему: (Всего: 1, Пользователи: 0, Гости: 1)