- Crawlee là framework scraping của Apify, wrap Playwright/Puppeteer, có sẵn queue, retry, fingerprint random, anti-detection.
- Chạy trên Cloud VPS 80 (4GB RAM, 799k/tháng) đủ scrape 10-20k page/ngày với concurrency 5-10 browser.
- Proxy rotation residential 30-100 USD/tháng (Bright Data, Smartproxy) là yếu tố quyết định không bị ban.
- Pattern: queue URL -> Playwright fetch -> extract data -> save JSON/Postgres -> retry fail.
- Tuân thủ robots.txt, rate limit reasonable (1-2 req/s/domain), tôn trọng ToS website.
Cần data từ web cho dự án: price monitoring, lead generation, SEO competitor analysis, training ML model. Scraping bằng requests + BeautifulSoup làm được với site cũ, nhưng site hiện đại (SPA React, Cloudflare bot protection, fingerprint detection) thì phải dùng full browser. Playwright là tool tốt nhất hiện tại, Crawlee thêm 1 lớp infrastructure (queue, storage, anti-detection) giúp scrape production-ready.
Mình đã chạy 4 scraper production trong 2025: monitor giá 50k SKU Tiki/Shopee mỗi giờ, scrape job listing TopCV/VietnamWorks daily, news aggregator 200 site VN, lead gen LinkedIn (compliant). Bài này chia sẻ setup full stack, tips tránh ban, ethics scraping.
1. Tại sao Crawlee thay vì viết raw Playwright?
| Feature | Raw Playwright | Crawlee |
|---|---|---|
| Queue management | Tự code Redis/SQLite | Built-in |
| Retry failed request | Tự code | Built-in |
| Fingerprint random | Tự code (khó) | Built-in (FingerprintGenerator) |
| Proxy rotation | Tự setup | Built-in |
| Concurrency control | Tự code | Built-in (AutoscaledPool) |
| Storage (dataset, KV) | Tự code | Built-in (file/Postgres/S3) |
| Session pool | Tự code | Built-in |
| Setup time | 2-3 ngày | 30 phút |
Crawlee free Apache 2.0, không lock-in. Chỉ là wrapper sang Playwright, vẫn dùng được mọi API gốc khi cần customize.
2. Cài đặt trên VPS AlmaLinux/Ubuntu
# Cloud VPS 80 cho production scraper
dnf install nodejs npm git -y # AlmaLinux 9
# hoặc apt install nodejs npm git -y # Ubuntu
# Install Chromium dependencies cho Playwright
dnf install -y nss atk gtk3 gtk4 cups-libs libdrm libxcomposite
libxdamage libxfixes libxkbcommon libxrandr mesa-libgbm
pango cairo alsa-lib
# Hoặc đơn giản:
npx playwright install-deps chromium
mkdir -p /opt/scraper && cd /opt/scraper
npm init -y
npm install crawlee playwright dotenv
npx playwright install chromium3. Scraper đầu tiên: Crawlee PlaywrightCrawler
// /opt/scraper/src/main.js
import { PlaywrightCrawler, Dataset } from 'crawlee'
const crawler = new PlaywrightCrawler({
maxConcurrency: 5,
maxRequestRetries: 3,
navigationTimeoutSecs: 60,
launchContext: {
launchOptions: {
headless: true,
args: ['--no-sandbox', '--disable-blink-features=AutomationControlled']
}
},
async requestHandler({ request, page, log, enqueueLinks }) {
log.info(`Processing ${request.url}`)
await page.waitForSelector('h1', { timeout: 30000 })
const data = await page.evaluate(() => ({
title: document.querySelector('h1')?.textContent?.trim(),
price: document.querySelector('[data-price]')?.textContent?.trim(),
description: document.querySelector('.description')?.textContent?.trim(),
images: Array.from(document.querySelectorAll('img')).map(i => i.src),
url: window.location.href,
scrapedAt: new Date().toISOString()
}))
await Dataset.pushData(data)
// Follow pagination/category links
await enqueueLinks({
selector: 'a.product-link, a.next-page',
strategy: 'same-domain'
})
},
async failedRequestHandler({ request, log }) {
log.error(`Request ${request.url} failed too many times`)
}
})
await crawler.run(['https://target-site.example.com/category/all'])
console.log(`Scraped ${await Dataset.getInfo()} items`)Chạy: node src/main.js. Output JSON ở storage/datasets/default/. Có thể export CSV/JSON/Postgres.
4. Anti-detection: fingerprint random
import { PlaywrightCrawler } from 'crawlee'
import { FingerprintGenerator } from '@apify/fingerprint-suite'
const fingerprintGenerator = new FingerprintGenerator({
browsers: [{ name: 'chrome', minVersion: 110 }],
devices: ['desktop'],
operatingSystems: ['windows', 'macos', 'linux'],
locales: ['en-US', 'vi-VN']
})
const crawler = new PlaywrightCrawler({
browserPoolOptions: {
useFingerprints: true,
fingerprintOptions: {
fingerprintGeneratorOptions: {
browsers: ['chrome'],
devices: ['desktop']
}
}
},
// ...
})Mỗi session browser nhận user-agent, screen resolution, timezone, fonts, navigator properties random nhưng coherent (Chrome 120 trên Windows 11, không phải Chrome 50 trên Win 95). Detection rate giảm xuống dưới 5%.
5. Proxy rotation residential
import { ProxyConfiguration } from 'crawlee'
const proxyConfiguration = new ProxyConfiguration({
proxyUrls: [
'http://user:[email protected]:22225',
'http://user:[email protected]:22225',
'http://user:[email protected]:22225'
],
newUrlFunction: (sessionId) => {
// Rotate per session
const region = sessionId.includes('vn') ? 'vn' : 'us'
return `http://user-region-${region}:[email protected]:22225`
}
})
const crawler = new PlaywrightCrawler({
proxyConfiguration,
// ...
})Residential proxy giá 30-100 USD/tháng cho 10-50GB traffic. Bright Data, Smartproxy, IPRoyal là phổ biến. Đắt hơn datacenter proxy nhưng detection rate cực thấp.
6. Session pool và cookie persistence
import { SessionPool } from 'crawlee'
const sessionPool = await SessionPool.open({
maxPoolSize: 100,
sessionOptions: {
maxAgeSecs: 3600,
maxErrorScore: 3,
maxUsageCount: 50
},
persistStateKey: 'session-pool-state'
})
const crawler = new PlaywrightCrawler({
useSessionPool: true,
persistCookiesPerSession: true,
// ...
})Mỗi session có cookie persistence riêng, simulate user real (login, browse, action liên tục). Session bị block tự bị remove khỏi pool.
7. Rate limit để không quá tải target
import { PlaywrightCrawler } from 'crawlee'
const crawler = new PlaywrightCrawler({
maxConcurrency: 5,
maxRequestsPerMinute: 60, // max 1 request/giây overall
preNavigationHooks: [
async ({ page, request }) => {
// Delay 1-3s random trước mỗi request tới cùng domain
const delay = 1000 + Math.random() * 2000
await new Promise(r => setTimeout(r, delay))
}
],
// ...
})1-2 req/s/domain là rate ethical. Cao hơn là DDoS, site phát hiện ngay và ban IP.
8. Lưu dữ liệu sang Postgres
import { Dataset } from 'crawlee'
import pg from 'pg'
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })
// Trong requestHandler
const data = { /* scraped */ }
await pool.query(
`INSERT INTO products (url, title, price, description, images, scraped_at)
VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (url) DO UPDATE SET
title = EXCLUDED.title,
price = EXCLUDED.price,
scraped_at = NOW()`,
[data.url, data.title, data.price, data.description, JSON.stringify(data.images)]
)ON CONFLICT UPDATE giúp re-scrape không tạo duplicate. Index URL unique để tăng tốc query.
9. Schedule với cron và systemd timer
# /etc/systemd/system/scraper.service
[Unit]
Description=Daily Product Scraper
[Service]
Type=oneshot
User=scraper
WorkingDirectory=/opt/scraper
EnvironmentFile=/opt/scraper/.env
ExecStart=/usr/bin/node /opt/scraper/src/main.js
# /etc/systemd/system/scraper.timer
[Unit]
Description=Run scraper every 6 hours
[Timer]
OnCalendar=*-*-* 02,08,14,20:00:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl enable --now scraper.timer
systemctl list-timers10. Handle Cloudflare và bot challenge
- Dùng undetected-playwright hoặc patchright cho stealth mode.
- 2captcha/capsolver service cho captcha solver (~3 USD/1000 captcha).
- Wait selector kiên nhẫn (60s+) cho Cloudflare challenge tự pass.
- Residential proxy IP cao chất lượng giảm chance gặp challenge 90%.
- Slow down: 1 request/5s qua proxy mới, đôi khi tốt hơn 5 req/s qua proxy nóng.
11. Ethics và legal scraping
- Đọc robots.txt và tôn trọng Disallow path.
- User-Agent string identify rõ bot và contact info: "MyBot/1.0 ([email protected])".
- Rate limit reasonable (1-2 req/s/domain) không gây tải site.
- Tôn trọng ToS website. Một số site cấm scraping rõ ràng.
- Không scrape personal data (email, phone) cho spam. GDPR/PDPA Vietnam phạt nặng.
- Public data, no auth required, mục đích nghiên cứu/cá nhân thường OK.
- Khi nghi ngờ, hỏi luật sư hoặc public API thay vì scrape.
12. Monitor scraper health
// Slack notification khi error rate cao
const stats = crawler.stats
if (stats.requestsFailed / stats.requestsFinished > 0.2) {
await fetch(process.env.SLACK_WEBHOOK, {
method: 'POST',
body: JSON.stringify({
text: `Scraper warning: ${stats.requestsFailed}/${stats.requestsFinished} failed`
})
})
}Setup Uptime Kuma push monitor: scraper xong gọi healthcheck URL. Không gọi trong 6 giờ là alert ngay.
13. Scale lên nhiều worker và queue Redis
import { RedisRequestQueue } from '@crawlee/redis'
const queue = await RedisRequestQueue.open({
redisUrl: 'redis://localhost:6379'
})
// Worker 1, 2, 3... cùng pull từ queue
const crawler = new PlaywrightCrawler({
requestQueue: queue,
// ...
})10 worker chạy parallel trên 2-3 VPS, scrape được 100k+ page/ngày. Queue Redis đảm bảo không duplicate request.
14. Storage và backup data scraped
- Postgres cho structured data (price, title, description), index theo URL.
- MinIO/S3 cho raw HTML và images (compress gzip).
- Cron daily dump Postgres sang gz, push lên Cloudflare R2 backup.
- Retention: hot data 90 ngày trên Postgres, archive cold sang Parquet trên S3.
15. Cost breakdown realistic
| Resource | Spec | Cost/tháng |
|---|---|---|
| VPS scraper | Cloud VPS 80 (4GB RAM) | 799k |
| Residential proxy | 50GB/tháng (Bright Data) | ~1.500k (60 USD) |
| VPS database | Cloud VPS 40 Postgres | 399k |
| Captcha solver | 10k captcha/tháng | ~750k (30 USD) |
| Backup R2 | 100GB | ~37k (1.5 USD) |
| Tổng | 10-20k page/ngày | ~3.500k |
So với mua data từ broker (5-10k USD/tháng cho data tương đương), self-scrape rẻ hơn 90%. Đáng đầu tư nếu data critical cho business.
16. Bài học sau 2 năm chạy scraper production
- Proxy quality > model framework. Residential proxy tốt là 70% success.
- Mỗi site có quirk riêng, phải tune selector và wait condition cho từng cái.
- Site target update layout đột ngột, monitor parse rate và alert ngay.
- Lưu raw HTML, parse sau. Khi schema thay đổi không phải re-scrape.
- Ethical scraping bền vững hơn aggressive. Rate limit tốt giữ IP sống lâu.
17. Headless detection: tránh navigator.webdriver=true
Site phát hiện bot bằng cách check window.navigator.webdriver, plugin list, WebGL fingerprint, audio context. Crawlee + Playwright + FingerprintGenerator giải quyết phần lớn, nhưng sites siết chặt (DataDome, PerimeterX) cần thêm patchright hoặc undetected-playwright:
npm install patchright
// Thay vì playwright:
import { chromium } from 'patchright'
const browser = await chromium.launch({ headless: true })Patchright là fork Playwright vá các leak điểm phát hiện bot. Hoạt động drop-in replacement, đa số code không cần đổi. Anti-detection score tăng 30-40%.
18. Distributed scraping với multiple VPS
Scale lên 100k+ page/ngày cần nhiều VPS:
- 1 VPS Cloud VPS 40 chạy Redis Request Queue + Postgres metadata.
- 3-5 VPS Cloud VPS 80 chạy worker Playwright, pull job từ Redis queue.
- Mỗi worker gắn proxy provider khác nhau hoặc region khác nhau để tránh chung pool IP bị burn.
- Grafana dashboard show queue length, processing rate, error rate per worker.
Tổng chi phí 5 VPS Cloud VPS 80 + 1 Cloud VPS 40 = 4.4 triệu/tháng + proxy 2-5 triệu/tháng. Scrape được 200-500k page/ngày, tuỳ độ phức tạp site.
19. Browser warm-up và human-like behavior
async function humanLikeWarmup(page) {
// Random mouse move
for (let i = 0; i < 3; i++) {
await page.mouse.move(
Math.random() * 1000,
Math.random() * 800,
{ steps: 10 }
)
await page.waitForTimeout(500 + Math.random() * 1500)
}
// Random scroll
await page.evaluate(() => {
window.scrollTo({
top: Math.random() * document.body.scrollHeight,
behavior: 'smooth'
})
})
await page.waitForTimeout(2000 + Math.random() * 3000)
}
// Trong requestHandler
await humanLikeWarmup(page)
const data = await page.evaluate(...)Behavior random mouse, scroll, delay làm session trông giống user thật, đặc biệt hiệu quả với site dùng behavior analytics (Datadome, Akamai Bot Manager).
20. Parsing strategy: cheerio vs page.evaluate
page.evaluate chạy JavaScript trong context browser, hỗ trợ dynamic content. Cheerio parse HTML server-side, nhanh hơn cho site static:
import * as cheerio from 'cheerio'
const html = await page.content()
const $ = cheerio.load(html)
const data = {
title: $('h1').first().text().trim(),
price: $('[data-price]').attr('content'),
variants: $('.variant').map((i, el) => ({
color: $(el).find('.color').text(),
size: $(el).find('.size').text()
})).get()
}Cheerio nhanh hơn 5-10x so với page.evaluate cho parse phức tạp. Dùng khi đã load xong DOM và chỉ cần extract data structured.
21. Caching response để re-scrape nhanh
// Lưu HTML raw vào MinIO, parse khi cần
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
const s3 = new S3Client({ /* MinIO config */ })
async requestHandler({ page, request }) {
const html = await page.content()
await s3.send(new PutObjectCommand({
Bucket: 'scraper-raw-html',
Key: `${new Date().toISOString().slice(0,10)}/${encodeURIComponent(request.url)}.html.gz`,
Body: await gzipPromise(html),
ContentEncoding: 'gzip',
ContentType: 'text/html'
}))
// Parse và save structured data
const data = parseHtml(html)
await db.insert(data)
}Khi site thay đổi schema, không cần re-scrape từ đầu. Pull HTML raw từ MinIO, run parser mới. Tiết kiệm bandwidth và thời gian.
22. Tổng kết: stack scraper production ổn định
Sau 2 năm chạy production, stack ưa thích của mình là: Crawlee + Patchright + FingerprintGenerator + Bright Data residential proxy + Redis Request Queue + Postgres + MinIO raw HTML + systemd timer schedule + Grafana monitor. Tổng setup 1-2 tuần, sau đó maintenance gần như zero ngoại trừ khi site target update layout. Đầu tư ban đầu cao nhưng ROI rất tốt cho team thực sự cần data web.
VPS chạy Playwright scraper production
Cloud VPS TND sẵn AlmaLinux 9, Ubuntu 22/24, Debian 12/13. SSD CEPH, snapshot 1-click, backup hằng ngày, network 200Mbps trong nước. Cloud VPS 80 (4GB RAM) đủ chạy 5-10 browser concurrent, scrape 10-20k page/ngày. Scale lên Cloud VPS 160 cho 50k+ page.
Xem 8 cấu hình Cloud VPS →FAQ
Crawlee có miễn phí không?
Có, license Apache 2.0. Apify Platform (cloud hosted) thì tính phí, nhưng SDK Crawlee self-host hoàn toàn free. Bạn chỉ tốn tiền VPS và proxy.
Cần proxy không hay scrape qua IP VPS cũng được?
Tuỳ site target. Site nhỏ, ít bot protection, scrape qua IP VPS được. Site lớn (Amazon, LinkedIn, Cloudflare Enterprise) gần như bắt buộc residential proxy. Test 100 request đầu, nếu rate fail > 30% thì cần proxy.
Playwright vs Puppeteer cho scraping?
Playwright API tốt hơn, hỗ trợ multi-browser (Chromium, Firefox, WebKit), stealth tốt hơn out-of-box. Puppeteer chỉ Chromium, API cũ hơn. Mình recommend Playwright cho mọi project mới.
Scrape có hợp pháp tại Việt Nam không?
Public data, không bypass paywall hay auth, không spam personal data thì OK. Nghị định 13/2023 (PDPA VN) cấm thu thập dữ liệu cá nhân trái phép. Cẩn thận khi scrape email/phone. Tốt nhất hỏi luật sư cho use case cụ thể.
Làm sao tránh bị Cloudflare ban?
1) Residential proxy chất lượng. 2) Fingerprint random coherent. 3) Rate limit thấp (1 req/3s). 4) Cookie persistence per session. 5) Headers giả user thật. 6) Random user behavior (scroll, hover). Combine cả 6, bypass Cloudflare basic challenge dễ.
23. Khi nào nên skip scraping và dùng API
Nhiều site cung cấp public API hoặc data dump miễn phí: Wikipedia, OpenStreetMap, GitHub, StackOverflow, Reddit. Trước khi build scraper, search xem có official API hay không. API tốn ít resource hơn, ổn định hơn, không bị ban. Cho data E-commerce VN (Tiki, Shopee, Lazada), có vài data marketplace bán data với giá hợp lý (1-5 triệu/tháng), đôi khi rẻ hơn tự scrape sau khi tính proxy và maintenance cost. Đánh giá cẩn thận build vs buy trước khi commit code.



