บทความ “ตัวแยกวิเคราะห์ HTML Python ที่เร็วที่สุด” เกือบทุกชิ้นมักจะหยิบ selectolax ขึ้นมาอ้างถึง และมักจะสรุปกันสั้น ๆ แค่ว่า “เร็วกกว่า BeautifulSoup มาก” ซึ่งก็จริง แต่สิ่งที่แทบไม่มีใครพูดต่อคือ ถ้าเอา selectolax ไปเทียบกับ lxml แบบตรง ๆ จะเป็นยังไง — เพราะพอถึงตรงนั้น คำว่า “เร็วที่สุด” ก็ต้องมีเครื่องหมายดอกจันต่อท้ายทันที
ผมเลยทำ benchmark ให้เป็นเรื่องเป็นราว: เอา selectolax ทั้งสอง backend ไปเทียบกับ lxml, BeautifulSoup บน html.parser และบน lxml, รวมถึง parsel ครอบคลุม 5 ขนาดหน้า ตั้งแต่ 1 KB ถึง 10 MB โดยวัดค่ามัธยฐานจากการรันแยกโปรเซส 3 ครั้งต่อชุด ผลคือ selectolax ชนะ BeautifulSoup แบบขาดลอย และสูสีกับ raw lxml — แต่กลับแพ้ lxml ในขั้นตอน pure parsing ข้อมูลทั้งหมดด้านล่างเป็นผลชั่วคราวจากเครื่องเดียว (macOS arm64, Python 3.14.2) และสคริปต์ได้คอมมิตไว้แล้ว ดังนั้นถ้าจะอ้างอิง ให้ลองรันบนเครื่องตัวเองก่อน
selectolax คืออะไรกันแน่ และไม่ใช่อะไร
selectolax คือ Python binding สำหรับ C engine 2 ตัว — Modest และ Lexbor — ใช้แยกวิเคราะห์ HTML5 และคิวรีด้วย CSS selector มันไม่ใช่ crawler, ไม่ใช่ browser และไม่ใช่ “scraper” แบบกดปุ่มแล้วจบ มันคือส่วนที่คุณส่ง HTML ก้อนหนึ่งให้ หลังจากคุณดึงข้อมูลมาแล้ว คำอธิบายสั้น ๆ ของผู้ดูแลโปรเจกต์คือ “A fast HTML5 parser with CSS selectors, written in Cython, using Modest and Lexbor engines.”
backend มี 2 แบบ และความต่างของมันสำคัญกว่าที่เอกสารอาจจะสื่อ:
LexborHTMLParser(Lexbor engine) — ตัวที่ README แนะนำให้ใช้ในปี 2024HTMLParser(Modest engine) — ตัวดั้งเดิม ซึ่งไลบรารี C เบื้องหลัง “ไม่ได้ดูแลต่อแล้ว” ตามที่ README เดียวกันระบุ
มีข้อเท็จจริงบางอย่างที่ควรรู้ก่อนจะไปเรื่องความเร็ว จาก repo snapshot ที่เก็บเมื่อ 2026-07-10, selectolax มี 1,653 stars และรีลีสล่าสุดคือ v0.4.10 (พฤษภาคม 2026) ส่วน PyPI ระบุรองรับ Python >=3.9,<3.15 การติดตั้งคือส่วนที่ดราม่าน้อยที่สุดในรีวิวนี้: pip install selectolax ดึง wheel แบบ prebuilt ขนาด 2.3 MB สำหรับ cp314 มาใช้ได้ทันทีบน Python 3.14 — ไม่ต้องโหลด browser, ไม่ต้องมีขั้นตอน doctor, และไม่ต้องคอมไพล์ นี่แหละข้อได้เปรียบเงียบ ๆ ของ parser ล้วนเมื่อเทียบกับเครื่องมือที่พ่วง browser มา มันแค่นำเข้าแล้วก็ใช้งานได้เลย
มีประเด็นเรื่องไลเซนส์ที่ควรพูดตั้งแต่ตรงนี้ ไม่ควรซ่อนไว้ทีหลัง: ตัว Python binding ใช้ MIT แต่ wheel ที่แจกมาพ่วง engine ที่คอมไพล์มาแล้ว ซึ่งมีไลเซนส์ของตัวเอง — Modest เป็น LGPL-2.1, Lexbor เป็น Apache-2.0 ดังนั้นการบอกว่า “selectolax เป็น MIT” นั้นจริงเฉพาะโค้ด Python และยังไม่ครบถ้วนสำหรับ binary ที่คุณเอาไป ship หากทีมกฎหมายของคุณสนใจองค์ประกอบที่กระจายต่อได้ จุดนี้คือสิ่งที่ต้องยกธงไว้
คำถามเรื่องความเร็ว ด้วยตัวเลขจริง
งานที่ผมจับเวลาไว้คือ: parse สตริง HTML, ดึงข้อความของ <h3 class="title"> ทุกตัว, และดึงค่า href ของ <a> ทุกตัว วัด latency หน่วยมิลลิวินาที โดยใช้ค่ามัธยฐานจากการรันแยกโปรเซส 3 ครั้ง; ความแปรปรวนระหว่างรันอยู่ไม่เกินราว 5% สำหรับ parser ที่ทำงานบน C ในหลายขนาดหน้า ก่อนวัดแต่ละ cell เอาต์พุตของ parser ทุกตัวจะถูกย่อเหลือเป็น content hash เพื่อกัน parser ที่แอบทำงานน้อยกว่าถูกนับรวม — หน้าเหล่านี้ทั้ง 6 ตัวให้ผลตรงกันทุกขนาด จึงเป็นการเทียบแบบ apples-to-apples จริง ๆ ข้อมูลเต็มอยู่ใน bench_parse.json ที่คอมมิตไว้แล้ว

