Spis treści:

Kategoria:BotyC#


Przykładowy BOT w C#

Pajączek zwraca się do pajączka:
- Wiesz co? - Zrobiłem taką pajęczynę, że...

...że mucha nie siada!

Jakiś czas temu pisałem o problemach z robotami, a także sztucznych wpisach i komentarzach generowanych przez te natrętne istoty. Napisałem też o jednej z technik blokowania takich niepożądanych ataków w postaci obrazków z tekstem, czyli tzw. captcha. Całość można przeczytać tutaj: Jak stworzyć własne CAPTCHA w C# .NET.

Roboty to jednak nie tylko zło. To, że Google, Bing i inne wyszukiwarki działają, to też zasługa robotów. Roboty mogą też służyć do wykrywania nieprawidłowych hiperłączy na naszych stronach. Pokażę więc, jak takiego robota zrobić. Mam nadzieję, że będzie wykorzystywany tylko w dobrych intencjach.

Jak zrobić własnego robota internetowego?

Jak zwykle przy tworzeniu nowej rzeczy potrzebny jest jakiś plan. Zawsze polecam pochylić się nad problemem i stworzyć coś, co może być potem wykorzystane. Stworzę zatem klasę, której jedynym argumentem potrzebnym do działania będzie adres strony. Klasa będzie pobierała zawartość tejże strony i powiadamiała resztę aplikacji przy pomocy zdarzenia. Zdarzenie będzie zawierało pełną treść strony, adres, który był przeszukiwany w celu identyfikacji zadania, a także wszystkie hiperłącza (linki) umieszczone na tej stronie. Dzięki liście hiperłączy aplikacja będzie mogła kontynuować poszukiwania budując sobie wewnętrzną sieć.

Warto wiedzieć, że operacje sieciowe mogą na krótszy bądź dłuższy czas zablokować główny wątek aplikacji. Serwer pobieranej strony może być przeciążony, zawartość może być na tyle duża, że pobranie jej zajmie klika sekund. Aby aplikacja mogła wykonywać swoje zadania, robot będzie swoje operacje realizował w wątku. Pozwala to na puszczenie jednocześnie kilku lub kilkunastu takich robotów internetowych.

Reasumując:

  • Klasa przyjmuje adres strony do przeszukania
  • Klasa posiada zdarzenie informujące o zakończeniu procesu
  • Klasa posiada metodę pozwalającą uruchomić analizę
  • Wartości zwracane: treść strony, lista hiperłączy, przeszukiwany adres

Projekt klasy robota internetowego

Zanim przejdę do treści właściwej przedstawię wstępny projekt klasy robota. Nie będzie tutaj żadnych zaskakujących elementów. Projekt klasy pokazany jest na poniższym listingu:

public class BotTask
{
    private EventHandler<BotData> pageParsed;
    public EventHandler<BotData> PageParsed
    {
        get { return pageParsed; }
        set { pageParsed = value; }
    }

    private string BaseAddress { getset; }

    public BotTask(string baseAddress)
    {
        BaseAddress = baseAddress;
    }

    public void StartDownload()
    {
        //Pobieranie danych strony
    }
}

public class BotData : EventArgs
{
    //Dane wynikowe
}

Kolejny etap to uzupełnienie brakujących linijek kodu - tych najważniejszych.

Asynchroniczne pobieranie danych ze strony www

Do pobrania danych ze strony użyjemy klasy WebClient. Jest to typowy klient sieciowy HTTP. Klasa WebClient zawiera w zasadzie wszystko to, co będzie nam potrzebne i znacznie więcej. Tym więcej nie będę się zajmował. Opiszę tylko to, co jest niezbędne. Wspomniałem, że wypadałoby realizować operację pobierania danych w oddzielnym wątku. Tu pierwsza niespodzianka - klasa WebClient zrobi to za nas. Tak prawdę mówiąc zdziwiłoby mnie to, gdyby rozsądna klasa nie miała takiej możliwości. Problem opóźnień w ruchu sieciowym jest na tyle powszechny, że zwykłych, niewątkowych wywołań w praktyce się nie stosuje.

Klasa WebClient informuje o dostępności strumienia danych przy pomocy zdarzenia OpenReadCompleted. Tak - dostępności strumienia. Nasza klasa będzie zwracała prostszy typ: string. WebClient nawiązuje połączenie, pobiera pakiety z sieci, a nam udostępnia te dane w postaci strumienia.

Projekt zakłada, że nasza klasa upubliczni metodę do rozpoczęcia pobierania danych. Nie jest to rozwiązanie oryginalne - klasa WebClient też ma taką metodę OpenReadAsync. W najprostszym wydaniu przyjmuje ona obiekt Uri, czyli nasz bazowy adres HTTP wskazujący stronę przeszukiwaną przez klasę robota.

Aby żądanie nie zostało odrzucone przez serwery musimy jeszcze się przedstawić. Służy do tego właściwość Headers klasy WebClient. Przekażemy tam parę następującą parę klucz-wartość:"user-agent"-"dev.cdur.pl - Przykładowy BOT"

