สไปเดอร์ Scrapy ของคุณอาจดูเหมือนทำงานได้ดีในช่วงทดสอบ 100 หน้าแรก แต่พอไต่ไปถึง 10,000 หน้าแล้วดันล้มเอาเฉยๆ นั่นมักไม่ใช่บั๊กของการสแครปจริงๆ — แต่มันคือปัญหาเครือข่ายที่แต่งตัวมาเป็นบั๊กต่างหาก วิธีแก้ที่คนมักหยิบมาใช้ก็คือ Proxy และการใส่มันลงใน request.meta ใช้เวลาแค่ราวสามสิบวินาที
ปัญหาคือ วิธีแก้แบบสามสิบวินาทีมักเป็นจุดที่บทความพื้นฐานส่วนใหญ่หยุดไว้ พวกเขาโชว์แค่ meta={"proxy": "http://IP:PORT"} บางทีก็แถม middleware class มาให้ดูนิดหน่อย แล้วก็จบ แต่แทบไม่พูดถึงกรณีที่ Proxy ล่มระหว่าง crawl, การเผลอปล่อย credentials หลุดไปอยู่ใน Git history, หรือเหตุผลที่การ retry อาจย้อนกลับไปใช้ Proxy ตัวเดิมที่พังแล้วอีกครั้ง ประเด็นระดับโปรดักชันพวกนี้แหละคือสิ่งที่คู่มือนี้จะครอบคลุม: การตั้งค่าแบบ fail-closed, การจัดการ secret อย่างปลอดภัย, การเลือก Proxy อย่างมีเหตุผลตอน retry, ข้อจำกัดของโปรโตคอล, และการชั่งน้ำหนักต้นทุนที่วัดผลได้
Proxy Middleware ใน Scrapy คืออะไร?
Proxy middleware คือโค้ดชิ้นหนึ่งที่อยู่ใน downloader pipeline ของ Scrapy และทำหน้าที่ตัดสินว่า request แต่ละตัวควรถูกมองว่ามาจาก IP ไหน ก่อนจะถูกส่งออกไปจริง Scrapy มีตัว built-in อยู่แล้วชื่อ HttpProxyMiddleware ซึ่งจะอ่านคีย์ proxy จาก request.meta, แนบข้อมูลยืนยันตัวตนของ Proxy ให้ถ้าจำเป็น, แล้วส่ง request ต่อให้ download handler ที่เป็นคนเปิดการเชื่อมต่อจริง Custom proxy middleware จึงไม่ได้มาแทนขั้นตอน transport นั้น — แต่มันคือ ตัวเลือก ที่ตัดสินใจว่า Proxy ตัวไหนจะถูกใส่เข้าไป ก่อนที่กลไก built-in จะทำงาน ความต่างตรงนี้สำคัญกว่าที่ฟังดู และมันคือสาเหตุหลักของรายงานบั๊กประเภท “custom middleware ของฉันไม่ทำงาน” เสียเป็นส่วนใหญ่
ทำไม Proxy ถึงสำคัญกับโปรเจกต์ Scrapy ระดับโปรดักชัน
Proxy จะเปลี่ยนเส้นทางเครือข่ายและ IP ต้นทางที่มองเห็นได้ ซึ่งช่วยได้ทั้งในงานทดสอบตามภูมิภาค, การกระจายทราฟฟิกที่ได้รับอนุญาต, และการแยกความผิดพลาดของเครือข่ายออกจากกัน แต่มันไม่ได้ให้สิทธิ์พิเศษ, ไม่ได้ช่วยเลี่ยง rate limit อย่างถูกต้อง, และไม่รับประกันการเข้าถึง ทุกการ crawl จะต้องใช้ Proxy หรือไม่ ขึ้นอยู่กับเป้าหมาย, เงื่อนไขการใช้งาน, อัตราการส่ง request, และระดับความเสถียรที่งานนั้นต้องการ
Proxy แต่ละแบบก็ไม่ได้มีคุณภาพเท่ากัน และถ้าเลือกผิด ก็อาจกลายเป็น incident ในโปรดักชันได้ไม่แพ้กัน:
| ประเภท Proxy | ความเสถียร | ความเร็ว | ความเสี่ยงถูกตรวจจับ | ต้นทุนโดยทั่วไป |
|---|---|---|---|---|
| Free public proxies | ผันผวนมาก | ผันผวน | มักสูง | ไม่เสียค่าบริการ แต่มีความเสี่ยงด้านความปลอดภัยและการปฏิบัติการสูง |
| Datacenter proxies | ขึ้นอยู่กับผู้ให้บริการและเป้าหมาย | มักเร็ว | ขึ้นอยู่กับเป้าหมาย | มักคิดราคาเป็น GB หรือเป็น IP |
| Residential proxies | ขึ้นอยู่กับผู้ให้บริการและเป้าหมาย | ผันผวน | ขึ้นอยู่กับเป้าหมาย | มักคิดราคาเป็น GB |
| ISP proxies | ขึ้นอยู่กับผู้ให้บริการและเป้าหมาย | ผันผวน | ขึ้นอยู่กับเป้าหมาย | แตกต่างตามผู้ให้บริการ |
ควรพูดถึง free proxy แยกต่างหาก เพราะความเสี่ยงที่วัดได้มันสูงจริงๆ งานวิจัย Free Proxies Unmasked ระยะเวลา 30 เดือน ติดตามที่อยู่มากกว่า 640,000 รายการจากผู้ให้บริการ 11 ราย พบว่า 34.5% เปิดใช้งานอย่างน้อยหนึ่งครั้ง และมี 16,923 รายการที่มีการแก้ไขเนื้อหา ข้อมูลชุดนี้ช่วยยืนยันคำเตือนด้านความปลอดภัยสำหรับ public list ได้ชัดมาก แต่ไม่ได้แปลว่าอัตราความล้มเหลวแบบเดียวกันจะใช้ได้กับทุก list, ทุก pool แบบเสียเงิน, ทุกเป้าหมาย, หรือทุก workload ในปัจจุบัน
Proxy ก็ไม่ได้ทำให้ bot detection สมัยใหม่พังไปเองโดยอัตโนมัติ บริการอย่าง Cloudflare ประเมิน request จากสัญญาณนับสิบ — ตั้งแต่ TLS fingerprint, ความสอดคล้องของ headers, การรัน JavaScript, ไปจนถึงพฤติกรรมการใช้งาน — โดย IP เป็นเพียงหนึ่งในหลายปัจจัยเท่านั้น ต่อให้ใช้ residential proxy ที่ดูสะอาด แต่ request มี headers ขัดกันและไม่มี cookie jar ก็ยังถูกตั้งธงได้อยู่ดี อย่าคาดหวังเกินจริงจนสร้างสถาปัตยกรรมทั้งระบบบนแนวคิดว่า “แค่หมุน IP ก็พอ”
ก่อนเริ่มต้น
- ระดับความยาก: ปานกลาง
- เวลาที่ใช้: ประมาณ 30–45 นาทีสำหรับการตั้งค่าแบบโปรดักชันเต็มรูปแบบ, ประมาณ 5 นาทีสำหรับการทดสอบแบบเร็ว
- สิ่งที่ต้องมี: Python 3.9+, ติดตั้ง Scrapy แล้ว (คู่มือนี้ทดสอบกับ Scrapy 2.17.0 ซึ่งเปิดตัวในเดือนกรกฎาคม 2026), สไปเดอร์ที่ใช้งานได้จาก
scrapy startproject, และอย่างน้อยหนึ่ง Proxy endpoint (ทดลองใช้ฟรีจากผู้ให้บริการ datacenter proxy รายใดก็ได้ก็พอสำหรับการทดสอบ)
ขั้นตอนที่ 1: ทดสอบ Proxy ด้วยพารามิเตอร์ Request Meta
วิธีที่เร็วที่สุดในการยืนยันว่า Proxy ใช้ได้จริง คือข้ามสถาปัตยกรรม middleware ไปก่อน แล้วลองใช้มันตรงๆ เลย
HttpProxyMiddleware ที่มีอยู่ใน Scrapy จะอ่านคีย์ proxy จาก request.meta โดยตรง แล้วส่ง request ผ่านมันไป ไม่ต้องแก้ settings, ไม่ต้องสร้าง middleware class — ใช้แค่ keyword argument เดียวเท่านั้น
import scrapy
class ProxyTestSpider(scrapy.Spider):
name = "proxy_test"
start_urls = ["https://httpbin.org/ip"]
def start_requests(self):
for url in self.start_urls:
yield scrapy.Request(
url,
meta={"proxy": "http://203.0.113.10:8080"},
callback=self.parse,
)
def parse(self, response):
self.logger.info(response.text)
รันด้วย scrapy runspider proxy_test.py ถ้า Proxy ใช้งานได้ httpbin.org/ip จะสะท้อน IP ของ Proxy กลับมาแทน IP ของคุณเอง — นั่นคือสัญญาณยืนยันของคุณ ถ้า response ค้างแล้วสุดท้ายขึ้น TCP connection timed out แปลว่า Proxy ตายหรือเข้าถึงไม่ได้ ซึ่งพูดตรงๆ ก็คือเกิดขึ้นบ่อยกว่าที่ผู้ขาย Proxy อยากยอมรับ
วิธีนี้เหมาะกับสไปเดอร์แบบทำครั้งเดียวหรือใช้ทดสอบเร็วๆ แต่จะเริ่มลำบากทันทีที่คุณมีสไปเดอร์มากกว่าหนึ่งตัว เพราะจากนั้นคุณจะต้อง hardcode สตริง Proxy เดิมซ้ำไปมาหลายไฟล์
ขั้นตอนที่ 2: สร้าง Custom Proxy Middleware
ถ้าไม่ใช่สไปเดอร์ตัวเดียว คุณควรรวม logic ของ Proxy ไว้ที่จุดเดียว สร้างคลาส ProxyMiddleware ใน middlewares.py ของโปรเจกต์:
from scrapy.exceptions import NotConfigured
class ProxyMiddleware:
def __init__(self, proxy_url):
self.proxy_url = proxy_url
@classmethod
def from_crawler(cls, crawler):
proxy_url = crawler.settings.get("PROXY_URL")
if not proxy_url:
raise NotConfigured("PROXY_URL is required; refusing silent direct fallback")
return cls(proxy_url=proxy_url)
def process_request(self, request, spider):
if "proxy" not in request.meta:
request.meta["proxy"] = self.proxy_url
แล้วลงทะเบียนใน settings.py:
import os
PROXY_URL = os.environ.get("PROXY_URL")
DOWNLOADER_MIDDLEWARES = {
"myproject.middlewares.ProxyMiddleware": 350,
}
สังเกตว่าใช้ from_crawler classmethod แทน __init__ ธรรมดา นี่คือรูปแบบที่ Scrapy แนะนำจริงสำหรับการอ่าน settings และเป็น hook เดียวกับที่เราจะกลับมาใช้อีกครั้งในขั้นตอนที่ 4 ตอนพูดถึง secret ข้อดีของแนวทางนี้ตรงไปตรงมามาก: เปลี่ยนค่า setting ตัวเดียว สไปเดอร์ทุกตัวในโปรเจกต์จะใช้ Proxy ใหม่ทันที ไม่ต้องไล่ grep หา IP ที่ตายไปตามไฟล์ต่างๆ
ขั้นตอนที่ 3: เข้าใจลำดับการทำงานของ Middleware (ไม่งั้น Proxy ของคุณอาจเหมือนไม่ทำอะไรเลย)
นี่คือส่วนที่บทความส่วนใหญ่มักข้ามด้วยประโยคประมาณว่า “ตั้ง priority เป็น 350 ก็พอ เชื่อฉัน” Downloader middleware ใน Scrapy ทำงานตามลำดับที่แน่นอนและคาดเดาได้ ถ้าคุณไม่เข้าใจ มันจะกลายเป็นบั๊กที่ดูเหมือนไม่เกี่ยวกับ Proxy เลย
hook ฝั่ง request (process_request) จะทำงานตามลำดับ priority แบบ น้อยไปมาก — เลขต่ำก่อน ส่วน hook ฝั่ง response (process_response, process_exception) จะทำงานแบบ มากไปน้อย — เลขสูงก่อน แล้วค่อยย้อนกลับลงมาตาม chain
เส้นทาง Request (ascending):
Spider → [100] RobotsTxt → [300] HttpAuth → [350] YOUR PROXY MIDDLEWARE
→ [500] UserAgent → [550] Retry → [700] Cookies → [750] HttpProxy
→ Downloader
เส้นทาง Response (descending):
Downloader → [750] HttpProxy → [700] Cookies → [550] Retry
→ [500] UserAgent → [350] YOUR PROXY MIDDLEWARE → [300] HttpAuth
→ [100] RobotsTxt → Spider
นี่คือตาราง priority ค่าเริ่มต้นปัจจุบันของ built-in middleware ใน Scrapy ที่ตรวจสอบกับ 2.17.0 แล้ว:
| Middleware | Priority เริ่มต้น |
|---|---|
| RobotsTxtMiddleware | 100 |
| HttpAuthMiddleware | 300 |
| DownloadTimeoutMiddleware | 350 |
| DefaultHeadersMiddleware | 400 |
| UserAgentMiddleware | 500 |
| RetryMiddleware | 550 |
| RedirectMiddleware | 600 |
| CookiesMiddleware | 700 |
| HttpProxyMiddleware | 750 |
| DownloaderStats | 850 |
| HttpCacheMiddleware | 900 |
เมื่อ RetryMiddleware สั่ง retry มันจะคัดลอก request ที่ล้มเหลว—including metadata—แล้ว request ใหม่นั้นจะกลับเข้าสู่ downloader middleware chain อีกครั้ง ความสัมพันธ์ของ selector กับ priority 550 จึง ไม่ได้ การันตีการ rotate การหมุน Proxy จะเกิดขึ้นก็ต่อเมื่อโค้ดของ selector ตรวจจับได้ว่าเป็น retry แล้วจงใจเขียนทับ meta["proxy"] ที่ถูกคัดลอกมา Priority 350 เป็นตำแหน่งที่สะดวกสำหรับ selector เพราะมันทำงานก่อน transport step ของ built-in ที่ 750 แต่ตัวมันเองไม่ใช่กลไก rotation

