Terakhir diperbarui: 30 Oktober 2026.
Pemantauan Harga E-commerce dengan Playwright dan Proksi Residensial
Harga yang ditampilkan untuk sebuah produk tidak selalu berupa nilai tunggal yang universal. Toko daring dapat menampilkan informasi berbeda bergantung pada halaman produk, mata uang, tujuan pengiriman, lokal browser, cookie, atau akun yang masuk.
Itulah mengapa pemantauan harga lebih dari sekadar membaca angka dari HTML. Monitor yang berguna juga harus mencatat kondisi saat halaman diperiksa—dan membedakan observasi harga yang nyata dari permintaan atau ekstraksi yang gagal.
Dalam tutorial ini, kita akan membangun monitor Python kecil dengan Playwright. Monitor membuka katalog produk, menangkap nama produk, harga yang ditampilkan, dan ketersediaan, lalu menyimpan catatan JSON dan CSV bertanda waktu. Proksi residensial bersifat opsional: proksi mengubah rute jaringan yang digunakan browser, tetapi tidak secara otomatis mengatur negara pengiriman toko, mata uang, atau status akun.
Cakupan demo ini: contoh menggunakan Books to Scrape , situs latihan yang harganya ditetapkan secara acak dan tidak memiliki makna dunia nyata. Cocok untuk mempelajari alur kerja pemantauan, bukan untuk membuktikan perbedaan harga regional. Untuk meneliti perbedaan pasar yang sebenarnya, gunakan etalase yang Anda berwenang mengaksesnya dan yang memang memvariasikan data yang ditampilkan menurut pasar.
Kapan sebaiknya Anda menggunakan Playwright?
Gunakan API produk resmi atau feed data terlebih dahulu jika keduanya menyediakan informasi dan cakupan yang Anda butuhkan. Biasanya lebih stabil dan lebih mudah dipelihara daripada otomasi browser.
Playwright berguna ketika:
- informasi produk yang relevan dirender oleh JavaScript;
- Anda perlu memeriksa konten yang benar-benar diterima browser;
- ketersediaan atau harga yang terlihat di etalase tidak tersedia melalui feed yang sesuai.
Untuk halaman statis, klien HTTP biasa mungkin lebih sederhana dan lebih cepat. Panduan ini berfokus pada satu kali menjalankan browser dan satu konteks pengujian yang dikonfigurasi. Ini bukan penjadwal multi-negara siap pakai.
Untuk perencanaan yang lebih luas, lihat panduan pemantauan harga e-commerce kami. Jika Anda membutuhkan bantuan mengonfigurasi proksi di Playwright, lihat Cara Menggunakan Proksi Residensial dengan Playwright .
Apa yang harus ada dalam catatan pemantauan harga?
Observasi harga hanya berguna jika Anda dapat memahami cara pengumpulannya. Minimal, catat:
| Bidang | Mengapa penting |
|---|---|
| Nama produk dan URL | Mengidentifikasi item yang dipantau |
| Stempel waktu UTC | Menetapkan kapan observasi dibuat |
| Label pasar | Mendeskripsikan konfigurasi pengujian; bukan bukti lokasi IP |
| IP keluar proksi | Membantu mendiagnosis rute jaringan, jika proksi digunakan |
| Lokal dan zona waktu browser | Mencatat preferensi browser yang dapat memengaruhi penyajian halaman |
| Negara pengiriman | Mencatat pengaturan pengiriman yang benar-benar digunakan, jika berlaku |
| Status akun | Membedakan observasi keluar masuk dan masuk |
| Teks harga mentah | Menyimpan persis apa yang ditampilkan halaman |
| Jumlah terurai dan kode mata uang | Memudahkan perbandingan observasi yang valid |
| Teks dan status ketersediaan | Menyimpan kata-kata halaman dan interpretasi yang dinormalisasi |
| Status jalankan dan ekstraksi | Memisahkan pemeriksaan gagal dari observasi yang valid |
Jangan anggap variabel-variabel ini dapat saling menggantikan. Hasil geolokasi IP tidak membuktikan pasar pengiriman mana yang digunakan toko. Lokal browser tidak mengatur alamat pengiriman. Simbol mata uang saja bisa ambigu—misalnya, $ dapat merujuk ke beberapa mata uang.
Skrip contoh mencatat kode mata uang yang dikonfigurasi. Skrip tidak mendeteksi mata uang dari halaman secara otomatis.
1. Buat proyek
Buat folder dengan file-file berikut:
price-monitor/
├── .env
├── requirements.txt
└── monitor.py
Tambahkan dependensi ke requirements.txt:
playwright>=1.40
python-dotenv>=1.0
Instal dependensi tersebut dan browser Chromium:
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python -m playwright install chromium
Di Linux, instal dependensi sistem Chromium jika Playwright melaporkan shared library yang hilang:
sudo .venv/bin/playwright install-deps chromium
2. Konfigurasi konteks pengujian
Buat file .env:
# Optional proxy settings. Leave all three blank for a direct practice run.
PROXY_SERVER=http://gw.kindproxy.com:12000
PROXY_USERNAME=your_dashboard_username
PROXY_PASSWORD=your_dashboard_password
PRODUCT_URL=https://books.toscrape.com/
REGION_LABEL=UK
BROWSER_LOCALE=en-GB
BROWSER_TIMEZONE=Europe/London
CURRENCY_CODE=GBP
DELIVERY_COUNTRY=
ACCOUNT_STATE=signed_out
Gunakan endpoint proksi dan kredensial yang disediakan di akun Anda. Salin nama pengguna dan kata sandi yang dihasilkan secara tepat; jangan menambahkan secara manual parameter sesi atau lokasi dari penyedia lain. Jauhkan .env dari kontrol versi dan jangan pernah memublikasikan kredensial proksi di tangkapan layar atau log.
Untuk latihan langsung, biarkan PROXY_SERVER, PROXY_USERNAME, dan PROXY_PASSWORD kosong.
Nilai REGION_LABEL, DELIVERY_COUNTRY, dan ACCOUNT_STATE adalah metadata yang dicatat bersama hasil. Demo ini tidak memverifikasi bahwa proksi keluar di negara yang disebutkan, tidak memasukkan alamat pengiriman, dan tidak masuk ke toko. Tambahkan langkah-langkah tersebut hanya jika Anda memiliki akun pengujian dan alur kerja yang berwenang, dan catat status yang benar-benar digunakan.
Playwright mendukung konfigurasi proksi saat peluncuran browser, termasuk kolom nama pengguna dan kata sandi terpisah untuk proksi terautentikasi. Lihat dokumentasi jaringan Playwright Python .
3. Jalankan monitor
Simpan yang berikut sebagai monitor.py. Selektor ditulis untuk kartu katalog Books to Scrape. Selektor perlu diadaptasi untuk situs lain.
import asyncio
import csv
import json
import os
import re
from datetime import datetime, timezone
from decimal import Decimal, InvalidOperation
from pathlib import Path
from urllib.parse import urljoin
from dotenv import load_dotenv
from playwright.async_api import async_playwright
BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")
PRODUCT_URL = os.getenv("PRODUCT_URL", "https://books.toscrape.com/").strip()
REGION = os.getenv("REGION_LABEL", "not-set").strip()
LOCALE = os.getenv("BROWSER_LOCALE", "en-GB").strip()
BROWSER_TIMEZONE = os.getenv("BROWSER_TIMEZONE", "Europe/London").strip()
CURRENCY = os.getenv("CURRENCY_CODE", "GBP").strip().upper() or None
DELIVERY_COUNTRY = os.getenv("DELIVERY_COUNTRY", "").strip() or None
ACCOUNT_STATE = os.getenv("ACCOUNT_STATE", "signed_out").strip()
OUTPUT_DIR = BASE_DIR / "output"
JSON_PATH = OUTPUT_DIR / "snapshots.json"
CSV_PATH = OUTPUT_DIR / "snapshots.csv"
# These selectors are specific to Books to Scrape's catalogue cards.
CARD_SELECTOR = "article.product_pod"
TITLE_SELECTOR = "h3 a"
PRICE_SELECTOR = ".price_color"
AVAILABILITY_SELECTOR = ".availability"
CSV_FIELDS = [
"record_type", "checked_at", "status", "error",
"product_name", "product_url", "region", "locale",
"browser_timezone", "delivery_country", "account_state",
"proxy_exit_ip", "price_text", "price_amount",
"currency", "availability_text", "availability_status",
"comparison",
]
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def parse_price(raw_text: str) -> str | None:
"""Parse a decimal amount from the demo site's GBP-style price text."""
match = re.search(r"\d[\d,]*(?:\.\d{1,2})?", raw_text)
if not match:
return None
try:
return format(Decimal(match.group(0).replace(",", "")), ".2f")
except InvalidOperation:
return None
def normalize_availability(raw_text: str) -> str:
value = raw_text.casefold()
if "in stock" in value:
return "in_stock"
if "out of stock" in value:
return "out_of_stock"
return "unknown"
def load_records() -> list[dict]:
if not JSON_PATH.exists():
return []
try:
data = json.loads(JSON_PATH.read_text(encoding="utf-8"))
return data if isinstance(data, list) else []
except (OSError, json.JSONDecodeError):
return []
def save_records(records: list[dict]) -> None:
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
JSON_PATH.write_text(
json.dumps(records, ensure_ascii=False, indent=2),
encoding="utf-8",
)
with CSV_PATH.open("w", newline="", encoding="utf-8-sig") as file:
writer = csv.DictWriter(file, fieldnames=CSV_FIELDS, extrasaction="ignore")
writer.writeheader()
writer.writerows(records)
def compare_with_previous(records: list[dict], current: dict) -> str:
if current.get("status") != "success" or not current.get("price_amount"):
return "not_compared"
dimensions = (
"product_url", "region", "locale", "browser_timezone",
"delivery_country", "account_state", "currency",
)
previous = next(
(
item for item in reversed(records)
if item.get("record_type") == "product"
and item.get("status") == "success"
and item.get("price_amount") is not None
and all(item.get(key) == current.get(key) for key in dimensions)
),
None,
)
if previous is None:
return "baseline"
old_price = Decimal(str(previous["price_amount"]))
new_price = Decimal(str(current["price_amount"]))
if new_price < old_price:
return "price_decreased"
if new_price > old_price:
return "price_increased"
return "no_change"
def base_record(record_type: str, status: str, **extra) -> dict:
record = {
"record_type": record_type,
"checked_at": utc_now(),
"status": status,
"error": None,
"product_name": None,
"product_url": None,
"region": REGION,
"locale": LOCALE,
"browser_timezone": BROWSER_TIMEZONE,
"delivery_country": DELIVERY_COUNTRY,
"account_state": ACCOUNT_STATE,
"proxy_exit_ip": None,
"price_text": None,
"price_amount": None,
"currency": CURRENCY,
"availability_text": None,
"availability_status": "unknown",
"comparison": "not_compared",
}
record.update(extra)
return record
async def main() -> None:
if not PRODUCT_URL:
raise ValueError("PRODUCT_URL must be set.")
proxy_server = os.getenv("PROXY_SERVER", "").strip()
proxy_username = os.getenv("PROXY_USERNAME", "").strip()
proxy_password = os.getenv("PROXY_PASSWORD", "").strip()
if bool(proxy_username) != bool(proxy_password):
raise ValueError("Set both PROXY_USERNAME and PROXY_PASSWORD, or neither.")
if (proxy_username or proxy_password) and not proxy_server:
raise ValueError("Set PROXY_SERVER when proxy credentials are provided.")
proxy = None
if proxy_server:
proxy = {"server": proxy_server}
if proxy_username:
proxy["username"] = proxy_username
proxy["password"] = proxy_password
records = load_records()
run_record = base_record("run", "started")
new_records = [run_record]
async with async_playwright() as playwright:
browser = await playwright.chromium.launch(proxy=proxy)
try:
context = await browser.new_context(
locale=LOCALE,
timezone_id=BROWSER_TIMEZONE,
)
page = await context.new_page()
exit_ip = None
if proxy:
try:
ip_response = await page.goto(
"https://api.ipify.org?format=json",
wait_until="domcontentloaded",
timeout=30_000,
)
if ip_response and ip_response.ok:
ip_data = json.loads(await page.locator("body").inner_text())
exit_ip = ip_data.get("ip")
except Exception as exc:
new_records.append(
base_record(
"diagnostic", "failed",
error=f"Proxy exit-IP check failed: {type(exc).__name__}: {exc}",
)
)
try:
response = await page.goto(
PRODUCT_URL,
wait_until="domcontentloaded",
timeout=45_000,
)
if response and response.status >= 400:
raise RuntimeError(f"Target returned HTTP {response.status}")
body_text = (await page.locator("body").inner_text()).casefold()
challenge_markers = (
"verify you are human",
"unusual traffic",
"captcha",
"access denied",
"automated requests",
)
if any(marker in body_text for marker in challenge_markers):
raise RuntimeError(
"Challenge or access-restriction page detected; "
"the monitor stopped without retrying or rotating IPs."
)
cards = page.locator(CARD_SELECTOR)
await cards.first.wait_for(state="visible", timeout=15_000)
count = await cards.count()
for index in range(count):
card = cards.nth(index)
try:
title_link = card.locator(TITLE_SELECTOR)
title = (await title_link.get_attribute("title")) or (
await title_link.inner_text()
).strip()
href = await title_link.get_attribute("href")
product_url = urljoin(PRODUCT_URL, href or "")
price_text = (await card.locator(PRICE_SELECTOR).inner_text()).strip()
price_amount = parse_price(price_text)
availability_text = (
await card.locator(AVAILABILITY_SELECTOR).inner_text()
).strip()
if price_amount is None:
item = base_record(
"product", "extraction_failed",
error="Price text was present but could not be parsed.",
product_name=title,
product_url=product_url,
proxy_exit_ip=exit_ip,
price_text=price_text,
availability_text=availability_text,
availability_status=normalize_availability(availability_text),
)
else:
item = base_record(
"product", "success",
product_name=title,
product_url=product_url,
proxy_exit_ip=exit_ip,
price_text=price_text,
price_amount=price_amount,
availability_text=availability_text,
availability_status=normalize_availability(availability_text),
)
item["comparison"] = compare_with_previous(records + new_records, item)
new_records.append(item)
except Exception as exc:
new_records.append(
base_record(
"product", "extraction_failed",
error=f"{type(exc).__name__}: {exc}",
proxy_exit_ip=exit_ip,
)
)
run_record["status"] = "success"
run_record["error"] = None
run_record["products_seen"] = count
except Exception as exc:
run_record["status"] = "check_failed"
run_record["error"] = f"{type(exc).__name__}: {exc}"
run_record["products_seen"] = 0
finally:
await context.close()
finally:
await browser.close()
records.extend(new_records)
save_records(records)
print(f"Run status: {run_record['status']}")
print(f"Records written: {len(new_records) - 1} product/diagnostic records")
print(f"JSON: {JSON_PATH}")
print(f"CSV: {CSV_PATH}")
if run_record["error"]:
print(f"Error: {run_record['error']}")
if __name__ == "__main__":
asyncio.run(main())
Jalankan:
python monitor.py
Demo yang berhasil selesai dengan 20 catatan produk dan menyimpan kedua file keluaran:

Skrip menulis:
output/
├── snapshots.json
└── snapshots.csv
File JSON menyimpan catatan lengkap. File CSV nyaman untuk ditinjau di spreadsheet. Catatan tingkat jalankan disimpan bahkan saat navigasi gagal atau halaman tantangan terdeteksi; jalankan yang gagal tidak diubah menjadi observasi harga nol.
Berikut contoh catatan produk yang berhasil diekstrak. Catatan mencakup harga tampilan asli, jumlah terurai, mata uang yang dikonfigurasi, ketersediaan, dan IP keluar proksi:

4. Pahami keluaran dan aturan perbandingan
Monitor menyimpan teks harga asli dan jumlah numerik yang diuraikan. Misalnya, halaman mungkin menampilkan £51.77; parser menyimpan 51.77, sementara kode mata uang berasal dari CURRENCY_CODE=GBP.
Parser ini sengaja sempit. Cocok untuk format angka bergaya GBP pada demo, bukan setiap lokal. Parser produksi harus diadaptasi ke format angka dan metadata mata uang toko target.
Perbandingan hanya menggunakan catatan sukses sebelumnya dengan URL produk, label pasar, lokal, zona waktu, pengaturan negara pengiriman, status akun, dan mata uang yang dikonfigurasi yang sama. Harga yang hilang atau tidak dapat diuraikan ditandai sebagai extraction_failed; tidak pernah diperlakukan sebagai nol.
Label pasar tetap hanya label. Sebelum membandingkan pasar nyata, konfirmasikan secara independen lokasi keluar proksi yang sebenarnya serta pengaturan pengiriman dan akun etalase.
Dalam demo, jalankan pertama membuat baseline untuk 20 produk. Jalankan kedua menambahkan 20 catatan lagi, dan harga yang tidak berubah diberi label no_change dengan benar. Bidang delivery_country tetap kosong karena tidak ada negara pengiriman yang dikonfigurasi; ini diharapkan untuk situs latihan Books to Scrape.

