---
title: "C# ile Web Sitesinden Veri Çekme: HttpClient ve Proxy"
description: "C# ile veri çekmek için tek bir HttpClient örneği kurar, HTML'i HtmlAgilityPack ile ayrıştırır, proxy'yi handler üzerinden WebProxy ile verirsiniz."
url: https://proxynet.io/tr/blog/csharp-web-scraping
date: 2026-09-19
author: "Acar Diveroli"
category: "Nasıl Yapılır, Web Scraping"
lang: tr
---

# C# ile Web Sitesinden Veri Çekme: HttpClient ve Proxy

Türkçe kaynaklarda C# ile veri çekme denince karşınıza çoğunlukla `WebClient` ve tek bir sayfayı indiren on beş satırlık bir örnek çıkar. O örnek ilk denemede çalışır, sonra ölçek büyüyünce üç ayrı yerden kırılır: her istekte yeni bir istemci nesnesi oluşturulduğu için soketler tükenir, aynı adresten saniyede onlarca istek gören site `429` döndürmeye başlar, sayfa yapısı değişince ayrıştırma kodu boş bir referansla patlar. Bunların üçü de kütüphane seçiminden değil, istemcinin nasıl kurulduğundan kaynaklanır.

Bu yazıda güncel .NET ile bir veri çekme işinin nasıl kurulacağını anlatıyoruz: `HttpClient` örneğinin yaşam döngüsü, HTML ayrıştırma için HtmlAgilityPack ve AngleSharp arasındaki seçim, `HttpClientHandler` ya da `SocketsHttpHandler` üzerinden proxy tanımlama, kullanıcı adı ve parolayla kimlik doğrulama, SOCKS5 adresleri, zaman aşımı ve iptal, eşzamanlılık sınırı ve yeniden deneme. Örnek hedef, tam da bu iş için yayımlanmış bir alıştırma sitesi olan `books.toscrape.com`.

> **Not: Kısa cevap**
>
> C# ile bir siteden veri çekmek üç parçadan oluşur: uygulama boyunca yaşayan tek bir `HttpClient` örneğiyle sayfayı indirmek, gelen HTML'i HtmlAgilityPack ya da AngleSharp ile ayrıştırmak, çıkan kayıtları dosyaya ya da veritabanına yazmak. Proxy bu zincire istemcinin kendisinden değil, altındaki handler'dan girer: `HttpClientHandler` ya da `SocketsHttpHandler` nesnesinin `Proxy` özelliğine bir `WebProxy` verilir, kullanıcı adı ve parola da `NetworkCredential` olarak o `WebProxy` nesnesine eklenir.

## C# ile veri çekme hangi adımlardan oluşur?

Dilden bağımsız olarak her scraping işi aynı beş adımı izler. C# tarafında her adımın karşılığı şudur:

1. **Adres listesini üretmek.** Kategori sayfaları, sayfalama bağlantıları ya da site haritası. Liste bir kuyruk olarak tutulur, böylece iş yarıda kalırsa kaldığı yerden sürdürülebilir.
2. **İsteği göndermek.** `HttpClient` üzerinden `GetAsync`. Yanıtın durum kodu kontrol edilir, gövde metne dönüştürülür.
3. **HTML'i ayrıştırmak.** Gelen metin bir belge nesnesine çevrilir, aradığınız alanlar XPath ya da CSS seçicisiyle bulunur.
4. **Veriyi normalleştirmek.** Fiyattaki para birimi işareti, satırdaki fazladan boşluk, eksik alanlar burada temizlenir.
5. **Kaydetmek.** JSON, CSV ya da bir veritabanı tablosu. Kayıt adımı ayrı tutulursa ayrıştırma kodunu değiştirdiğinizde veriyi yeniden indirmeniz gerekmez.

Bu adımların arasına iki şey daha girer: istekler arasındaki bekleme ve hata durumunda ne yapılacağı. İkisi de sonradan eklenen süsler değil, işin ilk sürümünde bulunması gereken parçalardır. Aşağıdaki bölümler tek tek bunları kuruyor.

## Başlamadan önce: robots.txt, hız ve dürüst bir istemci kimliği

