🔬 آموزش ساخت خزنده (Crawler) در ASP.NET Core (آموزش عمیق)

crawler در asp.net core همه سطوح
parsakarimidev.ir

🔬 آموزش ساخت خزنده (Crawler) در ASP.NET Core (آموزش عمیق)

crawler در asp.net coreفریم‌ورک سطح: همه سطوح 1405/07/03

آموزش ساخت خزنده (Crawler) در ASP.NET Core

⚡ خلاصه سریع — اصل کاری‌ها

این‌ها مهم‌ترین چیزهایی هستن که اگه هیچی ندونی، فقط اینا رو بدونی کافیه:

  • HttpClient: برای ارسال درخواست HTTP استفاده کن؛ همیشه از IHttpClientFactory بسازش.
  • IHttpClientFactory: عمر و اتصال HttpClient را مدیریت می‌کند؛ از new HttpClient پرهیز کن.
  • HTML Parser: برای استخراج داده از HtmlAgilityPack یا AngleSharp استفاده کن، نه Regex.
  • SelectNodes/XPath یا QuerySelectorAll: داده‌ها را با XPath یا CSS Selector انتخاب کن.
  • Robots.txt: قبل از خزیدن حتماً قوانین سایت را بخوان.
  • Rate Limiting: بین درخواست‌ها تاخیر بذار تا IP بلاک نشی.
  • Crawl Queue: لینک‌ها را در صف قرار بده و URLهای تکراری را حذف کن.
  • User-Agent: هدر User-Agent معتبر بفرست.
  • Timeout/CancellationToken: برای همه درخواست‌ها timeout و cancellation بذار.
  • Headless Browser: برای سایت‌های JavaScript محور از PuppeteerSharp یا Playwright استفاده کن.

۳ اشتباه رایج که باید ازشون پرهیز کنی:

  1. استفاده از Regex برای parsing HTML.
  2. ساختن HttpClient به ازای هر درخواست.
  3. خزیدن بدون robots.txt و با سرعت زیاد.

اگر فقط ۵ دقیقه وقت داری، این‌ها رو یاد بگیر:

  1. IHttpClientFactory
  2. GetAsync + ReadAsStringAsync
  3. HtmlDocument.LoadHtml و SelectNodes
  4. رعایت delay
  5. حذف URLهای تکراری

۱. مقدمه

این موضوع چیه؟

خزنده وب (Web Crawler) یا اسپایدر، برنامه‌ای است که صفحات وب را به‌صورت خودکار می‌خواند، محتوای آن‌ها را تجزیه می‌کند، لینک‌ها را استخراج می‌کند و داده‌های موردنیاز را ذخیره می‌کند. در ASP.NET Core می‌توانی یک خزنده را به‌صورت یک سرویس داخل Web API پیاده‌سازی کنی.

چرا باید یادش بگیری؟

  • جمع‌آوری داده‌های رقبا، قیمت‌ها، محصولات
  • ساخت خبرخوان و aggregator
  • تحلیل سئو و لینک‌های داخلی سایت
  • ساخت دیتاست برای آموزش مدل‌های هوش مصنوعی
  • مانیتورینگ صفحات و تغییرات محتوا

کجا استفاده می‌شه؟

  • موتورهای جستجو مثل Google و Bing
  • ابزارهای مقایسه قیمت مثل Torob
  • خزنده‌های خبری مثل Google News
  • ربات‌های شبکه‌های اجتماعی
  • سیستم‌های تحلیل محتوا و سئو

۲. پیش‌نیازها

چیزهایی که باید از قبل بدونی

  • مبانی زبان C# و برنامه‌نویسی شی‌گرا
  • آشنایی با ASP.NET Core (DI، Minimal API یا MVC)
  • مفهوم async/await و Task
  • مبانی HTML و CSS یا XPath
  • آشنایی اولیه با JSON و REST

ابزارهای لازم

  • .NET SDK 8 یا بالاتر
  • Visual Studio یا VS Code
  • Postman یا curl برای تست
  • بسته‌های NuGet:
    • HtmlAgilityPack برای parsing HTML
    • AngleSharp (اختیاری) برای CSS Selector و DOM قوی‌تر
    • Polly (اختیاری) برای retry و resiliency

۳. نصب و راه‌اندازی

ساخت پروژه

dotnet new webapi -n CrawlerApi
cd CrawlerApi

نصب پکیج‌ها

dotnet add package HtmlAgilityPack

اگر بخواهی از AngleSharp استفاده کنی:

dotnet add package AngleSharp

راه‌اندازی HttpClient در Program.cs

var builder = WebApplication.CreateBuilder(args);

// فعال‌سازی IHttpClientFactory
builder.Services.AddHttpClient();

builder.Services.AddControllers();

var app = builder.Build();

app.MapControllers();
app.Run();

اولین پروژه ساده: دریافت HTML یک صفحه

app.MapGet("/fetch", async (string url, IHttpClientFactory factory) =>
{
    var client = factory.CreateClient();
    var html = await client.GetStringAsync(url);
    return Results.Content(html, "text/html");
});

⚠️ همیشه URL ورودی را اعتبارسنجی کن تا از SSRF جلوگیری کنی.

۴. مفاهیم پایه

۱. HttpClient

