Tutorial Scrapy + BeautifulSoup: Data Collection untuk ML

# Tutorial 9: Scrapy + BeautifulSoup - Pengumpulan Data untuk ML ## Daftar Isi 1. [Pendahuluan](#pendahuluan) 2. [Prasyarat](#prasyarat) 3. [Dasar-Dasar BeautifulSoup](#dasar-dasar-beautifulsoup) 4....

By Ruby Abdullah · · tutorial
ScrapyBeautifulSoupWeb ScrapingData CollectionETLPython

Tutorial 9: Scrapy + BeautifulSoup - Pengumpulan Data untuk ML

Daftar Isi

  • Pendahuluan
  • Prasyarat
  • Dasar-Dasar BeautifulSoup
  • Fundamental Scrapy
  • Membangun Spider Scrapy
  • Items dan Pipelines
  • Konfigurasi Middleware
  • Menangani Paginasi dan JavaScript
  • Pipeline Pembersihan Data
  • Menyimpan Data ke CSV, JSON, dan Database
  • Scraping Etis dan Pembatasan Laju
  • Membangun Dataset ML dari Data Web
  • Praktik Terbaik
  • Kesimpulan

  • 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

    pengumpuldataml/

    init.py

    items.py # Model data

    middlewares.py # Pemrosesan request/response

    pipelines.py # Pipeline pemrosesan data

    settings.py # Konfigurasi proyek

    spiders/

    init.py

    spiderkutipan.py


    Membangun Spider Scrapy

    Spider Dasar

    # pengumpuldataml/spiders/spiderkutipan.py
    

    import scrapy

    class SpiderKutipan(scrapy.Spider):

    name = "kutipan"

    alloweddomains = ["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"

    starturls = ["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 = "spiderjs"

    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"

    starturls = ["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

  • Selalu patuhi robots.txt. Ini adalah persyaratan etis sekaligus pertimbangan hukum. Konfigurasikan ROBOTSTXTOBEY = True di pengaturan Scrapy.
  • Terapkan pembatasan laju. Gunakan DOWNLOAD_DELAY, AUTOTHROTTLE, dan delay acak. Jangan pernah membombardir server dengan permintaan yang cepat.
  • Identifikasikan diri Anda. Atur User-Agent yang deskriptif dan menyertakan informasi kontak agar pemilik situs bisa menghubungi Anda jika diperlukan.
  • Tangani error dengan baik. Terapkan logika coba ulang dengan exponential backoff. Catat kegagalan untuk ditinjau kemudian daripada membiarkan program crash.
  • Deduplikasi lebih awal dan sering. Periksa URL duplikat sebelum meminta dan konten duplikat setelah ekstraksi.
  • Cache respons saat pengembangan. Aktifkan HTTPCACHE untuk menghindari pengunduhan ulang halaman saat men-debug spider Anda.
  • Validasi data Anda. Jalankan pemeriksaan validasi pada dataset Anda sebelum menggunakannya untuk pelatihan ML. Periksa null, duplikat, dan masalah kualitas.
  • Gunakan Items dan Pipelines. Strukturkan data Anda dengan Scrapy Items dan proses melalui Pipeline daripada melakukan semuanya di spider.
  • Pantau spider Anda. Lacak tingkat keberhasilan, tingkat error, dan jumlah item. Atur peringatan untuk penurunan volume data yang tidak terduga.
  • Hormati ketentuan layanan. Beberapa situs web secara eksplisit melarang scraping. Selalu tinjau ketentuan situs sebelum membangun scraper.

  • 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.

    Artikel Terkait

    Tutorial Dagster: Orkestrasi Data dengan Software-Defined Assets

    Dagster: Orkestrasi Data Modern dengan Software-Defined Assets Dagster adalah orkestrator data yang menyusun pipeline be...

    Firecrawl: Web Scraping API untuk Aplikasi AI dan LLM

    Firecrawl: Web Scraping API untuk Aplikasi AI dan LLM Firecrawl adalah managed web scraping API yang dirancang khusus un...

    Crawl4AI: Web Crawling yang Dioptimalkan untuk Pipeline AI

    Crawl4AI: Web Crawling yang Dioptimalkan untuk Pipeline AI Dalam membangun aplikasi AI modern, terutama yang berbasis Re...

    Tutorial Lengkap Prefect: Modern Workflow Orchestration untuk ML

    Tutorial Lengkap Prefect: Modern Workflow Orchestration untuk ML Prefect adalah platform workflow orchestration modern y...