| หน้า | selectolax (Lexbor) | selectolax (Modest) | lxml | parsel | BS (lxml) | BS (html.parser) |
|---|---|---|---|---|---|---|
| 1 KB | 0.027 | 0.029 | 0.036 | 0.044 | 0.281 | 0.323 |
| 10 KB | 0.160 | 0.171 | 0.166 | 0.228 | 1.705 | 2.049 |
| 100 KB | 1.464 | 1.565 | 1.423 | 2.020 | 16.5 | 20.6 |
| 1 MB | 14.901 | 16.9 | 14.177 | 20.9 | 181.9 | 232.6 |
| 10 MB | 159.9 | 247.9 | 172.9 | 231.9 | 2261.6 | 2788.7 |
เทียบกับ BeautifulSoup: ราว 12-17 เท่า และตัวเลขที่ลือกันยังประเมินต่ำไป
ถ้าแปลงเป็นอัตราส่วน จะได้ว่า selectolax-Lexbor เร็วกว่า BeautifulSoup(html.parser) ราว 12 เท่าบนหน้า 1 KB และเพิ่มเป็นราว 17 เท่าที่ 10 MB และยัง เร็วกว่า BeautifulSoup(lxml) ประมาณ 10-14 เท่า ตลอดช่วงเดียวกัน ตัวเลขที่มักถูกพูดต่อกันว่า “selectolax เร็วกกว่า BeautifulSoup ประมาณ 4-5 เท่า” นั้นต่ำเกินไปเมื่อเทียบกับ html.parser และจะใกล้เคียงก็ต่อเมื่อเทียบกับ BeautifulSoup ที่พึ่ง lxml เท่านั้น ตัวคูณที่แท้จริงขึ้นอยู่กับว่าคุณหมายถึง BeautifulSoup แบบไหน และดึงข้อมูลจากหน้าแต่ละหน้าเยอะแค่ไหน
สิ่งนี้สอดคล้องกับ benchmark ใน README ของตัวมันเองด้วย ซึ่งชี้ว่ามีความได้เปรียบ 25.5 เท่าเหนือ BeautifulSoup(html.parser) ทั้งสองตัวเลขไม่ได้ผิด เพียงแต่งานใน README (ดึง title, link, script และ meta จากหน้า homepage ขนาดเล็ก) มีการ extraction น้อยกว่าในหน้าขนาดเล็ก จึงถ่วง overhead ต่อการ parse ของ BeautifulSoup ให้เด่นขึ้นกว่าเดิม ถ้าดูในกรอบจำกัดแล้ว สรุปได้ว่า selectolax เร็วกกว่า BeautifulSoup ราว 10-15 เท่าในการ parse และดึงข้อมูลแบบใช้งานจริง โดยจะสูงกว่านี้บนหน้าเล็กและงาน extraction เบา ๆ
ถ้าคอขวดปัจจุบันของคุณคือโค้ด BeautifulSoup ที่กำลังประมวลผลหน้าเว็บจำนวนมาก นี่คือการย้ายที่คุ้มค่าและคืนทุนได้จริง เรื่องนี้แทบไม่มีใครเถียง ต่อไปคือกรณีที่เถียงกันมากกว่า
เทียบกับ lxml: สูสี และ lxml ชนะในส่วนที่คนมักลืมแยกออกมา
ย้อนกลับไปดูแถว 100 KB และ 1 MB จะเห็นว่า Lexbor กับ lxml ต่างกันไม่ถึง 5%, ช่วงเวลาของแต่ละรันซ้อนทับกัน และตามวิธีวัดของผมถือว่าเสมอกัน — ไม่มีผู้ชนะ ไม่มีคำว่า “เร็วกว่า” จุดที่ selectolax แซงได้จริงคือหน้า 10 MB (159.9 ms เทียบกับ 172.9 ms ต่างกัน 8.1% และช่วงไม่ทับกัน) ดังนั้นเมื่อดูงานเต็ม selectolax แค่สูสีกับ lxml และชนะได้จริงเฉพาะเอกสารที่ใหญ่มากเท่านั้น