مسئول ارسال درخواست HTTP و دریافت پاسخ است. بهتر است از IHttpClientFactory استفاده کنی.

var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync("https://example.com");

۲. HTML Parsing

بعد از دریافت HTML، باید آن را به ساختار DOM تبدیل کنیم:

var doc = new HtmlDocument();
doc.LoadHtml(html);
var title = doc.DocumentNode.SelectSingleNode("//title")?.InnerText;

۳. XPath و CSS Selector

XPath برای HtmlAgilityPack:

var links = doc.DocumentNode.SelectNodes("//a[@href]");

CSS Selector با AngleSharp:

var title = document.QuerySelector("title")?.TextContent;

۴. صف خزیدن و Deduplication

لینک‌ها را در Queue<string> نگه می‌داری و URLهای تکراری را با HashSet<string> حذف می‌کنی:

var visited = new HashSet<string>();
var queue = new Queue<string>();
queue.Enqueue(startUrl);

while (queue.Count > 0)
{
    var url = queue.Dequeue();
    if (!visited.Add(url)) continue;
    // crawl url
}

۵. تاخیر و احترام به سرور

بین درخواست‌ها تاخیر بذار:

await Task.Delay(500); // نیم ثانیه

۵. مثال‌های کد ساده

مثال ۱: دریافت و نمایش عنوان صفحه

using HtmlAgilityPack;

public async Task<string?> GetTitleAsync(string url)
{
    var client = _httpClientFactory.CreateClient();
    var html = await client.GetStringAsync(url);
    var doc = new HtmlDocument();
    doc.LoadHtml(html);
    return doc.DocumentNode.SelectSingleNode("//title")?.InnerText;
}

مثال ۲: استخراج همه لینک‌ها

public async Task<List<string>> GetLinksAsync(string url)
{
    var client = _httpClientFactory.CreateClient();
    var html = await client.GetStringAsync(url);
    var doc = new HtmlDocument();
    doc.LoadHtml(html);
    
    var links = doc.DocumentNode.SelectNodes("//a[@href]");
    return links?
        .Select(a => a.GetAttributeValue("href", ""))
        .ToList() ?? new List<string>();
}

مثال ۳: استخراج محصولات از یک صفحه فروشگاهی

var nodes = doc.DocumentNode.SelectNodes("//div[contains(@class,'product')]");
foreach (var node in nodes)
{
    var name = node.SelectSingleNode(".//h2")?.InnerText;
    var price = node.SelectSingleNode(".//span[@class='price']")?.InnerText;
    Console.WriteLine($"{name} => {price}");
}

مثال ۴: خزیدن با عمق محدود

public async Task CrawlAsync(string startUrl, int maxPages)
{
    var visited = new HashSet<string>();
    var queue = new Queue<string>();
    queue.Enqueue(startUrl);
    var pages = 0;
    var client = _httpClientFactory.CreateClient();

    while (queue.Count > 0 && pages < maxPages)
    {
        var url = queue.Dequeue();
        if (!visited.Add(url)) continue;

        string html;
        try
        {
            html = await client.GetStringAsync(url);
        }
        catch
        {
            continue;
        }

        var doc = new HtmlDocument();
        doc.LoadHtml(html);
        var links = doc.DocumentNode.SelectNodes("//a[@href]")?
            .Select(a => a.GetAttributeValue("href", ""))
            .Where(h => !string.IsNullOrWhiteSpace(h))
            .Select(h => new Uri(new Uri(url), h).AbsoluteUri)
            .ToList() ?? new List<string>();

        foreach (var link in links)
            if (!visited.Contains(link))
                queue.Enqueue(link);

        pages++;
        await Task.Delay(500);
    }
}

۶. مثال واقعی و کاربردی

پروژه کوچک: خزنده ساده با ASP.NET Core Web API

Model

public record CrawledPage(string Url, string? Title, List<string> Links);

Interface

public interface ICrawlerService
{
    Task<IReadOnlyList<CrawledPage>> CrawlAsync(string startUrl, int maxPages, CancellationToken ct = default);
}

Implementation

public class CrawlerService : ICrawlerService
{
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<CrawlerService> _logger;

    public CrawlerService(IHttpClientFactory httpClientFactory, ILogger<CrawlerService> logger)
    {
        _httpClientFactory = httpClientFactory;
        _logger = logger;
    }

    public async Task<IReadOnlyList<CrawledPage>> CrawlAsync(
        string startUrl, int maxPages, CancellationToken ct = default)
    {
        var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var queue = new Queue<string>();
        var results = new List<CrawledPage>();

        queue.Enqueue(startUrl);
        var client = _httpClientFactory.CreateClient();
        client.DefaultRequestHeaders.UserAgent.ParseAdd("MyCrawler/1.0 (+https://example.com/bot)");

        while (queue.Count > 0 && results.Count < maxPages && !ct.IsCancellationRequested)
        {
            var url = queue.Dequeue();
            if (!visited.Add(url)) continue;

            HttpResponseMessage response;
            try
            {
                response = await client.GetAsync(url, ct);
                response.EnsureSuccessStatusCode();
            }
            catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
            {
                _logger.LogWarning(ex, "Fetch failed for {Url}", url);
                continue;
            }

            var html = await response.Content.ReadAsStringAsync(ct);
            var doc = new HtmlDocument();
            doc.LoadHtml(html);

            var title = doc.DocumentNode.SelectSingleNode("//title")?.InnerText?.Trim();

            var links = doc.DocumentNode.SelectNodes("//a[@href]")?
                .Select(a => a.GetAttributeValue("href", ""))
                .Where(href => !string.IsNullOrWhiteSpace(href))
                .Select(href => new Uri(new Uri(url), href).AbsoluteUri)
                .Distinct()
                .ToList() ?? new List<string>();

            results.Add(new CrawledPage(url, title, links));

            foreach (var link in links)
            {
                if (!visited.Contains(link))
                    queue.Enqueue(link);
            }

            // احترام به سرور
            await Task.Delay(500, ct);
        }

        return results;
    }
}

