รายงานราคาทองผ่าน LINE Chatbot แบบอัตโนมัติ ด้วยเทคนิค Web Scraping

Jirawatee
LINE Developers Thailand
4 min readJan 27, 2021

--

ถ้าวันนี้คุณได้รับโจทย์ให้พัฒนาระบบรายงานข้อมูล ที่มีการเปลี่ยนแปลงของข้อมูลอยู่ตลอดเวลา เช่น ผลฟุตบอล, สภาพอากาศ, ค่า PM 2.5, อัตราแลกเปลี่ยน, ราคาหุ้น, ราคาน้ำมัน หรือ ราคาทอง เป็นต้น สิ่งแรกที่คุณจะทำคงเป็นการหา API จากบริการต้นทาง แต่ถ้าโชคไม่ดี มันไม่มี API คุณก็อาจจะต้องสร้างระบบฐานข้อมูลแล้วมากรอกค่าต่างๆเอง ซึ่งถ้าข้อมูลมันเปลี่ยนบ่อยมากๆ คุณก็คงทำเองไม่ไหวแน่ๆ

ดังนั้นบทความนี้จะพาคุณไปรู้จักกับเทคนิค Web Scraping(มหาเวทดูดดาว) ด้วยการพัฒนาระบบรายงานราคาทองผ่าน LINE Chatbot แบบที่ไม่ต้องพึ่ง API และไม่ต้องใช้ admin มานั่งอัพเดทข้อมูลเอง โดยที่ระบบจะรายงานเฉพาะกรณีที่ราคาทองนั้นมีการเปลี่ยนแปลง

มาดู 5 ขั้นตอนในการพัฒนากันเลย

  1. เตรียม LINE Chatbot ที่พัฒนาด้วย Cloud Functions for Firebase
  2. ตั้งเวลาดึงข้อมูลด้วย Cloud Scheduler (Cron Job)
  3. ดึงข้อมูลราคาทองด้วยเทคนิค Web Scraping
  4. เช็คและอัพเดทข้อมูลราคาทองด้วย Cloud Firestore
  5. รายงานราคาทองผ่าน 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 กัน โดยให้เราเปิดหน้าเว็บ สมาคมค้าทองคำ ในเบราว์เซอร์ แล้วมองหาส่วนของข้อมูลที่ต้องการก่อน ซึ่งตัวอย่างนี้ผมต้องการราคา ซื้อ-ขาย ในกรอบสีแดงตามรูปด้านล่าง

ที่มา: https://goldtraders.or.th/

รอไรอะ คลิกขวาแล้ว inspect เล็งหาพิกัดราคาทองคำกันเลย… Ta da! เจอ identity ของ HTML DOM แล้ว

ที่มา: https://goldtraders.or.th/

กลับมาที่ 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 ไว้

ที่มา: https://goldtraders.or.th/

จากภาพ เราจะมาสร้าง 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 ไว้ด้วยนะครับ เพื่อที่จะได้ไม่พลาดบทความตอนใหม่ๆจากพวกเรา สำหรับวันนี้ผมต้องขอตัวลาไปก่อน แล้วพบกันใหม่บทความหน้า 🙏 สวัสดีพี่น้องนักพัฒนาชาวไทย

--

--

Jirawatee
LINE Developers Thailand

Technology Evangelist at LINE Thailand / Google Developer Expert in Firebase