จากนั้นผมแยกการสร้าง tree ออกจากการคิวรีด้วย CSS แล้วผลก็พลิกไปในแบบที่บทความส่วนใหญ่มักพลาด สำหรับ pure parsing ล้วน ไม่มี query เลย, lxml เร็วกว่า selectolax-Lexbor ราว 33-34% บนเครื่องนี้ — 77.9 ms เทียบกับ 116.6 ms บนหน้า 10 MB บนงานเต็มทั้งคู่กลับมาชนกันอยู่ดี และสมมติฐานการทำงานของผม (ซึ่งยังไม่ได้พิสูจน์ด้วยการทดลองแจกแจงสาเหตุ) คือ บนหน้าเหล่านี้เวลาในการ query CSS เป็นเพียงสัดส่วนเล็ก ๆ ของทั้งหมด ทำให้ความได้เปรียบด้าน parse ของ lxml ถูกเฉลี่ยจนตัวเลขรวมมาเท่ากัน
นี่คือข้ออ้างที่โดนโต้แย้งได้มากที่สุดในรีวิวนี้ และผมอยากบอกตรง ๆ ว่าทำไม มันสวนกับความเชื่อทั่วไป และ benchmark ที่ผมหาเจอซึ่งแยก parse-only ออกมาตัวเดียวคือ aows.jpt.sh กลับรายงานตรงข้าม โดยให้ selectolax เร็วกว่าประมาณ 4 เท่า ดังนั้นผมจึงป้องกันความเสี่ยงไว้: ผลนี้มาจากแพลตฟอร์มเดียว (macOS arm64, Python 3.14, wheel cp314 แบบ prebuilt — ยังไม่ทดสอบ Linux x86_64 หรือ build จาก source), ตรวจซ้ำครอบคลุม 4 ขนาดหน้า และได้ผลเหมือนกันทุกขนาด รวมถึงตรวจยืนยันด้วย lxml API สองแบบเพื่อกัน artifact จาก API ทั้งสอง API ของ lxml ก็ยังชนะ selectolax-Lexbor ทุกขนาด ผมไม่ได้เสนอว่า “lxml parse เร็วกว่า” เป็นข้อสรุปตายตัว — ผมเสนอว่ามันคือผลจาก benchmark ของผม พร้อมสคริปต์ประกอบ เทียบกับตัวเลขที่เผยแพร่ส่วนใหญ่ รันบนเครื่องของคุณเองจะดีที่สุด
แยกอีกมุมหนึ่ง: ตอนคิวรี <a> 100,000 ตัวบนหน้าแบบ flat lxml กับ selectolax-Modest เสมอกัน (33.30 ms เทียบกับ 34.19 ms, ช่วงทับกัน) ขณะที่ selectolax-Lexbor ช้ากว่าทั้งคู่ราว 15% สิ่งที่ทั้งสาม C engine มีเหมือนกันคือเร็วกว่ามาก — ราว 5-7 เท่า — เมื่อเทียบกับ parsel หรือ BeautifulSoup ในงาน bulk selection ซึ่งสาเหตุหลักมาจากโมเดล Python-object-per-node ที่หน่วงจริง ๆ ดังนั้นข้ออ้างว่า “selectolax เร็วที่สุดสำหรับการเลือก CSS แบบ bulk” ก็ยังไม่จริง Modest แค่เสมอกับ lxml ส่วน Lexbor แพ้มัน
ข้อสรุปที่ผมพร้อมจะยืนอยู่ตรงนี้คือ: ความได้เปรียบของ selectolax เหนือ lxml ไม่ได้กว้างขวางในแง่ความเร็วแบบรวมงาน มันชนะเฉพาะหน้าที่ใหญ่ที่สุดเท่านั้น จุดยืนของมันจริง ๆ อยู่ที่อย่างอื่น — ความง่ายของ API, พฤติกรรมเมื่อเจอ input เละ ๆ และ CSS สมัยใหม่ — ซึ่งเป็นพื้นที่ที่รีวิวส่วนที่เหลือจะพูดถึงต่อ
หน่วยความจำและ cold start: จัดอันดับด้วย RSS ไม่ใช่ profiler ของคุณ
เรื่องหน่วยความจำคือจุดที่ผมต้องแก้ตัวเลขเก่าของตัวเอง และการแก้นี่แหละคือประเด็น วัดเป็น RSS delta บนหน้า 10 MB โดยปิด tracemalloc แล้ว BeautifulSoup ใช้หน่วยความจำมากกว่า selectolax หรือ lxml ราว 1.5-1.8 เท่า — ช่วงตั้งแต่ 1.51 เท่า (BS-lxml ที่ 218.4 MB เทียบกับ Lexbor ที่ 144.6 MB) ไปจนถึง 1.75 เท่าในฝั่งบน selectolax กับ lxml อยู่ในกลุ่มที่กินหน่วยความจำค่อนข้างประหยัด; lxml ประหยัดที่สุดเมื่อดูจาก RSS