ข้อผิดพลาดที่พบบ่อยเกี่ยวกับลำดับ Middleware
- ตั้ง middleware ของคุณที่ priority เดียวกับ
HttpProxyMiddleware(750): จะเกิด race condition ที่ทำให้ dict-ordering ของ Scrapy (ไม่ใช่ logic ของคุณ) เป็นตัวตัดสินว่า middleware ไหนได้ประมวลผล request ก่อน อาการคือพฤติกรรม Proxy แปลกๆ แบบเกิดบ้างไม่เกิดบ้าง - ใช้
setdefault()หรือif "proxy" not in request.metaใน selector แบบหมุน Proxy: retry ที่ถูกคัดลอกจะเก็บ Proxy ตัวเดิมไว้ อาการคือ retry ทุกครั้งวิ่งเส้นทางเดิมที่ล้มเหลวซ้ำๆ วิธีแก้คือจับให้ได้ว่าเป็น retry (เช่นretry_times > 0) แล้วเขียนทับค่า proxy ที่ selector เป็นคนดูแลโดยตรง - ปิด
HttpProxyMiddlewareไปเลย: บางบทความแนะนำให้ตั้ง"scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware": Noneเพราะบอกว่า “custom middleware จัดการให้แล้ว” แต่จริงๆ ไม่ใช่ — custom middleware ของคุณเป็นแค่ selector ไม่ใช่ transport layer การปิด built-in ตัวนี้หมายความว่า header สำหรับ proxy authentication จะไม่ถูกแนบไป และ request ของคุณจะถูกส่งออกไปแบบไม่ยืนยันตัวตน (หรือไม่ถูกส่งออกไปเลย)
ขั้นตอนที่ 4: เลิก hardcode credentials ของ Proxy
บทความสอน Scrapy เกี่ยวกับ Proxy ที่ขึ้นอันดับสูงๆ แทบทุกชิ้นที่ผมเจอ จะเขียน http://username:password@proxy.example.com:8080 ลงในไฟล์ Python ตรงๆ นั่นคือ credentials ที่จะติดอยู่ใน Git history ตลอดไป โผล่ในทุก clone ทุก fork และทุก log dump ถ้ามีใครเผลอใช้ print() แบบไม่ระวัง
จริงๆ แล้วการทำแบบนี้มีอยู่สามระดับ:
| วิธีการ | ความปลอดภัย | ความยืดหยุ่น | เหมาะกับ |
|---|---|---|---|
| hardcode ใน spider/settings.py | แย่ — secret อยู่ใน repo | ต่ำ | ทดสอบ local แบบรวดเร็วเท่านั้น |
environment variable http_proxy (รองรับโดย Scrapy โดยตรง) | ดีกว่า — ไม่อยู่ในโค้ด | ต่ำ (Proxy เดียว) | CI/CD pipelines, Docker |
.env + python-dotenv + from_crawler | ดีที่สุด — ไม่อยู่ในโค้ด, แยกตาม environment ได้ | สูง (หลาย Proxy, rotation) | ตัวดึงข้อมูลระดับโปรดักชัน |
ตัวเลือกที่สามคุ้มค่าที่จะทำให้ถูกต้องตั้งแต่แรก ติดตั้ง python-dotenv, สร้างไฟล์ .env (แล้วรีบเพิ่มเข้า .gitignore ทันที — พูดจริง ทำเดี๋ยวนี้เลย):
PROXY_USER=myuser
PROXY_PASSWORD=my$ecret!Pass
PROXY_HOST=proxy.example.com
PROXY_PORT=8080
โหลดค่าขึ้นมาตั้งแต่ต้นของ settings.py:
from dotenv import load_dotenv
import os
load_dotenv()
PROXY_USER = os.getenv("PROXY_USER")
PROXY_PASSWORD = os.getenv("PROXY_PASSWORD")
PROXY_HOST = os.getenv("PROXY_HOST")
PROXY_PORT = os.getenv("PROXY_PORT")
จากนั้นอ่านค่าอย่างปลอดภัยภายใน middleware โดยใช้ from_crawler — นี่คือรูปแบบที่ช่วยแก้ความสับสนที่ผมเห็นในฟอรัมบ่อยๆ ว่า “แล้วจะตั้งค่านี้ก่อน scrapy crawl ยังไง ถ้า credentials ต้องเปลี่ยนทุกครั้งที่รัน?”:
import os
from urllib.parse import quote
class SecureProxyMiddleware:
def __init__(self, user, password, host, port):
self.user = quote(user, safe="")
self.password = quote(password, safe="")
self.host = host
self.port = port
@classmethod
def from_crawler(cls, crawler):
settings = crawler.settings
return cls(
user=settings.get("PROXY_USER"),
password=settings.get("PROXY_PASSWORD"),
host=settings.get("PROXY_HOST"),
port=settings.get("PROXY_PORT"),
)
def process_request(self, request, spider):
proxy_url = f"http://{self.user}:{self.password}@{self.host}:{self.port}"
request.meta["proxy"] = proxy_url
สังเกตการเรียก urllib.parse.quote() ครอบ credentials ไว้ หากรหัสผ่านของคุณมี @, :, หรือ / อยู่ด้วย (และ password generator ก็มักสุ่มสัญลักษณ์พวกนี้มาให้) URL parsing จะพังทันทีถ้าไม่เข้ารหัสก่อนแบบ percent-encoding นี่คือบรรทัดเดียวที่ช่วยประหยัดเวลาหงุดหงิดเป็นชั่วโมงกับ error แบบ “invalid proxy URL” ที่จริงๆ แล้วไม่ได้เกิดจาก Proxy ผิดอะไรเลย
เก็บ credentials ให้พ้นจาก logs และทดสอบ percent-encoding ด้วยค่าตัวอย่างที่ดูสมจริงแต่เป็นข้อมูลปลอมไว้ก่อน HTTP tunneling, SOCKS5, และ credentials ที่ไม่ใช่อักษรละตินมีขอบเขตการทำงานที่ต่างกันตาม handler; อย่าคิดว่ารูปแบบ authentication เดียวจะใช้ได้เหมือนกันทุกกรณีโดยไม่ทำ integration test แบบล็อกเวอร์ชัน

