Görüşümüz: Biz Geonode olarak proxy satışı yapıyoruz; dolayısıyla genel web’deki tablolar işimizle yakından ilgilidir. Burada dikkat edilmesi gereken nokta şudur: read_html adresinden bir URL’yi doğrudan almak, istek üzerinde hiçbir kontrol sağlamaz — ne özel başlıklar, ne oturum, ne yeniden deneme mantığı, ne de isteği herhangi bir yoldan yönlendirme imkânı yoktur. İşbirliği yapılan bir sitedeki tek seferlik bir tablo için bu yeterli ve mevcut en hızlı yöntemdir. Tekrarlayan herhangi bir şey için, uygun bir HTTP istemcisiyle HTML'yi kendiniz alın ve dizgiyi read_html adresine aktarın. Bu ayrım, fazladan bir satırlık bir maliyet gerektirir ve yerleşik fetch işlevinin sunmadığı her şeyi size sağlar.
Temel Bilgiler
import pandas as pd
tables = pd.read_html("https://example.com/data")
print(len(tables))
df = tables[0]
pandas belgelerinden alınan kritik ayrıntı: bu işlev “bir DataFrame listesi” döndürür. Tek bir DataFrame değil. Altı tablo içeren bir sayfa size, belgedeki sıraya göre altı tane verir; tables[0]
ise istediğiniz verilerden ziyade bir gezinme düzeni olabilir.
Belgelerde ayrıca, fonksiyonun "her zaman bir DataFrame listesi döndüreceği veya tamamen başarısız olacağı" açıkça belirtilmiştir — "<td>
'da yalnızca boşluklar içeren tek bir satır" gibi olağandışı durumlar dışında boş bir liste döndürmez. Dolayısıyla, boş bir sonuç normal bir çıktıdan ziyade, garip bir şeyin meydana geldiğinin işaretidir.
Ayrıca HTML'yi doğrudan da aktarabilirsiniz; bu, hızlı bir bakıştan öteye geçen her şey için tercih edilmesi gereken yöntemdir:
import requests
html = requests.get(url, headers={"User-Agent": "MyBot/1.0 (+https://example.com/bot)"}).text
tables = pd.read_html(html)
Neleri Görebilir, Neleri Göremez
Kapsamını anlamak, hayal kırıklıklarının çoğunu önler.
Yalnızca <table> öğelerini okur. Belgelerde, "<table> öğelerini arar ve yalnızca <tr>, <th> satırlarını ve <td> öğelerini işler" denilmektedir. Çoğu modern web tasarımında olduğu gibi, <div> öğelerinden oluşturulmuş ve ızgara şeklinde stil verilmiş bir düzen, ekranda ne kadar tablo gibi görünürse görünsün, bulunabilecek bir tablo içermez.
JavaScript'i çalıştırmaz. Tablo istemci tarafında oluşturulmuşsa, read_html'un gördüğü HTML'de bu tablo yer almaz. Bu, görünürde bir tablo bulunan bir sayfada "hiçbir tablo bulunamadı" hatasının en yaygın nedenidir.
Spanning'i doğru şekilde işler. colspan ve rowspan öznitelikleri "doğru şekilde işlenir"; bu, birçok el yapımı ayrıştırıcının başardığından daha fazlasıdır.
Başlıklar için <thead>'i tercih eder ve böyle bir şey yoksa bunları gövdede aramaya geçer.
Varsayılan olarak display: none'i dikkate alır. displayed_only=True varsayılan ayarı, "display: none içeren öğeleri hariç tutar"; bu genellikle istenen bir durumdur, ancak bazen bir sitenin kasıtlı olarak yalnızca belirli görüntüleme alanlarında erişime açtığı verileri gizleyebilir.
İki bağımlılık notu. Ayrıştırma motorları öncelikle lxml'dir; bu bulunmadığında bs4 ve html5lib kullanılır — belgelerde, "'bs4' ve 'html5lib'in" varyant adları olarak eşanlamlı olduğu belirtilmektedir. Ayrıca bilinmesi gereken bir URL tuhaflığı vardır: “lxml yalnızca http, ftp ve file URL protokollerini kabul eder. 'https' ile başlayan bir URL’niz varsa, 's' kısmını kaldırmayı deneyebilirsiniz.” Uygulamada, sayfayı kendiniz getirmeniz bu sorunu tamamen ortadan kaldırır.
Önemli Parametreler
Tam imza on sekiz parametre içerir. Bunlardan altısı işin büyük kısmını üstlenir.
**match
** (varsayılan '.+'
) metni bir düzenli ifadeyle eşleşen tabloları filtreler. Bu, en kullanışlı parametredir ve yeterince kullanılmamaktadır:
tables = pd.read_html(html, match="Population")
Bir listenin içindeki bir dizini tahmin etmek yerine, tablonun içerdiği bir öğeyi belirtirsiniz. Sayfanın düzeni değiştiğinde çok daha sağlamdır, çünkü içerik genellikle tablonun konumunu değiştiren bir yeniden tasarımdan etkilenmez.
**attrs
**, HTML özniteliklerine göre filtreleme yapar; bu, belirli bir tabloyu tanımlamanın diğer bir yoludur:
tables = pd.read_html(html, attrs={"id": "results", "class": "data"})
**header
**, başlık satırını belirtir. Belgelerde, kullanıcıların dikkatini çeken bir sıralama detayı belirtilmiştir: "header
argümanı, skiprows
uygulandıktan sonra uygulanır". Dolayısıyla, iki satırı atlayıp ardından header=0
talebinde bulunursanız, atlamadan sonraki ilk satırı alırsınız.
**skiprows
**, ayrıştırma işleminden önce satırları kaldırır — gerçek başlığın üzerinde başlık satırları bulunan tablolar için kullanışlıdır.
**index_col
** bir sütunu dizin olarak ayarlar.
**thousands
** (varsayılan ','
) ve **decimal
** (varsayılan '.'
) sayısal biçimlendirmeyi yönetir. Bunlar göründüğünden daha önemlidir: 1.234,56
kullanan bir Avrupa tablosu için thousands='.'
ve decimal=','
gereklidir; bunlar olmadan her sayı sessizce bir dizeye veya yanlış bir değere dönüşür.
Bilmeniz gereken iki şey daha:
**converters
**, ayrıştırma sırasında her sütuna bir işlev uygular; bu, türleri sonradan düzeltmekten daha temiz bir yöntemdir.
**extract_links
**, hücreler içindeki bağlantıların yalnızca metnini değil, href
adreslerini de yakalar — tablonun satırları, sizin de istediğiniz ayrıntı sayfalarına bağlantı verdiğinde gerçekten kullanışlıdır.
Kimsenin Kaçınamadığı Temizleme İşlemi
Belgeler beklentileri dürüstçe ortaya koyuyor ve bunu kendiniz keşfetmek yerine sorumluluk reddi beyanını okumakta fayda var:
Bu işlevi çağırdıktan sonra bir miktar temizleme işlemi yapmanız gerekeceğini unutmayın. Örneğin, ``header=0`
argümanını geçerken sütun adlarıNaN` değerine dönüştürülürse, sütun adlarını manuel olarak atamanız gerekebilir.
Ve:
Tablonun yapısı hakkında mümkün olduğunca az varsayımda bulunmaya çalışırız ve tabloda bulunan HTML’nin kendine özgü özelliklerini kullanıcıya bırakırız.
İkinci cümle, tasarım felsefesini açıkça ifade etmektedir. read_html
size HTML’nin içeriğini sunar; onu düzenlemek ise sizin işinizdir.
Her seferinde karşımıza çıkan düzenleme:
Başlıkları kapsayan MultiIndex sütunları. İki satırlık başlığı olan bir tablo, MultiIndex
gibi bir sonuç üretir; bu doğru olmakla birlikte garip görünür. Bunu düzleştirin:
df.columns = [" ".join(str(c) for c in col).strip() for col in df.columns]
Boşluklar ve kırılmaz boşluklar. HTML,
ile doludur; bu, \xa0
olarak gelir ve düz .strip()
'yi bozar:
df = df.replace("\xa0", " ", regex=True)
df.columns = df.columns.str.replace("\xa0", " ", regex=False).str.strip()
Dize olan sayılar. Para birimi sembolleri, yüzde işaretleri ve dipnot işaretçileri:
df["Price"] = (df["Price"].astype(str)
.str.replace(r"[^\d.,-]", "", regex=True)
.str.replace(",", "")
.pipe(pd.to_numeric, errors="coerce"))
errors="coerce"
, ayrıştırılamayan değerleri hata vermeden NaN
'ye dönüştürür; bu sayede, tek bir hatalı hücre yüzünden tüm işlemi kaybetmek yerine kaç tanesinin başarısız olduğunu sayabilirsiniz.
Dipnot satırları ve toplamlar. Birçok tablo, veri içermeyen bir özet satırıyla sona erer. Son satırın güvenli olduğunu varsaymak yerine, bunu açıkça filtreleyin.
Doğru Tabloyu Seçme
Dayanıklılık derecesi artan sırayla dört yaklaşım.
İndekse göre — tables[0]
. Keşif amaçlı kullanım için uygundur, ancak bir komut dosyasında kırılgandır. Sizin tablonuzun üzerine eklenen yeni bir tablo, hiçbir uyarı vermeden onu bozar; çünkü 0 numaralı indeks hâlâ mevcuttur ve artık başka bir şey içerir.
**match
ile** — en iyi varsayılan seçenektir. İstediğiniz tabloda bulunan ve başka hiçbir yerde bulunmayan bir dizeyi belirtin.
**attrs
ile** — tablo bir id'ye veya ayırt edici bir sınıfa sahip olduğunda en iyisidir, çünkü bunlar geliştirici tarafından kasıtlı olarak seçilir.
Şekline göre, yüklendikten sonra — onu tanımlayan başka hiçbir şey olmadığında:
candidates = [t for t in pd.read_html(html)
if {"Name", "Price"}.issubset(t.columns)]
if len(candidates) != 1:
raise ValueError(f"expected 1 matching table, found {len(candidates)}")
df = candidates[0]
Bu doğrulama önemli kısımdır. Üç eşleşmeden ilkini sessizce alan bir komut dosyası, aylarca yanlış veriler üretecektir. Sayının beklenmedik olması durumunda hata vermesi, bir veri sorununu hata mesajına dönüştürür.
Birden Fazla Sayfayı Tek Bir DataFrame’e Okuma
Tek bir tablo çalışır hale geldiğinde atılacak doğal bir sonraki adım budur ve bu noktada edinilen birkaç alışkanlık, ciddi sorunları önleyebilir.
Döngü içinde sonuna eklemek yerine birleştirin. DataFrame’i aşamalı olarak oluşturmak yavaştır ve parçalanmış bir dizin oluşturur. Çerçeveleri toplayın ve tek seferde birleştirin:
frames = []
for page in range(1, 11):
df = fetch_table(f"https://example.com/data?page={page}",
match="Population",
expected_columns=["Country", "Population"])
df["source_page"] = page
frames.append(df)
combined = pd.concat(frames, ignore_index=True)
Her satırın nereden geldiğini kaydedin. Yukarıdaki source_page
sütunu hiçbir maliyeti yoktur ve eninde sonunda size sorulacak soruyu yanıtlar: Bu garip değeri hangi sayfa üretti? Zaman içinde toplanan her şey için bir zaman damgası da ekleyin. Kaynağı bilinmeyen bir veri kümesinin hatalarını gidermek çok zordur ve denetlenmesi imkansızdır.
Döngüyü yavaşlatın. Bağlantının izin verdiği en yüksek hızda on sayfanın birden alınması, sunucu tarafında bir saldırı gibi görünen bir ani yük oluşturur. İstekler arasında bir saniyelik bir duraklama hem nezakettir hem de çoğu sitede işlemi tamamlayabilmekle hız sınırlamasına takılmak arasındaki farkı belirler:
import time, random
time.sleep(1 + random.random())
**Çalıştırmayı yarıda bırakmak yerine, her sayfa için hataları ayrı ayrı ele alın.**Düzeni değişen tek bir sayfa, diğer dokuzunu da kaybetmenize neden olmamalıdır:
frames, failures = [], []
for page in range(1, 11):
try:
frames.append(fetch_table(url_for(page), match="Population", expected_columns=COLS))
except Exception as exc:
failures.append((page, str(exc)))
if failures:
print(f"{len(failures)} pages failed:", failures)
Birleştirmeden önce yapıların uyumlu olup olmadığını kontrol edin. Yedinci sayfada diğerlerinde olmayan bir sütun varsa, pd.concat
uyuşmayan satırlar için NaN
ile dolu bir çerçeve üretecektir — geçerli, biçimsel olarak doğru ama yanlış. Birleştirmeden önce sütun kümelerini karşılaştırmak, bunu görebileceğiniz bir hataya dönüştürür.
Ve ardından yinelenenleri temizleyin. Sayfalandırılmış tablolar, özellikle tarama sırasında altta yatan veriler değiştiğinde, sayfa sınırları arasında sık sık satırları tekrarlar. Sütunların anlamlı bir alt kümesinde combined.drop_duplicates()
komutunu kullanmak, aynı kaydı iki kez saymamak için ucuz bir önlemdir.
Ne Zaman Başka Bir Şey Kullanmalı?
read_html, kullanımı kolaylaştıran bir sarmalayıcıdır. Dört durumda farklı bir araç kullanılması gerekir.
Veriler <table> biçiminde değilse. Kart düzenleri, tanım listeleri, <div> tabloları. Gerçek bir ayrıştırıcı kullanın — lxml veya CSS seçiciler ya da XPath ile BeautifulSoup — ve DataFrame’i kendiniz oluşturun:
from lxml import html as lh
tree = lh.fromstring(page)
rows = [{"name": c.cssselect("h3")[0].text_content().strip(),
"price": c.cssselect(".price")[0].text_content().strip()}
for c in tree.cssselect("div.product-card")]
df = pd.DataFrame(rows)
Sayfanın JavaScript gerektirdiği durumlarda. Önce Playwright veya benzer bir araçla sayfayı işleyin, ardından işlenmiş HTML'yi read_html adresine aktarın. Bu kombinasyon iyi sonuç verir ve genellikle en kısa yoldur:
html = page.content() # rendered DOM, not source
tables = pd.read_html(html)
İsteğin kontrol edilmesi gerektiğinde. Başlıklar, çerezler, oturumlar, yeniden denemeler, zaman aşımları, proxy'ler — read_html, sizin adınıza veri alırken bunların hiçbirini ortaya çıkarmaz. Ayrı olarak alın ve dizgiyi aktarın.
Site yapılandırılmış veriler sunuyorsa. Bir CSV indirme dosyası, bir API veya sayfaya gömülü bir JSON-LD. Bunların herhangi biri, kararlılık açısından işlenmiş HTML'yi ayrıştırmaktan daha iyidir ve bir şey yazmadan önce otuz saniye ayırıp kontrol etmeye değer.
Komut Dosyasında Doğru Şekilde Yapmak
Birden fazla kez çalıştırılan her şey için geçerli olan şablon.
import pandas as pd
import requests
HEADERS = {"User-Agent": "AcmeDataBot/1.0 (+https://acme.example.com/bot)"}
def fetch_table(url, match, expected_columns):
resp = requests.get(url, headers=HEADERS, timeout=30)
resp.raise_for_status()
tables = pd.read_html(resp.text, match=match)
if len(tables) != 1:
raise ValueError(f"{url}: expected 1 table matching {match!r}, got {len(tables)}")
df = tables[0]
df.columns = [str(c).replace("\xa0", " ").strip() for c in df.columns]
missing = set(expected_columns) - set(df.columns)
if missing:
raise ValueError(f"{url}: missing columns {missing}; got {list(df.columns)}")
if df.empty:
raise ValueError(f"{url}: table matched but contains no rows")
return df
Buradaki beş unsur, hatasını yüksek sesle bildiren bir komut dosyası ile hatasını sessizce bildiren bir komut dosyası arasındaki farkı oluşturur.
**raise_for_status()
**, HTTP hatalarını yakalar; çünkü bir hata sayfasında read_html
komutu ya hiç tablo bulamaz ya da yanlış tabloları bulur.
**match
yerine bir dizin** kullanılır; böylece bir düzen değişikliği, yanlış tablo yerine bir hata üretir.
Tam olarak bir eşleşmenin doğrulanması; böylece belirsiz bir sonuç, sessizce ilk sonucu almak yerine durur.
Beklenen sütunları kontrol etmek; bu, sütunların adını değiştiren veya sırasını yeniden düzenleyen bir yeniden tasarım hatasını yakalar — aksi takdirde, bu hata, biçimsel olarak doğru ancak yanlış verilerin sonsuza kadar üretilmesine neden olur.
Boş olup olmadığını kontrol etmek; çünkü satırı olmayan eşleşen bir tablo, neredeyse her zaman bir sonuçtan ziyade bir belirtidir.
İletişim URL’si olan dürüst bir kullanıcı aracısı hiçbir maliyeti yoktur ve sizi, operatörlerin engellemek zorunda kaldıkları bir istemci yerine, izin vermeyi tercih edebilecekleri bir istemci haline getirir.
Sık Sorulan Sorular
pandas.read_html ne işe yarar?
HTML’yi ayrıştırır ve bulduğu her <table> öğesini bir DataFrame listesi olarak döndürür. colspan ve rowspan adreslerini işler, varsa başlıklar için <thead> kullanır ve varsayılan olarak display: none ile gizlenmiş öğeleri hariç tutar.
read_html neden bir liste döndürür?
Çünkü bir sayfa herhangi bir sayıda tablo içerebilir ve pandas bunların hepsini belge sırasına göre döndürür. Her zaman bir liste döndürür ya da hata verir — olağandışı durumlar dışında boş bir liste döndürmez — bu nedenle boş bir sonuç, başlı başına bir şeylerin ters gittiğinin işaretidir.
read_html neden "No tables found" (Tablo bulunamadı) diyor?
Ya sayfada <table> öğeleri yok — modern sayfa düzenlerinde bunun yerine genellikle stil uygulanmış <div> öğeleri kullanılır — ya da tablo, pandas'ın çalıştırmadığı bir JavaScript ile oluşturulmuştur. Hangisi olduğunu anlamak için tarayıcının görüntülediği sayfa yerine ham HTML kaynağını kontrol edin.
Belirli bir tabloyu nasıl seçerim?
İstediğiniz tablonun içindeki metne uyan bir düzenli ifadeyle match kullanın ya da bir id veya sınıfa göre filtrelemek için attrs kullanın. Her ikisi de, tablonuzun üstüne bir tablo eklendiğinde sessizce bozulabilen listeye indeksleme yönteminden çok daha sağlamdır.
read_html, JavaScript ile işlenmiş sayfalarda çalışır mı?
Hayır. Bu işlev HTML'yi ayrıştırır ve komut dosyalarını çalıştırmaz. Önce sayfayı bir tarayıcı otomasyon aracıyla işleyin, ardından elde edilen HTML dizesini read_html adresine aktarın — bu kombinasyon iyi sonuç verir ve genellikle en kısa yoldur.
Daha sonra DataFrame’i nasıl temizlerim?
MultiIndex sütunlarını başlıkların yayılmasını önleyecek şekilde düzleştirmeniz, \xa0 şeklinde gelen kırılmaz boşlukları kaldırmanız, pd.to_numeric(errors="coerce") ile para birimi ve yüzde dizelerini sayılara dönüştürmeniz ve dipnot veya toplam satırlarını kaldırmanız gerekecektir. Pandas belgeleri, temizlik işleminin gerekli olacağını açıkça belirtir.
read_html ile proxy veya özel başlıklar kullanabilir miyim?
URL’yi kendisi aldığında kullanamazsınız — herhangi bir istek seçeneği sunmaz. Sayfayı requests veya başlıkları, zaman aşımlarını, oturumları ve proxy’leri kontrol edebileceğiniz başka bir istemciyle alın, ardından HTML dizesini read_html’a aktarın.
thousands ve decimal parametreleri ne işe yarar?
Bu parametreler pandas’a sayıların nasıl biçimlendirileceğini bildirir; varsayılan değerleri sırasıyla ',' ve '.' şeklindedir. 1.234,56 gibi Avrupa biçimlendirmesi için thousands='.' ve decimal=',' kullanmanız gerekir — bunlar olmadan değerler sessizce yanlış ayrıştırılır veya dizgi olarak bırakılır.
Sonuç
read_html, dar bir kapsamda gerçekten kullanışlı bir işlevdir: size verilen HTML’deki <table> öğelerini bulur ve bunları DataFrame’lere dönüştürür. Bu kapsam dahilinde, hücreleri birleştirme, başlık algılama ve gizli öğeler gibi zorlu kısımları çoğu elle yazılmış ayrıştırıcıdan daha iyi bir şekilde halleder.
Unutmamanız gereken iki nokta şunlardır: bu işlev bir DataFrame yerine bir liste döndürür ve belgelerde de belirtildiği gibi temizleme işleminin gerçekleşeceğini beklemelisiniz. İndeks yerine match veya attrs ile seçim yapıp tam olarak bir tablonun eşleştiğini doğruladığınızda, en yaygın sessiz hatalar bir hata mesajına dönüştürülür.
Birden fazla kez çalışan herhangi bir şey için HTML’yi kendiniz alın. Ekstra bir satır size başlıklar, zaman aşımları, yeniden denemeler, oturumlar ve yerleşik fetch’in sağlamadığı diğer her şeyi sağlar — ve aynı zamanda lxml’in URL protokolündeki tuhaflığını da atlatır.
Sayfada tablo bulunmuyorsa, parametre aramayı bırakın. <div> adresindeki bir tablo veya istemci tarafında işlenen bir sayfa farklı bir sorundur; bunun çözümü ise gerçek bir ayrıştırıcı, bir görüntüleme adımı ya da — en iyisi — sitenin muhtemelen henüz bakmadığınız bir yerde zaten yayınladığı yapılandırılmış verilerdir.