ثبت سرویس در Program.cs

builder.Services.AddHttpClient();
builder.Services.AddScoped<ICrawlerService, CrawlerService>();

کنترلر

[ApiController]
[Route("api/[controller]")]
public class CrawlerController : ControllerBase
{
    private readonly ICrawlerService _crawlerService;

    public CrawlerController(ICrawlerService crawlerService)
    {
        _crawlerService = crawlerService;
    }

    [HttpPost]
    public async Task<IActionResult> Crawl([FromBody] CrawlRequest request)
    {
        var result = await _crawlerService.CrawlAsync(request.StartUrl, request.MaxPages);
        return Ok(result);
    }
}

public record CrawlRequest(string StartUrl, int MaxPages = 10);

۷. مباحث پیشرفته

۱. کنترل همزمانی (Concurrency)

برای خزیدن سریع‌تر اما کنترل‌شده، از SemaphoreSlim و Task.WhenAll استفاده کن:

var semaphore = new SemaphoreSlim(5); // حداکثر ۵ درخواست همزمان
var tasks = urls.Select(async url =>
{
    await semaphore.WaitAsync(ct);
    try
    {
        await CrawlAsync(url, ct);
    }
    finally
    {
        semaphore.Release();
    }
});
await Task.WhenAll(tasks);

۲. استفاده از Polly برای Retry

builder.Services.AddHttpClient("crawler")
    .AddPolicyHandler((services, request) => Policy<HttpResponseMessage>
        .Handle<HttpRequestException>()
        .OrResult(r => r.StatusCode >= HttpStatusCode.InternalServerError)
        .WaitAndRetryAsync(3, retry => TimeSpan.FromSeconds(Math.Pow(2, retry))));

۳. پردازش سایت‌های JavaScript محور

بسیاری از سایت‌ها محتوا را با JS بارگذاری می‌کنند. برای این حالت از PuppeteerSharp یا Playwright استفاده کن:

using PuppeteerSharp;

using var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
await using var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true });
await using var page = await browser.NewPageAsync();
await page.GoToAsync(url);
var content = await page.GetContentAsync();

۴. ذخیره‌سازی در دیتابیس

با EF Core داده‌ها را ذخیره کن:

public class CrawlDbContext : DbContext
{
    public DbSet<CrawledPageEntity> CrawledPages { get; set; }
}

۵. صف توزیع‌شده

برای خزیدن در مقیاس بزرگ، از RabbitMQ یا Kafka یا Redis Queue استفاده کن.

۶. Rotating Proxy

برای جلوگیری از بلاک شدن، از پراکسی‌های چرخشی استفاده کن.

۷. تشخیص تغییرات صفحات

با هش کردن محتوای صفحه، تغییرات را شناسایی کن و فقط در صورت تغییر ذخیره کن.

۸. اشتباهات رایج

  1. استفاده از Regex برای parsing HTML
    ❌ Regex.Match(html, "<title>(.*?)</title>")
    ✅ استفاده از HtmlAgilityPack یا AngleSharp

  2. ساختن HttpClient در هر درخواست
    ❌ using var client = new HttpClient(); داخل حلقه
    ✅ استفاده از IHttpClientFactory

  3. نادیده گرفتن robots.txt
    ❌ خزیدن بدون توجه به قوانین سایت
    ✅ بررسی robots.txt قبل از شروع

  4. ارسال درخواست‌های پشت سر هم بدون تاخیر
    ❌ باعث بلاک شدن IP می‌شود
    ✅ تاخیر بین درخواست‌ها

  5. عدم مدیریت Timeout
    ❌ اگر سایتی پاسخ ندهد، برنامه hangs می‌کند
    ✅ استفاده از CancellationToken و HttpClient.Timeout

  6. عدم اعتبارسنجی URL ورودی
    ❌ ممکن است به SSRF یا دسترسی به سیستم داخلی منجر شود
    ✅ بررسی scheme و host

  7. استفاده نادرست از XPath
    ❌ فرض اینکه SelectNodes همیشه نان‌نال است
    ✅ همیشه null check کن

  8. خزیدن بدون محدودیت عمق یا تعداد صفحات
    ❌ ممکن است کل اینترنت را بخزی!
    ✅ حتماً maxPages و فیلتر host بذار