การวัดรอบก่อนของผมที่บอกว่า “ประมาณ 3 เท่า” นั้นผิดด้วยเหตุผลที่เอาไว้ใช้สอนคนได้ดี: ผมวัดตอน tracemalloc ทำงานอยู่ และ bookkeeping ต่อ allocation ของ tracemalloc ทำให้ RSS ที่เห็นของ parser ที่ allocate เยอะพุ่งขึ้นเกือบสองเท่า ดังนั้นขอเน้นสำหรับคนที่ benchmark หน่วยความจำของ parser: ให้จัดอันดับด้วย RSS ตอนปิด profiler อยู่ การจัดอันดับ parser ด้วย peak จาก tracemalloc จะสลับลำดับ parser ที่ใช้ C โดยเฉพาะ — มันทำให้ selectolax-Lexbor ดูหนักกว่า Modest ทั้งที่ RSS จริงใกล้กันมาก BeautifulSoup หนักที่สุดจริงในกลุ่มนี้ แต่อยู่ในระดับที่ไม่ถึง 3 เท่าตามเครื่องมือที่ปนสัญญาณรบกวน
cold start มีผลไม่มากแต่มีอยู่จริง: selectolax ใช้เวลานำเข้าประมาณ 14 ms ใกล้กับ lxml และเร็วกว่า bs4 หรือ parsel ราว 2.3 เท่า ถ้าคุณกำลังปล่อย CLI tool หรือ serverless function ที่เวลา import มีผลกับทุกการเรียก นี่คือความต่างที่ควรนับ
ความครอบคลุมของ CSS selector: แข็งแรง แต่มีรอยรั่วจริงอยู่บ้าง
การครอบคลุม CSS ถูกทดสอบด้วยเมทริกซ์ 41 เคส โดยแต่ละ selector ถูกตรวจบน fixture ที่มีชุดคำตอบถูกต้องแน่นอน และมีรอบเจาะจงเพื่อหาจุดพังของ Lexbor engine ด้วย ทุกเคสรันใน subprocess ของตัวเอง ซึ่งจำเป็นมาก เพราะมีบางเคสที่ทำให้ interpreter ล่มทั้งตัว ผลลัพธ์คือ:

| Engine | PASS | WRONG | UNSUPPORTED | PROCESS_ABORT |
|---|---|---|---|---|
| soupsieve | 41 | 0 | 0 | 0 |
| selectolax Lexbor | 39 | 0 | 2 | 0 |
| lxml (cssselect) | 37 | 1 | 3 | 0 |
| parsel (cssselect) | 37 | 1 | 3 | 0 |
| selectolax Modest | 35 | 3 | 2 | 1 |
เมื่อรวม selector ที่จงใจออกแบบให้กดดันเข้าไป Lexbor ก็ไม่ได้ชนะขาด — ตัวที่ทำได้สมบูรณ์คือ soupsieve ด้วย 41/41 เทียบกับ Lexbor ที่ 39/41 ข้อที่ Lexbor พลาด 2 ตัวคือ :lang(en) และ :dir(rtl) ซึ่งมันปฏิเสธด้วย parse error นอกนั้นทำได้หมด รวมถึง :has(), :is(), :where() และแอตทริบิวต์แบบไม่สนตัวพิมพ์เล็กใหญ่ด้วย
สิ่งที่ Lexbor เด่นจริง ๆ คือเมื่อเทียบกับ cssselect stack ตัว selector เด่นของ README — div > :nth-child(2n+1):not(:has(a)) — ให้ชุดผลลัพธ์ที่ถูกต้องทั้งบน selectolax สอง backend และบน soupsieve แต่ ให้ชุดที่ผิดบน lxml และ parsel โดยไม่แจ้ง error ใด ๆ scraper ที่คัด selector นี้ไปใช้ใน Scrapy หรือ parsel จะได้ผลลัพธ์ผิดแบบเงียบ ๆ ให้พูดให้ชัด: cssselect รองรับ :has() ตั้งแต่เวอร์ชัน 1.2.0 (2022) และผมทดสอบกับ 1.4.0 ดังนั้นกรณีนี้คือ “รองรับแต่ประเมิน compound ผิด” ไม่ใช่ “ไม่รองรับ” พฤติกรรมผิดเงียบของ compound นี้ใน cssselect tracker ก็ยังไม่ได้ถูกบันทึกไว้ในรูปแบบนั้น ซึ่งบันทึกไว้เฉพาะข้อจำกัดของ :has() ที่เป็น error แบบถูกโยนออกมา Lexbor ยังรองรับธงแอตทริบิวต์แบบไม่สนตัวพิมพ์ [data-role="LEAD" i] ที่ cssselect ปฏิเสธทันทีด้วย
อย่างไรก็ตาม มีช่องว่างสองจุดที่มีผลต่อการย้ายระบบจริง ๆ: selectolax ไม่รองรับ XPath เลย — backend ทั้งสองไม่มี xpath() — และไม่มี pseudo-element แบบ ::text / ::attr() เพราะนั่นเป็น extension ของ parsel/Scrapy ไม่ใช่ CSS จริง ๆ หาก scraper เดิมของคุณพึ่ง XPath อยู่ นั่นคือกำแพงใหญ่ที่สุดที่คุณจะเจอ เพราะคุณต้องเขียน selector ใหม่ ไม่ใช่แค่สลับไลบรารี อีกด้านหนึ่ง Lexbor มี pseudo-class :lexbor-contains("text" i) สำหรับจับข้อความแบบไม่สนตัวพิมพ์ ซึ่งไม่มีใน lxml, parsel หรือ CSS มาตรฐาน และมันทำงานได้ตามเอกสาร
ความทนทานกับ HTML ที่เละ ซึ่งเป็นจุดที่ selectolax เอาตัวรอดได้ดี
งาน scraping จริงหมายถึงเอา parser ไปป้อนขยะ แล้วหวังว่ามันจะไม่ล้ม ผมลองอินพุตเชิงรุก 18 แบบ และนี่คือหมวดที่ selectolax สู้กับ lxml ได้ดีที่สุด
ถ้าส่งสตริงว่างหรือ whitespace ให้ lxml.html.fromstring มันจะโยน ParserError("Document is empty") ออกมา แต่ selectolax ทั้งสอง backend จะคืน tree ว่างที่ถูกต้องให้แทน สำหรับ scraper ที่วนลูปรายชื่อ URL และมีบาง response กลับมาว่าง ๆ นี่คือ try/except หนึ่งชั้นที่ไม่ต้องครอบทุกอย่างอีก selectolax ยังรับมือกับ element จำนวน 100,000 ตัวโดยไม่เกิด stack overflow ด้วย
การ nesting ลึกเป็นจุดที่เห็นความต่างชัดสุด บน <div> ที่ซ้อนกันลึก 1,000 และ 5,000 ชั้น lxml ตัดเนื้อหาชั้นลึกทิ้งแบบเงียบ ๆ แต่ selectolax เก็บไว้ครบ libxml2 จำกัดความลึกในการ parse ไว้ราว 256 ชั้นและตัด tree ทิ้งโดยไม่แจ้ง error ดังนั้นข้อความที่อยู่ลึกสุดจึงกลายเป็นเข้าถึงไม่ได้ ทั้ง selectolax backend คืน tree สมบูรณ์ทั้งหมด มันคือภาพสะท้อนของกับดัก <template> ที่จะพูดต่อไป: ตรงนั้น Lexbor ตัดเนื้อหาที่ตัวอื่นเก็บไว้; แต่ตรงนี้ lxml ตัดเนื้อหาที่ selectolax เก็บไว้
ไม่ใช่ทุกช่องจะเป็นฝ่ายชนะ backend Modest จะทำให้ Python interpreter ล่มด้วย SIGABRT ทันทีเมื่อเจอ :dir() — ไม่ใช่ exception ที่จับได้ แต่เป็นการฆ่าโปรเซสแบบ hard kill นี่คือข้อควรระวังของ backend เก่าโดยตรง และเป็นประเภทปัญหาที่จะไม่โผล่ให้เห็นจนกว่ามันจะล้มงาน production ตอนตีสาม
กับดักข้อมูลหายเงียบ 2 จุดที่ต้องรู้ก่อนนำไปใช้จริง
สองกรณีนี้ไม่ใช่การค้นพบใหม่ — upstream บันทึกไว้แล้วทั้งคู่ — แต่ทั้งคู่ทำให้ข้อมูลหายจริงแบบเงียบ ๆ และใน README ก็ไม่ได้พูดให้เด่นพอ
Lexbor ไม่ดึง <a> ที่อยู่ใน <template>
บนหน้า MDN จริงที่ผมทดสอบ selectolax-Lexbor พบลิงก์ 497 รายการ ในขณะที่ lxml, BeautifulSoup ทั้งสอง backend และแม้แต่ selectolax backend แบบ Modest เอง พบ 508 รายการ ลิงก์ที่หายไป 11 รายการนั้นเป็นลิงก์สลับภาษาและลิงก์ discussions ที่อยู่ใน <template> (หน้าใช้ Lit web components)

