- ทดลองสร้างเว็บสแครปเปอร์แบบมี AI ช่วย โดยใช้ structured outputs ของ OpenAI API เพื่อดึงตาราง HTML ออกมาเป็นข้อมูลแบบมีโครงสร้าง และ GPT-4o แสดงคุณภาพการดึงข้อมูลจากตารางที่ซับซ้อนบางแบบได้ดีกว่าที่คาด
- ในตารางที่มี โครงสร้างการแสดงผลซับซ้อน เช่น พยากรณ์อากาศ 10 วันของ Weather.com ระบบสามารถแยกพยากรณ์ช่วงกลางวัน/กลางคืนได้ และยังค้นหาค่าที่ไม่แสดงบนหน้าจอจากซอร์ส HTML ได้ด้วย
- แต่ในตารางอย่าง Human Development Index ของ Wikipedia ซึ่งมี แถวที่ถูกรวม ค่าที่ซ้ำกันเข้าไว้ด้วยกัน จำนวนค่าของแต่ละคอลัมน์ไม่ตรงกัน ทำให้ประกอบกลับเป็นตารางได้ยาก และแก้ไม่สำเร็จด้วยการปรับพรอมป์ต์เพียงอย่างเดียว
- เพื่อลดค่าใช้จ่ายของการเรียก API จึงลองให้โมเดลสร้าง XPath ด้วย แต่ได้ XPath ที่ผิดหรือไม่คืนผลลัพธ์ และพบว่าวิธีที่เสถียรกว่าคือดึงข้อมูลออกมาก่อน แล้วค่อยขอ XPath โดยอ้างอิงจากค่าที่ได้
- ใช้เงินไป $24 กับการทดลองตลอดสองวัน และเมื่อปรับลดแอตทริบิวต์ใน HTML จำนวนอักขระลดลงครึ่งหนึ่งโดยไม่เห็นว่าประสิทธิภาพแย่ลง แต่ข้อจำกัดด้านค่าใช้จ่ายทำให้การตรวจสอบเพิ่มเติมทำได้จำกัด
การดึงตาราง HTML ด้วย structured outputs
- สร้าง เว็บสแครปเปอร์ แบบมี AI ช่วยโดยใช้ฟีเจอร์ structured outputs ของ OpenAI API
- การทดลองแรกคือส่งสตริง HTML ให้ GPT-4o แล้วขอให้ดึงข้อมูลตารางออกมาในรูปแบบที่มีโครงสร้าง
- โมเดล Pydantic ที่ใช้ประกอบด้วย
ParsedTableสำหรับเก็บชื่อตารางและรายการคอลัมน์ และParsedColumnสำหรับเก็บชื่อคอลัมน์กับรายการค่า - ใน system prompt กำหนดบทบาทให้เป็นเว็บสแครปเปอร์ผู้เชี่ยวชาญด้านการดึง ข้อมูลแบบมีโครงสร้าง จากตาราง HTML
- มีการลองใช้ GPT-4o mini ด้วย แต่ผลลัพธ์แย่กว่ามาก จึงทำการทดลองต่อด้วย GPT-4o
จุดแข็งที่เห็นจากตารางซับซ้อน
- หลังจากตารางแบบง่ายแล้ว ได้นำตาราง พยากรณ์อากาศ 10 วันของ Weather.com มาเป็นอินพุต
- ตารางนี้มีแถวใหญ่หนึ่งแถวอยู่ด้านบน และอีก 9 วันที่เหลือเป็นแถวขนาดเล็กกว่า
- GPT-4o มองเห็นโครงสร้างพยากรณ์กลางวัน/กลางคืนของอีก 9 วัน และเพิ่มคอลัมน์
Day/Nightขึ้นมา - ตอนแรกคอลัมน์
Conditionที่ดึงออกมาดูเหมือนเป็นอาการหลอน แต่เมื่อเช็กซอร์ส HTML พบว่าเป็นข้อมูลที่มีอยู่จริงในแท็กที่ไม่แสดงบนหน้าจอ
ความล้มเหลวที่เกิดกับแถวที่ถูกรวม
- ตาราง Human Development Index ของ Wikipedia ดูเผิน ๆ เหมือนง่าย แต่มีการ รวม แถวที่มีค่าซ้ำกันไว้ ทำให้ GPT-4o จัดการได้ไม่ดี
- โมเดลดึงแต่ละคอลัมน์ออกมาตามคำสั่ง แต่จำนวนค่าในแต่ละคอลัมน์ไม่เท่ากัน จึงยากที่จะนำผลลัพธ์มาแสดงเป็นตาราง
- แม้จะเปลี่ยน system prompt เป็นแนวว่า “ถ้าเป็นแถวที่รวมอยู่ ให้ดึงออกมาเป็นค่า JSON หลายค่าเพื่อให้ทุกคอลัมน์มีจำนวนแถวเท่ากัน” ก็ยังไม่ทำงาน
- ยังไม่ได้ลองพรอมป์ต์ที่สั่งให้ดึงข้อมูลแบบ รายแถว แทนการดึงแบบรายคอลัมน์
ข้อจำกัดของวิธีสร้าง XPath
- เนื่องจากการเรียก OpenAI API ทุกครั้งอาจมีค่าใช้จ่ายสูง จึงลองให้โมเดลคืน XPath แทนข้อมูลที่ดึงได้
- เป้าหมายคือเมื่อข้อมูลในหน้าเดิมมีการอัปเดต จะสามารถสแครปด้วย XPath ได้อีกครั้งโดยไม่ต้องเรียกโมเดล
- พรอมป์ต์ถูกออกแบบให้รับเนื้อหา HTML และชื่อคอลัมน์ แล้วคืนสตริง XPath ที่สามารถนำไปประเมินผลใน
driver.find_elements(By.XPATH, xpath)ของ Selenium ได้ - แต่วิธีนี้ไม่เสถียร
- บางครั้ง GPT-4o คืน XPath ที่ไม่ถูกต้อง
- แม้จะบรรเทาได้บางส่วนด้วยการอ้างถึง Selenium แต่ก็ยังมี XPath ที่คืนข้อมูลผิดหรือไม่คืนผลลัพธ์เลย
การผสานการดึงข้อมูลกับการสร้าง XPath
- ความพยายามถัดไปคือให้ GPT-4o ดึงข้อมูลออกมาก่อน แล้วใช้ผลลัพธ์นั้นเป็น ค่าอ้างอิง เพื่อขอ XPath
- วิธีผสมนี้ทำงานได้ดีกว่าการขอ XPath ตั้งแต่แรกอย่างมาก
- มีบางกรณีที่ XPath ที่สร้างขึ้นไม่คืนผลลัพธ์ จึงเพิ่ม ตรรกะการลองซ้ำ แบบง่าย ๆ ว่าถ้าไม่พบผลลัพธ์ก็ให้ลองใหม่
- สำหรับตารางที่ทดสอบ วิธีลองซ้ำนี้ได้ผล
- อย่างไรก็ตามยังมีปัญหาใหม่
- ในขั้นแรกของการดึงข้อมูล บางครั้งมีการแปลงรูปภาพเป็นข้อความ
- ตัวอย่างเช่น รูปลูกศรชี้ขึ้นอาจถูกดึงออกมาเป็นข้อความอย่าง
arrow-upwards - จากนั้นขั้นที่สองจะพยายามค้นหาข้อความที่ไม่มีอยู่จริงใน HTML และจึงล้มเหลว
- ปัญหานี้ยังไม่ได้รับการแก้ไขแยกต่างหาก
ค่าใช้จ่ายและการจัดระเบียบ HTML
- การใช้ GPT-4o ทำเว็บสแครปอาจมี ค่าใช้จ่าย สูง เพราะแม้แต่ตาราง HTML ขนาดเล็กก็มีอักขระจำนวนมาก
- ใช้เงินไปแล้ว $24 ระหว่างการทดลองสองวัน
- เพื่อลดค่าใช้จ่าย จึงเพิ่มตรรกะสำหรับลบข้อมูลที่ไม่จำเป็นออกจากสตริง HTML ก่อนส่งให้โมเดล
- เมื่อลบแอตทริบิวต์ทั้งหมดออก ยกเว้น
class,id,data-testidที่ XPath มักใช้งาน จำนวนอักขระของตารางลดลงครึ่งหนึ่ง - หลังจัดระเบียบนี้ไม่เห็นว่าประสิทธิภาพลดลง และอาจเป็นไปได้ด้วยว่าคุณภาพการดึงข้อมูลดีขึ้นจริง
- ปัจจุบัน ขั้นตอนสร้าง XPath จะเรียกโมเดลหนึ่งครั้งต่อหนึ่งคอลัมน์ของตาราง
- แม้จะสามารถปรับปรุงให้สร้าง XPath หลายรายการพร้อมกันได้ แต่ยังไม่ได้ลองหรือประเมินประสิทธิภาพ
เดโมและไอเดียทดลองเพิ่มเติม
- คุณภาพการดึงข้อมูลของ GPT-4o น่าประทับใจ แต่ค่าใช้จ่ายที่ต้องจ่ายให้ OpenAI ก็รู้สึกสูงมากเช่นกัน
- เดโมแบบง่ายที่สร้างด้วย Streamlit สามารถดูได้ที่ https://orange-resonance-9766.ploomberapp.io
- ซอร์สโค้ดเผยแพร่ไว้บน GitHub พร้อมคำเตือนว่าไม่ควรคาดหวังว่าเป็นเครื่องมือที่สมบูรณ์มาก
- แม้อยากทดสอบกับตารางให้มากกว่านี้ แต่เพราะค่าใช้จ่ายของ OpenAI เพิ่มขึ้น จึงได้ลองเพียงไม่กี่ตาราง
- หากมีเวลาเพิ่ม ผู้เขียนวางแผนจะลองสิ่งต่อไปนี้
- เดโมปัจจุบันเป็นกระบวนการครั้งเดียวที่ให้ผู้ใช้กรอก URL และ XPath เริ่มต้น แต่ประสบการณ์ผู้ใช้ที่ดีกว่าอาจเป็นการให้ผู้ใช้คลิกเลือกตารางที่ต้องการดึง และระบุแถวตัวอย่าง
- สำหรับตารางที่ซับซ้อน อาจดึงทั้งคอลัมน์ได้ยากด้วย XPath เดียว จึงอยากทดลองวิธีให้ LLM คืนค่าเป็น โปรแกรม เช่น Python แทน
- เนื่องจากค่าใช้จ่ายในการใช้ GPT-4o สูง และรู้สึกว่ายังส่งข้อมูลที่ไม่จำเป็นไปมาก จึงควรทดลองวิธีจัดระเบียบ HTML เพิ่มเติม
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ได้ผลดีที่สุดเมื่อแปลง HTML เป็น รูปแบบที่เรียบง่ายอย่าง Markdown ก่อน แทนที่จะป้อน HTML เข้า LLM โดยตรง
เครื่องมือที่เคยลองใช้มี Extractus https://github.com/extractus และ dom-to-semantic-markdown https://github.com/romansky/dom-to-semantic-markdown
ภายใน Magic Loops https://magicloops.dev/ ที่รันบนคลาวด์ ใช้ Apify https://apify.com/ และ Firecrawl https://www.firecrawl.dev/ ส่วนใน Chrome Extension ใช้ dom-to-semantic-markdown
ตอนนี้กำลังทดลองวิธีสร้าง XPath สำหรับแต่ละไซต์ผ่าน flow ที่มีผู้ใช้ช่วย แล้วค่อยดึงเฉพาะองค์ประกอบที่จำเป็นก่อนเรียก LLM และเมื่อทำให้ปัญหาง่ายลง แม้แต่ GPT-4o mini ก็ทำได้ค่อนข้างดี
สามารถรัน Chrome Extension ใน headless browser ได้ จึงแปลงเป็น Markdown เชิงความหมายภายในเบราว์เซอร์ก่อนนำข้อมูลออกมาข้างนอกได้
ยังมีหน้าจอสดของเบราว์เซอร์ที่ฝังผ่าน iFrame ได้ด้วย ทำให้รับฟีดแบ็กจากผู้ใช้ต่อ XPath ที่กำลังสร้างได้ทันที: https://docs.browserbase.com/features/session-live-view#give...
จากประสบการณ์ของผม ประสิทธิภาพค่อนข้างใกล้เคียงกับ Markdown และการแปลงก็ง่ายกว่า แถมมี exception น้อยกว่า
หน้าที่เจอเป็น “Alpha Draft” เลยคิดว่าคงยังไม่มีคลังข้อความ Semantic Markdown มากนัก และการขาดข้อมูลฝึกอาจรบกวนความเข้าใจของ LLM ได้
แต่ก็เป็นรูปแบบที่อ่านง่ายพอสมควร ถ้า LLM ปฏิบัติต่อ metadata แบบมีโครงสร้างเหมือนคำอธิบายในวงเล็บ ก็น่าจะจัดการได้ค่อนข้างดี
OpenAI เพิ่งประกาศ Batch API ทำให้เตรียม prompt ทั้งหมดไว้แล้วรันเป็นชุดได้ และลดค่าใช้จ่ายลงเหลือ 50%: https://platform.openai.com/docs/guides/batch
ก่อนหน้านี้เคยใช้กับ GPT-4o mini ค่อนข้างมาก และสามารถประมวลผล 3,000 รายการได้ภายใน 5 นาที
สำหรับแอปพลิเคชันที่ไม่ต้องการแบบเรียลไทม์ อาจเป็นตัวเลือกที่ดีมาก
vLLM เพิ่มการรองรับ “offline batch mode” ในรูปแบบเดียวกันมาบางส่วนแล้ว แต่ยังไปไม่ถึงขั้น implement endpoint ของ OpenAI
เป็นฟีเจอร์ที่สมเหตุสมผลมาก
สำหรับเนื้อหาที่มีโครงสร้าง เช่น รายการหรือ table แบบง่าย ๆ จริง ๆ แล้ว ไม่จำเป็นต้องใช้ LLM
ช่วงหลังผมทำเว็บสแครปเปอร์ https://easyscraper.com ที่ทำงานอัตโนมัติกับเว็บไซต์ใดก็ได้ เวอร์ชันแรกทำด้วย AI แต่ท้ายที่สุด heuristic จาก attribute และตำแหน่งขององค์ประกอบกลับเร็วกว่า ถูกกว่า และแม่นยำกว่า
ในเว็บไซต์ส่วนใหญ่ แนวทางที่ไม่ใช้ AI ทำงานได้ดีมาก ดังนั้นควรตรวจสอบก่อนว่า AI จำเป็นจริงหรือไม่ เช่น เมื่อข้อมูลไม่มีโครงสร้าง หรือเมื่อต้องอนุมานรูปแบบ output จากข้อมูลบนหน้าเว็บ
เหมือนผู้เขียน ให้ LLM สร้าง XPath แล้วปกติใช้ XPath นั้นทำ scraping แบบทั่วไป และถ้ามันพังก็ให้ LLM อัปเดต XPath
แต่ถ้าข้อมูลยังไม่กลับมาไหล หรือ pipeline ด้านหลังพังเพราะรูปแบบที่คาดไม่ถึง ค่อยแจ้งเตือนมนุษย์ตอนนั้นก็ได้
ผมสงสัยมาตลอดว่าทำไมถึงไม่มีเครื่องมือแบบนี้
สงสัยว่ามีสิ่งที่คล้ายกับ “HTML reducer” อยู่แล้วหรือไม่
ถ้าดูซอร์สของหน้าเว็บตรง ๆ โทเคน 90% จะเป็นขยะเพราะมี JavaScript แบบสุ่ม โฆษณา แอตทริบิวต์ที่ไม่จำเป็น และการซ้อนชั้นมากเกินไปเพื่อการเรนเดอร์
ถ้าใช้ DOM parser เดินไล่แล้วเหลือไว้เฉพาะโหนดที่มีข้อความ โครงสร้าง HTML และแอตทริบิวต์ของแท็กที่จำเป็น (ประมาณ class/id) ก็น่าจะลดค่าใช้จ่ายได้มาก และวิธีแบบ XPath ก็น่าจะทำงานได้ดีขึ้นด้วย
Readability ก็ใช้เหมือนกัน แต่จะเสียโครงสร้าง DOM ไป และคุณภาพจะแย่ลงในเว็บไซต์ที่มี JavaScript เยอะหรือหน้าที่ขยายข้อความแบบ “continue reading”
เลยสงสัยว่าเครื่องมือที่ใกล้เคียงมาตรฐานสำหรับงานลักษณะนี้คืออะไร
ถ้าสนใจก็เปิดเป็นโอเพนซอร์สได้ และน่าประหลาดใจที่เอาต์พุตจาก LLM ดีขึ้นมากหลังผ่าน reducer
แค่เติม https://r.jina.ai/ ไว้หน้า URL ใด ๆ ก็จะส่งคืน เวอร์ชัน Markdown ของเนื้อหาหลักของหน้านั้นที่เหมาะจะป้อนเข้า LLM
ตัวอย่างคือ https://r.jina.ai/https://simonwillison.net/2024/Sep/2/anato... และหน้าต้นฉบับคือ https://simonwillison.net/2024/Sep/2/anatomy-of-a-textual-us...
โค้ดเป็นโอเพนซอร์ส จึงรันเองได้: https://github.com/jina-ai/reader
เขียนด้วย TypeScript และใช้ Puppeteer กับ https://github.com/mozilla/readability
ผมใช้เฉพาะ Readability โดยไม่แปลงเป็น Markdown เพื่อดึงชื่อหน้าและเนื้อหา และมีสูตรสำหรับรันด้วย Playwright กับ shot-scraper ด้วย: https://shot-scraper.datasette.io/en/stable/javascript.html#...
เป็นวิธีที่นำมาจาก vimium แล้วปรับแก้ โดยกลั่น HTML ให้เหลือเฉพาะส่วนสำคัญ พร้อมจัดการ edge cases หลายแบบ
น่าประหลาดใจที่ผู้เขียนทำงานและเขียนบทความไปแล้ว 99% แต่ไม่ได้ทำส่วนที่เหลืออีก 1% คือดาวน์โหลดเอนจินที่ใช้ ollama หรือ llama.cpp มาทดสอบ LLM local ที่พอใช้ได้
ในกรณีใช้งานนี้ โมเดล 7B หรือ 30B ก็อาจทำได้ดี และต้นทุนการรันก็ต่ำพอจนไม่จำเป็นต้องใช้ GPT-4o
ตอนนี้เพิ่งเริ่มลองจับ LLM และตั้งใจจะดู โมเดลที่โฮสต์แบบ local ด้วย
ที่ Kadoa https://kadoa.com เราทำเว็บสแครปปิงอัตโนมัติด้วย AI มาแล้ว และการทดลองช่วงแรกก็คล้ายกับบทความนี้
เริ่มมาตั้งแต่ยุคที่มีแต่ GPT-3 ซึ่งแพงและช้า จึงต้องการโซลูชันที่คุ้มต้นทุนในสเกลใหญ่
สุดท้ายในขั้นตอน extraction เราไม่ได้ใช้ LLM ทุกครั้ง แต่ใช้ การสร้างโค้ด เพื่อสร้างโค้ด extraction ด้วย CSS selector หรือ XPath แล้วให้ปรับโค้ด scraper ตามการเปลี่ยนแปลงของเว็บไซต์ภายหลัง
สำหรับการทำความสะอาดและแปลงข้อมูลใช้ LLM ขนาดเล็กที่ fine-tune แล้ว ส่วนการตรวจสอบใช้วิธีดั้งเดิมอย่าง reverse search ร่วมกับ LLM-as-a-judge เพื่อประเมินคุณภาพข้อมูล
การนำไปใช้กับแหล่งข้อมูลเรียบง่ายไม่กี่แห่ง กับการรันให้เสถียร ขยายได้ และคุ้มต้นทุนบนเว็บไซต์นับพันแห่ง เป็นคนละปัญหากันโดยสิ้นเชิง และแนวทางที่ถูกต้องคือผสมผสานวิศวกรรม ETL แบบดั้งเดิมกับขั้นตอน LLM ขนาดเล็กที่ประเมินผลมาอย่างดี
ถ้าให้ตัวอย่าง HTML ที่อยากสแครป แล้วขอ โค้ดสั้น ๆ ด้วย BeautifulSoup ก็ทำได้ค่อนข้างดี
โดยทั่วไปโครงสร้างที่ต้องการดึงมักคงเดิม แต่การเขียนสตริงซับซ้อนเพื่อพาร์สเองเป็นงานน่าเบื่อ
การให้ LLM ทำการพาร์สจริงนั้นเกินความจำเป็น แถมยังเสี่ยงที่ผลลัพธ์จะปนเปื้อนจาก hallucination
ถ้าประมวลผล HTML ล่วงหน้าก่อน จะได้ผลลัพธ์ที่ถูกกว่าและดีกว่า
ดูเหมือนยังไม่มีใครพูดถึง แต่ส่วนตัวได้ผลลัพธ์ดีจาก trafilatura https://trafilatura.readthedocs.io/en/latest/
แค่ส่งเฉพาะข้อความให้ LLM ก็ลดค่าใช้จ่ายได้มหาศาล
ผมใช้ในโปรเจกต์ล่าสุด https://github.com/philippe2803/contentmap ด้วย เป็นไลบรารี Python ง่าย ๆ สำหรับสร้าง vector store ของเว็บไซต์ใด ๆ โดยใช้ XML sitemap ของโดเมนเป็นจุดเริ่มต้น
เพราะโครงสร้าง HTML ต่างกันไปตามแต่ละโดเมน จึงต้องดึงเฉพาะเนื้อหาจริงและลบแท็ก HTML ฯลฯ ออก ซึ่ง Trafilatura ทำงานนี้ให้ได้กับแทบทุก URL ด้วยโค้ดเพียงไม่กี่บรรทัด
แทบไม่เคยเห็นโพสต์ที่ติดแท็กสีส้มเยอะขนาดนี้มาก่อน
ที่ NewsCatcher เรากำลังทดสอบ GPT-4o กันอยู่มาก และต้องครอว์ลเว็บไซต์ข่าวกว่า 100,000 แห่ง แล้วพาร์สคอนเทนต์ข่าว
โมเดลแบบอิงกฎสำหรับดึงข้อมูลจากบทความใด ๆ ทำงานได้ค่อนข้างดีอยู่แล้ว และเรายังไม่พบวิธีที่จะปรับปรุงให้ดีขึ้นด้วย GPT
สิ่งที่น่าสนใจกว่าคือฝั่ง การครอว์ล เพราะต้องรู้ทุกตำแหน่งที่บทความข่าวอาจถูกเผยแพร่ และบางครั้งก็มีหมวดย่อยมากกว่า 50 หมวด
โครงสร้างของแต่ละเว็บไซต์ไม่ได้เปลี่ยนบ่อยนัก เลยมองว่าโปรเจกต์จำนวนมากอาจใช้วิธีสร้างโค้ดสำหรับดึงข้อมูลก็เพียงพอแล้ว
ดังนั้นเรากำลังดูแนวทางการใช้ LLM สร้างโค้ดพาร์ส HTML อยู่ หากสนใจติดต่อได้ที่ artem [at] newscatcherapi.com
ถ้ามีระบบสมัครใช้งานเองได้ก็คงดี
เว็บสแครปปิงคือแรงจูงใจจริง ๆ ที่ทำให้ผู้ร่วมก่อตั้งกับผมสร้าง openpipe.ai ขึ้นมาในปัจจุบัน
GPT-4 ทำงานนี้ได้ดีมากจริง ๆ แต่แพงเกินไป
อย่างไรก็ตาม ความสามารถในการสแครปไซต์บางประเภทนั้นค่อนข้างง่ายที่จะกลั่นออกมาเป็นโมเดลไฟน์จูนที่ถูกกว่ามาก และมันก็สแครปไซต์ประเภทนั้นได้ดีอย่างเสถียร
เราก็ทำปัญหานี้มานานพอสมควรแล้ว และจะติดต่อไปเพื่อให้ดูว่าเรามาถึงจุดไหนแล้ว