Trzeba wiedzieć, że wiele stron ma zabezpieczenia przed robotami. Zły robot może całkowicie zablokować serwer wybranej witryny. Jednym z najprostszych zabezpieczeń jest odrzucanie podejrzanych żądań. Żądanie jest podejrzane między innymi wtedy, gdy usługa pobierająca dane nie przedstawi się. W świecie HTTP realizowane jest to przez atrybut user-agent przesyłany w nagłówku żądania. Najczęściej są tam wpisane dane przeglądarki, przede wszystkim jej nazwa i numer wersji. Można tam jednak wpisać dowolny tekst. Nasz przykładowy BOT będzie tam miał wartość "dev.cdur.pl - Przykładowy BOT".<

Zgodnie z powyższym wypełnijmy sobie metodę StartDownload naszej klasy. Pełna zawartość pokazana jest na poniższym listingu:

public void StartDownload()
{
    //Pobieranie danych strony
    WebClient client = new WebClient();
    client.Headers.Add("user-agent""dev.cdur.pl - Przykładowy BOT");
    client.OpenReadCompleted += new OpenReadCompletedEventHandler(client_OpenReadCompleted);
    client.OpenReadAsync(new Uri(BaseAddress));
}

To cały kod metody. Gdy obiekt klasy WebClient zrobi co trzeba i pobierze dane, wywołana zostanie metoda client_OpenReadCompleted. To tam przetworzymy strumień i pobierzemy dane.

Przetwarzanie strumienia HTTP

Zadaniem metody client_OpenReadCompleted jest przetworzenie strumienia danych, utworzenie klasy z danymi i wywołanie zdarzenia, które poinformuje o zakończeniu przetwarzania. Nie ma tutaj żadnych niekonwencjonalnych rozwiązań, dlatego przyjrzyjmy się od razu poniższemu listingowi:

private void client_OpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
{
    WebClient client = sender as WebClient
    StreamReader reader = new StreamReader(e.Result);
    string pageContent = reader.ReadToEnd();
    BotData botData = new BotData(BaseAddress, pageContent);
    reader.Dispose();
    e.Result.Dispose();
    client.Dispose();
    if (PageParsed != null)
        PageParsed(this, botData);
}

Pokazana metoda w zasadzie kończy prace nad klasą BotTask realizującą zadanie pobierania kodu HTML witryny internetowej. Reszta zadań realizowana jest przez klasę przekazującą dane wynikowe przy pomocy parametru zdarzenia PageParsed.

Metoda pobiera strumień, wczutuje do zmiennej pageContent całą zawartość, a następnie zamyka (usuwa) wszystkie możliwe strumienie i klasy implementujące interfejs IDisposable. Ostatni etap to rozgłoszenie wszystkim obiektom oczekującym na zdarzenie, że proces pobierania zakończył się. Jako parametr, oprócz referencji na źródło zdarzenia (sender), przekazywane jest wystąpienie klasy BotData z zawartością.

Przetwarzanie otrzymanych wyników

Klasa BotData ma jeden konstruktor, który przyjmuje przeszukiwany adres bazowy oraz całe źródło strony. Jej zadaniem jest takie przetworzenie danych wejściowych, aby końcowy użytkownik mógł otrzymać to, co opisaliśmy w założeniach. Treść strony mamy, adres mamy, ale potrzebyjemy listy hiperłączy. Przyznam się bez bicia, że pełne rozwiązanie problemu jest probcesem bardzo złożonym. Wiemy doskonale, że strona może zawierać kod JavaScript, HTML może być nieprawidłowy, strony mogą zawierać niedomknięte znaczniki. Wszystko to utrudnia prawidłowe przetwarzanie takiej zawartości. Aby nie zaśmiecać przykładu zastosowałem proste rozwiązanie wykorzystujące wyrażenie regularne. W skrócie - na podstawie wyrażenia spróbujemy znaleźć wszystkie ciągi znaków rozpoczynające się od <a, po których występuje biały znak, potem znaki różne od >, co pozwala nam ominąć atrybuty znacznika a inne niż href. Gdy znajdziemy href, zajmujemy się jego zawartością, która staje się jednocześnie wartością wyrażenia. Znów pomijamy wszystko aż do >, przechodzimy przez elementy umieszczone wewnątrz znacznika i dochodzimy do </a>. Prościej, szukamy takich wyrażeń:

<a ... href="Wartość" ...>...</a>

Tak pobraną wartość przetwarzamy w sposób następujący:

  • Jeżeli adres zaczyna się od http://, bierzemy go bez zmian, najprawdopodobniej wskazuje na obcą witrynę,
  • Jeżeli nie, to najprawdopodobniej podany jest względem przetwarzanej domeny/poddomeny, czyli tzw. hosta. W takim przypadku pobieramy z adresu bazowego schemat (http), dorzucamy ://, dorzucamy nazwę hosta i doklejamy adres względny. Taki zlepek staje się pełnym adresem sieciowym gotowym do dalszego przetwarzania przez kolejne obiekty klasy BotTask.

