WW warin.me

ONE LAB · THREE TOOLS · ONE DATA CONTRACT

เรียน XPath, BeautifulSoup และ Selenium จากโจทย์เดียวกัน

เริ่มจากระบุตำแหน่ง node แล้วดึงและอ่าน Static HTML ก่อนขยับไปควบคุม browser เฉพาะเมื่อ JavaScript หรือ interaction จำเป็น

THE TOOL LADDER1 → 2 → 3XPath → BeautifulSoup → Selenium

LEARNING MAP

สามอย่างนี้ไม่ใช่คู่แข่งกันทั้งหมด แต่ทำงานคนละชั้น

01

XPath

ภาษาเลือก node
บอกว่าจะหยิบ element ใดจาก DOM ใช้ได้ผ่าน browser, lxml หรือ DOMXPath

→
02

BeautifulSoup

ตัวอ่าน Static HTML
รับข้อความ HTML จาก requests แล้วค้นหา แปลง และจัดรูปข้อมูล

→
03

Selenium

ตัวควบคุม browser
เปิดหน้า รอ JavaScript คลิก เลื่อน และอ่าน Rendered DOM

SHARED TASK

โจทย์เดียวของทั้งแล็ป

ดึง record จากหน้า Policy Interest Rate ของธนาคารแห่งประเทศไทย โดยต้องได้ field ชุดเดียวกันทุกวิธี

titleหัวข้อหน้า
descriptionคำอธิบาย
as_ofวันที่อ้างอิง
download_urlลิงก์ข้อมูล
Inspect→Fetch→Select→Clean→Validate→Store

BEFORE CODE · SCRAPING CONTRACT

ตกลงขอบเขตและความหมายของข้อมูลก่อนเขียน selector

ตารางนี้ทำให้ผู้สอนและผู้เรียนตรวจคำตอบเดียวกันได้ และช่วยให้โปรแกรมรู้ว่าเมื่อใดควรหยุด

คำถามสิ่งที่ต้องระบุเก็บได้หรือไม่Terms, licence, robots.txt หรือการอนุมัติหน้าใดอยู่ในขอบเขตHTTPS, host allowlist และ canonical URLหนึ่ง record คืออะไรfield, type, required value และ composite keyข้อมูลต้องใหม่แค่ไหนรอบเวลา, source timestamp และ fetched_atถ้าล้มเหลวทำอย่างไรretry เฉพาะชั่วคราว, quarantine และผู้รับผิดชอบ
01

SELECT THE NODE

XPath: อ่าน DOM เป็นความสัมพันธ์

Step 1 · มองโครงสร้างก่อนเขียน selector

<main>
  <h1>Policy Interest Rate</h1>
  <p class="as-of">As of 27 Aug 2026</p>
  <a href="rates.xlsx">Download</a>
</main>

Step 2 · แปลความต้องการเป็น XPath

ต้องการXPath
หัวข้อแรก//main//h1[1]
ข้อความวันที่//*[contains(normalize-space(.),'As of')]
ลิงก์ Excel//a[contains(@href,'.xlsx')]

เลือกจากความหมาย เช่น tag, attribute และข้อความ ก่อนพึ่งตำแหน่งอย่าง div[3]

Step 3 · ใช้ XPath ผ่าน Python lxml

import requests
from lxml import html

source = requests.get(url, timeout=10).text
tree = html.fromstring(source)

title = tree.xpath('string(//main//h1[1])').strip()
links = tree.xpath("//a[contains(@href,'.xlsx')]/@href")
→
RESULTPolicy Interest Ratehttps://…/rates.xlsx

ตรวจว่าได้ 1 หัวข้อและอย่างน้อย 1 ลิงก์

TRY IT · SELECTOR SANDBOX

ทดลองเลือก node แล้วดูผลทันที

DOM ตัวอย่างอยู่ด้านซ้าย ลองแก้ selector แล้วกด Run เพื่อดูจำนวนและข้อความของ node ที่ตรงเงื่อนไข

Policy Interest Rate

As of 27 Aug 2026

Download XLSX
27 Aug 20261.50
25 Jun 20261.75
รอทดลอง selector

SELECTORS ARE ASSUMPTIONS

selector ทุกตัวมีสมมติฐานว่าหน้าเว็บจะยังคงรูปเดิม

/html/body/div[3]/…

ผูกกับตำแหน่ง

