รายงานราคาทองผ่าน LINE Chatbot แบบอัตโนมัติ ด้วยเทคนิค Web Scraping
ถ้าวันนี้คุณได้รับโจทย์ให้พัฒนาระบบรายงานข้อมูล ที่มีการเปลี่ยนแปลงของข้อมูลอยู่ตลอดเวลา เช่น ผลฟุตบอล, สภาพอากาศ, ค่า PM 2.5, อัตราแลกเปลี่ยน, ราคาหุ้น, ราคาน้ำมัน หรือ ราคาทอง เป็นต้น สิ่งแรกที่คุณจะทำคงเป็นการหา API จากบริการต้นทาง แต่ถ้าโชคไม่ดี มันไม่มี API คุณก็อาจจะต้องสร้างระบบฐานข้อมูลแล้วมากรอกค่าต่างๆเอง ซึ่งถ้าข้อมูลมันเปลี่ยนบ่อยมากๆ คุณก็คงทำเองไม่ไหวแน่ๆ
ดังนั้นบทความนี้จะพาคุณไปรู้จักกับเทคนิค Web Scraping(มหาเวทดูดดาว) ด้วยการพัฒนาระบบรายงานราคาทองผ่าน LINE Chatbot แบบที่ไม่ต้องพึ่ง API และไม่ต้องใช้ admin มานั่งอัพเดทข้อมูลเอง โดยที่ระบบจะรายงานเฉพาะกรณีที่ราคาทองนั้นมีการเปลี่ยนแปลง
มาดู 5 ขั้นตอนในการพัฒนากันเลย
- เตรียม LINE Chatbot ที่พัฒนาด้วย Cloud Functions for Firebase
- ตั้งเวลาดึงข้อมูลด้วย Cloud Scheduler (Cron Job)
- ดึงข้อมูลราคาทองด้วยเทคนิค Web Scraping
- เช็คและอัพเดทข้อมูลราคาทองด้วย Cloud Firestore
- รายงานราคาทองผ่าน LINE Chatbot
หมายเหตุ: เนื้อหาในบทความนี้ เขียนขึ้นโดยมีวัตถุประสงค์เพื่อการศึกษาเท่านั้น
1. เตรียม LINE Chatbot ที่พัฒนาด้วย Cloud Functions for Firebase
สำหรับใครที่ยังไม่เคยพัฒนา LINE Chatbot ด้วย Cloud Functions for Firebase ให้ทำตามขั้นตอนของบทความด้านล่างนี้(ข้อ 1 และ 2 ก็พอ) แต่หากใครมีประสบการณ์ตรงนี้แล้ว ข้ามไปขั้นตอนที่ 2 ได้เลย
หมายเหตุ: เนื่องจากการพัฒนา LINE Chatbot ด้วย Cloud Functions คุณจำเป็นต้องเรียก API นอกโดเมน Google ทำให้คุณต้องเปลี่ยนแพลนการใช้งานจาก Spark ไปเป็น Blaze แต่ข้อดีคือคุณจะได้โควต้าฟรีในการเรียกใช้งานฟังก์ชันเพิ่มจาก 125,000 ครั้ง/เดือน ไปเป็น 2,000,000 ครั้ง/เดือน
2. ตั้งเวลาดึงข้อมูลด้วย Cloud Scheduler (Cron Job)
ขั้นตอนนี้ผมจะสร้างฟังก์ชันชื่อ gold ขึ้นมาใน index.js โดยฟังก์ชันดังกล่าวจะเป็นแบบที่สามารถตั้งเวลาให้ทำงานอัตโนมัติได้ ซึ่งในตัวอย่างนี้ผมจะตั้งเวลาให้ฟังก์ชันทำงานชั่วโมงละครั้ง เนื่องจากราคาทองอัพเดททั้งวัน แต่ก็ไม่ได้อัพเดทถี่ระดับนาที
exports.gold = functions.pubsub.schedule('0 */1 * * *').timeZone('Asia/Bangkok').onRun(async context => {
// ...
})
สำหรับรายละเอียดการสร้างฟังก์ชันเพื่อตั้งเวลา อ่านเพิ่มเติมได้ที่บทความด้านล่างนี้
3. ดึงข้อมูลราคาทองด้วยเทคนิค Web Scraping
ขั้นตอนนี้เราจะไปดึงข้อมูลราคาทองจาก สมาคมค้าทองคำ ด้วยเทคนิค Web Scraping(การแกะข้อมูลจาก HTML DOM) กัน โดยเริ่มแรกให้เปิดไฟล์ package.json แล้วเพิ่ม dependency เข้าไป 2 ตัว
- Axios: HTTP client ผู้มาแทน request-promise ที่ผมมักใช้ประจำก่อนหน้านี้ โดยสาเหตุที่เปลี่ยนเพราะ request-promise มัน deprecated ไปแล้วนั่นเอง
- Cheerio: ตัวแกะ DOM ที่มี Selector แบบเดียวกับ jQuery
"dependencies": {
"firebase-admin": "^9.4.2",
"firebase-functions": "^3.13.1",
"axios": "^0.21.1",
"cheerio": "^1.0.0-rc.5"
}
ถัดมาให้เปิดไฟล์ index.js แล้ว import ตัว dependency ทั้ง 2 เข้ามา
const axios = require('axios')
const cheerio = require('cheerio')
ภายในฟังก์ชัน gold ให้ใช้ axios ดูด HTML ของเว็บ สมาคมค้าทองคำ ออกมา
const response = await axios.get(`https://goldtraders.or.th`)
const html = response.data
จากนั้นเราจะใช้ cheerio ในการแปลง HTML ทั้งหมดมาเป็น DOM Model
const $ = cheerio.load(html)
ลำดับถัดไปเราจะสร้างตัว selector กัน โดยให้เราเปิดหน้าเว็บ สมาคมค้าทองคำ ในเบราว์เซอร์ แล้วมองหาส่วนของข้อมูลที่ต้องการก่อน ซึ่งตัวอย่างนี้ผมต้องการราคา ซื้อ-ขาย ในกรอบสีแดงตามรูปด้านล่าง
รอไรอะ คลิกขวาแล้ว inspect เล็งหาพิกัดราคาทองคำกันเลย… Ta da! เจอ identity ของ HTML DOM แล้ว
กลับมาที่ index.js กันต่อ ให้สร้าง selector พร้อมสกัดเอา HTML ออกให้หมดด้วย .text() แล้วก็เอาราคา ซื้อ-ขาย ทั้ง 4 มาต่อกันโดยคั่นด้วย pipe
const sell1 = $("#DetailPlace_uc_goldprices1_lblBLSell").text()
const buy1 = $("#DetailPlace_uc_goldprices1_lblBLBuy").text()
const sell2 = $("#DetailPlace_uc_goldprices1_lblOMSell").text()
const buy2 = $("#DetailPlace_uc_goldprices1_lblOMBuy").text()
const priceCurrent = sell1 + "|" + buy1 + "|" + sell2 + "|" + buy2// ผลลัพธ์จะได้ประมาณนี้ 26,400.00|26,300.00|26,900.00|25,832.64
แถมจ้าแถม (Optional)
โชคดีที่เว็บ สมาคมค้าทองคำ เขามี id ระบุใน element ของราคา ซื้อ-ขาย ทั้ง 4 ตัวทำให้เราสามารถสร้าง selector ได้โดยง่าย แต่คำถามคือ ถ้าเราจะไปดึงข้อมูลจากเว็บที่ไม่มี identity ระบุไว้ในแต่ละ element หละ จะดึงยังไง…ดังนั้นผมจะแถมวิธีการดึงข้อมูลทั้ง 4 ในอีกแบบ ไว้เผื่อเป็นแนวทางกับนักพัฒนาด้วยครับ
เริ่มจากหา identity ที่เป็นเหมือน parent ของส่วนข้อมูลที่เราสนใจให้เจอ แล้วหา element ชั้นสุดท้ายที่ครอบราคา ซื้อ-ขาย ทั้ง 4 ไว้
จากภาพ เราจะมาสร้าง selector ใหม่แบบนี้
const selector = $("#DetailPlace_uc_goldprices1_GoldPricesUpdatePanel font[color]")
หมายเหตุ: สาเหตุที่ผมระบุ font[color] แทนที่จะ font โดดๆ มาจากที่ element ภายใน id ซึ่งเป็น parent มี <font> หลายจุด แต่เฉพาะจุดที่แสดงราคา ซื้อ-ขาย จะมี attribute ชื่อ color มาด้วย
เพื่อความรอบคอบในกรณีที่หน้าเว็บ สมาคมค้าทองคำ มีการเปลี่ยนแปลง เราจะเพิ่มเงื่อนไขว่าข้อมูลราคา ซื้อ-ขาย นั้นยังสามารถดึงได้นะ หากไม่ได้เราจะให้ฟังก์ชันนี้หยุดทำงานทันที
if (selector.length !== 4) {
return null
}
หลังจากเราได้ selector มาแล้ว คราวนี้ก็มาสกัดเอาราคา ซื้อ-ขาย ทั้ง 4 ออกมา จากนั้นก็เอามาต่อกันและคั่นด้วย pipe
let priceCurrent = ""
selector.each((index, element) => {
if (index === 0) {
priceCurrent = $(element).text()
} else {
priceCurrent = priceCurrent.concat("|", $(element).text())
}
})// ผลลัพธ์จะได้ประมาณนี้ 26,400.00|26,300.00|26,900.00|25,832.64
4. เช็คและอัพเดทข้อมูลราคาทองด้วย Cloud Firestore
ครั้นจะให้ส่งข้อมูลราคาทองคำหาผู้ใช้ทุกชั่วโมงโดยที่ราคาไม่เปลี่ยนแปลง ก็ดูจะสแปมเกินไป อีกทั้งเปลืองโควต้าการส่งข้อความใน LINE OA อีกด้วย ดังนั้นในขั้นตอนนี้เราจะใช้ Cloud Firestore ซึ่งเป็น database มาเก็บข้อมูลราคา และนำมาตรวจสอบความเปลี่ยนแปลงกับข้อมูลในปัจจุบัน ก่อนจะส่งข้อความไปหาผู้ใช้กัน
กลับมาที่ไฟล์ index.js ให้ import ตัว dependency ชื่อ firebase-admin เข้ามา และให้ initial มันให้เรียบร้อย
const admin = require('firebase-admin');
admin.initializeApp()
ตัวอย่างนี้ผมจะออกแบบ database ให้มี collection ที่ชื่อ line, document ชื่อ gold และ field ชื่อ price ดังรูป
ถัดไปให้เราดึงข้อมูลราคา ซื้อ-ขาย ล่าสุดใน database ออกมา(priceLast) แล้วสร้างเงื่อนไขว่าหากยังไม่มีข้อมูล หรือ ข้อมูลราคา ซื้อ-ขาย ไม่ตรงกับข้อมูลล่าสุด(priceCurrent) ก็ให้อัพเดทข้อมูลล่าสุดลงไปใน database
let priceLast = await admin.firestore().doc('line/gold').get()
if (!priceLast.exists || priceLast.data().price !== priceCurrent) {
await admin.firestore().doc('line/gold')
.set({ price: priceCurrent })
}
5. รายงานราคาทองผ่าน LINE Chatbot
ขั้นตอนสุดท้ายให้สร้างฟังก์ชันชื่อ broadcast ที่รับตัวแปร priceCurrent ขึ้นมา แล้วแยก priceCurrent ออกเป็น 4 ส่วนด้วยการ split() จากนั้นก็เขียนโค้ดสำหรับ Broadcast ข้อมูลราคาทองคำไปให้ผู้ใช้ทุกคน
const broadcast = (priceCurrent) => {
const prices = priceCurrent.split('|');
return axios({
method: 'post',
url: 'https://api.line.me/v2/bot/message/broadcast',
headers: {
"Content-Type": "application/json",
Authorization: 'Bearer xxxxx'
},
data: JSON.stringify({
messages: [{
type: 'text',
text: "ตัวอย่าง: " + prices[0]
}]
})
})
}
โดยเราจะเรียกใช้งานฟังก์ชัน broadcast() ในกรณีที่มีการอัพเดท database เท่านั้น
if (!priceLast.exists || priceLast.data().price !== priceCurrent) {
// ..
broadcast(priceCurrent)
}
หน้าตาโค้ดทั้งหมดจากขั้นตอนที่ 1–5 ก็จะมีประมาณนี้ครับ
ถึงตรงนี้แล้วก็ deploy ได้เลย โดย shell ไปที่โฟลเดอร์ /functions แล้วใช้คำสั่ง
firebase deploy --only functions
แล้วก็มาดูผลลัพธ์กัน
สรุป
นอกจากเทคนิคทั้ง 5 ขั้นตอนแล้ว นักพัฒนาก็สามารถนำไปต่อยอด โดยให้ผู้ใช้ส่งข้อความมาขอข้อมูลล่าสุดเอง จากนั้นให้ Chatbot ไปดึงข้อมูลจาก database ไปตอบ ซึ่งจะเร็วกว่าการไปดึงข้อมูลจากหน้าเว็บโดยตรง
ส่วนโค้ดฉบับสมบูรณ์ + ข้อความที่เปลี่ยนจาก Text เป็น Flex Message ผมเตรียมมาให้ใน repo ด้านล่างนี้แล้ว clone ไปเล่นกันได้ตามอัธยาศัยเลยครับ
ก่อนจากฝากทุกท่านกด Follow ตัว Publication ไว้ด้วยนะครับ เพื่อที่จะได้ไม่พลาดบทความตอนใหม่ๆจากพวกเรา สำหรับวันนี้ผมต้องขอตัวลาไปก่อน แล้วพบกันใหม่บทความหน้า 🙏 สวัสดีพี่น้องนักพัฒนาชาวไทย