Bir HTML ayrıştırıcısı piyasaya birkaç ayda bir daha hızlı bir alternatif çıktığında, benchmark sonuçları ortada dolaşır ve biri eski neslin artık miadını doldurduğunu ilan eder. Sonra belirli bir kelimeyi içeren tüm paragrafları seçmeniz ya da eşleşen bir düğümün üst öğesini almanız gerekir; işte tam o anda lxml neden hâlâ diğer sekmede açık duruyor, bunu hatırlarsınız.
lxml, 20 yıllık bir libxml2 bağlayıcısıdır. Heyecan verici değildir. Yeni değildir. Ama tek bir işte — gerçek XPath gerektiren her şeyde — ana akım Python ekosisteminde ona rakip olan bir şey yoktur. Bu yazı, lxml’nin pratikte neler sunduğunu, nerede sessizce öne çıktığını ve hangi varsayılanlarının farkında olmazsanız başınızı ağrıtabileceğini uygulamalı biçimde ele alan bir inceleme.
lxml Tek Paragrafta: Gerçekte Nedir?
lxml, libxml2 ve libxslt C kütüphaneleri için bir Python bağlayıcısıdır. Bir parser ve serializer’dır; bir scraper ya da tarayıcı değildir — işaretlemeyi sorgulanabilir ve düzenlenebilir bir ağaç yapısına dönüştürür, sonra bu ağacı yeniden baytlara çevirir. ElementTree ile uyumlu bir API, tam bir XPath 1.0 motoru, XSLT 1.0 ve şema doğrulaması sunar. Stefan Behnel tarafından, "Python dilinde XML ve HTML işlemek için en zengin özellikli ve kullanımı en kolay kütüphane" sloganıyla sürdürülmektedir.
14.07.2026 tarihinde alınmış bir GitHub ve PyPI anlık görüntüsüne göre durum şu şekildedir:
| Alan | Değer |
|---|---|
| Repo | lxml/lxml |
| Yıldız | 3,043 |
| Fork | 620 |
| Açık issue | 16 |
| Lisans | BSD-3-Clause |
| Oluşturulma | 2011-02-11 |
| Son push | 2026-07-02 |
| PyPI kararlı sürüm | 6.1.1 (2026-05-18) |
| Paketlenen motor | libxml2 2.14.6 + libxslt 1.1.43 |
Abarttığımı düşünen olmasın diye en baştan söyleyeyim: bu incelemede gizli bir numara yok. lxml o kadar eski ki burada gördüğünüz her davranışın açıklaması bir yerlerde zaten mevcut; lxml dokümanlarında, bir libxml2 değişiklik kaydında ya da bir Launchpad tartışma dizisinde bulunabilir. Ben burada belgelenmemiş, özel bir hile keşfetmedim ve uydurmayacağım da. Bu yazının değeri yeni olmasında değil; sistematik, ölçülmüş ve lxml odağında düzenlenmiş olmasında.
Test Kurulumu (ve Neden Zamanlama Sayıları Ödünç)
Bu incelemeye iki farklı veri türü giriyor ve bunlar iki ayrı kaynaktan geldiği için hangisinin hangisi olduğunu açıkça belirteceğim.
Yetenek testleri — XPath davranışı, iki parser API’si, namespace’ler, kodlama, düğüm yaşam döngüsü — tek bir makinede yeniden çalıştırıldı: macOS arm64, Python 3.14.2, lxml 6.1.1, libxml2 2.14.6. artifacts/raw/*.json dosyalarındaki her sayı elle girilmedi; bir script çalıştırılarak üretildi. Yetenek testleri deterministik boolean ve enum sonuçlar verdiği için tek bir koşu stabil olur; makine yükü //a/@href ifadesinin bir attribute dizisi döndürüp döndürmemesini değiştirmez.
Zamanlama ve bellek ayak izi sayıları bu paketten değil. Bunlar, önceki selectolax benchmark paketinden olduğu gibi yeniden kullanıldı — aynı makine, aynı sanal ortam, aynı lxml ve libxml2 derlemesi, 2026-07-13 itibarıyla ölçümler — ve burada tekrar çalıştırılmadı. Bu bilinçli bir tercihti. Zamanlama benchmark’larını yetenek script’leriyle aynı anda yeniden çalıştırmak CPU çakışması yaratır; bu da ödünç alınan sayıları kirletir. Ayrıca bu ikinci kez yapılan bir iştir: lxml zaten o pakette tam ölçülmüş bir kontrol kütüphanesiydi. Aynı bench’i yeniden kullanmak, ikinci ve hafifçe farklı bir ölçüm sokmak yerine her şeyi birebir karşılaştırılabilir tutar. Dolayısıyla aşağıda bir milisaniye değeri görürseniz bunu “aynı test düzeneği, 2026-07-13 itibarıyla” şeklinde okuyun; “bunu bugün yeniden zamanladım” diye değil.
Bulguların yanında bir güven etiketi yer alıyor: deterministik yetenek testleri için single-observation, yeniden kullanılan zamanlama dağılımları için triple-run, doğrudan izole etmediğim bir mekanizmayı önerdiğim yerler için hypothesis.
XPath: selectolax ve BeautifulSoup’un Sahip Olmadığı Tek Şey
Manşet bu olduğu için doğrudan buradan başlayayım.

lxml’nin xpath() metodunu, önceden kayıt altına alınmış 37 maddelik bir matriste test ettim — her durum için beklenen sonuç kaynak koda testten önce yazılmıştı, yani sonucu sonradan yumuşatma şansım yoktu. On eksen, dokuz predicate stili, on yerleşik fonksiyon, üç skaler dönüş tipi ve lxml’nin 1.0 motorunun reddetmesi gereken XPath 2.0 sözdizimi kullanan beş kasıtlı tuzak vaka.
| Kategori | Kapsam | Sonuç |
|---|---|---|
| Eksenler | child / descendant / parent / ancestor / following-sibling / preceding-sibling / self / attribute / following / preceding | 10/10 geçti |
| Predicate’ler | [1] / last() / position()<n / attribute eşitliği / attribute varlığı / and / or / iç içe [.//a] / not() | 9/9 geçti |
| Fonksiyonlar | text() / contains() / starts-with() / count() / string-length() / normalize-space() / concat() / substring() / name() / string() | 10/10 geçti |
| Dönüş tipleri | boolean / number skalerleri | 3/3 geçti |
| Tuzak vakalar | matches() / sequences / if-then-else / except / sözdizimi hatası | 5/5 doğru biçimde reddedildi |
Sonuç 37/37 ve önemli olan da tuzak sütunu. matches(), sequence ifadeleri, if/then/else ve except XPath 2.0 sözdizimidir; libxml2’nin 1.0 motoru bunları yarım yamalak desteklemez — sessizce yanlış bir düğüm kümesi döndürmek yerine XPathEvalError fırlatır ve işlemi reddeder. Yani burada tam puan, önce kırmayı deneyerek alınmış tam puandır; kolay sorulardan toplanmış bir başarı değil. Buradaki her davranış, lxml XPath dokümanlarının anlattığıyla birebir uyumludur; asıl mesele de budur.
Harness’in yanlış yakaladığı bir şeyi de itiraf edeyim; çünkü güvenebileceğiniz türden bir “37/37” ancak böyle olur. //div[.//a[@href]] için ilk beklenen setimde iki sonuç varsaydım; çalıştırma bir sonuç döndürdü. Yaklaşık otuz saniye boyunca lxml’nin hatalı olduğunu sandım, sonra fixture’a bakıp ikinci öğenin <div> değil <footer> olduğunu gördüm — hata motorun değil, benim beklentimindi. Beklenen seti düzelttim ve hatayı kaynak kod yorumunda bıraktım. Suçlama sırası doğrudur: önce testinizi sorgulayın, sonra 20 yıllık C kütüphanesini.
XPath vs CSS: CSS’te İfade Edemeyeceğiniz Şeyler
“XPath daha güçlüdür” cümlesi soyut kalmasın diye farkı sayısallaştırdım. lxml size hem .xpath() hem de .cssselect() verir; ikincisi CSS’i içeride XPath’e çevirir. On seçim hedefi aldım ve CSS’in hangilerini gerçekten ifade edebildiğine baktım.

| Hedef | XPath | CSS (cssselect) |
|---|---|---|
Metin içeriğine göre filtreleme (contains(text(),"bargain")) | Evet | Metin predicate’i yok |
Çocuktan ebeveyn seçme (//b/parent::p) | Evet | Parent selector yok |
Bir attribute değerini döndürme (//a/@href) | Evet | Sadece elementler |
Bir text node döndürme (//p/text()) | Evet | Text node yok |
Ancestor ekseni (//td/ancestor::div) | Evet | Yukarı yönlü gezinme yok |
Çocuk sayısına göre ebeveyn filtreleme (//ul[count(li)=4]) | Evet | Count predicate’i yok |
Metin uzunluğuna göre filtreleme (string-length(text())>5) | Evet | Uzunluk predicate’i yok |
nth-child / last-child / bitişik kardeş | Evet | Evet (3 temel kullanım) |
On hedefin yedisine CSS’te hiç karşılık yok. Metin içeriği filtreleme, ebeveynlere ve atalara doğru yukarı çıkma, bir attribute değerini ya da çıplak bir text node’u sonuç olarak alma, sayım temelli predicate’ler — CSS bunların hiçbirini ifade edemez. Yalnızca üçü (nth-child, last-child, bitişik kardeş) iki tarafta da çalışıyor. lxml’ye yönelince ne kazandığınızın sayısal cevabı budur. selectolax yalnızca CSS ile çalışır ve hiç xpath() metodu yoktur; dolayısıyla o yedi sorgu türü orada ya çok adımlı Python döngülerine dönüşür ya da hiç yapılamaz. Scraping mantığınız bunlardan herhangi birine dayanıyorsa, kararınız zaten verilmiştir.
(Ve evet, harness burada da beni ikinci kez yakaladı: string-length(text())>5 için boş küme bekledim, ama altı karakterli iki dize eşleşti. Beklentiyi düzelttim, aracı değil.)
Üç Kademeli Sıkılık: etree vs recover vs lxml.html
XPath, lxml’yi seçmenin sebebi. Üç kademeli sıkılık kontrolü ise onu kullanmaya devam etmenin sebebi.

Çoğu parser bozuk girdiye karşı tek bir davranış sunar. lxml üç davranış sunar ve bunlar yeterince öngörülebilir olduğu için altı farklı bozuk işaretleme sınıfını üçünden de geçirip her yol için sonucu önceden kayıt altına aldım.
| Bozuk girdi | lxml.etree (katı) | etree + recover=True | lxml.html (esnek) |
|---|---|---|---|
Kapanmamış etiket <root><a>x</root> | hata verir | düzeltir | kabul eder |
Yanlış iç içe geçmiş <b><i></b></i> | hata verir | düzeltir | kabul eder |
Tanımsız entity | hata verir | düzeltir | kabul eder |
Çıplak & (Tom & Jerry) | hata verir | düzeltir | kabul eder |
Birden fazla kök <a>1</a><b>2</b> | hata verir | düzeltir | kabul eder |
| İyi biçimlenmiş XML | kabul eder | kabul eder (0 hata) | kabul eder |
Boolean attribute <input disabled> | hata verir | düzeltir | kabul eder |
Yedide yedi, önceden kayıt altına alınan beklentiyle eşleşti. lxml.etree, bu altı bozuk sınıfın hepsinde XMLSyntaxError fırlatıyor. Aynı parser’a recover=True eklerseniz hataları yutarak kullanılabilir bir ağaç yeniden kuruyor — ve burada az bilinen ama çok değerli olan kısım şu: parser.error_log, yutulan her hatayı tek tek listeliyor. lxml.html ise hiçbir şey olmamış gibi her şeyi kabul ediyor.
“hata verildi / düzeltildi / kabul edildi” ayrımını belirleyen sınıflandırıcı, sabit kodla değil, çalışma anındaki error_log uzunluğuyla çalışıyor; bu yüzden recover=True altında iyi biçimlenmiş bir belge doğru biçimde “kabul eder” olarak etiketleniyor (“düzeltir” değil, çünkü log boş). İlk sürümde recover=True sonucunu otomatik olarak “düzeltir” diye etiketlemiştim ve temiz girdiyi yanlış sınıflandırmıştım; gerçek error_log’u okumak bunu düzeltti.
Pratikte bunun anlamı şu: bozuk bir feed’in yüksek sesle başarısız olması gerekiyorsa lxml.etree kullanın. Kirli, gerçek dünya HTML’ini sadece geçip gitmeniz gerekiyorsa lxml.html kullanın. Ve çoğu aracın yapamadığı ara durum için — “esnek davran ama tam olarak neyin bozuk olduğunu söyle ki loglayabileyim” — recover=True kullanın ve hata log’unu okuyun. selectolax’ın esnek modu var, başka da bir şeyi yok; ne katı modu ne de hata log’u.
iterparse: selectolax’ın Hiç Sahip Olmadığı Akış Modu
Bu bir hız düğmesi değil, yetenek satırı. selectolax yalnızca tüm string’i bir kerede içeri alır — artımlı bir arayüzü yoktur. lxml’nin iterparse fonksiyonu ise elementler kapanırken onları üretir; klasik fast_iter deseniyle birlikte kullanıldığında (elem.clear() çağırıp ilerledikçe önceki kardeşleri silmek), doküman ne kadar büyürse büyüsün bellek kullanımını sabit tutar.

Bellek davranışını doğrudan ölçtüm — ru_maxrss üzerinden peak RSS, her örnek ayrı yeni bir süreçte, toplamda yaklaşık 26.7 MB (26,744,801 bayt) tutan 300,000 <record> öğesi üzerinde.
| Mod | Peak RSS farkı | Not |
|---|---|---|
iterparse + clear (fast_iter) | ~1-2 MB | ilerledikçe serbest bırakır; sayıdan bağımsız düz kalır |
iterparse clear olmadan | ~386 MB | referansları tutar; tam yük kadar ağır |
etree.parse (tam yük, referans) | ~386 MB | yüksek maliyetli olduğunu doğrular; ölçümün ölçeğini gösterir |
Sınırlı mod, peak RSS farkını tam yükün ~386 MB’ına karşı yaklaşık 1-2 MB civarında tutuyor — büyüklük olarak %0.3-0.4 fark — ve ilk record olayı dosya daha tam okunmadan tetikleniyor; yani bu gerçekten artımlı bir akış, sahte streaming değil. Öğretici olan orta satır. Aynı iterparse döngüsünü çalıştırıp clear() çağrısını atlayın, bellek yeniden ~386 MB’a çıkar; çünkü her şeyi referans olarak tutuyorsunuz. Kazanç iterparse’ın kendisinde değil, clear() kullanımında yatıyor. Sınırlı mod ile tam yük arasındaki belirgin fark, RSS ölçerin gerçekten büyüklük farkını görebildiğini de doğruluyor. (Bu bellek testi bu pakette benim yeniden çalıştırdığım testlerden biri; ödünç alınan zamanlama sayılarından ayrı bir ayak izi ölçümü.)
Gerçek dünyadaki karşılığı şu: RAM’e sığmayan çok gigabaytlık bir XML dışa aktarma için selectolax tarafında hiçbir akış yolu yoktur. Ya lxml’nin streaming parser’ını kullanırsınız ya da başka bir dile geçersiniz.
Namespace’ler: RSS, SVG ve Varsayılan Namespace Tuzakları
On iki namespace vakası; üç namespace üzerinden RSS, varsayılan namespace ile xlink kullanan SVG ve default-namespace XML dahil. On ikisi de geçti.
lxml, bir RSS beslemesinden //dc:creator/text() ile tam olarak ["Alice", "Bob"] değerini çıkarır; aynı belgede üç ayrı namespace üzerinden //atom:link/@href ve //content:encoded alanlarını çözer; SVG’nin ikinci namespace’inde //s:rect ve //s:use/@xlink:href ifadelerini işler; Clark gösterimi {uri}local adlarını QName ile ayırır ve nsmap üzerinden iç gözlem yapar. Bu, belgelenmiş ve sürdürülmekte olan davranıştır; selectolax’ın hiç dokunmadığı bambaşka bir boyuttur, çünkü selectolax HTML5 odaklıdır ve keyfi XML namespace’lerini işlemez.
Aklınızda tutmaya değer bir belgeli tuzak var. XPath’te varsayılan namespace kavramı yoktur. xmlns="urn:..." tanımlı bir belgeye //book uyguladığınızda sıfır sonuç alırsınız — boş prefix XPath için tanımsızdır; lxml dokümanlarının da anlattığı budur. Yapay bir prefix bağlamanız gerekir (namespaces={"c": "urn:..."} ile //c:book; bu üç sonuç buldu) ya da //*[local-name()='book'] kullanmanız gerekir (bu da üç sonuç verir). Hata değil — XPath standardı böyle, lxml de onu sadakatle uygular. Sadece herkesin bir kez şaşırdığı bir nokta.
Gerçek Kirli Sayfalar: 11 Canlı Scrape Üzerinde Doğruluk
Sentetik testler temizdir; web öyle değildir. selectolax paketindeki fixture setinden alınan on bir gerçek yakalanmış sayfayı (2026-07-10 itibarıyla, salt okunur) yeniden kullandım ve lxml odağında lxml.html üzerinden işlettim.
| Fixture | Boyut | Link sayısı | libxml2 tarafından düzeltilen hata | Strict XML |
|---|---|---|---|---|
| news_bbc.html | 398 KB | 255 | 4 | ok |
| docs_mdn_array.html | 243 KB | 508 | 0 | hata verdi |
| wiki_scraping.html | 227 KB | 460 | 0 | hata verdi |
| gov_whitehouse.html | 289 KB | 154 | 0 | hata verdi |
| oldstyle_craigslist.html | 561 KB | 351 | 0 | hata verdi |
| forum_reddit.html | 129 KB | 318 | 0 | hata verdi |
| docs_python.html | 80 KB | 341 | 2 | hata verdi |
| ecommerce_books.html | 51 KB | 94 | 0 | hata verdi |
| news_hackernews.html | 35 KB | 229 | 0 | hata verdi |
| ecommerce_webscraper_allinone.html | 16 KB | 35 | 0 | hata verdi |
| spa_quotes_js.html | 6 KB | 5 | 0 | hata verdi |
On birinin de lxml.html ile ayrıştırılması başarılı oldu ve bağlantı, başlık ve görsel sayıları selectolax paketindeki yeniden kullanılan lxml sayılarıyla on birinin tamamında eşleşti — çapraz kontrol true. Bu uyum bana, yeniden kullanımın gerçekten elma ile elmayı karşılaştırdığını, aynı etiketi taşıyan iki farklı ölçüm olmadığını gösteriyor.
Yan bulgu şu: katı XML parser on bir sayfanın onunda hata verdi. Gerçek web sayfalarının ezici çoğunluğu iyi biçimlenmiş XML değildir; işte libxml2’nin HTML kurtarma modunun onları yutmak için var olmasının sebebi de budur. Tek istisna, Next.js ile render edilmiş ve katı XML ayrıştırmadan geçecek kadar iyi biçimlenmiş BBC News’ti. “HTML” etiketli her şeyin kurtarma moduna ihtiyacı yok.
Kolayca gözden kaçan bir sayım detayı var. docs_python.html üzerinde //a[@href] (attribute varlığı) 343 sayarken, selectolax paketindeki if n.get("href") (truthy değer) 341 saydı. Fazladan iki sonuç, boş href="" bağlantılarıdır. Bu parser farkı değil, sayım kuralı farkıdır — attribute mevcut mu, yoksa boş olmayan bir değer mi taşıyor? Predicate’inizi hizaladığınızda sayılar da oturur. Scrape ederken bunu bilmek önemlidir: boş href’lerin sayılıp sayılmayacağı parser’ın değil, filtrenizin kararıdır.
Hata Gibi Görünen Ama Hata Olmayan Derinlik Sınırı
selectolax paketinde, lxml’nin 1,000 ve 5,000 seviyelik iç içe <div> işaretlemesinde en derindeki içeriği düşürdüğü kaydedilmişti ve bu, "lxml sessizce en derin içeriği kaybediyor" şeklinde çerçevelenmişti. Mekanizmayı anlamak istedim ve default parser’ı huge_tree=True ile karşılaştırdım.

| İstenen derinlik | Default parser’ın ulaştığı | huge_tree=True ile ulaşılan |
|---|---|---|
| 300 | 253 (geri kalan düşer) | 299 (geri kazanılır) |
| 1000 | 253 (geri kalan düşer) | 999 (geri kazanılır) |
| 5000 | 253 (geri kalan düşer) | 2045 (hâlâ düşer) |
Default parser yaklaşık 253 seviyede kesiyor ve daha derini sessizce düşürüyor. Bu bir bug değil — bu, libxml2’nin DoS koruması; kötü niyetli bir belgenin stack’i taşırmasını engelleyen yaklaşık 256 seviyelik bir iç içe geçme limiti ve lxml Launchpad tartışmasında XML_PARSE_HUGE kapsamında belgelenmiş durumda. huge_tree=True ayarlayın; 300 ve 1,000 derinlikleri eksiksiz geri gelir. Ancak 5,000 derinlikte, huge_tree açık olsa bile yalnızca 2,045 seviyeye kadar ulaşılır — burada yapılandırılabilir limitin üzerinde ikinci, daha sert bir libxml2 recursion tavanı var ve huge_tree onu kaldıramıyor.
Yapılması gereken net: güvenilir bir kaynaktan gelen derin işaretlemeyi ayrıştırırken lxml.html.HTMLParser(huge_tree=True) kullanın. Bu paketin ödünç alınan gözleme eklediği şey mekanizma (veri bozulması değil, bir güvenlik limiti), çözüm (huge_tree) ve bu çözümün ulaşamadığı ikinci bir tavanın varlığıdır.
DOM Okuma/Yazma, Serialization, Kodlama
lxml salt okunur bir çıkarıcı değil, tam okuma/yazma destekli bir ağaç yapısıdır; düzenleme yüzeyini tek tek doğruladım. Sekiz DOM işleminin tamamı geçti: SubElement, insert, remove, replace, strip_tags (etiketleri kaldırıp metnini tutar), strip_elements (etiketleri ve metnini kaldırır), drop_tree (lxml.html özel bir fonksiyon) ve yeni başlayanları şaşırtan text/tail çift yuvalı model — <p>head<b>bold</b>tail</p> içinde p.text "head", b.text "bold", b.tail ise "tail" olur.
Serialization beşte beş geçti: XML ve HTML modlarında tostring (HTML, boş öğeleri doğru şekilde self-close etmez), pretty_print, C14N canonicalization (method="c14n", yine lxml’ye özel) ve temiz bir round-trip.
Kodlama konusunda lxml sessizce ayrışıyor. UTF-8 olmayan baytlar verin — "<p>café éè</p>".encode("latin-1") çıktısını lxml.html.fromstring üzerinden geçirin — café éè ifadesini eksiksiz kurtarır; U+FFFD yerine geçen karakterler yok, düşen bayt yok. Bu, selectolax paketinde onun “temiz referans” rolünü doğrudan yeniden üretir; aynı girdi diğer iki motorda sessizce bozuluyordu (Lexbor yerine koyma karakterleri üretti, Modest ise bayt düşürdü). lxml’nin libxml2 tabanlı karakter kümesi tespiti burada daha kararlıdır.
Ters tarafta, kodlamayı nasıl belirttiğiniz konusunda katıdır. XML bildirimi içinde encoding="latin-1" yazarsanız XMLSyntaxError: Unsupported encoding: latin-1 hatası alırsınız; IANA’nın kanonik biçimi olan encoding="ISO-8859-1" ise sorunsuz ayrıştırılır ve café döner. libxml2 yalnızca kanonik kodlama adlarını kabul eder, takma adları değil — bu ayrıntı launchpad #613302 içinde belgelenmişti. Bilmiyorsanız can sıkıcı, öğrendikten sonra önemsiz.
Son olarak, düğüm yaşam döngüsü. İzole subprocess’lerde üç stale-handle senaryosu çalıştırdım (sert çökme, sıfırdan farklı çıkış kodu olarak görünürdü): ağacı garbage collection sonrası elde tutulan bir düğüm, drop_tree() sonrasında bir handle okunması ve remove() sonrası bir düğümün kullanılması. Hiçbirinde segfault olmadı — lxml, use-after-free’i önlemek için bir düğümün ağacına olan referansını canlı tutuyor. Bu testte selectolax’ın aldığı temiz notun aynısını aldı.
Hız ve Bellek (Ödünç Alınmış, ve Dürüstçe Söylüyorum)
Bu bölümdeki her şey selectolax paketinden, 2026-07-13 itibarıyla yeniden kullanıldı. Bu paketin ürettiği tek bir zamanlama sayısı bile yok; bir şeyi iki kez söylemeyi, yanlışlıkla yeniden zamanladığınızı düşündürmeye tercih ederim.
| Boyut | lxml değeri | Okuma |
|---|---|---|
| Salt parse p50 (10 MB) | 77.9 ms | selectolax-Lexbor’dan yaklaşık %33-34 daha hızlı |
| Tam parse + çıkarım p50 (1 MB / 10 MB) | 14.18 ms / 172.9 ms | küçük boyutlarda Lexbor ile hemen hemen eşit |
| 100k düğümlü CSS throughput | 3,002,646 düğüm/s | üç C motoru içinde en üst seviye |
| 10 MB RSS farkı | 128.9 MB | ölçülen altı parser arasında en hafifi; BeautifulSoup’tan yaklaşık 1.7x daha hafif |
| Import soğuk başlangıç | 14.1 ms | parsel tarzı import’lardan yaklaşık 2.3x daha hızlı |
Salt parse ve throughput sayıları güçlü; lxml, ölçülen altı parser arasında en düşük bellek tüketen seçenektir. Ama threading tarafında bir not düşmek gerekiyor. Yeniden kullanılan veriler, 4 thread’li toplam süre hızlanmasının yalnızca 1.21x olduğunu ve sonucun belirsiz kaldığını gösteriyor — fakat bu, paylaşılan varsayılan parser yolu. lxml SSS açıkça söylüyor: her thread kendi parser’ını kullandığında (ya da kopyalanmış bir default parser olduğunda) GIL parse sırasında bırakılır; paylaşılan parser ise erişimi seri hale getirir. Doğru kullanım için API yüzeyini yapısal olarak doğruladım (XMLParser.copy() var, get/set_default_parser var, XPathEvaluator iç kilit taşıyor), ancak thread başına parser ile hızlanmayı ölçmedim — bu yeni bir zamanlama ölçümü olurdu ve bu paket böyle ölçümler üretmiyor. Bu yüzden 1.21x değerini lxml’nin threading tavanı değil, “naif paylaşılan yol” olarak okuyun.
Ve tüm bu sayılara bir dipnot: bunlar tek platformlu sayılar, macOS arm64. lxml’nin salt parse performansının Lexbor’u geçmesi, Lexbor tabanlı parser’ın genelde en hızlı olduğu yönündeki yaygın görüşle çelişiyor; dolayısıyla bir Linux x86_64 yeniden kontrolü yapılmadan bunu kesin kabul etmek doğru olmaz.
Lisans: Sıkıcı Ama Güzel Kazanç
lxml BSD-3-Clause lisansıyla dağıtılır; paketlediği C kütüphaneleri — libxml2 ve libxslt — ise MIT lisanslıdır. Bu, hiçbir noktada copyleft içermeyen tamamen izin verici bir zincirdir ve yeniden dağıtım yapmaya başladığınız anda önem kazanır. Karşılaştırma için, selectolax wheel’i LGPL-2.1 Modest ve Apache-2.0 Lexbor içerir; bu yüzden kapalı bir üründe dağıtım açısından lxml daha temiz bir hikâye sunar.
Kurulum tarafında da pratik bir avantajı var: lxml, libxml2 ve libxslt’yi statik olarak bağlayan önceden derlenmiş wheel’ler yayınlar; dolayısıyla pip install lxml çoğu durumda sistemde ayrıca libxml2 ya da bir derleyici gerektirmez — kaynaktan derlemeye göre farklı bir deneyim.
lxml Nerede Uyar — ve AI Çıkarma Katmanı Nerede Devreye Girer
Sınırı netleştirelim; çünkü burada kategori hatası yapmak çok kolay. lxml bir ayrıştırma kütüphanesidir. Size bir ağaç ve mükemmel bir sorgu motoru verir; o ağacın etrafındaki her şey ise hâlâ sizin işinizdir: sayfayı çekmek, JavaScript’i render etmek, bot önlemlerini aşmak, XPath’i yazıp sürdürmek ve sonucu yapılandırmak. Bu, yönetilen bir extraction servisinden farklı bir katmandır; ikisi rakipten çok komşudur.
Fetch-render-select-maintain yığınını sahiplenmek istemeyen geliştiriciler için üst katmanda Thunderbit gibi bir şey devreye girer — ve bu kitle için önemli olan tarayıcı eklentisi değil, API, MCP sunucusu ve CLI’dır. Thunderbit Open API bir sayfayı temiz Markdown’a çevirmek için POST /distill ve JSON Schema’ya göre yapılandırılmış veri çekmek için POST /extract uç noktalarını sunar; renderMode anahtarı ve yüksek hacim için batch job’lar bulunur. Aynı motor, ajanlar ve kodlama asistanları için bir MCP sunucusu (thunderbit_suggest_fields, thunderbit_distill, thunderbit_extract) olarak ve doğrudan terminalden npx @thunderbit/thunderbit-cli ile çalıştırabileceğiniz bir CLI olarak da mevcuttur. JS render, bot koruması ve CAPTCHA’ları kutudan çıktığı gibi ele alır ve şema ile eşleşen JSON döndürür — yani ayrıştırmanın üst katmanıdır, yerine geçeni değil.
Web Verisi Çıkarmak için Thunderbit’i Deneyin
Çerçeve basit: kontrol ettiğiniz ve ağacı üzerinde cerrahi XPath kontrolü istediğiniz pipeline’larda lxml’ye yönelin. Selector’ları ve render işini hiç yönetmek istemiyorsanız AI tabanlı bir extraction API’ye yönelin. Gerçek sistemlerin çoğu ikisini birden kullanır — kontrol ettikleri yapılandırılmış feed’ler için lxml, kontrol etmedikleri dağınık uzun kuyruk sayfaları için extraction servisi.
Bu İncelemenin Test Etmediği Şeyler
Bu çalışma nihai bir skor tablosu değil, geçici bir incelemedir; dolayısıyla kapsamı dışında kalan noktaları da açıkça söyleyeyim.
Tüm zamanlama ve bellek rakamları yeniden kullanılmış, tek platformlu (macOS arm64, Python 3.14) ve o paketin uyarılarını devralıyor — “lxml salt parse’ta daha hızlı” sonucu yaygın görüşe aykırı ve Linux x86_64 üzerinde yeniden kontrol istiyor. Thread başına parser ile sağlanan threading hızlanması test edilmedi (yeni zamanlama gerektirirdi). 300k kayıt üzerinde iterparse bellek ölçtüm ama GB ölçekli gerçek XML üzerinde, HTML ile XML arasında iterparse farkında ya da çok saatlik soak testinde ölçüm yapmadım. lxml’nin XSLT 1.0 desteği, RelaxNG / XMLSchema / DTD doğrulaması ve EXSLT eklentileri burada hiç test edilmedi — yetenek alanı geniş, ama ayrıştırma ve seçim çekirdeğinin dışında. İkinci derinlik tavanını 2,045 olarak gözlemledim, ancak libxml2’nin tam recursion sabitini netleştirmedim. Sadece kararlı 6.1.1 test edildi; 7.0.0 alpha değil. Windows, kaynak derlemeler ve free-threaded 3.14t derlemesi de test edilmedi. XPath içinde ise yerleşik fonksiyonları kapsadım ama XPath değişkenlerini, özel Python extension fonksiyonlarını ya da önceden derlenmiş etree.XPath nesnesinin yeniden kullanımını kapsamadım.
Karar
lxml hızlı ve yeni olan şey değil; tavsiyenin kendisi de bu yüzden değişmiyor. Bu, ana akım Python’da eşine rastlanmayan tam XPath 1.0 motoruna sahip, iki on yıllık bir libxml2 bağlayıcısı; ortada hata log’u olan üç öngörülebilir ayrıştırma sıkılık modu; belleğe sığmayan belgeler için gerçek bir streaming parser; doğru çoklu namespace ve kodlama desteği; ve tamamen izin verici bir lisans. Birkaç keskin kenar — yaklaşık 253 seviyelik derinlik sınırı ve paylaşılan parser threading sonucu — belgelenmiş, ayarlanabilir ve artık açıklanmış durumda.
Scraping pipeline’ının sahibi sizseniz ve XPath’e dayanıyorsanız, hâlâ ilk akla gelecek parser lxml’dir. Selector’lar ve render ile uğraşmak istemiyorsanız, Thunderbit API, MCP ve CLI gibi bir AI extraction katmanı tam bunun içindir — rakip değil, net bir iş bölümü. Her iki durumda da bu sayıları geçici kabul edin ve bir tasarım dokümanında kullanmadan önce kendi platformunuzda zamanlamayı yeniden kontrol edin.
Web Verisi Çıkarmak için Thunderbit’i Deneyin Get Started Free
SSS
lxml bir web scraper mıdır?
Hayır. lxml bir parser ve serializer’dır — işaretlemeyi düzenlenebilir, sorgulanabilir bir ağaca dönüştüren libxml2/libxslt için bir Python bağlayıcısıdır. Sayfaları çekmez, JavaScript render etmez ve bot önlemlerini yönetmez; istek katmanını siz sağlarsınız (requests, httpx, headless browser ya da bir scraping servisi üzerinden) ve baytları lxml’ye verirsiniz.
BeautifulSoup veya selectolax yerine ne zaman lxml kullanmalıyım? XPath gerektiğinde lxml’ye yönelin. BeautifulSoup arka planda lxml’yi parser olarak kullanabilir ama yerleşik XPath sunmaz; selectolax ise yalnızca CSS odaklıdır ve kendi dar alanında daha hızlıdır. Seçim mantığınız metin içeriği filtreleme, ebeveyn ya da ata gezintisi, attribute/text-node çıkarımı veya count predicate’leri gerektiriyorsa, bunları doğrudan ifade edebilen tek ana akım Python seçeneği lxml’nin XPath motorudur.
lxml neden derin iç içe geçmiş içeriği sessizce düşürüyor?
Varsayılan parser, iç içe geçmeyi yaklaşık 253 seviyede sınırlar — bu, kötü niyetli belgelere karşı libxml2’nin DoS savunmasıdır; bir bug değildir. huge_tree=True ayarlarsanız (örneğin lxml.html.HTMLParser(huge_tree=True)), 300 ve 1,000 derinliklerini eksiksiz geri kazanır. Ancak huge_tree’nin kaldıramadığı, yaklaşık 2,045 seviyede ikinci ve daha sert bir recursion tavanı olduğunu unutmayın.
lxml çok iş parçacıklı ayrıştırmada GIL’i serbest bırakır mı? Yalnızca doğru koşullarda. lxml SSS’ine göre, her thread kendi parser’ını veya kopyalanmış bir varsayılan parser’ı kullanıyorsa parse sırasında GIL serbest bırakılır; paylaşılan parser ise erişimi seri hale getirir. Yeniden kullanılan 4 thread’lik 1.21x hızlanma, thread başına parser tavanını değil, naif paylaşılan parser yolunu yansıtıyor; o tavan burada ölçülmedi.
lxml 2026’da hâlâ bakımlı mı? Evet. Kararlı 6.1.1 sürümü 2026-05-18’de yayınlandı, repo en son 2026-07-02’de güncellendi ve 7.0.0 alpha üzerinde çalışmalar sürüyor. Yaklaşık 3.000 GitHub yıldızı ve altta aktif biçimde bakımı yapılan libxml2 ile hâlâ güncel ve desteklenen bir kütüphane; eski bir miras ürünü değil.