เพิ่ม wrapper เพียงชั้นเดียวก็พัง ใช้โครงสร้างที่สื่อความหมายหรือ attribute ที่เสถียรกว่า

tr:nth-child(2)

ผูกกับลำดับ

แถวโฆษณาหรือ header ใหม่ทำให้เลือกผิด ใช้ค่าภายใน cell หรือ key ของ record

table:first

ผูกกับสิ่งแรก

อาจอ่านตาราง navigation โดยไม่ error ตรวจชื่อ column และ identity ของชุดข้อมูลเสมอ

text.split(',')

ผูกกับตัวคั่น

comma ภายในค่าทำให้ข้อมูลแตกผิด อ่าน cell จาก DOM แล้วค่อย normalize type

02

FETCH + PARSE

BeautifulSoup: เปลี่ยน Static HTML เป็น record

  1. GETดาวน์โหลด HTML ด้วย requests
  2. CHECKตรวจ status, Content-Type และขนาด
  3. PARSEสร้าง tree ด้วย BeautifulSoup
  4. SELECTเลือกด้วย CSS selector หรือ find()
  5. CLEANตัด whitespace และแปลงชนิดข้อมูล

โค้ดครบจาก URL ถึง record

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin

r = requests.get(url, timeout=10,
    headers={'User-Agent': 'teaching-lab/1.0'})
r.raise_for_status()
assert 'text/html' in r.headers['Content-Type']

soup = BeautifulSoup(r.text, 'html.parser')
record = {
  'title': soup.select_one('main h1').get_text(' ', strip=True),
  'download_url': urljoin(url,
      soup.select_one("a[href$='.xlsx']")['href'])
}
→

ตรวจผลก่อนบันทึก

assert record['title']
assert record['download_url'].startswith('https://')
assert 'bot.or.th' in record['download_url']

print(record)
BeautifulSoup อ่าน HTML ที่ server ส่งมา หากข้อมูลเกิดหลัง JavaScript ทำงาน selector ที่ถูกต้องก็ยังหาไม่พบ

ROBUST HTTP REQUEST

request ที่ดีต้องล้มเหลวอย่างมีขอบเขต

r = requests.get(
    url,
    timeout=(3.05, 15),
    headers={'User-Agent': USER_AGENT},
    allow_redirects=False
)
r.raise_for_status()

assert r.headers['Content-Type'].startswith('text/html')
assert len(r.content) <= MAX_BYTES

จุดที่ควรสังเกตทันที

  • timeout ป้องกันงานค้างไม่สิ้นสุด
  • raise_for_status() เปิดเผย 4xx และ 5xx
  • 429 ต้องรอตาม Retry-After หรือ backoff
  • redirect ต้องยังอยู่ใน host ที่อนุญาต
  • Content-Type และขนาดต้องตรงกับ contract
Retry เฉพาะ network error ชั่วคราว ข้อมูลผิด schema ต้องหยุดและแยกตรวจ ไม่ควร retry จนข้อมูลผิดไหลเข้าฐาน
03

RENDER + INTERACT

Selenium: ใช้เมื่อข้อมูลเกิดหลังหน้าเว็บทำงาน

Static source มี 2 แถว

กดปุ่มเพื่อจำลอง JavaScript โหลดข้อมูลเพิ่ม หลังคลิก Rendered DOM จะมี 3 แถว

RENDERED DOM
27 Aug 20261.50
25 Jun 20261.75

2 rows

รอเงื่อนไข ไม่ใช้ sleep แบบเดาสุ่ม

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

driver = webdriver.Chrome()
driver.get(url)
wait = WebDriverWait(driver, 10)

wait.until(EC.element_to_be_clickable(
  (By.CSS_SELECTOR, 'button.load-more'))).click()
rows = wait.until(EC.presence_of_all_elements_located(
  (By.CSS_SELECTOR, 'table tbody tr')))
assert len(rows) >= expected_min
driver.quit()
→

หลักฐานที่ควรเก็บ

  • URL ก่อนและหลัง interaction
  • จำนวนแถวก่อนและหลัง
  • selector และ timeout
  • ข้อความ error เมื่อรอไม่สำเร็จ
  • screenshot เมื่อทดสอบล้มเหลว
ถ้าหา API request ที่หน้าเว็บใช้ได้และได้รับอนุญาต ควรเรียก API โดยตรงแทนการขับ browser

SAME WORKLOAD · FAIR COMPARISON

เทียบด้วย input และ output เดียวกัน