ขั้นตอนที่ 5: เพิ่ม Proxy Rotation
Proxy เดียว — ต่อให้ดีแค่ไหน — ถ้าวิ่ง 5,000 requests ไปยังเว็บเดียวกัน สุดท้ายก็มีโอกาสโดนจับได้ คุณต้องมี pool
ตัวเลือก A — สร้างเอง วิธีนี้ง่ายจริงๆ:
import random
class RotatingProxyMiddleware:
def __init__(self, proxy_pool):
self.proxy_pool = proxy_pool
@classmethod
def from_crawler(cls, crawler):
return cls(proxy_pool=crawler.settings.getlist("PROXY_POOL"))
def process_request(self, request, spider):
request.meta["proxy"] = random.choice(self.proxy_pool)
วิธีนี้ใช้ได้กับงานพื้นฐาน แต่ไม่มีการรับรู้เลยว่า Proxy ตัวไหนยังมีชีวิตอยู่จริง คุณกำลังโยนลูกเต๋าใหม่ทุก request
ตัวเลือก B — ใช้ scrapy-rotating-proxies แพ็กเกจจากภายนอกตัวนี้เพิ่มการตรวจจับการถูกบล็อกและ backoff อัตโนมัติมาให้ตั้งแต่ต้น:
pip install scrapy-rotating-proxies
ขอพูดตรงๆ ตรงนี้: release ล่าสุดบน PyPI คือเวอร์ชัน 0.6.2 ที่ลงวันที่ปี 2019 และโปรเจกต์ถูกแท็กว่า Alpha มันไม่ได้หมายความว่ามันพังแน่ๆ บน Scrapy รุ่นใหม่ แต่คำว่า “ไม่ได้ดูแลมาตั้งแต่ 2019” ไม่ได้แปลว่า “พร้อมรับทราฟฟิกโปรดักชันปี 2026” ให้ pin เวอร์ชัน, ทดสอบกับเว็บเป้าหมายจริงของคุณ, และอย่าคิดว่ามันรองรับ authenticated proxy endpoint ได้สมบูรณ์ — ส่วนใหญ่แล้วมันไม่รองรับ
ขั้นตอนที่ 6: สร้าง Proxy Middleware ที่ทนต่อความผิดพลาดได้ (ตรวจจับ Proxy ตาย)
นี่คือส่วนที่บทความคู่แข่งแทบทุกชิ้นข้ามไปหมด และมันคือความต่างระหว่างเดโม กับระบบที่รัน crawl ยาว 6 ชั่วโมงแบบไม่มีคนเฝ้าแล้วยังรอด
| สถานการณ์ | สิ่งที่บทความส่วนใหญ่มักแสดง | สิ่งที่ middleware นี้เพิ่มให้ |
|---|---|---|
| Proxy ตอบ 407 | ไม่ได้กล่าวถึง | มองว่าเป็นความล้มเหลวของการยืนยันตัวตนกับ Proxy |
| เป้าหมายตอบ 403/429 | มักจับรวมกันหมด | แยกสัญญาณด้าน policy/rate ออกจากสุขภาพของ Proxy |
| Proxy timeout | ไม่ได้กล่าวถึง | ตั้ง threshold timeout ได้, health score ลดลง |
| Proxy ตายหมด | ไม่ได้กล่าวถึง | fallback อย่างเหมาะสม หรือหยุด crawl พร้อม log เตือน |
| Proxy กระพริบไปมา (ติดๆ ดับๆ) | ไม่ได้กล่าวถึง | มีช่วงพักก่อนนำกลับเข้ากลุ่ม |
import time
import random
from scrapy.exceptions import IgnoreRequest
class FaultTolerantProxyMiddleware:
MAX_FAILURES = 3
COOLDOWN_SECONDS = 300
def __init__(self, proxy_pool):
self.pool = {p: {"failures": 0, "banned_until": 0} for p in proxy_pool}
@classmethod
def from_crawler(cls, crawler):
return cls(proxy_pool=crawler.settings.getlist("PROXY_POOL"))
def _healthy_proxies(self):
now = time.time()
return [p for p, s in self.pool.items() if s["banned_until"] < now]
def process_request(self, request, spider):
healthy = self._healthy_proxies()
if not healthy:
spider.logger.warning("Proxy ทั้งหมดมีปัญหา — กำลังพัก crawl")
raise IgnoreRequest("No healthy proxies available")
request.meta["proxy"] = random.choice(healthy)
def process_response(self, request, response, spider):
proxy = request.meta.get("proxy")
if proxy and response.status == 407:
self._mark_failure(proxy)
return response
def process_exception(self, request, exception, spider):
proxy = request.meta.get("proxy")
if proxy:
self._mark_failure(proxy)
def _mark_failure(self, proxy):
state = self.pool[proxy]
state["failures"] += 1
if state["failures"] >= self.MAX_FAILURES:
state["banned_until"] = time.time() + self.COOLDOWN_SECONDS
state["failures"] = 0
มีข้อสังเกตจากประสบการณ์จริงตอนสร้างแบบนี้: อย่ามองทุก 403 ว่าเป็นหลักฐานว่า Proxy ตาย — RFC 9110 ให้ความหมาย 403 ว่า “server เข้าใจแต่ปฏิเสธ” ซึ่งอาจหมายถึง headers หรือ session ของคุณน่าสงสัยก็ได้ ไม่ใช่เพราะ Proxy อย่างเดียว ส่วน 407 นั้นหมายความว่า Proxy เองกำลังปฏิเสธการยืนยันตัวตนของคุณ — สัญญาณนี้แรงและเฉพาะเจาะจงกว่ามาก การเอาสัญญาณพวกนี้ไปรวมกองเดียวกันมักทำให้คนเผา Proxy ที่ยังดีอยู่ทิ้งไปโดยไม่มีเหตุผล
ในโปรดักชันควรตั้งค่า retry ของ Scrapy ให้ชัดเจน เพื่อให้ reviewer เห็น policy ได้เลย:
RETRY_HTTP_CODES = [500, 502, 503, 504, 522, 524, 408, 429]
RETRY_TIMES = 2
นี่คือค่า default ของ Scrapy 2.17 อย่าใส่ 403 หรือ 407 แบบหว่านรวมเข้าไป: 403 คือการปฏิเสธจากเป้าหมายที่อาจมีหลายสาเหตุ ส่วน 407 คือปัญหา proxy authentication ที่ retry request ทั่วไปแก้ไม่ได้ ถ้ามีเหตุผลเฉพาะของสัญญาเป้าหมายที่ต้อง retry โค้ดสถานะอื่น ให้บันทึกเหตุผลนั้นให้ชัดและทดสอบแยกต่างหาก