ต้นเหตุสมเหตุสมผล: ตามสเปก HTML5 เนื้อหาใน <template> จะถูก parse เข้า fragment แยกที่ inert ไม่ใช่ DOM ปกติ และ Lexbor ก็ทำตามเคร่งครัด — tree.css("a") จึงไม่ลงไปใน template content lxml, BeautifulSoup ทั้งสอง backend และ Modest จะ flatten เนื้อหา template เข้าสู่ tree หลัก จึงเจอลิงก์เหล่านั้น ปัญหานี้มีบันทึกไว้แล้ว (selectolax#146 โดยรากของ engine อยู่ที่ lexbor#170) และทั้งสองมุมมองพอรับได้ — จริง ๆ แล้ว Lexbor อาจจะถูกต้องตามสเปกมากกว่า แต่สำหรับนักพัฒนาที่ใช้ backend “แนะนำ” อยู่ เขาจะพลาดข้อมูลนี้แบบเงียบ ๆ โดยไม่มี error ฝั่งกลับกันก็ต้องพูด: parser อื่น ๆ ดึง inert template content ที่ browser จริงไม่ render ออกมา จึงอาจส่งข้อมูลหลอนที่ผู้ใช้มองไม่เห็นมาให้คุณ ทางแก้ที่น่าเชื่อถือสำหรับหน้าแบบนี้คือใช้ backend Modest หรือไลบรารีอื่น
ไบต์ที่ไม่ใช่ UTF-8 ทำให้ .text() เพี้ยนแบบเงียบ ๆ
ถ้าส่ง bytes ที่ไม่ใช่ UTF-8 ให้ selectolax parse จะผ่านไปได้ แต่ความเสียหายจะไปโผล่ทีหลัง และมันแย่กว่าการล้มแบบชัดเจน บน "<p>café éè</p>".encode("latin-1"), .text() ของ Lexbor จะคืน replacement character, .text() ของ Modest จะตัดไบต์ที่มีปัญหาทิ้งแบบเงียบ ๆ, และทั้งสอง engine จะโยน UnicodeDecodeError เฉพาะตอนที่คุณแตะ .html ตัว binding จะ decode ตอนอ่านกลับแบบ strict UTF-8 ไม่ได้ decode ตอน parse เรื่องนี้สัมพันธ์กับ issue ที่รู้กันอยู่ของ selectolax เรื่องความเข้มงวดของ encode/decode
วิธีแก้มีบรรทัดเดียวและควรจำให้ขึ้นใจ: decode bytes ด้วยตัวเองก่อน — LexborHTMLParser(resp.content.decode("latin-1")) — แล้วทั้งสอง engine จะคืน 'café éè' ได้ถูกต้อง ในทางปฏิบัติ ให้ส่ง str ให้ selectolax เสมอ อย่าส่ง raw non-UTF-8 bytes README ไม่ได้อธิบายจุดนี้ไว้ชัด
มิติด้าน production (วัดครั้งเดียว จึงให้ดูเป็นสัญญาณมากกว่าตัวเลขตายตัว)
ผลชุดถัดไปนี้ผมวัดครั้งเดียว ไม่ได้รัน 3 รอบ จึงขอชี้ว่าเป็นสัญญาณ ไม่ใช่ตัวเลขสรุปสุดท้าย
เรื่องการ scale ด้วย thread น่าสนใจที่สุด การ parse หน้า 1 MB จำนวน 48 ครั้งผ่าน 4 threads, selectolax เร็วขึ้นแบบ wall-clock ราว 3.5-3.9 เท่า — ซึ่งเป็นลายเซ็นเชิงประจักษ์ของไลบรารีที่ปล่อย GIL ระหว่างการ parse ด้วย C — ขณะที่ BeautifulSoup(lxml) กลับช้าลงหลายเท่าเมื่อใช้ thread ซึ่งสะท้อนว่างานกำลังถูก serialize อยู่กับ GIL ส่วน lxml อยู่กึ่งกลางและยังสรุปไม่ได้ สำหรับยุค free-threading ที่ Python กำลังก้าวเข้าไป selectolax ที่ parallelize ข้าม threads ได้ในขณะที่ BeautifulSoup ทำไม่ได้ ถือว่าเป็นข้อได้เปรียบที่มีตัวเลขรองรับ แม้จะยังเป็นผลชั่วคราวอยู่ก็ตาม นี่คือ thread count เดียวบนหน้าเดียว และกลไกที่อธิบายยังเป็นสมมติฐาน ไม่ใช่สิ่งที่ผมยืนยันจากการ instrument C code
เรื่อง leak: ในการวน parse-extract-drop 2,000 รอบบนหน้า 1 MB ไม่มี parser ตัวใดแสดงการไต่ RSS แบบเส้นตรงเหมือน leak — ทุกตัวลงตัวใน band ของ working set ที่จำกัด ผมเชื่อผลนี้เป็นพิเศษเพราะผมเอาตัวทดสอบที่รู้แน่ว่ามี leak ผ่านเครื่องมือเดียวกัน แล้วมันพุ่งขึ้น +198 MB ตามที่ออกแบบไว้ ซึ่งยืนยันว่าเครื่องมือมองเห็น leak ได้จริง แค่ไม่พบใน parser เหล่านี้ และ node handle ที่ยังถูกเก็บไว้หลัง tree เจ้าของหลุด scope ก็ยังใช้งานได้ ไม่ segfault ทั้งหมดนี้เป็น single-observation ไม่ใช่การ soak test หลายชั่วโมง
selectolax เหมาะกับตรงไหน และควรส่งต่อเมื่อไร
ทั้งหมดข้างบนพูดถึงงานเดียว: เอา HTML ที่มีอยู่แล้วมาแปลงเป็นข้อมูลมีโครงสร้างให้เร็วที่สุด selectolax ทำงานนี้ได้ดีมาก สิ่งที่มันตั้งใจไม่ทำคือดึงหน้าเว็บ, render JavaScript, หมุน proxy, แก้ CAPTCHA หรือเดาว่าคุณต้องการ element ไหน ทั้งหมดนั้นยังเป็นโค้ดของคุณเอง selectolax คือชั้น parse และไม่ได้แกล้งทำเป็นอย่างอื่น
นั่นคือเส้นแบ่งที่บริการ extraction แบบ managed เข้ามาวางอยู่เหนือ parser แทนที่จะมาแทน หากคุณไม่อยากสร้างและดูแลสแตก fetch-render-anti-bot-extract เอง Thunderbit ก็เปิดให้ใช้งานผ่าน API, MCP server และ CLI — POST /distill จะเปลี่ยนหน้าเว็บเป็น Markdown ที่สะอาด และ POST /extract จะคืน structured JSON ที่ตรง schema โดยจัดการ JS rendering และ anti-bot ให้เรียบร้อย นี่คืออีกชั้นของปัญหา: คุณจะเลือก selectolax เมื่อคุณมี HTML อยู่แล้วและต้องการความเร็วในการ parse แบบควบคุมเองเต็มที่ และจะเลือกอะไรอย่าง API, MCP server หรือ CLI ของ Thunderbit เมื่อคุณอยากให้การดึงและการ extract ถูกจัดการให้ และแค่ต้องการข้อมูลโครงสร้างกลับมา ไม่ใช่การสลับตัวกัน แต่เป็นคนละระดับบนสแตกเดียวกัน
ทดลองใช้ Thunderbit สำหรับการดึงข้อมูลเว็บ
ข้อดี ข้อจำกัด และใครควรใช้จริง
selectolax ชนะตรงไหน:
- เร็วกกว่า BeautifulSoup ราว 12-17 เท่า ในงาน parse-and-extract แบบใช้งานจริง และคงที่ตลอดช่วงขนาดหน้าตั้งแต่เล็กมากไปจนใหญ่
- ใช้หน่วยความจำประหยัดอยู่ในระดับ lxml และเบากว่า BeautifulSoup ราว 1.5-1.8 เท่า พร้อมเวลา import ประมาณ 14 ms
- รับมือ input ที่ทำให้ lxml พังได้ดี เช่น หน้าเปล่า, whitespace และการ nesting ลึกแบบผิดปกติ
- รองรับ CSS สมัยใหม่ เช่น
:has(),:is(),:where(), แอตทริบิวต์แบบไม่สนตัวพิมพ์ และ:lexbor-contains()ที่มีเฉพาะ Lexbor - DOM แบบอ่านเขียนที่ปลอดภัยกับ
None: element ที่หายไปจะคืนNoneหรือ[]แทนการโยน error และยังแก้ไขแล้ว serialize กลับได้จริง - ดูแลต่ออย่างต่อเนื่อง (v0.4.10, กลางปี 2026) และติดตั้งง่ายมาก
selectolax ไม่ชนะตรงไหน:
- ไม่ได้เร็วกกว่า lxml แบบกว้าง ๆ — งานเต็มเสมอกัน และบนเครื่องผมแพ้ในขั้น pure parsing
- ไม่มี XPath และไม่มี
::text/::attr()— ถ้า scraper ของคุณตั้งอยู่บน XPath นี่คือกำแพงย้ายระบบที่แท้จริง - มีกับดักข้อมูลหายเงียบ 2 จุด: เนื้อหาใน
<template>บน Lexbor และ bytes ที่ไม่ใช่ UTF-8 ผ่าน.text() - backend Modest เป็นของเก่าและจะ SIGABRT เมื่อเจอ
:dir() - ตัวเลขทั้งหมดในรีวิวนี้มาจากเครื่องเดียว (macOS arm64, Python 3.14) และยังเป็นผลชั่วคราว
แล้วควรใช้ selectolax ไหม? ควร ถ้าคุณต้องการความเร็วระดับ lxml แต่ชอบ API ที่ใช้ง่ายกว่า ปลอดภัยกับ None มากกว่า และรับมือ input ว่างหรือ malformed ได้ดีกว่า — โดยยอมอยู่ในโลก CSS-only หาก codebase ของคุณผูกกับ XPath อยู่ การย้ายมีต้นทุนจริงและควรประเมินอย่างตรงไปตรงมา และถ้าคุณกำลังมองหา “parser ที่เร็วที่สุดเพียงตัวเดียว” คำตอบที่แม่นจาก benchmark นี้คือ selectolax กับ lxml ใกล้กันมากจนตัวตัดสินควรเป็นความคล่องตัวและความทนทาน ไม่ใช่ความเร็วดิบ ซึ่งเอาจริง ๆ ก็เป็นเหตุผลที่ดีพอจะเลือกเครื่องมืออยู่แล้ว
ทดลองใช้ Thunderbit สำหรับการดึงข้อมูลเว็บ Get Started Free
คำถามที่พบบ่อย
selectolax เร็วกกว่า BeautifulSoup ไหม?
ใช่ ชัดเจน — เร็วกว่าประมาณ 12-17 เท่าเมื่อเทียบกับ BeautifulSoup(html.parser) และประมาณ 10-14 เท่าเมื่อเทียบกับ BeautifulSoup(lxml) ในงาน parse-and-extract แบบสมจริง โดยผลคงที่ตั้งแต่หน้า 1 KB ถึง 10 MB (macOS arm64, Python 3.14) ตัวเลขที่คนชอบอ้างว่า “4-5 เท่า” นั้นประเมินต่ำไปเมื่อเทียบกับ html.parser
selectolax เร็วกกว่า lxml ไหม? ไม่ใช่ในภาพรวม บนงาน parse-and-extract แบบเต็ม ทั้งคู่เสมอกันที่ 100 KB และ 1 MB โดย selectolax ชนะเฉพาะหน้า 10 MB เท่านั้น ส่วน pure parsing ที่ไม่มี query เลย lxml กลับเร็วกว่า selectolax ราว 33-34% บนเครื่องของผม — ซึ่งเป็นผลที่สวนความคาดหมาย และผมจึงป้องกันไว้ว่าเป็นผลจากแพลตฟอร์มเดียว ควรตรวจซ้ำบนฮาร์ดแวร์ของคุณเอง
ควรใช้ backend Lexbor หรือ Modest?
Lexbor ในแทบทุกกรณี — มันคือ engine ที่ดูแลต่อและครบฟีเจอร์ซึ่ง README แนะนำ พร้อมความครอบคลุม CSS ที่ดีกว่า ข้อยกเว้นคือหน้าที่ซ่อนเนื้อหาไว้ใน <template> เพราะพฤติกรรมที่ถูกต้องตามสเปกของ Lexbor จะตัดเนื้อหานั้นออก ในขณะที่ Modest กลับเก็บไว้ได้บังเอิญ แต่ Modest เองก็มีคมมีดที่อันตราย รวมถึงการ crash ของ interpreter เมื่อเจอ :dir()
selectolax รองรับ XPath ไหม?
ไม่รองรับ backend ทั้งสองไม่ expose เมธอด xpath() — selectolax เป็น CSS-only หาก scraper ของคุณพึ่ง XPath การย้ายหมายถึงต้องเขียน selector ใหม่ ซึ่งเป็นต้นทุนใหญ่ที่สุดของการเปลี่ยนจากสแตกที่ใช้ lxml หรือ parsel มาเป็น selectolax
ทำไมผลลัพธ์ selectolax ของฉันถึงเพี้ยนหรือมี element หายไป?
โดยทั่วไปมีผู้ต้องสงสัยหลัก 2 อย่าง ถ้าข้อความกลับมาพร้อม replacement character หรือขาดสำเนียง อาจเป็นเพราะคุณส่ง raw bytes ที่ไม่ใช่ UTF-8 เข้าไป ให้ decode เป็น str ก่อน (resp.content.decode("latin-1")) แล้วค่อย parse ถ้าลิงก์หรือ element หายไปบนเว็บสมัยใหม่ มันอาจอยู่ในแท็ก <template> ซึ่ง backend Lexbor จะไม่ลงไปอ่าน ควรสลับไปใช้ Modest หรือ parser อื่นกับหน้าดังกล่าว


