Kurs

Yazacağımız betiğin tamamlanmış ve çalışan bir örneğini burada bulabilirsiniz.
Web Scraping Nedir?
Peki, web scraping tam olarak nedir? Adından da anlaşılacağı gibi, web sayfalarından verileri "kazıma" veya çıkarma yöntemidir. Bu eğitim de dahil olmak üzere, tarayıcınızla internette gördüğünüz her şey yerel diskinize kazınabilir.
Web scraping'in birçok kullanım alanı vardır. Her türlü veri analizinde ilk adım veri edinimidir. İnternet, insanlığın tarihinin ve bilgisinin devasa bir deposudur ve dilediğiniz her şeyi çıkarıp o bilgilerle dilediğinizi yapma imkânına sahipsiniz.
Bu eğitimde, bir subreddit'ten bilgi almak için Python ve BeautifulSoup 4 paketini kullanacağız. datascience subredditi ile ilgileniyoruz. Subreddit'teki ilk 1000 gönderiyi alıp bir CSV dosyasına aktarmak istiyoruz. Kimin paylaştığını, kaç beğenisi ve yorumunun olduğunu bilmek istiyoruz.
Bu eğitimde ele alacaklarımız:
requestsile web sayfalarını alma- Bilgi için web sayfalarını tarayıcıda analiz etme
- BeautifulSoup ile ham HTML'den bilgi çıkarma
Not: Daha hafif yüklendiği ve dolayısıyla makinenizi daha az yorduğu için Reddit'in eski sürümünü kullanacağız.
Ön Koşullar
Bu eğitim aşağıdaki konuları bildiğinizi varsayar:
- Bilgisayarınızda Python betikleri çalıştırma
- HTML yapısına dair temel bilgi
Yukarıdaki becerileri DataCamp'in Python başlangıç kursu ile öğrenebilirsiniz. Bununla birlikte, burada kullanılan kavramlar oldukça minimaldir ve çok az Python bilgisiyle de ilerleyebilirsiniz.
Bunu da hallettiğimize göre, web scraper'ımızı yapmanın ilk kısmına geçebiliriz. Aslında, herhangi bir Python betiğini yazmanın ilk kısmına: içe aktarmalar.
Scraper'ımızda aşağıdaki paketleri kullanacağız:
requestsbeautifulsoup4
Bu paketleri elbette pip ile şöyle kurabilirsiniz:
pip install package_name
Paketlerin indirilmesi bittikten sonra, bunları kodunuza içe aktarın.
import requests
import csv
import time
from bs4 import BeautifulSoup4
Sonuçlarımızı bir CSV dosyasına yazmak için Python'un yerleşik csv modülünü kullanacağız. Yukarıdaki parçada dikkat çekici bir şey fark etmiş olabilirsiniz. beautifulsoup4 adlı bir paket indirdik ama bs4 adlı bir modülden içe aktardık. Bu Python'da mümkündür ve genel olarak hoş karşılanmasa da tam anlamıyla kurallara aykırı değildir.
Platform Politikaları Hakkında Önemli Not
Bir sonraki adımlarda göstereceğim scraping tekniklerini kullanmadan önce, Reddit veya kazımayı planladığınız diğer herhangi bir sitenin güncel hizmet şartlarını kontrol ettiğinizden emin olun. Web sitesi politikaları zaman içinde değişebilir. Eylül 2024 tarihli Reddit Kullanıcı Sözleşmesi, önceden yazılı onay olmaksızın scraping yapılmasını yasaklamaktadır (bkz. bölüm 7). Devam etmeden önce mutlaka sitenin en güncel kullanım yönergelerine uyduğunuzdan emin olun.
Reddit'te Tarama için İlk Adımlar
Ortamımızı kurduk ve hazırız. Sırada, kazımak istediğimiz web sayfasının URL'sine ihtiyacımız var. Bu eğitim için Reddit'in 'datascience' subredditini kullanıyoruz. Betiği yazmaya başlamadan önce biraz saha çalışması yapmamız gerekiyor. Bir web tarayıcısı açın ve ilgili subreddit'e gidin.
Scraper'ımız için subreddit'in eski sürümünü kullanacağımızı unutmayın. Yeni sürüm, web sayfasının derinlerinde bazı kritik bilgileri gizliyor. Bu bilgileri yeni siteden de çıkarmak mümkün, ancak basitlik adına her şeyi açıkça gösteren eski sürümü kullanacağız.
Bağlantıyı açtığınızda, bilgi bombardımanıyla karşılaşıyorsunuz. Bu kadarının içinde bizim tam olarak neye ihtiyacımız var? Tüm bağlantıları incelediğinizde, Reddit gönderilerinin iki tür olduğunu görürsünüz:
- Gelen (inbound)
- Giden (outbound)
Gelen bağlantılar, Reddit'in kendi içeriğine işaret eder; gidenler ise tam tersidir. Bu önemli, çünkü yalnızca gelen bağlantıları istiyoruz. Yani kullanıcıların yazdığı metni içeren, başka sitelere sadece bağlantı vermeyen gönderileri.
İstediğimizin ne olduğunu biliyoruz, peki bunu nasıl çıkaracağız? Gönderi başlıklarına bakarsanız, köşeli parantez içinde bir metinle takip edildiğini görürsünüz. İlgi alanımızdaki gönderiler ‘(self.datascience)’ ifadesiyle takip ediliyor. Mantıken, ‘self’ Reddit'in kök dizinine, ‘.datascience’ ise subreddite atıfta bulunuyor diyebiliriz.
Harika, artık hangi gönderilerin gelen hangilerinin giden olduğunu ayırt etmenin bir yoluna sahibiz. Şimdi bunu DOM yapısında tespit etmemiz gerekiyor. Etiketleri, sınıfları veya id'leri arayarak bu tanımlayıcıları DOM'da bulmanın bir yolu var mı? Elbette!
Web tarayıcınızda herhangi bir ‘self.datascience’ bağlantısına sağ tıklayın ve ‘öğeyi denetle’ye tıklayın. Modern web tarayıcılarının çoğunda, geliştiricilerin web sayfasının kaynak kodunun anlamlı kısımlarını dinamik olarak gezinmesine olanak tanıyan harika bir araç vardır. Safari kullanıyorsanız, Safari tercihleri içinde geliştirici araçlarının etkin olduğundan emin olun. Karşınıza, ‘(self.datascience)’ tanımlayıcısından sorumlu kaynak kodu kısmına kaydırılmış bir panel çıkacaktır.
Panelde, tanımlayıcının aslında bir bağlantı etiketiyle yüklendiğini görebilirsiniz.
<a href="/r/datascience">self.datascience</a>
Ancak bu bir span etiketiyle çevrelenmiştir.
<span class="domain">
"("
<a href="/r/datascience">self.datascience</a>
")"
</span>
Bu span etiketi, sayfada gördüğümüz metni yani ‘(self.datascience)’ ifadesini barındırır. Gördüğünüz gibi, ‘domain’ sınıfıyla işaretlenmiştir. Diğer tüm bağlantıların da aynı biçimi takip edip etmediğini kontrol edebilirsiniz ve nitekim; ediyorlar.
Sayfayı BeautifulSoup ile Alma
Sayfada ne istediğimizi biliyoruz, peki Python ile sayfanın içeriğini nasıl okuyacağız? Aslında bir insanın tarayıcıdan sayfanın içeriğini okumasıyla hemen hemen aynı şekilde.
Önce, ‘requests’ kütüphanesini kullanarak web sayfasını istememiz gerekiyor.
url = "https://old.reddit.com/r/datascience"
# Headers to mimic a browser visit
headers = {'User-Agent': 'Mozilla/5.0'}
# Returns a requests.models.Response object
page = requests.get(url, headers=headers)
Artık web sayfasının ham metnini içeren bir Response nesnemiz var. Şimdilik bununla bir şey yapamıyoruz. Elimizde, HTML dosyasının tüm kaynak kodunu içeren çok büyük bir dize var. Bunu anlamlandırmak için BeautifulSoup 4 kullanmamız gerekiyor.
Başlıklar (headers), bir tarayıcı ziyareti taklit etmemizi sağlar. Bir bota verilen yanıt ile bir tarayıcıya verilen yanıt farklı olduğundan ve referans noktamız tarayıcı olduğundan, tarayıcının yanıtını almak daha iyidir.
BeautifulSoup, sınıflar, id'ler veya etiket adlarının herhangi bir kombinasyonunu arayarak belirli etiketleri bulmamızı sağlar. Bu, bir sözdizimi ağacı oluşturarak yapılır, ancak bunun ayrıntıları hedefimiz açısından önemsizdir (ve bu eğitimin kapsamı dışındadır).
Hadi o sözdizimi ağacını oluşturalım.
soup = BeautifulSoup(page.text, 'html.parser')
Soup, ham kaynak kod içeren bir dizeden oluşturulan bir BeautifulSoup nesnesidir. HTML ayrıştırıcısını belirtmemiz gerektiğini unutmayın. Çünkü BeautifulSoup XML'den de soup oluşturabilir.
Etiketlerimizi Bulma
Hangi etiketleri istediğimizi biliyoruz (‘domain’ sınıflı span etiketleri) ve elimizde soup var. Sırada, soup'u gezinip bu etiketlerin tüm örneklerini bulmak var. BeautifulSoup ile bunun ne kadar basit olduğuna şaşırabilirsiniz.
domains = soup.find_all("span", class_="domain")
Az önce ne yaptık? Soup nesnesi üzerinde ‘find_all’ yöntemini çağırdık; bu yöntem, ikinci argümanda iletilen parametrelerle eşleşen tüm bağlantı etiketlerini (anchor) kendi içinde arar. Yalnızca bir kısıt verdik (sınıf ‘domain’ olmalı), ancak bunu birçok başka şeyle birleştirebilir veya sadece bir id kullanabiliriz.
‘class’ ifadesi Python'da sınıf tanımlamak için ayrılmış bir anahtar kelime olduğundan ‘class_=‘ kullanıyoruz
Birden fazla parametre iletmek isterseniz, tek yapmanız gereken ikinci parametreyi dahil etmek istediğiniz argümanlardan oluşan bir sözlük yapmak, şöyle:
soup.find_all("span", {"class": "domain", "height", "100px"})
Tüm span etiketleri ‘domains’ listemizde, ancak bizim istediğimiz ‘(self.datascience)’ alan adları.
for domain in domains:
if domain != "(self.datascience)":
continue
print(domain.text)
Artık sayfadaki gönderi türlerinin, ‘(self.datascience)’ olmayanları hariç, bir listesini görmelisiniz. Ama aslında istediğimiz her şeye sahibiz. Gelen tüm gönderilere referanslarımız var.
Bilgimizi Bulma
‘(self.datascience)’ satırlarını yazdırmak güzel ama sonra ne olacak? İlk hedefimizi hatırlayın. Gönderi başlığını, yazarını, beğeni sayısını ve yorum sayısını alın.
Bunun için tekrar tarayıcıya dönmemiz gerekiyor. Tanımlayıcımızın üzerinde denetleyiciyi tekrar açarsanız, span etiketimizin bir div etiketinin içinde yuvalandığını göreceksiniz… O da başka bir div içinde ve böyle devam ediyor. Yukarı doğru gitmeye devam edin ve tüm gönderi için üst div'e ulaşacaksınız.
<div class=" thing id-t3_8qccuv even link self" ...>
...
<div class="entry unvoted">
<div class="top-matter">
<p class="title">
...
<span class="domain">
...
</p>
</div>
</div>
</div>
Gördüğünüz gibi, ortak ebeveyn DOM yapısında 4 seviye yukarıda. Üç nokta (…) gereksiz gürültüyü temsil ediyor. Ancak şimdi ihtiyacımız olan, domains listemizdeki her bir gönderi için gönderi div'ine bir referans. Soup içinde herhangi bir öğenin ebeveynini BeautifulSoup yöntemleriyle bulabiliriz.
for domain in soup.find_all("span", class_="domain"):
if domain != "(self.datascience)":
continue
parent_div = domain.parent.parent.parent.parent
print(parent_div.text)
Betik çalıştırıldığında, gelen tüm gönderilerin metnini yazdıracaktır. Ancak bunun kötü bir kod gibi göründüğünü düşünebilirsiniz. Haklısınız. DOM'un yapısal bütünlüğüne tamamen güvenmek asla güvenli değildir. Sürekli güncelleme gerektiren ve kodun zaman ayarlı bomba eşdeğeri olan bir kırp-kes çözümüdür.
Bunun yerine, her gönderinin üst div'ine bakalım ve gelen-giden bağlantıları doğrudan ebeveynden ayırıp ayıramayacağımıza bakalım. Her üst div'in ‘data-domain’ adlı bir niteliği var ve değeri tam da istediğimiz şey! Tüm gelen gönderilerde data-domain ‘self.datascience’ olarak ayarlanmış.
Daha önce belirttiğimiz gibi, BeautifulSoup ile niteliklerin bir kombinasyonunu kullanarak etiketleri arayabiliriz. Şansımıza, Reddit her gönderinin üst div'ini ‘thing’ sınıfıyla etiketlemeyi seçmiş.
attrs = {'class': 'thing', 'data-domain': 'self.datascience'}
for post in soup.find_all('div', attrs=attrs):
print(post.attrs['data-domain'])
‘attrs’ değişkeni, aramak istediğimiz nitelikleri ve değerlerini içeren bir sözlüktür. Bu, daha az hareketli parça içerdiği için gönderileri bulmanın çok daha güvenli bir yaklaşımıdır. Zaten arama yaparken ‘self.datascience’ı argüman olarak eklediğimiz için, yalnızca ‘self.datascience’ data-domain niteliğine sahip gönderiler alacağımız garanti edildiğinden yinelemeleri atlamak için if deyimi kullanmak zorunda değiliz.
Artık istediğimiz tüm bilgilere sahip olduğumuza göre, geriye sadece bilgiyi çocuklardan çıkarmak kalıyor. Bu da düşündüğünüz kadar basit bu arada.
Bilgilerimizi Reddit'ten Çıkarmak
Her gönderi için 4 bilgiye ihtiyacımız var.
- Başlık
- Yazar
- Beğeni
- Yorum
Gönderi div'inin yapısına bakarsak, tüm bu bilgileri bulabiliriz.
Başlık
Bu şimdiye kadarki en basit olanı. Her gönderi div'inde, birkaç katman div'in altında yuvalanmış bir paragraf etiketi bulunur. ‘title’ sınıfına sahip olduğu için özellikle kolay bulunur.
title = post.find('p', class_="title").text
post nesnesi, önceki for döngümüzün içindedir. Bu da bir BeautifulSoup nesnesidir ancak yalnızca gönderi div'i içindeki DOM yapısını barındırır. ‘find’ yöntemi, 'find_all'den farklı olarak yalnızca bir nesne döndürür; 'find_all' ise ölçütleri karşılayan nesnelerin bir listesini döndürür. Etiketi bulduktan sonra yalnızca başlığı içeren dizeyi istiyoruz, bu yüzden okumak için text özelliğini kullanıyoruz. Diğer nitelikleri de ‘object.attrs[‘attribute’]’ şeklinde okuyabiliriz.
Yazar
Bu da nispeten basit; başlığın altındaki herhangi bir yazar adında denetleyiciyi açın. Yazar adının, ‘author’ sınıfına sahip bir bağlantı etiketinde olduğunu göreceksiniz.
author = post.find('a', class_='author').text
Yorumlar
Burada biraz ek çalışma gerekiyor ama yine de basit. Yorumları, başlık ve yazarı bulduğumuz yöntemle bulabiliriz.
comments = post.find('a', class_='comments').text
Bunu çalıştırdığınızda “49 comments” gibi bir şey alacaksınız. Bu kabul edilebilir, ancak sadece sayıyı alsak daha iyi olur. Bunun için biraz daha Python kullanmamız gerek.
Python'un ‘str.split()’ işlevini kullanırsak, boşluklara göre ayrılmış tüm öğelerin bir dizisini döndürür. Bizim durumumuzda ‘[“49", “comments”]’ listesini elde ederiz. Harika! Şimdi yapmamız gereken tek şey ilk öğeyi almak ve saklamak. İşlevleri satırımıza eklememiz yeterli.
comments = post.find('a', class_='comments').text.split()[0]
Yine de işimiz bitmedi çünkü bazen bir sayı gelmez. ‘comment’ gelir. Bu, bir gönderide hiç yorum olmadığında olur. Bunu bildiğimize göre, tek yapmamız gereken, sonuç olarak ‘comments’ alıp almadığımızı kontrol etmek ve bunu ‘0’ ile değiştirmek.
if comments == "comment":
comments = 0
Beğeniler
Beğeni sayısını bulmak çocuk oyuncağı ve yukarıdaki mantıkla uyumlu.
likes = post.find("div", attrs={"class": "score likes"}).text
Burada, iki sınıfın bir kombinasyonunu kullandığımıza dikkat etmiş olabilirsiniz. Bunun nedeni, ‘score’ veya ‘likes’ sınıfına sahip birden fazla div olması, ancak ‘score’ ve ‘likes’ birleşimine sahip yalnızca bir tane bulunmasıdır.
Beğeni sayısı 0 ise 0 alırız. Ancak gönderi o kadar yeniyse ki hiç beğenisi yoksa garip bir şey olur. “•” alırız. Nihai sonuçlarımızda karışıklığa yol açmaması için bunu “None” ile değiştirelim.
if likes == "•":
likes = "None"
Sonuçlarımızı CSV'ye Yazmak
Şu ana kadar, web sayfasındaki her gönderi için başlık, yazar, beğeni ve yorumları çıkaran bir döngümüz var. Python, ‘csv’ adlı müthiş bir yerleşik CSV yazma ve okuma modülüne sahiptir; Pythonik yaklaşımı takip ederek: basit tut. Her neyse, tek yapmamız gereken, döngü bloğunun sonuna, gönderinin ayrıntılarını bir CSV dosyasına ekleyen bir satır eklemek.
counter = 1
for post in posts:
...
post_line = [counter, title, author, likes, comments]
with open('output.csv', 'a') as f:
writer = csv.writer(f)
writer.writerow(post_line)
counter += 1
Oldukça anlaşılır değil mi? post_line, virgülle ayrılması gereken öğeleri depolayan bir dizi oluşturmak için kullanılır. counter değişkeni, kaç gönderi kaydettiğimizi takip etmek için kullanılır.
Sonraki Sayfaya Geçmek
Kaydettiğimiz gönderi sayısını saydığımıza göre, tek yapmamız gereken tüm mantığımızı, belirli bir sayıda gönderi kaydedilene kadar yeni sayfaları isteyecek başka bir döngünün içine almaktır.
counter = 1
while (counter <= 100):
for post in posts:
# Getting the title, author, ...
# Writing to CSV and incrementing counter...
next_button = soup.find("span", class_="next-button")
next_page_link = next_button.find("a").attrs['href']
time.sleep(2)
page = requests.get(next_page_link, headers=headers)
soup = BeautifulSoup(page.text, 'html.parser')
Az önce bir dizi işlem yaptık, değil mi? Satır satır bakalım. Öncelikle, while döngüsünün kullanabilmesi için counter değişkenini bir blok yukarı taşıdık.
Sonra, for döngüsünün ne yaptığını biliyoruz ama diğerleri? ‘next_button' değişkeni, 'sonraki' düğmesini bulmak ve saklamak için kullanılır. Ardından, içindeki bağlantı etiketini bulup 'href' niteliğini alabiliriz; bunu da 'next_page_link’ değişkeninde saklarız.
Bu bağlantıyı kullanarak bir sonraki sayfayı isteyebilir, ‘page’ değişkenine geri yazabilir ve BeautifulSoup ile yeni bir soup oluşturabiliriz.
Yukarıdaki kod parçası 100'de durur, ancak duracağı gönderi sayısını istediğiniz gibi ayarlayabilirsiniz.
Sorumlu Şekilde Scraping Yapmak
Yukarıda bahsetmediğimiz satır ‘time.sleep(2)’ idi. Çünkü bu satır kendi bölümünü hak ediyor. Her web sunucusunun sınırlı kaynakları vardır; bu yüzden hepsini tüketmemeye dikkat etmek bize düşer. Saniyeler içinde yüzlerce sayfa istemek yalnızca yasaklanmanıza yol açmakla kalmaz, aynı zamanda hoş da değildir.
Şunu akılda tutmak önemlidir: Web sitesi, isterseniz Python'un gönderdiği istek nesnesine bakarak veya ilk 10–20 istekte botları tespit ederek scraping'i engelleyebilir. IP döndürmeye gerek kalmadan scraping yapmanıza izin vermeleri oldukça nazikçe; bu yüzden botunuzu yavaşlatarak onlara aynı inceliği göstermelisiniz.
Bir web sitesinin tarama politikasını robots.txt dosyasında bulabilirsiniz. Bu genellikle sitenin kökünde yer alır (örn. http://www.reddit.com/robots.txt).
Sırada Ne Var?
Aslında, ne isterseniz. Az önce kanıtladığımız gibi, webde gördüğünüz her şey kazınabilir ve yerel olarak saklanabilir. Az önce edindiğimiz bilgileri çok çeşitli amaçlar için kullanabilirsiniz. Yalnızca bulduğumuz dört bilgiyle bile pek çok çıkarım yapabiliriz.
Bilgilerimizi daha fazla analiz ederek, hangi tür Reddit gönderilerinin en çok beğeni aldığını, hangi kelimeleri içerdiğini, kimin tarafından paylaşıldığını bulabiliriz. Veya tersine, beğeni-yorum oranını analiz ederek bir tartışma endeksi oluşturabiliriz.
Çok yorum alan ama hiç beğeni almayan bir gönderiyi düşünün. Bu, büyük olasılıkla gönderinin insanlarda bir şeye dokunduğu, ancak mutlaka hoşlarına giden bir şey olmadığı anlamına gelir.
Olasılıklar çok fazla ve bilgiyi nasıl kullanacağınıza siz karar vereceksiniz.
Bu eğitimi beğendiyseniz, BeautifulSoup ile Amazon scraping rehberimize ve Python kursları koleksiyonumuza da göz atmayı unutmayın.