5. Pemecahan masalah
| Gejala | Yang harus diperiksa |
|---|---|
| Chromium tidak dapat diluncurkan | Instal browser dan dependensi sistem Linux |
| Autentikasi proksi gagal | Periksa endpoint dan salin nama pengguna/kata sandi yang dihasilkan secara tepat |
| Pemeriksaan IP berhasil tetapi halaman target gagal | Tujuan mungkin memiliki aturan akses atau pembatasan terpisah |
| Kartu katalog tidak ditemukan | Pastikan halaman dimuat dan perbarui selektor CSS untuk situs tersebut |
| Teks harga ada tetapi jumlah kosong | Adaptasikan parse_price() ke format angka situs |
Ketersediaan adalah unknown | Tinjau kata-kata ketersediaan situs dan perbarui normalizer |
| Halaman CAPTCHA, unusual-traffic, atau access-denied muncul | Hentikan pemeriksaan, catat kegagalan, dan tinjau aturan situs serta jadwal permintaan Anda |
Jangan menanggapi tantangan dengan berulang kali mengubah IP atau mencoba mengotomatiskan tantangan. Proksi residensial tidak menjamin akses atau mencegah halaman CAPTCHA.
6. API, otomasi browser, atau proksi?
Alat-alat ini menyelesaikan masalah yang berbeda:
- API atau feed resmi: titik awal terbaik ketika menyediakan data produk terstruktur yang diizinkan.
- Playwright: berguna ketika informasi relevan dirender di browser atau Anda perlu memeriksa halaman etalase yang terlihat.
- Proksi residensial: mengubah rute jaringan. Dapat membantu menguji bagaimana situs merespons permintaan dari keluar jaringan yang berbeda, tetapi tidak mengatur negara pengiriman, mata uang, cookie, atau status akun dengan sendirinya.
Untuk katalog yang lebih besar, tambahkan penjadwalan, batas laju, kebijakan coba ulang untuk kesalahan jaringan sementara, peringatan, dan retensi data hanya setelah alur kerja sekali jalan andal. Hormati ketentuan situs target, panduan robots jika berlaku, dan hukum yang berlaku. Utamakan akses data resmi jika tersedia.
Jika Anda mengevaluasi opsi proksi untuk alur kerja pemantauan yang berwenang, lihat paket proksi residensial KindProxy .
FAQ
Bisakah Playwright memantau harga e-commerce?
Ya. Playwright dapat membaca bidang harga dan ketersediaan yang terlihat dari halaman yang Anda berwenang mengaksesnya. Selektor harus cocok dengan markup situs target, dan setiap observasi harus divalidasi sebelum dibandingkan dengan data sebelumnya.
Apakah proksi residensial menampilkan harga di negara lain?
Tidak secara otomatis. Toko dapat mempertimbangkan lokasi keluar jaringan, alamat pengiriman, mata uang, lokal, cookie, dan status akun. Proksi hanya mengubah rute jaringan.
Apakah proksi mencegah halaman CAPTCHA?
Tidak. Situs tetap dapat mengembalikan CAPTCHA, tantangan, pemblokiran, atau halaman tidak lengkap. Catat pemeriksaan yang gagal dan tinjau aturan akses alih-alih berulang kali merotasi IP.
Bagaimana jika harga tidak dapat diuraikan?
Simpan teks mentah, tandai catatan sebagai kegagalan ekstraksi, dan biarkan jumlah numerik kosong. Jangan pernah mengubah nilai yang hilang menjadi nol atau menghitungnya sebagai penurunan harga.
Penutup
Pemantauan harga yang andal bergantung pada kualitas observasi, bukan hanya kode scraping. Catat kondisi pengujian, pertahankan nilai halaman mentah, validasi setiap ekstraksi, dan bandingkan hanya catatan sukses yang sebanding.
Contoh Books to Scrape mendemonstrasikan alur kerja. Contoh ini tidak menetapkan perilaku harga regional yang nyata. Untuk produksi, adaptasikan selektor dan aturan penguraian ke etalase yang berwenang, verifikasi kondisi pasar secara independen, dan perlakukan pembatasan akses sebagai pemeriksaan gagal—bukan alasan untuk menghindarinya.