۹. بهترین روش‌ها (Best Practices)

  • از IHttpClientFactory استفاده کن، نه new HttpClient.
  • همیشه User-Agent معتبر و مشخص بفرست.
  • robots.txt را بررسی و رعایت کن.
  • بین درخواست‌ها حداقل ۰.۵ تا ۱ ثانیه تاخیر بذار.
  • URLها را قبل از اضافه شدن به صف، نرمال‌سازی و deduplicate کن.
  • محدودیت عمق و تعداد صفحات تعیین کن.
  • از CancellationToken برای لغو خزیدن استفاده کن.
  • برای محتوای بزرگ از HttpCompletionOption.ResponseHeadersRead استفاده کن.
  • خطاهای شبکه را لاگ و مدیریت کن.
  • داده‌های استخراج‌شده را سریعاً ذخیره کن تا با خطا از بین نروند.
  • در سایت‌های داینامیک از مرورگر headless استفاده کن.

۱۰. منابع و ادامه مسیر

۱۱. مرجع کامل توابع و متدها (API Deep Dive)

📌 نام متد: IHttpClientFactory.CreateClient()

امضای متد (Signature):

HttpClient CreateClient(string name);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
name string بله نام HttpClient ثبت‌شده در DI. اگر خالی باشد، کلاینت پیش‌فرض برمی‌گردد. "crawler"

مقدار برگشتی (Return Value):

  • نوع: HttpClient
  • توضیح: نمونه HttpClient با handler مدیریت‌شده توسط factory. تنظیمات از پیش اعمال‌شده (مثل BaseAddress یا Headers) روی آن موجود است.
  • اگر نام null باشد، ArgumentNullException پرتاب می‌شود.

کاری که انجام می‌ده (گام به گام):
۱. درخواست از DI برای IHttpClientFactory.
۲. Factory از pool مربوط به نام، handler را انتخاب می‌کند.
۳. یک HttpClient جدید با آن handler برمی‌گرداند.
۴. تنظیمات ثبت‌شده در AddHttpClient اعمال می‌شود.

مثال ساده و قابل اجرا:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddHttpClient();
var provider = services.BuildServiceProvider();
var factory = provider.GetRequiredService<IHttpClientFactory>();
var client = factory.CreateClient();
Console.WriteLine(client.GetStringAsync("https://example.com").Result);

مثال واقعی و کاربردی:

// Program.cs
builder.Services.AddHttpClient("crawler", client =>
{
    client.BaseAddress = new Uri("https://api.example.com");
    client.DefaultRequestHeaders.UserAgent.ParseAdd("MyCrawler/1.0");
});

// در سرویس
var client = _factory.CreateClient("crawler");
var response = await client.GetAsync("/products");

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
ArgumentNullException اگر name به‌صورت null پاس داده شود از نام غیرnull استفاده کن
InvalidOperationException اگر service provider به درستی ساخته نشده باشد DI را در Program.cs ثبت کن

Overloadها:
این متد overload دیگری ندارد.

اشتباهات رایج:

  • ❌ using var client = factory.CreateClient(); و dispose کردن آن در هر بار → ✅ لازم نیست دستی dispose کنی.
  • ❌ ساخت new HttpClient() به‌صورت مکرر → ✅ از factory استفاده کن.
  • ❌ ثبت نکردن AddHttpClient() → ✅ در Program.cs فراخوانی کن.

متدهای مرتبط:

  • IServiceCollection.AddHttpClient(): ثبت سرویس
  • IServiceCollection.AddHttpClient("name"): ثبت کلاینت نام‌دار
  • IHttpClientFactory.CreateClient(): ایجاد کلاینت پیش‌فرض

📌 نام متد: HttpClient.GetAsync()

امضای متد (Signature):

public Task<HttpResponseMessage> GetAsync(string? requestUri);
public Task<HttpResponseMessage> GetAsync(string? requestUri, HttpCompletionOption completionOption);
public Task<HttpResponseMessage> GetAsync(string? requestUri, CancellationToken cancellationToken);
public Task<HttpResponseMessage> GetAsync(Uri? requestUri);
// و نسخه‌های مشابه با Uri و CancellationToken

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
requestUri string? / Uri? بله آدرس صفحه یا منبع درخواستی "https://example.com"
completionOption HttpCompletionOption خیر زمان تکمیل Task: ResponseHeadersRead یا ResponseContentRead ResponseContentRead
cancellationToken CancellationToken خیر برای لغو درخواست ct

مقدار برگشتی (Return Value):

  • نوع: Task<HttpResponseMessage>
  • توضیح: نتیجه عملیات async که شامل HttpResponseMessage است. HttpResponseMessage شامل StatusCode، Headers و Content می‌شود.

کاری که انجام می‌ده (گام به گام):
۱. ساخت HttpRequestMessage با متد GET.
۲. ارسال درخواست به سرور.
۳. انتظار برای دریافت پاسخ (headers و در صورت نیاز content).
۴. بازگرداندن HttpResponseMessage.

مثال ساده و قابل اجرا:

var client = new HttpClient();
var response = await client.GetAsync("https://example.com");
var html = await response.Content.ReadAsStringAsync();
Console.WriteLine(html);

مثال واقعی و کاربردی:

var client = _httpClientFactory.CreateClient("crawler");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var response = await client.GetAsync(url, cts.Token);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync(cts.Token);

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
HttpRequestException خطای شبکه، DNS، یا پاسخ غیرموفق بررسی دسترسی شبکه و URL
TaskCanceledException timeout یا لغو از طریق CancellationToken افزایش timeout یا مدیریت لغو
InvalidOperationException URL نا‌معتبر یا null اعتبارسنجی URL قبل از ارسال

