آموزش ساخت خزنده (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 استفاده کن.
۳ اشتباه رایج که باید ازشون پرهیز کنی:
- استفاده از Regex برای parsing HTML.
- ساختن HttpClient به ازای هر درخواست.
- خزیدن بدون robots.txt و با سرعت زیاد.
اگر فقط ۵ دقیقه وقت داری، اینها رو یاد بگیر:
- IHttpClientFactory
- GetAsync + ReadAsStringAsync
- HtmlDocument.LoadHtml و SelectNodes
- رعایت delay
- حذف 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 HTMLAngleSharp(اختیاری) برای 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
برای جلوگیری از بلاک شدن، از پراکسیهای چرخشی استفاده کن.
۷. تشخیص تغییرات صفحات
با هش کردن محتوای صفحه، تغییرات را شناسایی کن و فقط در صورت تغییر ذخیره کن.
۸. اشتباهات رایج
استفاده از Regex برای parsing HTML
❌Regex.Match(html, "<title>(.*?)</title>")
✅ استفاده از HtmlAgilityPack یا AngleSharpساختن HttpClient در هر درخواست
❌using var client = new HttpClient();داخل حلقه
✅ استفاده ازIHttpClientFactoryنادیده گرفتن robots.txt
❌ خزیدن بدون توجه به قوانین سایت
✅ بررسی robots.txt قبل از شروعارسال درخواستهای پشت سر هم بدون تاخیر
❌ باعث بلاک شدن IP میشود
✅ تاخیر بین درخواستهاعدم مدیریت Timeout
❌ اگر سایتی پاسخ ندهد، برنامه hangs میکند
✅ استفاده ازCancellationTokenوHttpClient.Timeoutعدم اعتبارسنجی URL ورودی
❌ ممکن است به SSRF یا دسترسی به سیستم داخلی منجر شود
✅ بررسی scheme و hostاستفاده نادرست از XPath
❌ فرض اینکهSelectNodesهمیشه ناننال است
✅ همیشه null check کنخزیدن بدون محدودیت عمق یا تعداد صفحات
❌ ممکن است کل اینترنت را بخزی!
✅ حتماًmaxPagesو فیلتر host بذار
۹. بهترین روشها (Best Practices)
- از
IHttpClientFactoryاستفاده کن، نهnew HttpClient. - همیشه
User-Agentمعتبر و مشخص بفرست. robots.txtرا بررسی و رعایت کن.- بین درخواستها حداقل ۰.۵ تا ۱ ثانیه تاخیر بذار.
- URLها را قبل از اضافه شدن به صف، نرمالسازی و deduplicate کن.
- محدودیت عمق و تعداد صفحات تعیین کن.
- از
CancellationTokenبرای لغو خزیدن استفاده کن. - برای محتوای بزرگ از
HttpCompletionOption.ResponseHeadersReadاستفاده کن. - خطاهای شبکه را لاگ و مدیریت کن.
- دادههای استخراجشده را سریعاً ذخیره کن تا با خطا از بین نروند.
- در سایتهای داینامیک از مرورگر headless استفاده کن.
۱۰. منابع و ادامه مسیر
- Microsoft Docs: IHttpClientFactory
- Microsoft Docs: HttpClient
- HtmlAgilityPack Documentation
- AngleSharp Documentation
- Robots Exclusion Standard
- Polly Documentation
- PuppeteerSharp
- Playwright for .NET
۱۱. مرجع کامل توابع و متدها (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: دریافت مستقیم bodyGetStreamAsync: دریافت streamPostAsync: ارسال 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: سادهتر برای GETPostAsync: برای POSTPutAsync,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: دریافت streamCopyToAsync: کپی 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/fileHtmlDocument.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: مجموعه attributesInnerText: متن داخل گره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 SelectorQuerySelectorAll: انتخاب همه گرهها
۱۲. مقایسه کلاسها/توابع مشابه
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.LoadHtmlparse کردی - از XPath برای استخراج داده استفاده کردی
- لینکها را استخراج و deduplicate کردی
- صف خزیدن با
QueueوHashSetپیاده کردی - تاخیر بین درخواستها گذاشتی
- مدیریت خطا و cancellation را اعمال کردی
- با AngleSharp کار کردی
- با Polly برای retry آشنا شدی
- برای سایتهای JS محور از PuppeteerSharp استفاده کردی
- یک پروژه واقعی مثل price monitor یا news aggregator پیاده کردی
- نکات performance را رعایت کردی
- با اشتباهات رایج آشنا شدی و از آنها پرهیز کردی