XPath + lxmlBeautifulSoupSeleniumสิ่งที่ทำเลือก node จาก HTMLfetch + parse + cleanrender + interact + selectJavaScriptไม่ได้ไม่ได้ได้ความเร็วทั่วไปเร็วเร็วช้ากว่าใช้เมื่อต้องการ selector เชิงสัมพันธ์Static HTML และ PythonDynamic UI จำเป็นจริง

ลำดับตัดสินใจ

1. View Source มีข้อมูลไหม? → มี: ใช้ requests + BeautifulSoup/lxml

2. ต้องคลิก เลื่อน หรือรอ JavaScript ไหม? → ใช่: ใช้ Selenium

3. มี API ทางการไหม? → มี: ใช้ API ก่อน scraping

CODE → LIVE RESULT

รัน backend ที่มีอยู่จริง: PHP cURL + DOMXPath

ส่วนนี้ใช้ทรัพยากรเดิมของแล็ป ไม่สร้าง Docker หรือ engine ใหม่ ฝั่ง server จำกัด URL, timeout, ขนาด response และ cache ผล

$doc = new DOMDocument();
$doc->loadHTML($html, LIBXML_NONET);
$xp = new DOMXPath($doc);

$title = $xp->query('//h1')
            ->item(0)?->textContent;
$links = $xp->query('//a[@href]');
→

กำลังรอผลการดึงข้อมูลแบบมีขอบเขต

DATABASE RESULT

ข้อมูลที่ผ่าน validation แล้ว

ใช้ PostgreSQL runtime เดิมของ Data Systems และใช้ unique key เพื่อไม่สร้างข้อมูลซ้ำ

กำลังอ่านข้อมูลจากฐานข้อมูล…

Policy interest rate (source_url, as_of_date)

IDPublisherAs ofTitleStored at
ยังไม่มีข้อมูล

Currency rate (currency, rate_date)

IDCurrencyDateBuySellStored at
ยังไม่มีข้อมูล

VALIDATION BEFORE STORAGE

selector ทำงาน ไม่ได้แปลว่าข้อมูลถูกต้อง

HTTP & type

200, text/html และ response ไม่ใหญ่ผิดปกติ

Semantic check

หัวข้อต้องสื่อถึง Policy Interest Rate ไม่ใช่ error page ที่ตอบ 200

Date shape

วันที่ต้อง parse เป็นชนิดวันที่ได้ ไม่รับข้อความใด ๆ แบบเงียบ ๆ

Trusted link

ลิงก์ต้องเป็น HTTPS และอยู่ใต้โดเมนที่อนุญาต

Fail closed

ถ้า schema เปลี่ยน ให้หยุด ไม่บันทึกค่าว่างแทนข้อมูลจริง

Provenance

เก็บ source URL, fetched_at, selector version และ validation result

PRACTICE

งานทดลองสามรอบ

A · XPath

แก้ selector ให้ยังทำงานเมื่อเพิ่ม div ครอบเนื้อหา เปรียบเทียบ absolute XPath กับ semantic XPath

B · BeautifulSoup

ดึงทุกแถวของตาราง แปลงค่า rate เป็น float และหยุดเมื่อจำนวน cell ไม่ตรง schema

C · Selenium

คลิก Load more รอจำนวนแถวเพิ่ม แล้วเก็บ screenshot เมื่อ timeout

เกณฑ์ผ่าน

CONTROLLED FAILURES

ทดสอบให้เห็นว่าระบบหยุดถูกเหตุผล

เริ่มจาก Stable HTML แล้วเปลี่ยนทีละเงื่อนไข บันทึกว่า failure เกิดที่ Fetch, Select หรือ Validate

01 · Stable HTML

3 records ผ่านทุก check

02 · CSS class เปลี่ยน

selector ไม่พบ แล้ว parser ต้องหยุด

03 · Column เปลี่ยนชื่อ

schema contract ไม่ผ่าน

04 · Missing value

แยก batch ไป quarantine

05 · Duplicate key

uniqueness check จับ record ซ้ำ

06 · HTTP 429

รอตาม policy และ retry แบบมีขอบเขต

FORM INPUT → RESULT → DATABASE

กรอกข้อมูล ค้นหา และบันทึกสกุลเงิน

ตัวอย่างจำลองหน้าอัตราแลกเปลี่ยน ผู้ใช้ต้องเลือกสกุลเงินและวันที่ เมื่อผลผ่าน validation แล้ว backend จะบันทึก currency, rate_date, buy, sell ลง PostgreSQL