Overloadها:

public Task<HttpResponseMessage> GetAsync(string? requestUri);
public Task<HttpResponseMessage> GetAsync(Uri? requestUri);
public Task<HttpResponseMessage> GetAsync(string? requestUri, HttpCompletionOption completionOption);
public Task<HttpResponseMessage> GetAsync(Uri? requestUri, HttpCompletionOption completionOption);
public Task<HttpResponseMessage> GetAsync(string? requestUri, CancellationToken cancellationToken);
public Task<HttpResponseMessage> GetAsync(Uri? requestUri, CancellationToken cancellationToken);
public Task<HttpResponseMessage> GetAsync(string? requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken);
public Task<HttpResponseMessage> GetAsync(Uri? requestUri, HttpCompletionOption completionOption, CancellationToken cancellationToken);

اشتباهات رایج:

  • ❌ استفاده از .Result یا .Wait() به‌جای await → ✅ استفاده از await.
  • ❌ عدم بررسی IsSuccessStatusCode → ✅ EnsureSuccessStatusCode().
  • ❌ فراموش کردن cancellation → ✅ همیشه CancellationToken ارسال کن.

متدهای مرتبط:

  • SendAsync: ارسال درخواست سفارشی
  • GetStringAsync: دریافت مستقیم body
  • GetStreamAsync: دریافت stream
  • PostAsync: ارسال POST

📌 نام متد: HttpClient.SendAsync()

امضای متد (Signature):

public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request);
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption);
public Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
request HttpRequestMessage بله پیام کامل HTTP شامل متد، URL، headers و content new HttpRequestMessage(HttpMethod.Get, url)
completionOption HttpCompletionOption خیر زمان تکمیل Task ResponseHeadersRead
cancellationToken CancellationToken خیر لغو درخواست ct

مقدار برگشتی (Return Value):

  • نوع: Task<HttpResponseMessage>
  • توضیح: پاسخ HTTP شامل status، headers و content.

کاری که انجام می‌ده (گام به گام):
۱. ایجاد HttpRequestMessage.
۲. اضافه کردن headers دلخواه.
۳. ارسال از طریق handler pipeline.
۴. دریافت HttpResponseMessage.

مثال ساده و قابل اجرا:

var request = new HttpRequestMessage(HttpMethod.Get, "https://example.com");
request.Headers.UserAgent.ParseAdd("MyCrawler/1.0");
var client = new HttpClient();
var response = await client.SendAsync(request);

مثال واقعی و کاربردی:

var client = _httpClientFactory.CreateClient("crawler");
var request = new HttpRequestMessage(HttpMethod.Get, url);
request.Headers.Accept.ParseAdd("text/html");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20));
var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cts.Token);
var html = await response.Content.ReadAsStringAsync(cts.Token);

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
HttpRequestException خطای شبکه یا پاسخ ناموفق retry با Polly
TaskCanceledException timeout یا لغو افزایش timeout
InvalidOperationException request قبلاً ارسال شده باشد از request تازه استفاده کن

Overloadها: در بالا فهرست شده.

اشتباهات رایج:

  • ❌ استفاده مجدد از HttpRequestMessage بعد از ارسال → ✅ همیشه جدید بساز.
  • ❌ عدم تنظیم headers مهم → ✅ User-Agent و Accept را تنظیم کن.
  • ❌ فراموش کردن ResponseHeadersRead برای صفحات بزرگ → ✅ برای بررسی header قبل از body استفاده کن.

متدهای مرتبط:

  • GetAsync: ساده‌تر برای GET
  • PostAsync: برای POST
  • PutAsync, DeleteAsync

📌 نام متد: HttpContent.ReadAsStringAsync()

امضای متد (Signature):

public Task<string> ReadAsStringAsync();
public Task<string> ReadAsStringAsync(CancellationToken cancellationToken);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
cancellationToken CancellationToken خیر لغو خواندن content ct

مقدار برگشتی (Return Value):

  • نوع: Task<string>
  • توضیح: محتوای body به‌صورت رشته. اگر content خالی باشد، رشته خالی برمی‌گردد.

کاری که انجام می‌ده (گام به گام):
۱. دریافت stream از response content.
۲. خواندن stream به‌صورت async.
۳. تبدیل بایت‌ها به string با encoding مشخص‌شده.

مثال ساده و قابل اجرا:

var response = await client.GetAsync("https://example.com");
var html = await response.Content.ReadAsStringAsync();
Console.WriteLine(html);

مثال واقعی و کاربردی:

var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
var html = await response.Content.ReadAsStringAsync(ct);

var doc = new HtmlDocument();
doc.LoadHtml(html);

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
ObjectDisposedException اگر content dispose شده باشد از content قبل از dispose استفاده کن
TaskCanceledException لغو از طریق cancellationToken مدیریت لغو
InvalidOperationException اگر content قبلاً خوانده شده باشد (برای stream) فقط یک‌بار بخوان

Overloadها:

public Task<string> ReadAsStringAsync();
public Task<string> ReadAsStringAsync(CancellationToken cancellationToken);