Herhangi bir siteye istek göndermeden önce o sitenin `robots.txt` dosyasına bakın. Dosyanın biçimi ve yorumlanma kuralları [RFC 9309](https://www.rfc-editor.org/rfc/rfc9309.html) ile standartlaştırıldı. Standart, dosya sunucudan alınamıyorsa tarayıcının kaynaklara erişebileceğini söyler; yani `404` dönen bir adres yasak anlamına gelmez. Bu yazının örnek hedefi olan `books.toscrape.com` bunun tipik bir örneği: alıştırma için yayımlanmış bir vitrin sitesi olduğundan `robots.txt` dosyası yoktur ve adres `404` döner. Kuralların nasıl okunacağını [robots.txt Nedir ve Nasıl Okunur?](/tr/blog/robots-txt) yazımızda ayrıntılı anlattık.

`robots.txt` bir izin belgesi değildir, yalnızca sitenin otomatik istemcilere söylediği şeydir. Onun üstünde iki sorumluluk daha var. Birincisi hız: tek bir sayfayı saniyede birkaç kez istemek, sitenin sunucusunda gerçek kullanıcıların isteğiyle aynı sırada bekler. İstekler arasına bilinçli bir bekleme koyun ve aynı anda açık istek sayısını sınırlayın. İkincisi kimlik: gönderdiğiniz `User-Agent` değeri, sizinle iletişime geçmek isteyen bir yönetici için tek ipucudur. Kendi uygulamanızın adını ve bir iletişim adresini yazmak, popüler bir tarayıcının dizgesini kopyalamaktan hem daha dürüst hem daha pratiktir. Başlığın ne işe yaradığını [User-Agent Nedir?](/tr/blog/what-is-user-agent) yazımızda anlattık.

Üçüncü bir sınır da hukukidir. Kişisel veri içeren sayfalar, giriş arkasındaki içerik ve site şartlarının açıkça yasakladığı kullanımlar teknik bir soru değildir. Bu konudaki çerçeveyi [Web Scraping Yasal mı?](/tr/blog/web-scraping-legal) yazımızda topladık.

## HttpClient neden her istekte yeniden oluşturulmaz?

C# tarafındaki en yaygın hata burada. `HttpClient` nesnesi `IDisposable` olduğu için birçok örnek onu `using` bloğunun içinde oluşturur ve her istekte kapatır. Microsoft'un [HttpClient kullanım kılavuzu](https://learn.microsoft.com/tr-tr/dotnet/fundamentals/networking/http/httpclient-guidelines) bunun neden yanlış olduğunu açıkça yazıyor: bağlantı havuzu istemcinin altındaki handler nesnesinin içinde yaşar, istemciyi her istekte oluşturup atmak havuzu da beraberinde götürür. Kapanan TCP bağlantılarının portları hemen serbest kalmadığı için istek hızı yükseldiğinde işletim sistemindeki kullanılabilir port sınırına çarparsınız. Belirti, koddaki bir hatadan çok ağ arızasına benzer: iş bir süre sorunsuz çalışır, sonra bağlantı hataları peş peşe gelmeye başlar.

Doğrusu, uygulama boyunca yaşayan tek bir istemci kullanmaktır. Tek bir uzun ömürlü istemcinin tek dezavantajı DNS'tir: `HttpClient` alan adını yalnızca bağlantı kurulurken çözer ve DNS kaydındaki değişiklikleri izlemez. Kılavuz bunun çözümünü de veriyor, `SocketsHttpHandler` nesnesinin `PooledConnectionLifetime` özelliğine bir süre verildiğinde havuzdaki bağlantı o sürenin sonunda kapatılır ve yerine yenisi kurulurken DNS yeniden sorulur. Kılavuzun kod örneğinde bu değer on beş dakika, ama metin bunun yalnızca örnek için seçildiğini ve sürenin beklenen DNS değişikliği sıklığına göre belirlenmesi gerektiğini yazıyor; önerilen kullanım bölümünde geçen aralık ise iki dakika.

Alternatif yol `IHttpClientFactory` kullanmaktır. Fabrika, handler nesnelerini kendi havuzunda tutar ve ömrü dolmamış bir handler'ı yeni istemcilere yeniden verir; böylece soket sorunu yine ortaya çıkmaz. Bağımlılık enjeksiyonu kullanan bir ASP.NET Core uygulamasında ya da genel bir host içinde çalışıyorsanız fabrika daha uygun düşer. Tek başına çalışan bir konsol betiğinde ise `static` tek istemci artı `PooledConnectionLifetime` yeterlidir ve daha az parça içerir.

Bir nokta daha: handler'ın bağlantı ayarları ilk istek gönderildikten sonra değiştirilemez. Farklı proxy'lerle ya da farklı çerez kaplarıyla çalışmanız gerekiyorsa her yapılandırma için ayrı bir istemci nesnesi kurarsınız. "Tek istemci" kuralı "uygulamada tek nesne" demek değil, "istek başına yeni nesne yok" demektir.

## HtmlAgilityPack ve AngleSharp arasında nasıl seçim yapılır?

.NET tarafında HTML ayrıştırmanın iki yerleşik seçeneği var. İkisi de NuGet paketidir, ikisi de bozuk HTML ile baş eder, aradaki fark seçici dili ve çalışma modelidir.

| | HtmlAgilityPack | AngleSharp |
|---|---|---|
| Seçici dili | XPath (`SelectNodes`, `SelectSingleNode`) | CSS seçicileri (`QuerySelector`, `QuerySelectorAll`) |
| DOM modeli | Kendi ağaç modeli | W3C DOM API'sine yakın |
| Metni doğrudan ayrıştırma | `HtmlDocument.LoadHtml(html)` | Tarayıcı bağlamı üzerinden |
| Sayfayı kendisi indirme | Yok, HTML'i siz verirsiniz | Varsayılan yükleyiciyle var |
| Tipik kullanım | HttpClient ile indirilen HTML'i ayrıştırmak | Sayfayı açıp CSS seçicisiyle gezinmek |

HtmlAgilityPack'in kendi dokümanındaki [metinden yükleme örneği](https://html-agility-pack.net/from-string) kalıbı gösteriyor: bir `HtmlDocument` oluşturulur, `LoadHtml` ile HTML dizgesi verilir, `DocumentNode` üzerinden XPath sorgusu çalıştırılır (dokümandaki çağrı `SelectSingleNode`, birden çok düğüm için karşılığı `SelectNodes`). AngleSharp ise [kendi deposundaki örnekte](https://github.com/AngleSharp/AngleSharp) varsayılan yükleyiciyle kurulmuş bir bağlam üzerinden adresi kendisi açıyor ve `QuerySelectorAll` ile CSS seçicisi uyguluyor.

Seçim çoğu zaman alışkanlığa bakar. İsteği zaten `HttpClient` ile gönderiyorsanız, elinizde bir HTML dizgesi vardır ve HtmlAgilityPack'in `LoadHtml` yolu en kısa yoldur; bu yazıdaki örnekler de onu kullanıyor. Tarayıcı konsolunda denediğiniz CSS seçicisini aynen koda taşımak istiyorsanız AngleSharp daha rahattır. İki seçici dilinin karşılaştırmasını [CSS Seçicisi ve XPath Farkı](/tr/blog/css-selector-vs-xpath) yazımızda yaptık, burada tekrarlamıyoruz.

İki kütüphane de JavaScript çalıştırmaz. Veri yalnızca tarayıcıda çalışan bir betikle geliyorsa ayrıştırıcı değil, tarayıcı otomasyonu gerekir: [Statik ve Dinamik Sayfalar](/tr/blog/static-vs-dynamic-pages) yazımız ayrımı, [Playwright ve Selenium Farkı](/tr/blog/playwright-vs-selenium) yazımız da iki aracın karşılaştırmasını içeriyor.

## İlk örnek: tek sayfayı çekip ayrıştırmak

Aşağıdaki örnek tek bir katalog sayfasını indirir ve kitap adlarıyla fiyatları yazdırır. Kod .NET 8 ve sonrasının konsol şablonuna göre, üst düzey deyimlerle ve örtük `using` yönergeleriyle yazıldı; bu yüzden `System`, `System.Linq` ve `System.Net.Http` ayrıca yazılmıyor.

```csharp
using HtmlAgilityPack;

// Bağlantı havuzu handler'ın içinde yaşar; istemciyi bir kez kurup yeniden kullanıyoruz.
var handler = new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(2) // DNS değişikliği için bağlantı ömrü
};

using var client = new HttpClient(handler)
{
    Timeout = TimeSpan.FromSeconds(30)
};

// Kim olduğumuzu ve bize nasıl ulaşılacağını söyleyen dürüst bir kimlik.
client.DefaultRequestHeaders.UserAgent.ParseAdd("BookTracker/1.0 (+https://ornek.com/bot)");

var url = "https://books.toscrape.com/catalogue/page-1.html";

using var response = await client.GetAsync(url);
response.EnsureSuccessStatusCode(); // 2xx dışındaki kodlarda HttpRequestException fırlatır

var html = await response.Content.ReadAsStringAsync();

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

// SelectNodes hiçbir düğüm eşleşmezse null döner, bu yüzden önce kontrol ediyoruz.
var cards = document.DocumentNode.SelectNodes("//article[contains(@class,'product_pod')]");
if (cards is null)
{
    Console.WriteLine("Sayfa yapısı beklenenden farklı: ürün kartı bulunamadı.");
    return;
}

foreach (var card in cards)
{
    var title = card.SelectSingleNode(".//h3/a")?.GetAttributeValue("title", "");
    var price = card.SelectSingleNode(".//p[@class='price_color']")?.InnerText.Trim();
    Console.WriteLine($"{title} | {price}");
}
```

Üç ayrıntıya dikkat edin. `EnsureSuccessStatusCode`, durum kodu 200-299 aralığının dışındaysa `HttpRequestException` fırlatır; kodu kendiniz okumak istiyorsanız bunun yerine `IsSuccessStatusCode` özelliğine bakarsınız. `SelectNodes` eşleşme bulamazsa boş bir koleksiyon değil `null` döndürür; bu davranış, sonucu doğrudan `foreach` içine koyan kodlarda `NullReferenceException` olarak geri döner. Son olarak `SelectSingleNode` çağrılarının sonuna konan `?` işareti, tek bir karttaki eksik alanın bütün işi düşürmesini engeller.

## HttpClient'a proxy nasıl eklenir?

Proxy ayarı istemcinin üzerinde değil, altındaki handler'ın üzerindedir. `HttpClientHandler.Proxy` özelliği `IWebProxy` türünde bir nesne alır ve varsayılan değeri `null`'dur; yani hiçbir şey vermezseniz .NET işletim sisteminin ya da ortam değişkenlerinin proxy ayarını kullanır. Kullanıcı adı ve parolayla çalışan bir proxy için `WebProxy` nesnesi kurulur, kimlik bilgisi de o nesnenin `Credentials` özelliğine `NetworkCredential` olarak verilir.

```csharp
using System.Net;

var proxy = new WebProxy("http://pr.proxynet.io:8000")
{
    // Panelde verilen kullanıcı adı ve parola; kodda sabit tutmayın, ayardan okuyun.
    Credentials = new NetworkCredential("user", "pass")
};

var handler = new SocketsHttpHandler
{
    Proxy = proxy,
    UseProxy = true,
    PooledConnectionLifetime = TimeSpan.FromMinutes(2),
    MaxConnectionsPerServer = 4 // aynı sunucuya açılacak eşzamanlı bağlantı sayısı
};

using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };

using var response = await client.GetAsync("https://books.toscrape.com/");
Console.WriteLine((int)response.StatusCode);
```

`SocketsHttpHandler` sınıfı `Proxy` ve `UseProxy` özelliklerinin yanında `PooledConnectionLifetime`, `MaxConnectionsPerServer`, `ConnectTimeout` ve `AutomaticDecompression` gibi ayarları da taşır; bu yüzden proxy ile bağlantı havuzu ayarını birlikte kullanmak isteyen kod genellikle `HttpClientHandler` yerine onu seçer.

İki uyarı. Birincisi, parolada `@` ya da `:` gibi karakterler varsa bunları adres dizgesinin içine gömmeyin; `NetworkCredential` nesnesine ayrı alanlar olarak verildiğinde kodlama derdi kalmaz. İkincisi, proxy kimlik doğrulaması başarısız olduğunda hedef siteden değil proxy'den `407` yanıtı gelir, yani sorun sitenin kurallarında değil sizin yapılandırmanızdadır. İki kimlik doğrulama yöntemini ve bu hatanın teşhisini [Proxy Kimlik Doğrulama: User:Pass ve IP Whitelist](/tr/blog/proxy-authentication-methods) yazımızda anlattık.

Hangi proxy türüyle çalışacağınız işin kendisine bağlı. Kendi test ortamınıza ya da erişim kısıtı olmayan kaynaklara giden yüksek hacimli işlerde [Datacenter Proxy](https://proxynet.io/tr/datacenter-proxy) hız açısından avantajlıdır. Veri merkezi aralıklarına kapalı hedeflerde ise çıkış noktası bir internet servis sağlayıcısına ait olan [Residential Proxy](https://proxynet.io/tr/residential-proxy) tercih edilir; iki grubun ASN düzeyindeki farkını [ISP ve Residential Proxy Farkı](/tr/blog/isp-vs-residential-proxy) yazımızda açıkladık.

## SOCKS5 proxy C# tarafında nasıl verilir?

HTTP proxy'nin yanında SOCKS de kullanılabilir. Microsoft'un [HttpClient.DefaultProxy](https://learn.microsoft.com/tr-tr/dotnet/api/system.net.http.httpclient.defaultproxy) dokümanı desteklenen şemaları tek tek sayıyor: `http`, `https`, `socks4`, `socks4a` ve `socks5`. Aynı sayfa adres biçimlerini de veriyor; `socks5` için kalıp `socks5://[kullanıcı:parola@]sunucu[:port]` şeklinde. `socks4` ve `socks4a` satırlarında ise parolanın yok sayıldığı belirtiliyor, çünkü bu protokoller parola ile kimlik doğrulamayı desteklemiyor.

`DefaultProxy`, handler'da açıkça bir proxy verilmemiş bütün istemciler için geçerli olan statik özelliktir. Varsayılan örneği önce ortam değişkenlerinden kurulur; değişkenler tanımlı değilse Windows ve macOS'ta işletim sisteminin proxy ayarına bakılır, Linux'ta ise proxy kullanmayan bir örnek döner. Dokümandaki değişkenler `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` ve dışarıda bırakılacak alan adlarını tutan `NO_PROXY`. Yani kodu hiç değiştirmeden şu şekilde çalıştırmak mümkündür:

```powershell
# Uygulamayı başlatmadan önce (Windows PowerShell)
$env:ALL_PROXY = "socks5://user:pass@pr.proxynet.io:1080"
dotnet run
```

Ortam değişkenleriyle proxy vermenin ayrıntılarını ve diğer araçlardaki karşılıklarını [wget ile Proxy Kullanımı](/tr/blog/wget-proxy) yazımızda topladık. Protokolün kendisi, HTTP proxy'den farkları ve hangi işte hangisinin uygun olduğu [SOCKS5 Proxy](https://proxynet.io/tr/socks5-proxy) sayfamızda anlatılıyor.

## Zaman aşımı, iptal ve yeniden deneme nasıl kurulur?

`HttpClient.Timeout` özelliğinin varsayılan değeri 100 saniyedir ve o istemciden çıkan bütün isteklere uygulanır. Bir scraping işi için bu süre fazla uzundur: yanıt vermeyen tek bir adres, işin akışını bir buçuk dakika boyunca tutar. Otuz saniye gibi bir değer çoğu sayfa için yeterlidir.

Tek tek istekler için farklı bir süre gerekiyorsa `CancellationTokenSource` kullanılır. Microsoft'un dokümanı iki mekanizmanın birlikte çalıştığını, ikisinden **kısa olanın** geçerli olacağını söylüyor. Aynı jeton işin tamamı için bir üst sınır koymaya da yarar: kullanıcı işi durdurduğunda ya da toplam süre aşıldığında bekleyen bütün istekler tek noktadan iptal edilir.

Yeniden deneme mantığını sıfırdan yazmak zorunda değilsiniz. Microsoft'un `Microsoft.Extensions.Http.Resilience` NuGet paketi, `HttpClient` için hazır bir dayanıklılık işleyicisi sunuyor; `IHttpClientBuilder` üzerinde `AddStandardResilienceHandler` çağrıldığında hız sınırlayıcı, toplam zaman aşımı, yeniden deneme, devre kesici ve deneme başına zaman aşımı katmanları sırayla devreye giriyor. Varsayılan yeniden deneme stratejisi en fazla üç yeniden deneme yapıyor, üstel geri çekilme ve rastgele bileşen kullanıyor; ele aldığı yanıtlar arasında `408`, `429` ve `500` ve üzeri kodlar var. Paketin bağımlılık enjeksiyonu kullanmayan tek istemcilerle nasıl kurulacağı da kullanım kılavuzunda bir örnekle gösteriliyor.

Hangi durum kodunda yeniden denemenin anlamlı olduğu ayrı bir konudur ve kararı kütüphane değil siz verirsiniz: `429` ve `503` beklemeyle düzelir, `403` ve `407` düzelmez. `Retry-After` başlığının okunması, üstel geri çekilmenin doğru kurulması ve kodlara göre karar veren bir döngünün tam örneği [Scraping'de HTTP Hata Kodları](/tr/blog/http-status-codes-web-scraping) yazımızda; burada aynı kodu tekrarlamıyoruz.

## Tam örnek: sayfaları sırayla tarayan iş

Aşağıdaki örnek ilk on katalog sayfasını proxy üzerinden tarar, eşzamanlı istek sayısını ikiyle sınırlar, her istekten önce bekler ve sonucu JSON dosyasına yazar.

```csharp
using System.Collections.Concurrent;
using System.Net;
using System.Text.Json;
using HtmlAgilityPack;

var proxy = new WebProxy("http://pr.proxynet.io:8000")
{
    Credentials = new NetworkCredential("user", "pass")
};

var handler = new SocketsHttpHandler
{
    Proxy = proxy,
    UseProxy = true,
    PooledConnectionLifetime = TimeSpan.FromMinutes(2),
    MaxConnectionsPerServer = 4
};

using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(30) };
client.DefaultRequestHeaders.UserAgent.ParseAdd("BookTracker/1.0 (+https://ornek.com/bot)");

// İşin tamamı için üst sınır: süre dolarsa bekleyen bütün istekler iptal olur.
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(5));
var token = cts.Token;

var gate = new SemaphoreSlim(2); // aynı anda en fazla iki istek
var results = new ConcurrentBag<Book>();

var tasks = Enumerable.Range(1, 10).Select(async page =>
{
    await gate.WaitAsync(token);
    try
    {
        // İstekler arasına bilinçli bir bekleme koyuyoruz.
        await Task.Delay(TimeSpan.FromMilliseconds(500), token);

        var url = $"https://books.toscrape.com/catalogue/page-{page}.html";
        using var response = await client.GetAsync(url, token);

        if (!response.IsSuccessStatusCode)
        {
            Console.WriteLine($"{url}: HTTP {(int)response.StatusCode}");
            return;
        }

        var html = await response.Content.ReadAsStringAsync(token);
        foreach (var book in ParseBooks(html, page))
        {
            results.Add(book);
        }
    }
    finally
    {
        gate.Release();
    }
}).ToList();

await Task.WhenAll(tasks);

var options = new JsonSerializerOptions { WriteIndented = true };
var json = JsonSerializer.Serialize(results.OrderBy(book => book.Page), options);
await File.WriteAllTextAsync("books.json", json, token);

Console.WriteLine($"{results.Count} kayıt yazıldı.");

// Ayrıştırma ayrı bir fonksiyonda: sayfa yapısı değişince yalnız burası değişir.
static IEnumerable<Book> ParseBooks(string html, int page)
{
    var document = new HtmlDocument();
    document.LoadHtml(html);

    var cards = document.DocumentNode.SelectNodes("//article[contains(@class,'product_pod')]");
    if (cards is null)
    {
        yield break;
    }

    foreach (var card in cards)
    {
        var title = card.SelectSingleNode(".//h3/a")?.GetAttributeValue("title", "") ?? "";
        var price = card.SelectSingleNode(".//p[@class='price_color']")?.InnerText.Trim() ?? "";
        yield return new Book(title, price, page);
    }
}

record Book(string Title, string Price, int Page);
```

`SemaphoreSlim`, aynı anda kaç isteğin uçtuğunu belirler; `Task.WhenAll` listedeki bütün görevleri başlatsa da semafor ikiden fazlasının aynı anda ilerlemesine izin vermez. Bunu handler'daki `MaxConnectionsPerServer` ayarıyla karıştırmayın: biri açılacak TCP bağlantısı sayısını, diğeri kodunuzun ürettiği eşzamanlı iş sayısını sınırlar. İkisinin arasındaki kavramsal farkı [Eşzamanlılık ve Paralellik](/tr/blog/concurrency-vs-parallelism) yazımızda ele aldık.

Örnek sabit bir sayfa aralığı tarıyor. Gerçek bir işte sayfa sayısı önceden bilinmez, "sonraki" bağlantısı izlenir ve taranan adresler bir kuyrukta tutulur. Bu kurguyu [Scraping'de Sayfalama Nasıl Taranır?](/tr/blog/pagination-web-scraping) yazımızda kuruyoruz.

## Kullanım alanları

- **Fiyat ve stok takibi.** Kendi ürünlerinizin ve açık kataloglardaki emsallerin düzenli taranması. Genel kurgu [fiyat takibi çözümümüzde](/tr/price-monitoring) anlatılıyor.
- **Kurumsal veri toplama işleri.** C# ile çalışan banka, sigorta ve ERP ekipleri topladıkları veriyi zaten .NET tarafındaki bir veritabanına yazar; iş için ayrı bir dil eklemek yerine aynı çözümde kalmak bakım maliyetini düşürür. Kapsamlı işler için [veri kazıma çözümümüz](/tr/data-scraping) başlangıç noktasıdır.
- **Çok sayıda kaynağın düzenli taranması.** Yük tek bir çıkış noktasına yığılmasın diye [Rotating Proxy](https://proxynet.io/tr/rotating-proxy) kullanılır.
- **Oturum gerektiren kendi paneliniz.** Giriş yapılmış bir oturumun ortasında IP değişirse oturum düşer; bu tür işlerde [Sticky Proxy](https://proxynet.io/tr/sticky-proxy) tercih edilir.
- **Bölgeye göre değişen içerik kontrolü.** Aynı sayfanın farklı ülkelerden nasıl göründüğünü karşılaştırmak, çıkış konumunu değiştirmekle mümkündür.

## Sık yapılan hatalar

- **Her istekte yeni `HttpClient` oluşturmak.** İşin ilk yarısı sorunsuz geçer, sonra bağlantı hataları başlar. Belirti bir ağ arızasına benzediği için teşhis genelde yanlış yerde aranır.
- **Tek istemciyi kurup `PooledConnectionLifetime` vermemek.** Uzun çalışan bir serviste hedefin ya da proxy'nin DNS kaydı değiştiğinde istemci eski adrese gitmeye devam eder.
- **`SelectNodes` sonucunu kontrol etmemek.** Eşleşme yoksa dönen değer `null`'dur; `foreach` bu değeri sessizce atlamaz, istisna fırlatır.
- **Yanıt gövdesine bakmadan `200` kodunu başarı saymak.** Doğrulama sayfaları da `200` ile gelir. Beklediğiniz bir öğenin sayfada bulunduğunu doğrulayın.
- **Bekleme koymadan bütün adresleri aynı anda başlatmak.** `Task.WhenAll` listedeki yüzlerce görevi itirazsız başlatır; sınırı koyması gereken sizsiniz.
- **Kimlik bilgisini kodun içine yazmak.** Proxy kullanıcı adı ve parolası ayar dosyasından ya da ortam değişkeninden okunur, depoya girmez.
- **Parolayı adres dizgesinin içine gömüp kodlamayı unutmak.** `@` içeren bir parola adresin geri kalanını bozar; `NetworkCredential` bu sorunu ortadan kaldırır.

## Karar rehberi

| İhtiyaç | Öneri |
|---|---|
| Tek başına çalışan konsol işi | `static` tek `HttpClient` artı `PooledConnectionLifetime` |
| Bağımlılık enjeksiyonu olan uygulama | `IHttpClientFactory`, tercihen türlenmiş istemci yaklaşımı |
| XPath ile ayrıştırma | HtmlAgilityPack, `LoadHtml` artı `SelectNodes` |
| Tarayıcıda denenmiş CSS seçicisi | AngleSharp, `QuerySelectorAll` |
| JavaScript ile gelen içerik | Ayrıştırıcı değil, tarayıcı otomasyonu |
| Kullanıcı adı ve parolalı proxy | `WebProxy` artı `NetworkCredential` |
| SOCKS5 çıkışı | `socks5://` adresi, ortam değişkeni ya da `DefaultProxy` |
| Yeniden deneme ve devre kesici | `Microsoft.Extensions.Http.Resilience` |
| Eşzamanlılık sınırı | `SemaphoreSlim` artı `MaxConnectionsPerServer` |

## Sıkça sorulan sorular

### C# ile veri çekmek için hangi paketleri kurmam gerekir?

HTTP tarafı için ek pakete gerek yok, `System.Net.Http` çalışma zamanının parçasıdır. HTML ayrıştırma için bir NuGet paketi eklersiniz: XPath ile çalışacaksanız HtmlAgilityPack, CSS seçicileriyle çalışacaksanız AngleSharp. Yeniden deneme ve devre kesici isterseniz `Microsoft.Extensions.Http.Resilience` paketi eklenir. Veriyi JSON olarak yazmak için `System.Text.Json` zaten gelir.

### HttpClient nesnesini neden `using` bloğu içinde kullanmamalıyım?

Çünkü bağlantı havuzu istemcinin altındaki handler'ın içindedir. Her istekte yeni bir istemci oluşturup kapatmak havuzu da kapatır, kapanan TCP bağlantılarının portları hemen serbest kalmadığı için yoğun bir işte kullanılabilir port sınırına çarparsınız. Doğrusu, uygulama boyunca yaşayan tek bir istemci kullanmak ya da `IHttpClientFactory` ile yönetmektir.

### Proxy tanımladım ama 407 hatası alıyorum, nerede arayayım?

`407`, hedef siteden değil proxy sunucusundan gelir ve kimliğinizin doğrulanmadığını söyler. Önce `WebProxy` nesnesinin `Credentials` özelliğinin dolu olduğunu, sonra kullanıcı adı ile parolanın doğru yazıldığını kontrol edin. IP yetkilendirme yöntemiyle çalışıyorsanız isteğin çıktığı genel IP adresinin listede olması gerekir. Yeniden denemek bu hatayı çözmez.

### C# tarafında SOCKS5 proxy kullanılabilir mi?

Evet. Microsoft'un `HttpClient.DefaultProxy` dokümanı `http`, `https`, `socks4`, `socks4a` ve `socks5` şemalarını desteklenen adres biçimleri olarak listeliyor. `socks5` için kalıp `socks5://kullanıcı:parola@sunucu:port` şeklinde. `socks4` ve `socks4a` kullanırken verilen parolanın yok sayılacağı da aynı sayfada yazıyor, çünkü o protokollerde parola ile kimlik doğrulama yok.

### Sayfa yapısı değiştiğinde kodum kırılıyor, ne yapmalıyım?

Kırılmayı engelleyemezsiniz, erken fark edebilirsiniz. Ayrıştırmayı ayrı bir fonksiyonda tutun, `SelectNodes` sonucunun `null` gelmesini bir hata olarak günlüğe yazın ve her çalıştırmada bulunan kayıt sayısını kaydedin. Beklediğinizin çok altında kayıt çıkması, yapının değiştiğinin ilk işaretidir. Seçicileri mümkün olduğunca kısa ve anlamlı tutmak da kırılganlığı azaltır.

### Selenium ya da Playwright'a ne zaman geçmeliyim?

İndirdiğiniz HTML'de aradığınız veri hiç yoksa. Tarayıcının geliştirici araçlarında sayfayı açıp kaynağı görüntüleyin: veri kaynakta varsa `HttpClient` yeterlidir ve tarayıcı otomasyonu yalnızca yavaşlık getirir. Veri sonradan bir istekle geliyorsa, çoğu zaman o isteğin gittiği adresi doğrudan çağırmak en temiz çözümdür. İkisi de olmuyorsa [Selenium ile veri çekme](/tr/blog/selenium) yazımızdaki yola geçilir.

## Özetle

C# ile veri çekmenin zor tarafı ayrıştırma değil, istemcinin kurulumu. Uygulama boyunca yaşayan tek bir `HttpClient` örneği, `PooledConnectionLifetime` ile sınırlanmış bağlantı ömrü ve otuz saniye civarında bir zaman aşımı işi büyürken ayakta tutar. Proxy'yi handler'ın `Proxy` özelliğine `WebProxy` olarak verir, kimlik bilgisini `NetworkCredential` ile ayrı alanlarda tutarsınız. Hız sınırını `SemaphoreSlim` ile kendi kodunuzda kurar, yeniden denemeyi hazır bir dayanıklılık işleyicisine bırakırsınız. Ayrıştırmayı ayrı bir fonksiyonda tutmak da sayfa yapısı değiştiğinde tek bir yeri düzeltmenizi sağlar. Veri toplama işleriniz için uygun proxy türlerini [proxy hizmetlerimizde](/tr/proxy) bulabilirsiniz.