ขั้นตอนที่ 7: รูปแบบ Proxy-on-Retry แบบวัดผลได้
บาง target ที่ได้รับอนุญาตอาจตอบเนื้อหาปกติได้โดยตรง และต้องใช้ Proxy แค่หลังจากเจอสถานะชั่วคราวที่ระบุไว้แล้ว วิธีนี้ช่วยลด byte ที่ต้องผ่าน Proxy ได้ แต่ไม่มีเปอร์เซ็นต์ประหยัดแบบใช้ได้กับทุกกรณี หรืออัตราความสำเร็จของ direct request ที่ยกไปใช้ข้ามงานได้แบบน่าเชื่อถือ คุณต้องวัด workload ของตัวเองก่อนค่อยใช้แนวทางนี้
ใช้ helper สาธารณะ get_retry_request() ของ Scrapy และจำกัดการ escalation ไว้เฉพาะสถานะที่ผูกกับ target นั้นและได้จำแนกไว้ชัดเจนแล้ว ตัวอย่างนี้ถือว่า 429 และ 503 เป็นสัญญาณแรงกดดันที่ retry ได้; ตั้งใจไม่รวม 403 และ 407
from scrapy.downloadermiddlewares.retry import get_retry_request
class CostAwareEscalationMiddleware:
def process_response(self, request, response, spider):
if response.status not in {429, 503}:
return response
retry = get_retry_request(
request,
spider=spider,
reason=f"proxy_escalation_{response.status}",
max_retry_times=2,
)
if retry is None:
return response
current_tier = request.meta.get("proxy_tier", "direct")
if current_tier == "direct":
retry.meta["proxy"] = spider.settings["DATACENTER_PROXY"]
retry.meta["proxy_tier"] = "datacenter"
elif current_tier == "datacenter":
retry.meta["proxy"] = spider.settings["RESIDENTIAL_PROXY"]
retry.meta["proxy_tier"] = "residential"
else:
return response
return retry
ให้บันทึกอัตรา valid content ของ direct, อัตรา valid content ของ proxied, bytes ต่อหนึ่ง record ที่สำเร็จ, จำนวน retries ต่อความสำเร็จหนึ่งครั้ง, และ latency ที่เพิ่มขึ้น การออกแบบแบบ direct-first จะใช้ได้ก็ต่อเมื่อ direct access ได้รับอนุญาต และระบบต้อง fail closed ทันทีเมื่อจำเป็นต้องใช้ Proxy การประหยัดที่แท้จริงคือการลดปริมาณทราฟฟิกผ่าน Proxy ที่วัดได้ ไม่ใช่ตัวเลขเปอร์เซ็นต์ที่เดากันเอา
เมื่อการจัดการ Proxy ด้วยตัวเองไม่คุ้มค่า
ขอพูดตรงๆ ก่อนว่าไม่อยากทำให้ทั้งบทความดูไร้ประโยชน์: ทุกอย่างข้างบนเป็นวิศวกรรมที่จริงและมีประโยชน์ และสำหรับหลายโปรเจกต์ — การ crawl ปริมาณมาก, pipeline แบบกำหนดเอง, หรืออะไรที่ต้องควบคุม scheduling ของ request แบบเต็มรูปแบบ — นี่คือแนวทางที่ถูกต้อง
แต่ถ้าเป้าหมายจริงของคุณคือ “ดึงข้อมูลที่เป็นโครงสร้างจากหน้านี้” ไม่ใช่ “ดูแลโครงสร้างพื้นฐานของ Proxy” บางที extraction API อาจเป็นขอบเขตที่เหมาะกว่า เอกสาร POST /extract ล่าสุดของ Thunderbit รับ page URL และ JSON Schema แบบเลือกใส่ได้; ถ้าไม่ส่ง schema มา ระบบสามารถสร้าง schema จากเนื้อหาหน้าเว็บให้ได้ด้วย endpoint นี้ยังมีโหมด render แบบ none, basic, และ full พร้อมตัวควบคุม timeout และระยะรอหลังโหลด การดึงข้อมูลแบบใช้ prompt อย่างเดียวไม่ได้อยู่ใน request surface ที่รองรับอยู่ตอนนี้ ดังนั้นการทำ integration ระดับโปรดักชันควรยึดตามสัญญา schema ที่เอกสารระบุไว้ วิธีนี้ย้าย interface สำหรับ extraction ไปไว้หลัง request เดียว; แต่มันไม่ได้หมายความว่าจะรับประกันผลกับทุกเป้าหมายที่มี anti-bot หรือ CAPTCHA เสมอ
| ปัจจัย | DIY Scrapy + Proxy | การดึงข้อมูลผ่าน API (เช่น Thunderbit) |
|---|---|---|
| ความยุ่งยากในการตั้งค่า | สูง — ต้องมี middleware, rotation, retry logic | ต่ำ — เรียก API ครั้งเดียวพร้อม schema |
| พฤติกรรมเครือข่าย/rendering | คุณเป็นคนกำหนด handler, proxy, headers, และ delays | ควบคุมผ่านตัวเลือก API ที่ระบุไว้ |
| การดูแลรักษา | คุณต้องดูแล selector, สุขภาพของ pool, และการเปลี่ยนแปลงของ target เอง | คุณดูแลคุณภาพ schema, validation, และพฤติกรรม integration |
| รูปแบบต้นทุน | ค่า Proxy + compute + เวลา engineering | เอกสารปัจจุบันระบุ 20 units ต่อหนึ่งหน้า (ตรวจสอบเมื่อ 2026-08-10) |
| การควบคุม | เต็มที่ — custom pipeline, middleware chain | จำกัดตามความสามารถของ API |
| เหมาะที่สุดสำหรับ | การ crawl ซับซ้อน, ปริมาณสูง, logic เฉพาะทาง | การดึงข้อมูลเฉพาะจุด, การทำ prototype, enrichment |
ถ้าคุณส่วนใหญ่ต้องการดึงหน้าที่มีข้อมูลเป็นโครงสร้างสำหรับ lead list, ข้อมูลสินค้า, หรือการวิจัย ให้ลอง pilot กับ Thunderbit Chrome Extension หรือ API บนตัวอย่างงานจริง แล้วเทียบจำนวนเรคคอร์ดที่ใช้ได้, latency, units, และเวลาที่ต้องใช้ดูแลรักษา ถ้าโปรเจกต์ของคุณต้องการกราฟของการ crawl แบบกำหนดเองและควบคุม pipeline เต็มรูปแบบ Scrapy ยังคงเป็นตัวเลือกที่แข็งแรงกว่า
อ่านเพิ่มเติม
เคล็ดลับและข้อผิดพลาดที่พบบ่อย
- เคล็ดลับ: ทดสอบ Proxy กับ
httpbin.org/ipก่อนจะเอาไปยิงใส่ target จริง วิธีนี้เร็วที่สุดในการยืนยันว่า routing ทำงานถูกต้อง ก่อนที่คุณจะเพิ่มความซับซ้อนเข้าไปอีกชั้น - ข้อผิดพลาด: ตั้ง Proxy ไว้ใน
request.headersแทนrequest.metaนี่เป็นบั๊กที่คนพลาดกันบ่อยแบบใกล้เคียงกับการพิมพ์ผิดHttpProxyMiddlewareอ่านแค่meta["proxy"]และการพยายามใส่ผ่าน header จะล้มเหลวแบบเงียบๆ โดยไม่มี error ที่ชัดเจน - ข้อผิดพลาด: คิดว่า HTTP และ HTTPS proxy ตั้งค่าเหมือนกันทั้งหมด URL ของ HTTP proxy ที่ส่งต่อไปยังปลายทาง HTTPS โดยทั่วไปใช้งานได้ผ่าน CONNECT tunneling ภายใต้ handler ที่รองรับ แต่การใช้สคีม
https://สำหรับตัว endpoint ของ proxy เองเป็นคนละเรื่อง และรองรับน้อยกว่า อย่าสับสนสองอย่างนี้เข้าด้วยกัน - เคล็ดลับ: ถ้าคุณต้องใช้ SOCKS5 ให้ตรวจสอบความสามารถของ download handler ในเวอร์ชัน Scrapy ของคุณก่อน Scrapy 2.17 มี Httpx handler แบบ experimental ที่เพิ่ม SOCKS5 support ผ่าน
httpx[socks]แต่ยังถูกติดป้ายว่า experimental อยู่ — อย่าสร้าง dependency สำหรับโปรดักชันบนมันโดยไม่ทดสอบด้วยตัวเอง
วิธีอื่นที่เป็นไปได้
นอกจาก scrapy-rotating-proxies แล้ว บางทีมเลือกส่ง logic ของ Proxy ทั้งหมดผ่าน provider gateway — คือใช้ proxy URL เดียว แล้วปล่อยให้ vendor จัดการ rotation, session stickiness, และ geo-targeting อยู่เบื้องหลัง แนวทางนี้แลกความควบคุมบางส่วนกับการลดโค้ด middleware ลงมาก และควรลองประเมินราคาดูเทียบกับ pool ที่คุณดูแลเอง ก่อนจะลงมือสร้างทั้งหมดตั้งแต่ศูนย์
สรุป
การตั้ง Proxy ใน Scrapy ใช้แค่หนึ่งบรรทัด แต่การทำให้การตั้งค่านั้นรอดผ่าน crawl ระดับโปรดักชันจริง ต้องมี policy ความล้มเหลวที่ชัดเจน, credentials ที่ป้องกันไว้, boundary ของ handler ที่ผ่านการทดสอบ, และ selector code ที่ตั้งใจเขียนทับ metadata ของ Proxy ที่ถูกคัดลอกมาเมื่อ retry ถ้าจะจำแค่อย่างสองอย่าง ให้จำสองข้อนี้: fail closed เมื่อจำเป็นต้องใช้ Proxy และอย่า commit รหัสผ่านของ Proxy ลงในไฟล์ Python เด็ดขาด
คำถามที่พบบ่อย
จะตั้งค่า custom proxy ใน Scrapy พร้อมการยืนยันตัวตนได้อย่างไร?
ใช้รูปแบบ URL protocol://username:password@host:port แต่ถ้า username หรือ password มีอักขระพิเศษ ให้ percent-encode ด้วย urllib.parse.quote() ก่อน สำหรับโปรดักชัน ควรอ่าน credentials ผ่าน classmethod from_crawler ที่ดึงค่าจาก environment variables แทนการ hardcode
ควรใช้หมายเลข priority เท่าไหร่สำหรับ custom proxy middleware ใน Scrapy?
350 เป็นค่า priority ที่ใช้กันบ่อยสำหรับ selector เพราะมันทำงานก่อน HttpProxyMiddleware ที่ 750 แต่มันไม่ได้การันตีการ rotate การ retry จะได้ Proxy ใหม่ก็ต่อเมื่อ selector ตรวจจับ request ที่ถูกคัดลอกตอน retry ได้ แล้วเขียนทับค่า meta["proxy"] เดิม
จะจัดการ Proxy ที่ตายใน Scrapy แบบอัตโนมัติได้อย่างไร?
สร้าง middleware ที่นับความล้มเหลวของแต่ละ Proxy ใน process_response และ process_exception, นำ Proxy ออกจาก pool ที่ใช้งานอยู่เมื่อถึง threshold ของความล้มเหลว, แล้วค่อยนำกลับมาใช้ใหม่หลังช่วง cooldown แทนที่จะแบนถาวร
ใช้ Scrapy กับ SOCKS5 proxy ได้ไหม?
ใน Scrapy 2.17 HttpxDownloadHandler แบบ experimental รองรับ SOCKS5 เมื่อมี httpx[socks] ติดตั้งอยู่ ส่วน HTTP/1.1 handler เริ่มต้นไม่ได้รองรับ SOCKS proxy ให้ pin handler/version และทำ integration test ก่อนจะถือว่าใช้ในโปรดักชันได้
รูปแบบ proxy-on-retry ช่วยประหยัดต้นทุน proxy ได้มากแค่ไหน?
ไม่มีเปอร์เซ็นต์กลางที่ใช้ได้กับทุกกรณี ให้วัดสัดส่วน request ที่ได้รับอนุญาตและได้ผลลัพธ์ตรงๆ, bytes ที่วิ่งผ่านแต่ละ proxy tier, retries ต่อหนึ่งเรคคอร์ดที่สำเร็จ, และ latency ที่เพิ่มขึ้น การลดปริมาณทราฟฟิกผ่าน proxy ที่วัดได้คือเงินที่คุณประหยัดจริง; ถ้า direct-first access ไม่ได้รับอนุญาตหรือให้ผลไม่ถูกต้อง อย่าใช้รูปแบบนี้