اشتباهات رایج:

  • ❌ خواندن content چند بار → ✅ فقط یک‌بار می‌توانی body را بخوانی؛ اگر لازم است دوباره استفاده کنی، ابتدا ذخیره کن.
  • ❌ عدم بررسی موفقیت‌آمیز بودن response → ✅ EnsureSuccessStatusCode.
  • ❌ استفاده از ReadAsStringAsync().Result → ✅ await.

متدهای مرتبط:

  • ReadAsByteArrayAsync: دریافت body به‌صورت byte[]
  • ReadAsStreamAsync: دریافت stream
  • CopyToAsync: کپی content به stream دیگر

📌 نام متد: HtmlDocument.LoadHtml()

امضای متد (Signature):

public void LoadHtml(string html);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
html string بله رشته HTML که باید parse شود "<html><body><h1>Hi</h1></body></html>"

مقدار برگشتی (Return Value):

  • نوع: void
  • توضیح: خروجی ندارد. DOM را در DocumentNode بارگذاری می‌کند.

کاری که انجام می‌ده (گام به گام):
۱. دریافت رشته HTML.
۲. تجزیه و ساخت درخت DOM.
۳. آماده‌سازی DocumentNode برای queries.

مثال ساده و قابل اجرا:

var doc = new HtmlDocument();
doc.LoadHtml("<html><body><p>Hello</p></body></html>");
var p = doc.DocumentNode.SelectSingleNode("//p")?.InnerText;
Console.WriteLine(p); // Hello

مثال واقعی و کاربردی:

var response = await client.GetAsync(url);
var html = await response.Content.ReadAsStringAsync();
var doc = new HtmlDocument();
doc.LoadHtml(html);
var title = doc.DocumentNode.SelectSingleNode("//title")?.InnerText;

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
ArgumentNullException اگر html null باشد قبل از فراخوانی null check کن
XPathException اگر بعداً XPath نامعتبر استفاده شود بررسی XPath

Overloadها:

public void Load(Stream stream);
public void Load(TextReader reader);
public void Load(string filename); // بارگذاری از فایل، نه URL

اشتباهات رایج:

  • ❌ استفاده از Load برای fetch از اینترنت → ✅ Load فقط از فایل/stream می‌خواند؛ ابتدا با HttpClient بگیر.
  • ❌ فراموش کردن null check برای DocumentNode → ✅ check کن.
  • ❌ بارگذاری HTML بدون بررسی encoding → ✅ مطمئن شو encoding درست است.

متدهای مرتبط:

  • HtmlWeb.Load: دریافت و parse از اینترنت
  • HtmlDocument.Load: از stream/file
  • HtmlDocument.DocumentNode: دسترسی به ریشه DOM

📌 نام متد: HtmlNode.SelectNodes()

امضای متد (Signature):

public HtmlNodeCollection? SelectNodes(string xpath);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
xpath string بله عبارت XPath برای انتخاب گره‌ها "//a[@href]"

مقدار برگشتی (Return Value):

  • نوع: HtmlNodeCollection?
  • توضیح: مجموعه‌ای از گره‌های انتخاب‌شده. اگر هیچ گره‌ای پیدا نشود، null برمی‌گردد.

کاری که انجام می‌ده (گام به گام):
۱. ارزیابی عبارت XPath روی DOM.
۲. جمع‌آوری گره‌های مطابق.
۳. بازگرداندن HtmlNodeCollection.

مثال ساده و قابل اجرا:

var doc = new HtmlDocument();
doc.LoadHtml("<html><body><a href='/a'>A</a><a href='/b'>B</a></body></html>");
var nodes = doc.DocumentNode.SelectNodes("//a");
foreach (var node in nodes)
    Console.WriteLine(node.InnerText);

مثال واقعی و کاربردی:

var productNodes = doc.DocumentNode.SelectNodes("//div[contains(@class,'product')]");
if (productNodes == null) return;

