Selenium w .NET 6 — bot scraper krok po kroku

Jak skonfigurować Selenium.WebDriver i ChromeDriver w aplikacji konsolowej .NET 6, uruchomić Chrome w trybie headless i zautomatyzować wypełnianie formularza.

JAJarosław Armatys 7 min czytania
Wersja wideo tego wpisu — obejrzyj na YouTube

W tej części kursu WordPress Importera przełączamy się z TensorFlowSharp na Selenium. Początkowo planowałem, że opinie będziemy generować modelem ML-owym, ale po kolejnym research'u okazało się, że TensorFlowSharp nie dowozi tego, czego potrzebujemy. Dlatego zmieniamy strategię — zamiast generować opinie algorytmicznie, napiszemy bota, który będzie klikał po UI i dodawał komentarze na naszym portalu. Selenium służy oryginalnie do testów automatycznych, ale równie dobrze sprawdza się w scrapingu i automatyzacji operacji na stronach, do których nie masz pełnego API.

Po co Selenium w apce .NET

Większość operacji na WordPressie załatwiłeś REST API z poprzednich lekcji — dodawanie ogłoszenia, autoryzacja Bearer Tokenem, mapowanie odpowiedzi. Problem pojawia się, gdy strona nie wystawia endpointu dla tego, co chcesz zautomatyzować. W naszym przypadku formularz dodawania opinii jest renderowany po stronie WordPressa, a pod spodem klika kilka ukrytych pól i tabelkowych gwiazdek — nie ma tam prostego POST /reviews, który z palca strzelisz.

Tutaj wchodzi Selenium. Zamiast walczyć z czyimś front-endem, odpalasz prawdziwą przeglądarkę, która zachowuje się jak zwykły użytkownik: wchodzi na stronę, czeka aż DOM się załaduje, wypełnia inputy, klika elementy i wysyła formularz. Dla .NET-owca to czysty IWebDriver z fluent API — Navigate, FindElement, SendKeys, Click. Nic magicznego.

⚠️ Zanim napiszesz pierwszego bota, sprawdź Terms of Service strony, którą zamierzasz automatyzować. Wiele serwisów (LinkedIn, Allegro, Booking) zakazuje scrapingu i automatycznego dodawania treści w regulaminie. Bot na własnej stronie to Twoja bajka. Bot na cudzej — potencjalny problem prawny i etyczny.

W kontekście naszego projektu, zwrotkę z metody dodającej ogłoszenie zmieniłem tak, żeby zwracała link do nowo utworzonego wpisu. To ten URL przekazujemy dalej do bota — wchodzi w niego, przewija do sekcji "Wydaj opinię" i robi swoje. Tymczasowo w Program.cs zakomentowałem połączenie z bazą i wywołania Google Places, żeby nie palić kredytów API i nie zasypywać portalu śmieciowymi ogłoszeniami. Na sztywno wrzucamy testowy URL i pracujemy tylko na tym jednym wpisie.

NuGet: Selenium.WebDriver i ChromeDriver

Instalacja jest banalna, ale jest jeden haczyk. Z poziomu NuGeta potrzebujesz jednego pakietu:

dotnet add package Selenium.WebDriver

To jest sama biblioteka kliencka — API, z którego będziesz korzystał w kodzie. Selenium samo w sobie nie jest silnikiem przeglądarki, ono steruje istniejącą przeglądarką przez protokół WebDriver. Dlatego dodatkowo musisz dostarczyć chromedriver.exe — binarkę, która mówi Chrome'owi, co ma robić.

Dwie opcje:

  1. Ściągasz ręcznie z chromium.org/driver/downloads i wrzucasz do katalogu projektu. Wersja drivera musi zgadzać się z wersją Chrome'a zainstalowanego lokalnie. Sprawdzisz to w Ustawienia → Informacje o Chrome — jeśli masz 104, ściągasz ChromeDriver 104.
  2. Dorzucasz NuGeta Selenium.WebDriver.ChromeDriver, który robi to za Ciebie i dokłada driver do bin/Debug/net6.0/ przy builde.

Opcja pierwsza to to, co pokazałem na filmie — chromedriver.exe leży w głównym katalogu repo i kopiujemy go do bin/Debug/net6.0/ obok WPImporter.exe. Dopiero z tego samego poziomu WebDriver potrafi go odpalić. W produkcji polecam opcję drugą — mniej manualnej roboty, brak zgubionych plików.

⚠️ ChromeDriver to binarka, która otwiera port i słucha na poleceniach. Nie commituj jej do publicznego repozytorium bez świadomości, że może różnić się wersją u innych devów. Lepsze rozwiązanie to NuGet (Selenium.WebDriver.ChromeDriver) lub menedżer typu WebDriverManager, który sam dobiera odpowiednią wersję do lokalnego Chrome'a.

Pierwszy driver — headless Chrome

Klasa Bot ma konstruktor przyjmujący URL ogłoszenia. Dwa using-i wystarczą — OpenQA.Selenium i OpenQA.Selenium.Chrome. Tworzymy ChromeOptions, konfigurujemy tryb headless i otwieramy drivera w using, żeby po zakończeniu scope'a automatycznie zamknął przeglądarkę i posprzątał procesy.

var options = new ChromeOptions();
options.AddArgument("--headless=new");
options.AddArgument("--disable-gpu");