เลือก currency และวันสิ้นสุด

ยังไม่มีผลลัพธ์

ขอบเขตของปุ่มสาธิต: หน้านี้จำลองลำดับ interaction ที่ Selenium จะทำ แล้วส่งห้าวันเข้า backend เพื่อบันทึกจริง ตัว WebDriver Python ไม่ได้รันใน web container นี้ เพราะ runtime ปัจจุบันไม่มี Chrome/ChromeDriver
1 · Fillกำหนดค่า input2 · Submitส่ง form หรือ click3 · Waitรอผลชุดใหม่4 · Validateตรวจ currency และ date5 · StoreUPSERT เข้า PostgreSQL

ถ้าฟอร์มส่ง HTTP โดยตรง

payload = {'currency': 'USD',
           'date': '2026-08-26'}
r = requests.post(search_url, data=payload,
                  timeout=10)
soup = BeautifulSoup(r.text, 'html.parser')
row = soup.select_one('table.rates tbody tr')

ตรวจ method, endpoint และชื่อ field จาก DevTools → Network ก่อน

ถ้าฟอร์มทำงานผ่าน JavaScript

driver.find_element(By.ID, 'currency') \
      .send_keys('USD')
driver.find_element(By.ID, 'date') \
      .send_keys('2026-08-26')
driver.find_element(By.CSS_SELECTOR,
                    'button.search').click()
row = wait.until(EC.visibility_of_element_located(
    (By.CSS_SELECTOR, 'table.rates tbody tr')))

หลัง submit ต้องรอผลชุดใหม่และตรวจว่าไม่ใช่ค่าค้างจากการค้นหาก่อนหน้า

SELENIUM ONLY · END TO END

กรณีที่ Selenium ทำครบได้ตัวเดียว

ใช้แนวทางนี้เมื่อข้อมูลเกิดหลัง JavaScript ทำงานและต้องผ่านขั้นตอนบนหน้าเว็บ เช่น กรอกฟอร์ม คลิกปุ่ม รอผลชุดใหม่ หรือใช้ session ที่เกิดจากหน้าก่อนหน้า

เปิดหน้า→กรอก input→คลิก Search→รอ JavaScript→อ่านผล
driver = webdriver.Chrome()
wait = WebDriverWait(driver, 10)
try:
    driver.get(url)
    driver.find_element(By.ID, 'currency') \
          .send_keys('USD')
    driver.find_element(By.ID, 'date') \
          .send_keys('2026-08-26')
    driver.find_element(By.CSS_SELECTOR,
                        'button.search').click()
    row = wait.until(EC.visibility_of_element_located((
        By.XPATH,
        "//table[contains(@class,'rates')]//tr[td='USD']"
    )))
    print(row.text)
finally:
    driver.quit()

ตัวเดียว หมายถึงอะไร

Selenium เป็นโปรแกรมที่ควบคุมทุกขั้นตอน ไม่ต้องใช้ requests, BeautifulSoup หรือ lxml เพิ่ม

แล้ว XPath อยู่ตรงไหน

XPath และ CSS เป็นเพียงภาษาที่ Selenium ใช้ชี้ช่องกรอก ปุ่ม และแถวผลลัพธ์ ไม่ได้ทำงานเป็นโปรแกรมอีกตัวหนึ่ง

ใช้ Selenium ครบวงจรเมื่อ interaction จำเป็นจริง เพราะใช้เวลาและทรัพยากรมากกว่าการส่ง HTTP request โดยตรง

ใช้อย่างรับผิดชอบ

อ่าน robots.txt และเงื่อนไขการใช้บริการ จำกัดความถี่ ใช้ cache ไม่ดึงข้อมูลส่วนบุคคล และหยุดเมื่อเว็บไซต์แสดงสัญญาณปฏิเสธ สำหรับงาน production ที่มี API ทางการและ schema ชัดเจน ให้เลือก API ก่อน

BOT Exchange Rates API documentation →

BATCH SCRAPING · 16 URL SLOTS

ทดลอง URL 1–8 และเตรียมช่อง 9–16

ฝั่ง server ยอมรับเฉพาะ HTTPS ภายใต้ www.bot.or.th, cache แต่ละหน้า 1 ชั่วโมง และไม่รับเกิน 16 URL ต่อคำขอ

ผลจะแสดงเป็น URL → HTTP → title → H1 → validation