foreach (var product in productNodes)
{
    var name = product.SelectSingleNode(".//h2")?.InnerText;
    var price = product.SelectSingleNode(".//span[@class='price']")?.InnerText;
    Console.WriteLine($"{name}: {price}");
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
XPathException XPath نامعتبر بررسی syntax
ArgumentNullException اگر xpath null باشد از رشته معتبر استفاده کن

Overloadها:
این متد overload دیگری ندارد.

اشتباهات رایج:

  • ❌ فرض اینکه SelectNodes همیشه مقدار دارد → ✅ همیشه null check کن.
  • ❌ استفاده از //div[@class='product'] وقتی class شامل چند کلاس است → ✅ از contains(@class,'product') استفاده کن.
  • ❌ عدم استفاده از . در XPath داخلی → ✅ برای شروع از گره فعلی .//h2 بنویس.

متدهای مرتبط:

  • SelectSingleNode: انتخاب یک گره
  • XPathEvaluate: ارزیابی XPath با نوع دیگر
  • ChildNodes: دسترسی به فرزندان

📌 نام متد: HtmlNode.GetAttributeValue()

امضای متد (Signature):

public string GetAttributeValue(string name, string def);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
name string بله نام attribute موردنظر "href"
def string بله مقدار پیش‌فرض اگر attribute وجود نداشته باشد ""

مقدار برگشتی (Return Value):

  • نوع: string
  • توضیح: مقدار attribute در صورت وجود؛ در غیر این صورت مقدار def. هرگز null برنمی‌گرداند.

کاری که انجام می‌ده (گام به گام):
۱. بررسی وجود attribute با نام داده‌شده.
۲. اگر وجود داشت، مقدار آن را برمی‌گرداند.
۳. اگر نبود، def را برمی‌گرداند.

مثال ساده و قابل اجرا:

var doc = new HtmlDocument();
doc.LoadHtml("<a href='https://example.com'>Link</a>");
var a = doc.DocumentNode.SelectSingleNode("//a");
Console.WriteLine(a.GetAttributeValue("href", "")); // https://example.com
Console.WriteLine(a.GetAttributeValue("target", "_self")); // _self

مثال واقعی و کاربردی:

var links = doc.DocumentNode.SelectNodes("//a[@href]");
if (links == null) return;

foreach (var link in links)
{
    var href = link.GetAttributeValue("href", "");
    if (!string.IsNullOrWhiteSpace(href))
        Console.WriteLine(href);
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
ArgumentNullException اگر name null باشد از نام غیرnull استفاده کن

Overloadها:
این متد overload دیگری ندارد.

اشتباهات رایج:

  • ❌ دسترسی مستقیم node.Attributes["href"].Value که اگر attribute نباشد exception می‌دهد → ✅ از GetAttributeValue استفاده کن.
  • ❌ فراموش کردن مقدار پیش‌فرض → ✅ همیشه مقدار پیش‌فرض بده.
  • ❌ استفاده از GetAttributeValue روی گره‌ای که null است → ✅ ابتدا null check کن.

متدهای مرتبط:

  • Attributes: مجموعه attributes
  • InnerText: متن داخل گره
  • GetClasses: دریافت کلاس‌ها

📌 نام متد: AngleSharp.BrowsingContext.OpenAsync()

امضای متد (Signature):

public Task<IDocument> OpenAsync(string address, CancellationToken cancellationToken = default);

ورودی‌ها (Parameters):

نام پارامتر نوع (Type) اجباری؟ توضیح دقیق مثال مقدار
address string بله آدرس صفحه برای fetch و parse "https://example.com"
cancellationToken CancellationToken خیر لغو عملیات ct

مقدار برگشتی (Return Value):

  • نوع: Task<IDocument>
  • توضیح: سند DOM حاصل از parsing صفحه. امکان پرس‌وجو با CSS Selector را فراهم می‌کند.

کاری که انجام می‌ده (گام به گام):
۱. دریافت آدرس صفحه.
۲. استفاده از loader برای fetch محتوا.
۳. parse HTML به DOM.
۴. بازگرداندن IDocument.

مثال ساده و قابل اجرا:

using AngleSharp;
using AngleSharp.Dom;

var config = Configuration.Default.WithDefaultLoader();
using var context = BrowsingContext.New(config);
var document = await context.OpenAsync("https://example.com");
var title = document.QuerySelector("title")?.TextContent;
Console.WriteLine(title);

مثال واقعی و کاربردی:

using AngleSharp;
using AngleSharp.Dom;

var config = Configuration.Default.WithDefaultLoader();
using var context = BrowsingContext.New(config);
var document = await context.OpenAsync(url);
var products = document.QuerySelectorAll(".product");
foreach (var product in products)
{
    var name = product.QuerySelector("h2")?.TextContent;
    var price = product.QuerySelector(".price")?.TextContent;
    Console.WriteLine($"{name}: {price}");
}

خطاها و Exceptions احتمالی:

Exception زمان رخ دادن راه‌حل
HttpRequestException خطا در fetch بررسی URL و شبکه
TaskCanceledException لغو عملیات مدیریت cancellation
InvalidOperationException اگر loader فعال نباشد WithDefaultLoader() را اضافه کن

Overloadها:

public Task<IDocument> OpenAsync(string address, CancellationToken cancellationToken = default);
public Task<IDocument> OpenAsync(Url url, CancellationToken cancellationToken = default);
public Task<IDocument> OpenAsync(Stream stream, CancellationToken cancellationToken = default);

اشتباهات رایج:

  • ❌ فراموش کردن WithDefaultLoader() → ✅ بدون آن fetch انجام نمی‌شود.
  • ❌ عدم dispose کردن context → ✅ از using استفاده کن.
  • ❌ انتظار اجرای JavaScript → ✅ AngleSharp به‌صورت پیش‌فرض JS اجرا نمی‌کند.

متدهای مرتبط:

  • OpenNewAsync: باز کردن صفحه جدید
  • QuerySelector: انتخاب اولین گره با CSS Selector
  • QuerySelectorAll: انتخاب همه گره‌ها

۱۲. مقایسه کلاس‌ها/توابع مشابه

HttpClient در مقابل IHttpClientFactory

ویژگی HttpClient ساده IHttpClientFactory
مدیریت عمر دستی خودکار
Socket exhaustion محتمل جلوگیری می‌شود
تنظیمات مرکزی نه بله
مناسب برای اسکریپت‌های ساده برنامه‌های ASP.NET Core

HtmlAgilityPack در مقابل AngleSharp

ویژگی HtmlAgilityPack AngleSharp
Query Language XPath CSS Selector و XPath
اجرای JavaScript نه نه (بدون JS engine)
Performance خوب خوب
API ساده مدرن‌تر و کامل‌تر
کاربرد خزیدن HTML ساده پروژه‌های پیچیده DOM

XPath در مقابل CSS Selector

ویژگی XPath CSS Selector
Syntax //div[@class='x'] div.x
جهت حرکت parent, sibling, ancestor عمدتاً descendant
پیچیدگی توانمندتر ساده‌تر
پشتیبانی در HtmlAgilityPack بله نه
پشتیبانی در AngleSharp بله بله

۱۳. سناریوهای واقعی (Real-World Scenarios)

۱. دیده‌بانی قیمت (Price Monitoring)

هر چند ساعت صفحات محصول را خزش کن، قیمت را استخراج و در دیتابیس ذخیره کن. در صورت کاهش قیمت، هشدار ارسال کن.

ساختار:

  • BackgroundService با PeriodicTimer
  • خزیدن لیست محصولات
  • استخراج قیمت با HtmlNode
  • ذخیره تاریخچه قیمت
  • ارسال نوتیفیکیشن

۲. خبرخوان خودکار (News Aggregator)

از چند سایت خبری، تیتر و لینک خبرها را بگیر و در یک API ارائه کن.

ساختار:

  • لیست RSS/HTML منابع
  • خزیدن دوره‌ای
  • ذخیره‌سازی در EF Core
  • ارائه API به کاربر

۳. تحلیل سئو (SEO Crawler)

لینک‌های داخلی سایت را تحلیل کن: status code، عنوان، meta tags و broken links.

ساختار:

  • خزیدن تمام صفحات سایت
  • بررسی status code
  • استخراج title و meta description
  • گزارش‌دهی

نمونه کد برای سناریوی Price Monitor

public class PriceMonitorService : BackgroundService
{
    private readonly IHttpClientFactory _factory;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var timer = new PeriodicTimer(TimeSpan.FromHours(6));
        while (await timer.WaitForNextTickAsync(stoppingToken))
        {
            var products = await GetProductsAsync(stoppingToken);
            foreach (var product in products)
            {
                var price = await ExtractPriceAsync(product.Url, stoppingToken);
                if (price < product.PreviousPrice)
                    NotifyPriceDrop(product, price);
            }
        }
    }

    private async Task<decimal?> ExtractPriceAsync(string url, CancellationToken ct)
    {
        var client = _factory.CreateClient();
        var html = await client.GetStringAsync(url, ct);
        var doc = new HtmlDocument();
        doc.LoadHtml(html);
        var priceText = doc.DocumentNode.SelectSingleNode("//span[@class='price']")?.InnerText;
        return decimal.TryParse(priceText, out var price) ? price : null;
    }
}

۱۴. کارایی و Performance

۱. استفاده از IHttpClientFactory

با جلوگیری از socket exhaustion، کارایی بالا می‌رود.

۲. مدیریت Connection Pooling

HttpClientHandler به‌صورت پیش‌فرض connection pool دارد. با IHttpClientFactory این pool اشتراکی و بهینه است.

۳. تنظیم HttpCompletionOption.ResponseHeadersRead

اگر فقط هدرها را نیاز داری و محتوا بزرگ است، از این گزینه استفاده کن:

var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
// سپس در صورت نیاز:
var html = await response.Content.ReadAsStringAsync();

۴. فعال‌سازی Compression

از GZip و Brotli برای کاهش حجم داده استفاده کن:

builder.Services.AddHttpClient("crawler", client =>
{
    client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, brotli");
});

۵. محدودسازی همزمانی

با SemaphoreSlim تعداد درخواست‌های همزمان را کنترل کن تا CPU و Network saturation رخ ندهد.

۶. کش کردن نتایج

اگر یک URL را چند بار خزش می‌کنی، از IMemoryCache یا Redis استفاده کن.

۷. Batch ذخیره‌سازی

برای دیتابیس از AddRange و SaveChangesAsync به‌صورت batch استفاده کن.

۸. پرهیز از thread blocking

هرگز از .Result یا .Wait() در async استفاده نکن.

۹. استفاده از ConfigureAwait(false) در کتابخانه‌ها

در کتابخانه‌های عمومی برای کاهش context switching:

var html = await client.GetStringAsync(url).ConfigureAwait(false);

۱۰. مانیتورینگ

لاگ زمان هر request و تعداد صفحات خزش‌شده را ثبت کن تا bottleneck ها را پیدا کنی.

۱۵. چک‌لیست یادگیری

  • مفهوم خزنده و کاربردهای آن را می‌دانی
  • پروژه ASP.NET Core Web API ساختی
  • IHttpClientFactory را فعال کردی
  • یک صفحه را با GetAsync و ReadAsStringAsync دریافت کردی
  • HTML را با HtmlDocument.LoadHtml parse کردی
  • از XPath برای استخراج داده استفاده کردی
  • لینک‌ها را استخراج و deduplicate کردی
  • صف خزیدن با Queue و HashSet پیاده کردی
  • تاخیر بین درخواست‌ها گذاشتی
  • مدیریت خطا و cancellation را اعمال کردی
  • با AngleSharp کار کردی
  • با Polly برای retry آشنا شدی
  • برای سایت‌های JS محور از PuppeteerSharp استفاده کردی
  • یک پروژه واقعی مثل price monitor یا news aggregator پیاده کردی
  • نکات performance را رعایت کردی
  • با اشتباهات رایج آشنا شدی و از آن‌ها پرهیز کردی

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.