XPath
ภาษาเลือก node
บอกว่าจะหยิบ element ใดจาก DOM ใช้ได้ผ่าน browser, lxml หรือ DOMXPath
ONE LAB · THREE TOOLS · ONE DATA CONTRACT
Start by locating nodes, then fetch and parse static HTML, and finally control a browser only when JavaScript or interaction is necessary.
LEARNING MAP
ภาษาเลือก node
บอกว่าจะหยิบ element ใดจาก DOM ใช้ได้ผ่าน browser, lxml หรือ DOMXPath
ตัวอ่าน Static HTML
รับข้อความ HTML จาก requests แล้วค้นหา แปลง และจัดรูปข้อมูล
ตัวควบคุม browser
เปิดหน้า รอ JavaScript คลิก เลื่อน และอ่าน Rendered DOM
SHARED TASK
ดึง record จากหน้า Policy Interest Rate ของธนาคารแห่งประเทศไทย โดยต้องได้ field ชุดเดียวกันทุกวิธี
BEFORE CODE · SCRAPING CONTRACT
ตารางนี้ทำให้ผู้สอนและผู้เรียนตรวจคำตอบเดียวกันได้ และช่วยให้โปรแกรมรู้ว่าเมื่อใดควรหยุด
SELECT THE NODE
<main>
<h1>Policy Interest Rate</h1>
<p class="as-of">As of 27 Aug 2026</p>
<a href="rates.xlsx">Download</a>
</main>| ต้องการ | XPath |
|---|---|
| หัวข้อแรก | //main//h1[1] |
| ข้อความวันที่ | //*[contains(normalize-space(.),'As of')] |
| ลิงก์ Excel | //a[contains(@href,'.xlsx')] |
เลือกจากความหมาย เช่น tag, attribute และข้อความ ก่อนพึ่งตำแหน่งอย่าง div[3]
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")https://…/rates.xlsxตรวจว่าได้ 1 หัวข้อและอย่างน้อย 1 ลิงก์
TRY IT · SELECTOR SANDBOX
DOM ตัวอย่างอยู่ด้านซ้าย ลองแก้ selector แล้วกด Run เพื่อดูจำนวนและข้อความของ node ที่ตรงเงื่อนไข
SELECTORS ARE ASSUMPTIONS
/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
FETCH + PARSE
requestsfind()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)ROBUST HTTP 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_BYTEStimeout ป้องกันงานค้างไม่สิ้นสุดraise_for_status() เปิดเผย 4xx และ 5xx429 ต้องรอตาม Retry-After หรือ backoffRENDER + INTERACT
กดปุ่มเพื่อจำลอง JavaScript โหลดข้อมูลเพิ่ม หลังคลิก Rendered DOM จะมี 3 แถว
| 27 Aug 2026 | 1.50 |
| 25 Jun 2026 | 1.75 |
2 rows
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()SAME WORKLOAD · FAIR COMPARISON
1. View Source มีข้อมูลไหม? → มี: ใช้ requests + BeautifulSoup/lxml
2. ต้องคลิก เลื่อน หรือรอ JavaScript ไหม? → ใช่: ใช้ Selenium
3. มี API ทางการไหม? → มี: ใช้ API ก่อน scraping
CODE → LIVE RESULT
ส่วนนี้ใช้ทรัพยากรเดิมของแล็ป ไม่สร้าง 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
ใช้ PostgreSQL runtime เดิมของ Data Systems และใช้ unique key เพื่อไม่สร้างข้อมูลซ้ำ
(source_url, as_of_date)| ID | Publisher | As of | Title | Stored at |
|---|---|---|---|---|
| ยังไม่มีข้อมูล | ||||
(currency, rate_date)| ID | Currency | Date | Buy | Sell | Stored at |
|---|---|---|---|---|---|
| ยังไม่มีข้อมูล | |||||
VALIDATION BEFORE STORAGE
200, text/html และ response ไม่ใหญ่ผิดปกติ
หัวข้อต้องสื่อถึง Policy Interest Rate ไม่ใช่ error page ที่ตอบ 200
วันที่ต้อง parse เป็นชนิดวันที่ได้ ไม่รับข้อความใด ๆ แบบเงียบ ๆ
ลิงก์ต้องเป็น HTTPS และอยู่ใต้โดเมนที่อนุญาต
ถ้า schema เปลี่ยน ให้หยุด ไม่บันทึกค่าว่างแทนข้อมูลจริง
เก็บ source URL, fetched_at, selector version และ validation result
PRACTICE
แก้ selector ให้ยังทำงานเมื่อเพิ่ม div ครอบเนื้อหา เปรียบเทียบ absolute XPath กับ semantic XPath
ดึงทุกแถวของตาราง แปลงค่า rate เป็น float และหยุดเมื่อจำนวน cell ไม่ตรง schema
คลิก Load more รอจำนวนแถวเพิ่ม แล้วเก็บ screenshot เมื่อ timeout
CONTROLLED FAILURES
เริ่มจาก Stable HTML แล้วเปลี่ยนทีละเงื่อนไข บันทึกว่า failure เกิดที่ Fetch, Select หรือ Validate
3 records ผ่านทุก check
selector ไม่พบ แล้ว parser ต้องหยุด
schema contract ไม่ผ่าน
แยก batch ไป quarantine
uniqueness check จับ record ซ้ำ
รอตาม policy และ retry แบบมีขอบเขต
FORM INPUT → RESULT → DATABASE
ตัวอย่างจำลองหน้าอัตราแลกเปลี่ยน ผู้ใช้ต้องเลือกสกุลเงินและวันที่ เมื่อผลผ่าน validation แล้ว backend จะบันทึก currency, rate_date, buy, sell ลง PostgreSQL
ยังไม่มีผลลัพธ์
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 ก่อน
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
ใช้แนวทางนี้เมื่อข้อมูลเกิดหลัง JavaScript ทำงานและต้องผ่านขั้นตอนบนหน้าเว็บ เช่น กรอกฟอร์ม คลิกปุ่ม รอผลชุดใหม่ หรือใช้ session ที่เกิดจากหน้าก่อนหน้า
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 และ CSS เป็นเพียงภาษาที่ Selenium ใช้ชี้ช่องกรอก ปุ่ม และแถวผลลัพธ์ ไม่ได้ทำงานเป็นโปรแกรมอีกตัวหนึ่ง
อ่าน robots.txt และเงื่อนไขการใช้บริการ จำกัดความถี่ ใช้ cache ไม่ดึงข้อมูลส่วนบุคคล และหยุดเมื่อเว็บไซต์แสดงสัญญาณปฏิเสธ สำหรับงาน production ที่มี API ทางการและ schema ชัดเจน ให้เลือก API ก่อน
BATCH SCRAPING · 16 URL SLOTS
ฝั่ง server ยอมรับเฉพาะ HTTPS ภายใต้ www.bot.or.th, cache แต่ละหน้า 1 ชั่วโมง และไม่รับเกิน 16 URL ต่อคำขอ
ผลจะแสดงเป็น URL → HTTP → title → H1 → validation