Zebrane adresy wrzucamy do listy dbając jednocześnie o to, aby nie umieszczać dwa razy tego samego. Chyba, że nam na tym zależy i stosujemy algorytm uwzględniający częstość wystąpień strony. Są przypadki, że takie rozwiązanie jest rozsądne. Prosta heurystyka - im więcej wskazań na stronę, tym strona lepsza, im więcej wskazań tym częściej strona może być odwiedzana. Reguły wnioskowania zależą od przypadku i w pewnym sensie naszych potrzeb.

Po tym krótkim zboczeniu z głownej ścieżki przejdźmy do implementacji klasy BotData. Kod pokazany jest na poniższym listingu:

public class BotData : EventArgs
{
    private const string regexPattern = @"<a\s[^>]*href=""([^\""]*)\""[^>]*>.*?</a>";
    public List<string> Hrefs { getprivate set; }
    public string Page { getprivate set; }
    public string BaseAddress { getprivate set; }
    public BotData(string baseAddress, string page)
    {
        Page = page;
        BaseAddress = baseAddress;
        Hrefs = new List<string>();
        Regex r = new Regex(regexPattern);
        foreach (Match match in r.Matches(page))
        {
            string relativeUrl = match.Groups[1].ToString();
            if (!relativeUrl.StartsWith("http"))
            {
                Uri u = new Uri(BaseAddress);
                relativeUrl = u.Scheme + "://" + u.Host + relativeUrl;
            }
            if (!Hrefs.Contains(relativeUrl))
                Hrefs.Add(relativeUrl);
        }
    }
}

Rozwiązanie wykorzystuje wyrażenia regularne i jest to chyba najmniej czytelny element zaprezentowanego fragmentu kodu. Dokładne omówienia wyrażeń regularnych to temat na osobną książkę, nie mówiąc już o artykule. Zainteresowanych odsyłam do dokumentacji klasy Regex.

A tak poza tym to już jest koniec. Mamy w zasadzie wszystko, czego potrzebujemy i możemy przystąpić do uruchomienia naszego robota internetowego.

Wykorzystanie klasy robota do przeszukiwania internetu

Dobrze zaprojektowana klasa powinna być prosta w użyciu. Aby pokazać tę prostotę przyjrzyjmy się poniższemy listingowi:

static void Main(string[] args)
{
    BotTask bt = new BotTask("http://www.cdur.pl");
    bt.PageParsed += new EventHandler<BotData>(bot_pageParsed);
    bt.StartDownload();
    Console.ReadLine();
}

public static void bot_pageParsed(object sender, BotData e)
{
    Console.WriteLine(e.Page.Substring(0, 300) + "...");
    foreach (var href in e.Hrefs)
        Console.WriteLine("\t" + href);
}

Uruchomienie zadania przeszukiwanie internetu przez bota sprowadza się do przekazania w konstruktorze odpowiedniego adresu http, podpięcie zdarzenia PageParsed i wywołanie metody StartDownload. W tym momencie bot wykonuje swoją pracę w oddzielnym wątku pozwalając aplikacji konsolowej wykonywać inne rzeczy. Nic nie stoi na przeszkodzie, aby uruchomić jednocześnie kilka takich robotów. Gdy strona zostanie pobrana przez obiekt BotTask, uruchomiona zostanie metoda obsługi zdarzenia. W pokazanym przykładzie w oknie konsolowym wypisywane jest pierwsze 300 znaków, a następnie wszystkie znalezione na tej stronie hiperłącza. Te hiperłącza mogą być źródłem poszukiwań dla kolejnego bota.

Własny BOT - podsumowanie

Kod klasy nie jest jakimś szczególnie skomplikowanym tworem. Trzeba jednak wiedzieć, że to tylko początek trudnego procesu pozyskiwania danych ze stron. Po pierwsze, w kodzie pominięto wszystkie procedury obsługi błędów. Przykład jest bardziej przejrzysty, ale jakikolwiek błąd, nieprawidłowo przekazany adres, brak odpowiedzi serwera sprawi, że nasza aplikacja zakończy się w sposób pozostawiający wiele do życzenia. Po drugie, o czym już wspominałem, dokładne przetworzenie treści HTML strony jest dość trudne. Nigdy nie wiemy, co przyjdzie w odpowiedzi. A przyjść może wszystko. Trzeba sobie radzić z różnymi trikami stosowanymi przez webmasterów, javascriptem i tak dalej. Mam jednak nadzieję, że zaprezentowany przykład posłuży jako baza do bardziej skomplikowanych rozwiązań a innym pokaże, w jaki sposób działają boty.

Kategoria:BotyC#

, 2013-12-20

Komentarze:

Michal (2016-06-16 14:09:41)
Super poradnik :)
Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !
a z innej strony - co gdybym ciąg znaków chciał mieć rozbity nie na wiersze a na kolumny? Czyli ciąg ABCD: 1. kolumna: A, 2. kolumna: B, 3. kolumna: C, 4 kolumna: D?
Ciekawy artykuł.
Czy można za pomocą EF wysłać swoje zapytanie?
Czy lepiej do tego użyć ADO.net i DataTable?