using var driver = new ChromeDriver(options);
driver.Navigate().GoToUrl("https://example.com");

var title = driver.FindElement(By.CssSelector("h1")).Text;
var links = driver.FindElements(By.CssSelector("a"));

foreach (var link in links)
{
    Console.WriteLine(link.GetAttribute("href"));
}

--headless=new to nowy tryb headless wprowadzony w Chrome 109+ — bardziej stabilny niż stary --headless, który miał problemy z niektórymi stronami. --disable-gpu wyłącza akcelerację sprzętową, nieużywaną w trybie bez UI.

Na etapie rozwoju bota warto wyłączyć headless — widzisz wtedy, jak bot klika, co jest nieocenione przy debugowaniu selektorów. Po włączeniu headless odpalasz aplikację i nic się nie pojawia na ekranie, ale pod maską przeglądarka robi swoje. Idealny tryb do CI/CD, crona czy hostowania w Dockerze.

Drugi tip: po Navigate().GoToUrl() często trzeba chwilę poczekać, aż DOM się wyrenderuje. Bez Thread.Sleep albo WebDriverWait aplikacja poleci dalej i spróbuje złapać element, którego jeszcze nie ma w drzewie. Efekt: NoSuchElementException w losowych momentach. Na początek wystarczy naiwne Thread.Sleep(3000), docelowo — explicit waits z WebDriverWait.

Scraping elementów przez By.CssSelector

Selenium oferuje kilka strategii lokalizacji elementów — By.Id, By.Name, By.ClassName, By.XPath, By.CssSelector. W 90% przypadków wystarczy CssSelector — składnia znana z Fronta, łatwa do debugowania w DevToolsach (Ctrl+F w Elements i wpisujesz selektor).

Pola input[for=name] i textarea komentarza to prosta sprawa — łapiesz po id i wywołujesz SendKeys(...), który emituje eventy klawiatury. Z kolei tabela gwiazdek ocen jest bardziej podstępna: z wyglądu to input radio, ale kliknięcie trafia w nakładkę <label> ustawioną nad inputem. Próba input[value='5'].Click() rzuca ElementNotInteractableException, bo radio jest ukryty pod etykietą.

Rozwiązanie — selektor po atrybucie for, bo label ma dynamiczną wartość typu rating-kontakt-4:

var star = driver.FindElement(By.CssSelector($"label[for='rating-{category}-{rating}']"));
star.Click();

W _categories trzymam listę czterech kluczy: kontakt, jakoscWykonania, cena, terminowosc. W pętli budujemy selektor na podstawie bieżącej kategorii i oceny z parametru metody (1–5). To samo z przyciskiem submit — w HTMLu ma id="submit", więc prosty By.Id("submit") załatwia sprawę. Przed klikiem Thread.Sleep(3000) daje przeglądarce czas na zarejestrowanie wszystkich zmian w DOM, inaczej formularz leci z niekompletnymi danymi.

⚠️ Scraping konkurencji, agregowanie cen czy ekstrakcja danych osobowych podlegają pod RODO, prawo autorskie do bazy danych (sui generis) i czasem wprost pod klauzule umowne. Jeśli robisz bota do własnego portfolio albo własnej strony — spoko. Jeśli planujesz odpalić go komercyjnie przeciwko cudzemu serwisowi, skonsultuj się z prawnikiem zamiast walczyć z mechanizmami anty-bot.

Wstrzyknięcie do DI

W aplikacji konsolowej mamy już skonfigurowane Dependency Injection. Wpinamy bota jako scoped serwis — każde wywołanie pobiera fresh'ową przeglądarkę:

services.AddScoped<IWebDriver>(sp =>
{
    var options = new ChromeOptions();
    options.AddArgument("--headless=new");
    return new ChromeDriver(options);
});

Pamiętaj, że IWebDriver implementuje IDisposable — kontener DI automatycznie wywoła Dispose() na końcu scope'a, zamykając przeglądarkę. Jeśli pominiesz using-a i zapomnisz o dispose, procesy chromedriver.exe będą się nabijały w Task Managerze aż zapchasz RAM.

Dla naszego use case'u częściej lepsza będzie klasa Bot z metodą AddComment(string author, string content, int rating, ...), która sama w środku otwiera using var driver. Lifetime bota = lifetime jednego dodania komentarza.

Co dalej?

W następnym odcinku spinamy wszystko w całość — Google Places API wyciąga firmy, EF Core zapisuje do bazy, WordPress API dodaje ogłoszenie i zwraca link, a Selenium Bot dokłada komentarz z oceną. Pełne end-to-end flow bez zakomentowanych linijek i z normalnym error handlingiem. Planuję też osobny odcinek o WebDriverWait, strategiach czekania na elementy i anti-detection — bo Thread.Sleep(3000) to rozwiązanie na teraz, nie na produkcję.

Jeśli temat botów Cię wciąga — daj znać w komentarzu pod filmem. Mogę nagrać rozszerzenie z bardziej zaawansowanymi scenariuszami: logowanie, 2FA, proxy rotation, obsługa reCAPTCHA, headful vs headless trade-offy. Repo projektu: github.com/jarmatys/WPImporter.

Tagi:#course-dotnet-wordpress#dotnet#selenium#bot#scraping

Masz pytanie do tego wpisu?

Napisz do mnie — chętnie porozmawiam o szczegółach technicznych, albo o tym jak zastosować to u Ciebie w projekcie.