Tutorial 9: Scrapy + BeautifulSoup - Pengumpulan Data untuk ML
Daftar Isi
Pendahuluan
Model machine learning hanya sebaik data yang digunakan untuk melatihnya. Meskipun banyak dataset yang sudah dikurasi tersedia, proyek ML di dunia nyata sering kali memerlukan pengumpulan data kustom dari web. Baik Anda membutuhkan ulasan produk untuk analisis sentimen, artikel berita untuk klasifikasi teks, atau lowongan kerja untuk analisis pasar, web scraping adalah keterampilan penting bagi praktisi ML.
Tutorial ini membahas dua alat Python yang paling kuat untuk web scraping: BeautifulSoup untuk mem-parsing HTML dan Scrapy untuk membangun web crawler yang skalabel dan siap produksi. Anda akan belajar cara mengekstrak data terstruktur dari situs web, membersihkannya, menyimpannya dalam berbagai format, dan mengubahnya menjadi dataset yang siap untuk ML, sambil tetap menghormati pedoman scraping yang etis.
Prasyarat
- Python 3.9 atau lebih tinggi
- Pemahaman dasar tentang struktur HTML dan CSS selector
- Keakraban dengan struktur data Python
Instal paket yang diperlukan:
pip install scrapy beautifulsoup4 lxml requests pandas sqlalchemy
pip install scrapy-playwright # Untuk halaman yang di-render JavaScript
Dasar-Dasar BeautifulSoup
Mem-parsing Dokumen HTML
from bs4 import BeautifulSoup
import requests
Ambil halaman web
url = "https://quotes.toscrape.com/"
response = requests.get(url)
soup = BeautifulSoup(response.text, "lxml")
Tampilkan judul halaman
print(f"Judul halaman: {soup.title.string}")
Menavigasi Pohon DOM
# Temukan elemen berdasarkan tag
semuadiv = soup.findall("div")
print(f"Ditemukan {len(semuadiv)} elemen div")
Temukan berdasarkan nama kelas
kutipan = soup.findall("div", class="quote")
print(f"Ditemukan {len(kutipan)} kutipan di halaman")
Temukan berdasarkan ID
spesifik = soup.find(id="main-content")
Temukan dengan CSS selector
penulis = soup.select("small.author")
for p in penulis:
print(f"Penulis: {p.gettext()}")
Mengekstrak Data
# Ekstrak data terstruktur dari setiap kutipan
data = []
for kutipan in soup.findall("div", class="quote"):
teks = kutipan.find("span", class="text").gettext()
penulis = kutipan.find("small", class="author").gettext()
tag = [t.gettext() for t in kutipan.findall("a", class="tag")]
data.append({
"teks": teks,
"penulis": penulis,
"tag": tag,
})
for item in data[:3]:
print(f'"{item["teks"][:60]}..." - {item["penulis"]}')
print(f" Tag: {', '.join(item['tag'])}")
Bekerja dengan Atribut dan Tautan
# Ekstrak semua tautan dari halaman
tautan = []
for atag in soup.findall("a", href=True):
href = atag["href"]
tekstautan = atag.gettext(strip=True)
tautan.append({"url": href, "teks": tekstautan})
Ekstrak sumber gambar
gambar = []
for img in soup.findall("img"):
src = img.get("src", "")
alt = img.get("alt", "Tidak ada teks alt")
gambar.append({"src": src, "alt": alt})
Ekstrak atribut data
elemen = soup.findall(attrs={"data-category": True})
for el in elemen:
print(f"Kategori: {el['data-category']}")
Fundamental Scrapy
Membuat Proyek Scrapy
# Buat proyek Scrapy baru
scrapy startproject pengumpuldataml
cd pengumpuldataml
Buat spider
scrapy genspider spiderkutipan quotes.toscrape.com
Struktur Proyek
pengumpuldataml/
scrapy.cfg
pengumpul
dataml/
init.py
items.py # Model data
middlewares.py # Pemrosesan request/response
pipelines.py # Pipeline pemrosesan data
settings.py # Konfigurasi proyek
spiders/
init.py
spider
kutipan.py
Membangun Spider Scrapy
Spider Dasar
# pengumpuldataml/spiders/spiderkutipan.py
import scrapy
class SpiderKutipan(scrapy.Spider):
name = "kutipan"
allowed
domains = ["quotes.toscrape.com"]
starturls = ["https://quotes.toscrape.com/"]
def parse(self, response):
for kutipan in response.css("div.quote"):
yield {
"teks": kutipan.css("span.text::text").get(),
"penulis": kutipan.css("small.author::text").get(),
"tag": kutipan.css("a.tag::text").getall(),
"urlpenulis": response.urljoin(
kutipan.css("a[href='author']::attr(href)").get("")
),
}
# Ikuti paginasi
halamanberikutnya = response.css("li.next a::attr(href)").get()
if halamanberikutnya:
yield response.follow(halamanberikutnya, callback=self.parse)
CrawlSpider untuk Situs Kompleks
# pengumpuldataml/spiders/spiderberita.py
import scrapy
from scrapy.spiders import CrawlSpider, Rule
from scrapy.linkextractors import LinkExtractor
class SpiderBerita(CrawlSpider):
name = "berita"
alloweddomains = ["contoh-berita.com"]
starturls = ["https://contoh-berita.com/"]
rules = (
# Ikuti tautan kategori
Rule(LinkExtractor(allow=r"/kategori/\w+"), follow=True),
# Ekstrak data dari halaman artikel
Rule(
LinkExtractor(allow=r"/artikel/\d+"),
callback="parseartikel",
follow=False,
),
)
def parseartikel(self, response):
yield {
"judul": response.css("h1.article-title::text").get("").strip(),
"konten": " ".join(
response.css("div.article-body p::text").getall()
),
"penulis": response.css("span.author-name::text").get("").strip(),
"tanggalterbit": response.css(
"time.published::attr(datetime)"
).get(""),
"kategori": response.css("a.category-link::text").get("").strip(),
"tag": response.css("a.article-tag::text").getall(),
"url": response.url,
}
Spider dengan Autentikasi
# pengumpuldataml/spiders/spiderauth.py
import scrapy
class SpiderTerautentikasi(scrapy.Spider):
name = "spiderauth"
starturls = ["https://contoh.com/login"]
def parse(self, response):
# Ekstrak token CSRF jika ada
csrftoken = response.css(
"input[name='csrftoken']::attr(value)"
).get("")
return scrapy.FormRequest.fromresponse(
response,
formdata={
"username": "namapenggunaanda",
"password": "katasandianda",
"csrftoken": csrftoken,
},
callback=self.setelahlogin,
)
def setelahlogin(self, response):
if "Login gagal" in response.text:
self.logger.error("Login gagal!")
return
# Sekarang jelajahi halaman terautentikasi
yield response.follow("/dashboard/data", callback=self.parsedata)
def parsedata(self, response):
for baris in response.css("table.data-table tr"):
sel = baris.css("td::text").getall()
if sel:
yield {
"kolom1": sel[0].strip() if len(sel) > 0 else "",
"kolom2": sel[1].strip() if len(sel) > 1 else "",
"kolom3": sel[2].strip() if len(sel) > 2 else "",
}
Items dan Pipelines
Mendefinisikan Items
# pengumpuldataml/items.py
import scrapy
class ItemArtikel(scrapy.Item):
judul = scrapy.Field()
konten = scrapy.Field()
penulis = scrapy.Field()
tanggalterbit = scrapy.Field()
kategori = scrapy.Field()
tag = scrapy.Field()
url = scrapy.Field()
jumlahkata = scrapy.Field()
waktuscraping = scrapy.Field()
class ItemUlasanProduk(scrapy.Item):
namaproduk = scrapy.Field()
teksulasan = scrapy.Field()
rating = scrapy.Field()
pengulas = scrapy.Field()
tanggalulasan = scrapy.Field()
pembelianterverifikasi = scrapy.Field()
suaramembantu = scrapy.Field()
Membangun Pipeline Pemrosesan
# pengumpuldataml/pipelines.py
import re
from datetime import datetime
from itemadapter import ItemAdapter
class PipelineBersihkanTeks:
"""Membersihkan dan menormalisasi field teks."""
def processitem(self, item, spider):
adapter = ItemAdapter(item)
# Bersihkan field teks
for field in ["judul", "konten", "teksulasan"]:
if adapter.get(field):
teks = adapter[field]
teks = re.sub(r"\s+", " ", teks) # Normalisasi spasi
teks = teks.strip()
teks = re.sub(r"[^\x00-\x7F]+", "", teks) # Hapus non-ASCII
adapter[field] = teks
return item
class PipelineDeduplikasi:
"""Menghapus item duplikat berdasarkan URL atau konten."""
def init(self):
self.urlterlihat = set()
self.hashkontenterlihat = set()
def processitem(self, item, spider):
adapter = ItemAdapter(item)
import hashlib
# Periksa duplikasi URL
url = adapter.get("url", "")
if url and url in self.urlterlihat:
from scrapy.exceptions import DropItem
raise DropItem(f"URL duplikat: {url}")
self.urlterlihat.add(url)
# Periksa duplikasi konten
konten = adapter.get("konten", "") or adapter.get("teksulasan", "")
if konten:
hashkonten = hashlib.md5(konten.encode()).hexdigest()
if hashkonten in self.hashkontenterlihat:
from scrapy.exceptions import DropItem
raise DropItem("Konten duplikat terdeteksi")
self.hashkontenterlihat.add(hashkonten)
return item
class PipelineTambahMetadata:
"""Menambahkan metadata ke setiap item."""
def processitem(self, item, spider):
adapter = ItemAdapter(item)
adapter["waktuscraping"] = datetime.utcnow().isoformat()
# Hitung jumlah kata
konten = adapter.get("konten", "") or adapter.get("teksulasan", "")
if konten:
adapter["jumlahkata"] = len(konten.split())
return item
class PipelineFilter:
"""Menyaring item yang tidak memenuhi kriteria kualitas."""
PANJANGKONTENMINIMUM = 100
def processitem(self, item, spider):
adapter = ItemAdapter(item)
konten = adapter.get("konten", "") or adapter.get("teksulasan", "")
if not konten or len(konten) < self.PANJANGKONTENMINIMUM:
from scrapy.exceptions import DropItem
raise DropItem(f"Konten terlalu pendek: {len(konten)} karakter")
return item
Konfigurasi Middleware
Middleware Download Kustom
# pengumpuldataml/middlewares.py
import random
class MiddlewareRotasiUserAgent:
"""Merotasi header User-Agent untuk menghindari deteksi."""
DAFTARUSERAGENT = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10157) AppleWebKit/605.1.15 "
"(KHTML, like Gecko) Version/17.0 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x8664; rv:121.0) Gecko/20100101 Firefox/121.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) "
"Gecko/20100101 Firefox/121.0",
]
def processrequest(self, request, spider):
request.headers["User-Agent"] = random.choice(self.DAFTARUSERAGENT)
class MiddlewareCobaUlang:
"""Logika coba ulang kustom dengan exponential backoff."""
MAKSPERCOBAAN = 3
def processresponse(self, request, response, spider):
if response.status in [429, 500, 502, 503, 504]:
percobaan = request.meta.get("jumlahcoba", 0)
if percobaan < self.MAKSPERCOBAAN:
import time
waktutunggu = 2 percobaan
spider.logger.warning(
f"Mencoba ulang {request.url} (percobaan {percobaan + 1}), "
f"menunggu {waktutunggu} detik"
)
time.sleep(waktutunggu)
request.meta["jumlahcoba"] = percobaan + 1
return request
return response
Konfigurasi Settings
# pengumpuldataml/settings.py
BOTNAME = "pengumpuldataml"
SPIDERMODULES = ["pengumpuldataml.spiders"]
NEWSPIDERMODULE = "pengumpuldataml.spiders"
Patuhi robots.txt
ROBOTSTXTOBEY = True
Pembatasan laju
CONCURRENTREQUESTS = 8
DOWNLOADDELAY = 1.5
CONCURRENTREQUESTSPERDOMAIN = 4
RANDOMIZEDOWNLOADDELAY = True
Konfigurasi pipeline (angka lebih kecil = prioritas lebih tinggi)
ITEMPIPELINES = {
"pengumpuldataml.pipelines.PipelineBersihkanTeks": 100,
"pengumpuldataml.pipelines.PipelineFilter": 200,
"pengumpuldataml.pipelines.PipelineDeduplikasi": 300,
"pengumpuldataml.pipelines.PipelineTambahMetadata": 400,
}
Konfigurasi middleware
DOWNLOADERMIDDLEWARES = {
"pengumpuldataml.middlewares.MiddlewareRotasiUserAgent": 400,
"pengumpuldataml.middlewares.MiddlewareCobaUlang": 500,
}
Cache untuk pengembangan
HTTPCACHEENABLED = True
HTTPCACHEEXPIRATIONSECS = 86400
HTTPCACHEDIR = "httpcache"
Logging
LOGLEVEL = "INFO"
LOGFILE = "scraper.log"
Auto-throttle untuk scraping yang sopan
AUTOTHROTTLEENABLED = True
AUTOTHROTTLESTARTDELAY = 1
AUTOTHROTTLEMAXDELAY = 10
AUTOTHROTTLETARGETCONCURRENCY = 2.0
Menangani Paginasi dan JavaScript
Pola Paginasi
# pengumpuldataml/spiders/spiderberpaginasi.py
import scrapy
class SpiderBerpaginasi(scrapy.Spider):
name = "berpaginasi"
start
urls = ["https://contoh.com/produk?halaman=1"]
def parse(self, response):
# Ekstrak item dari halaman saat ini
for produk in response.css("div.product-card"):
yield {
"nama": produk.css("h3.product-name::text").get("").strip(),
"harga": produk.css("span.price::text").get("").strip(),
"deskripsi": produk.css("p.description::text").get("").strip(),
"rating": produk.css("span.rating::attr(data-value)").get(""),
}
# Tangani paginasi bernomor
halamansaatini = response.meta.get("halaman", 1)
totalhalaman = int(
response.css("span.total-pages::text").get("1")
)
if halamansaatini < totalhalaman:
halamanberikut = halamansaatini + 1
yield scrapy.Request(
url=f"https://contoh.com/produk?halaman={halamanberikut}",
callback=self.parse,
meta={"halaman": halamanberikut},
)
class SpiderGulirTanpaBatas(scrapy.Spider):
"""Menangani situs yang menggunakan infinite scroll berbasis API."""
name = "gulirtanpabatas"
def startrequests(self):
yield scrapy.Request(
url="https://contoh.com/api/produk?offset=0&limit=20",
callback=self.parseapi,
meta={"offset": 0},
)
def parseapi(self, response):
import json
data = json.loads(response.text)
for item in data.get("results", []):
yield {
"nama": item["name"],
"harga": item["price"],
"kategori": item["category"],
}
# Lanjutkan jika ada lebih banyak hasil
offset = response.meta["offset"] + 20
if data.get("hasmore", False):
yield scrapy.Request(
url=f"https://contoh.com/api/produk?offset={offset}&limit=20",
callback=self.parseapi,
meta={"offset": offset},
)
Menangani Halaman yang Di-render JavaScript
# pengumpuldataml/spiders/spiderjs.py
import scrapy
class SpiderJavaScript(scrapy.Spider):
"""Menggunakan Playwright untuk me-render halaman berat JavaScript."""
name = "spider
js"
starturls = ["https://contoh.com/halaman-spa"]
customsettings = {
"DOWNLOADHANDLERS": {
"http": "scrapyplaywright.handler.ScrapyPlaywrightDownloadHandler",
"https": "scrapyplaywright.handler.ScrapyPlaywrightDownloadHandler",
},
"TWISTEDREACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor",
"PLAYWRIGHTBROWSERTYPE": "chromium",
}
def startrequests(self):
for url in self.starturls:
yield scrapy.Request(
url,
meta={
"playwright": True,
"playwrightpagemethods": [
{"method": "waitforselector", "args": ["div.content-loaded"]},
{"method": "evaluate", "args": [
"window.scrollTo(0, document.body.scrollHeight)"
]},
{"method": "waitfortimeout", "args": [2000]},
],
},
)
def parse(self, response):
for item in response.css("div.dynamic-content"):
yield {
"judul": item.css("h2::text").get(""),
"isi": item.css("div.body::text").get(""),
}
Pipeline Pembersihan Data
Pembersihan Teks Komprehensif
# pengumpuldataml/utils/pembersihan.py
import re
import unicodedata
from typing import Optional
def bersihkanteks(teks: str) -> str:
"""Pembersihan teks komprehensif untuk dataset ML."""
if not teks:
return ""
# Normalisasi karakter Unicode
teks = unicodedata.normalize("NFKD", teks)
# Hapus entitas HTML
teks = re.sub(r"&[a-zA-Z]+;", " ", teks)
teks = re.sub(r"\d+;", " ", teks)
# Hapus URL
teks = re.sub(r"https?://\S+", "", teks)
# Hapus alamat email
teks = re.sub(r"\S+@\S+\.\S+", "", teks)
# Normalisasi spasi
teks = re.sub(r"\s+", " ", teks)
# Hapus spasi di awal/akhir
teks = teks.strip()
return teks
def bersihkanharga(strharga: str) -> Optional[float]:
"""Ekstrak harga numerik dari string."""
if not strharga:
return None
bersih = re.sub(r"[^\d.,]", "", strharga)
bersih = bersih.replace(",", "")
try:
return float(bersih)
except ValueError:
return None
def bersihkanrating(strrating: str) -> Optional[float]:
"""Ekstrak rating numerik dari string."""
if not strrating:
return None
cocok = re.search(r"(\d+\.?\d)", strrating)
if cocok:
return float(cocok.group(1))
return None
def normalisasitanggal(strtanggal: str) -> Optional[str]:
"""Normalisasi tanggal ke format ISO."""
from datetime import datetime
formattanggal = [
"%Y-%m-%d",
"%m/%d/%Y",
"%d/%m/%Y",
"%B %d, %Y",
"%b %d, %Y",
"%Y-%m-%dT%H:%M:%S",
"%d %B %Y",
]
for fmt in formattanggal:
try:
dt = datetime.strptime(strtanggal.strip(), fmt)
return dt.strftime("%Y-%m-%d")
except ValueError:
continue
return None
Menyimpan Data ke CSV, JSON, dan Database
Ekspor Bawaan Scrapy
# Ekspor ke JSON
scrapy crawl kutipan -o output.json
Ekspor ke CSV
scrapy crawl kutipan -o output.csv
Ekspor ke JSON Lines (lebih baik untuk dataset besar)
scrapy crawl kutipan -o output.jsonl
Pipeline Database Kustom
# pengumpuldataml/pipelines.py (pipeline database)
from sqlalchemy import createengine, Column, String, Float, Integer, DateTime, Text
from sqlalchemy.orm import sessionmaker, declarativebase
from datetime import datetime
Base = declarativebase()
class ModelArtikel(Base):
tablename = "artikel"
id = Column(Integer, primarykey=True, autoincrement=True)
judul = Column(String(500))
konten = Column(Text)
penulis = Column(String(200))
kategori = Column(String(100))
tanggalterbit = Column(String(50))
url = Column(String(1000), unique=True)
jumlahkata = Column(Integer)
waktuscraping = Column(DateTime, default=datetime.utcnow)
class PipelineDatabase:
"""Menyimpan item yang di-scrape ke database SQL."""
def init(self, databaseurl):
self.databaseurl = databaseurl
@classmethod
def fromcrawler(cls, crawler):
return cls(
databaseurl=crawler.settings.get(
"DATABASEURL", "sqlite:///datascraping.db"
)
)
def openspider(self, spider):
self.engine = createengine(self.databaseurl)
Base.metadata.createall(self.engine)
Session = sessionmaker(bind=self.engine)
self.session = Session()
def closespider(self, spider):
self.session.commit()
self.session.close()
def processitem(self, item, spider):
from itemadapter import ItemAdapter
adapter = ItemAdapter(item)
artikel = ModelArtikel(
judul=adapter.get("judul"),
konten=adapter.get("konten"),
penulis=adapter.get("penulis"),
kategori=adapter.get("kategori"),
tanggalterbit=adapter.get("tanggalterbit"),
url=adapter.get("url"),
jumlahkata=adapter.get("jumlahkata"),
)
try:
self.session.add(artikel)
self.session.commit()
except Exception:
self.session.rollback()
return item
Scraping Etis dan Pembatasan Laju
Menghormati robots.txt
# Selalu periksa robots.txt sebelum melakukan scraping
import urllib.robotparser
def bolehscraping(url: str, useragent: str = "*") -> bool:
"""Periksa apakah URL diizinkan oleh robots.txt."""
from urllib.parse import urlparse
parsed = urlparse(url)
robotsurl = f"{parsed.scheme}://{parsed.netloc}/robots.txt"
rp = urllib.robotparser.RobotFileParser()
rp.seturl(robotsurl)
rp.read()
return rp.canfetch(useragent, url)
Contoh penggunaan
url = "https://contoh.com/produk"
if bolehscraping(url):
print("Diizinkan untuk scraping URL ini")
else:
print("Diblokir oleh robots.txt - jangan lakukan scraping")
Implementasi Pembatasan Laju
import time
import random
from collections import defaultdict
class PembatasLaju:
"""Pembatas laju yang sadar domain untuk scraping yang sopan."""
def init(self, delaymin: float = 1.0, delaymaks: float = 3.0):
self.delaymin = delaymin
self.delaymaks = delaymaks
self.requestterakhir = defaultdict(float)
def tunggu(self, domain: str):
"""Tunggu waktu yang sesuai sebelum request berikutnya."""
berlalu = time.time() - self.requestterakhir[domain]
delay = random.uniform(self.delaymin, self.delaymaks)
if berlalu < delay:
waktutidur = delay - berlalu
time.sleep(waktutidur)
self.requestterakhir[domain] = time.time()
Membangun Dataset ML dari Data Web
Pipeline End-to-End: Dataset Analisis Sentimen
# bangundatasetsentimen.py
import pandas as pd
from scrapy.crawler import CrawlerProcess
import scrapy
class SpiderUlasan(scrapy.Spider):
name = "ulasan"
start
urls = ["https://quotes.toscrape.com/"]
customsettings = {
"FEEDS": {"ulasanmentah.jsonl": {"format": "jsonlines"}},
"ROBOTSTXTOBEY": True,
"DOWNLOADDELAY": 2,
"AUTOTHROTTLEENABLED": True,
}
def parse(self, response):
for kutipan in response.css("div.quote"):
item = {
"teks": kutipan.css("span.text::text").get(""),
"penulis": kutipan.css("small.author::text").get(""),
"tag": kutipan.css("a.tag::text").getall(),
}
yield item
halamanberikut = response.css("li.next a::attr(href)").get()
if halamanberikut:
yield response.follow(halamanberikut, callback=self.parse)
Langkah 1: Jalankan spider
process = CrawlerProcess()
process.crawl(SpiderUlasan)
process.start()
Langkah 2: Muat dan bersihkan data
df = pd.readjson("ulasanmentah.jsonl", lines=True)
print(f"Data mentah: {len(df)} rekaman")
Hapus duplikat
df = df.dropduplicates(subset=["teks"])
print(f"Setelah deduplikasi: {len(df)} rekaman")
Hapus baris dengan teks kosong
df = df.dropna(subset=["teks"])
Bersihkan teks
df["teksbersih"] = df["teks"].str.strip().str.replace(r"\s+", " ", regex=True)
Langkah 3: Buat pembagian train/test
from sklearn.modelselection import traintestsplit
dftrain, dftest = traintestsplit(df, testsize=0.2, randomstate=42)
Langkah 4: Simpan dataset siap ML
dftrain.tocsv("datasettrain.csv", index=False)
dftest.tocsv("datasettest.csv", index=False)
print(f"Set latih: {len(dftrain)} sampel")
print(f"Set uji: {len(dftest)} sampel")
Validasi Dataset
def validasidataset(df: pd.DataFrame, kolomteks: str = "teksbersih") -> dict:
"""Validasi dataset yang di-scrape untuk kesiapan ML."""
laporan = {
"totalrekaman": len(df),
"nilainull": df[kolomteks].isnull().sum(),
"stringkosong": (df[kolomteks] == "").sum(),
"duplikat": df.duplicated(subset=[kolomteks]).sum(),
"rataratapanjangteks": df[kolomteks].str.len().mean(),
"panjangteksminimum": df[kolomteks].str.len().min(),
"panjangteksmaksimum": df[kolomteks].str.len().max(),
}
print("Laporan Validasi Dataset:")
for kunci, nilai in laporan.items():
print(f" {kunci}: {nilai}")
# Pemeriksaan kualitas
if laporan["nilainull"] > 0:
print(" [PERINGATAN] Dataset mengandung nilai null")
if laporan["duplikat"] > 0:
print(" [PERINGATAN] Dataset mengandung duplikat")
if laporan["panjangteksminimum"] < 10:
print(" [PERINGATAN] Beberapa teks sangat pendek")
return laporan
validasidataset(dftrain)
Praktik Terbaik
ROBOTSTXTOBEY = True di pengaturan Scrapy.DOWNLOAD_DELAY, AUTOTHROTTLE, dan delay acak. Jangan pernah membombardir server dengan permintaan yang cepat.HTTPCACHE untuk menghindari pengunduhan ulang halaman saat men-debug spider Anda.Kesimpulan
Scrapy dan BeautifulSoup membentuk kombinasi yang kuat untuk mengumpulkan data web untuk proyek machine learning. BeautifulSoup unggul dalam tugas parsing cepat dan pembuatan prototipe, sementara Scrapy menyediakan infrastruktur yang diperlukan untuk crawling skala besar yang siap produksi dengan dukungan bawaan untuk pembatasan laju, pipeline, dan middleware.
Kunci keberhasilan web scraping untuk ML adalah memperlakukannya sebagai masalah rekayasa data: definisikan skema Anda di awal, bangun pipeline pembersihan yang kuat, validasi output Anda, dan selalu lakukan scraping secara etis. Dengan teknik-teknik yang dibahas dalam tutorial ini, Anda memiliki semua yang dibutuhkan untuk membangun dataset ML berkualitas tinggi dari data web.
Mulailah dengan BeautifulSoup untuk eksplorasi skala kecil, beralih ke Scrapy ketika Anda membutuhkan skala dan keandalan, dan selalu utamakan kualitas data di atas kuantitas.