การทำ Web Scrapping ก็คือการดึงเฉพาะส่วนที่เราต้องการใช้งานจากเว็ปไซต์เป้าหมายที่ไม่มี API ให้ใช้งาน มาเก็บไว้ที่เครื่องของเรา หรือเตรียมนำไปใช้งานทางด้านอื่นๆ ต่อไป เช่นนำไปทำ Dataset เพื่อทำ ML ในอนาคต
บอกไว้ตรงนี้ก่อนว่า ผมไม่สนับสนุนสินค้าละเมิดลิขสิทธิ์นะครับ อันนี้แค่ทำให้ดูเป็นตัวอย่างเฉยๆ
ลองนึกภาพว่าเราอยากอ่านการ์ตูนเรื่อง hikaru-no-go จากเว็ป https://www.mangathai.com/manga-hikaru-no-go เราจะต้องทำอะไรบ้าง
ทำอย่างนี้วนไปเรื่อยๆ -_-’ กว่าจะอ่านจบ เมื่อยมั้ยล่ะ งั้นเดี๋ยวเราจะคิดใหม่ทำใหม่ เขียน โปรแกรมให้มันทำอะไรซ้ำๆแทนเราไป แล้วเราก็นั่งรอให้มันทำงาน 1 - 4 ให้โดยอัตโนมัติ
ของที่เราจะใช้ในงานนี้ก็มี nodejs puppeteer, fs, https, mkdirp, url, path
บอกไว้ตรงนี้ก่อนว่า ผมไม่ค่อยถนัดเขียน nodejs สักเท่าไหร่ เคยเขียน jquery มาบ้างนานโพ้ดๆมาแล้ว อาจจะไม่เป้ะนะครับ
ก่อนอื่นเราสร้างไฟล์ขึ้นมาก่อนเลยชื่อ loadtoon.js
แล้วผมก็สั่ง
yarn add puppeteer mkdirp
หลังจากนั้นผมก็ตั้งค่าเรียกใช้ module ที่จำเป็นกันก่อนเลย
const puppeteer = require('puppeteer')const mkdirp = require('mkdirp')const https = require('https')const fs = require('fs')const url = require('url')const path = require('path')
โดย เจ้า const puppeteer = require(‘puppeteer’) เนี่ยพระเอกของงานนี้เลย
ส่วน const mkdirp = require(‘mkdirp’) ผมเอามาใช้เพื่อเวลาโหลดการ์ตูนมาจะได้จัด folder สวยๆ แต่ละตอนไม่ปะปนกัน
const https = require(‘https’) นี่ผมเตรียมไว้ เผื่อว่า รูปที่เรา Download ลงมา น่าจะเป็น https กันหมดแล้ว
ในส่วนของการบันทึกไฟล์ลงเครื่องใช้ const fs = require(‘fs’)
และใช้ const url = require(‘url’) และ const path = require(‘path’) ในการดึงเฉพาะชื่อไฟล์ออกมาจาก url เพราะขี้เกียจตั้งชื่อให้มัน
หลังจากนั้นผมจึงระบุ url ปลายทางของการ์ตูนที่ต้องการ ไว้ที่ตัวแปร target
let browserlet pageconst target = 'https://www.mangathai.com/manga-hikaru-no-go'
มาเขียน function ให้มัน run ก่อน แล้วเดี๋ยวเราจะเขียนขั้นตอนการทำงานลงไปใน function นี้
const run = async () => {}run()
เราจะ บันทึกทุกอย่างที่เรา load มาไว้ที่ folder download กัน เพราะอย่างนั้น สั่งให้ code เราสร้าง folder ก่อนเลยด้วยคำสั่ง
mkdirp('./download').then(made =>console.log(`made directories, starting with ${made}`))
ต่อไปเราจะสร้าง browser แบบ headless ขึ้นมา โดยตอน develop ผมจะสั่งให้เปิด devtools ไว้ให้ด้วย เพื่อที่จะได้ debug ง่ ายๆ แล้วสั่ง headless mode เป็น false ไว้ หลังจากนั้นก็สร้าง newPage ขึ้นมา เพื่อที่จะให้ browser ทำการเรียก url เป้าหมายของเรา
browser = await puppeteer.launch({headless: false,args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-infobars'],devtools: true,})const options = { waitUntil: 'load', timeout: 0, visible: true }page = await browser.newPage()await page.setViewport({ width: 1366, height: 768 })
พอมาถึงขั้นตอนนี้เราจะใ ห้ตัว code ของเรา ไปเปิดหน้า target ขึ้นมา เมื่อเปิดสำเร็จให้แสดง title ของ site ออกมาที่ terminal ของเรา โดยให้การทำงานหยุดรอ networkidle0 คือการให้หยุด โดยไม่มีการเชื่อมต่อแล้ว ถึงทำงานอันต่อไป
await page.goto(target, { waitUntil: 'networkidle0' })const title = await page.title()console.log(`connect to site... ${title}`)
ทีนี้ เราก็จะมาเลือกทำงานกับ elements ตรงกลางสุดที่คลุม table content ไว้ เมื่อเราเปิดอ่าน inspector เราจะพบว่ามันชื่อ ‘body > div.container’
const resultSelector = 'body > div.container'await page.waitForSelector(resultSelector, options)
ต่อมาเราะจะทำการ ดึงทุก link ที่เป็นแต่ละเล่มหรือตอนของการ์ตูนมาเก็บไว้เป็น array 1 ชุด เพื่อเอาไว้เข้าไปดึงแต่ละหน้าของเล่มนั้นอีกที หลังจากเปิด inspector เราจะเห็นว่า มี class ๆ นึงที่คลุม link ของแต่ละเล่มไว้ นั่นก็คือ .chapter-name
ต่อมาเราจะทำการเขียน code ให้ดึงข้อมูล .chapter-name ทั้งหน้าเลย มาใช้งาน แล้วดึง Text ที่อยู่ภายใต้ Tag a และ url ที่อยู่ใน attribute a ขึ้นมาเก็บไว้
const findingElement = '.chapter-name'const result = await page.evaluate((resultSelector, findingElement) => [...document.querySelectorAll(findingElement)].map(elem => {const data = {title: elem.querySelector(`${findingElement} > a`).textContent,link: elem.querySelector(`${findingElement} > a`).getAttribute('href')}return data}), resultSelector, findingElement)
เมื่อเรา log result ออกมาจะพบว่าเราได้ link ทั้งหมดมาแล้วจ้า
[nodemon] restarting due to changes...[nodemon] starting `node loadtoon.js`made directories, starting with undefinedconnect to site... อ่านการ์ตูน Hikaru no Go มังงะ Hikaru no Go แปลไทย TH ตอนที่ 1 ถึง ตอนล่าสุด ครบทุกตอน - mangathai.com[{title: 'อ่าน Hikaru no Go เล่ม 1 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-1/'},{title: 'อ่าน Hikaru no Go เล่ม 2 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-2/'},{title: 'อ่าน Hikaru no Go เล่ม 3 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-3/'},{title: 'อ่าน Hikaru no Go เล่ม 4 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-4/'},{title: 'อ่าน Hikaru no Go เล่ม 5 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-5/'},{title: 'อ่าน Hikaru no Go เล่ม 6 TH แปลไท ย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-6/'},{title: 'อ่าน Hikaru no Go เล่ม 7 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-7/'},{title: 'อ่าน Hikaru no Go เล่ม 8 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-8/'},{title: 'อ่าน Hikaru no Go เล่ม 9 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-9/'},{title: 'อ่าน Hikaru no Go เล่ม 10 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-10/'},{title: 'อ่าน Hikaru no Go เล่ม 11 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-11/'},{title: 'อ่าน Hikaru no Go เล่ม 12 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-12/'},{title: 'อ่าน Hikaru no Go เล่ม 13 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-13/'},{title: 'อ่าน Hikaru no Go เล่ม 14 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-14/'},{title: 'อ่าน Hikaru no Go เล่ม 15 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-15/'},{title: 'อ่าน Hikaru no Go เล่ม 16 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-16/'},{title: 'อ่าน Hikaru no Go เล่ม 17 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-17/'},{title: 'อ่าน Hikaru no Go เล่ม 18 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-18/'},{title: 'อ่าน Hikaru no Go เล่ม 19 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-19/'},{title: 'อ่าน Hikaru no Go เล่ม 20 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-20/'},{title: 'อ่าน Hikaru no Go เล่ม 21 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-21/'},{title: 'อ่าน Hikaru no Go เล่ม 22 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-22/'},{title: 'อ่าน Hikaru no Go เล่ม 23 TH แปลไทย',link: 'https://www.mangathai.com/manga-hikaru-no-go/ch-23/'}]
array ที่เราได้มามันจะเรียงจากเล่มมากสุดลงไปถึงเล่มน้อยสุด ทีนี้ ผมต้องการให้ code เราสร้าง folder 1-n เมื่อ n = จำนวนเล่มทั้งหมด แล้วค่อย download แต่ละหน้าเข้าไป save ใน folder เล่มนั้นๆ เราจะพบว่า index ที่ 0 จะเป็นเล่มที่ 23 ไล่ลงไปจนถึง index ที่ n จะเป็นเล่มที่ 1
งั้นเรามา reverse array กันก่อน จะได้ง่ายๆกับชีวิต
const ep = result.reverse()console.log(ep)
หลังจากนั้น เราจะมา loop array เพื่อ download กันทีละเล่ม ทีละหน้าเลย เราจะใช้ for of ในการเรียกใช้ Iterable Objects Array เพื่อให้มันรอ download file เล่มปัจจุบันให้เสร็จก่อน ค่อยไป download เล่มอื่นต่อ ถ้าใช้ forEach ก็พังครับ เพราะมันจะทำงาน Asynchronous กัน
for (const [index, item] of ep.entries()) {}
ภายใน for of เราจะ implement function ใหม่ขึ้นมา ทำหน้าที่ download file มาเก็บแยกตาม folder ของแต่ละเล่ม ชื่อว่า loadEP(page, item, index) โดยจะ pass ค่า page และ item ของ array แต่ละเล่มเข้ามาเพื่อเข้าไปยังเล่มต่อๆไป
const loadEP = async (page, item, index) => {}
มาดูการทำงานของ loadEP กัน เข้ามาเราจะสั่งให้สร้าง folder ของเล่มนั้นๆขึ้นมา แล้วเปิดหน้าของ เล่มนั้นๆ เพื่อที่จะวิ่งไป download ทีละหน้าๆ จนครบทั้งเล่ม แล้วค่อยออกจาก function โดยเราจะ download url จาก tag img attribute src
โดยทำการ selector ที่ class .img-manga ซึ่งเป็น class ที่คลุม รูปของเนื้อหาการ์ตูนที่เราจะ download นั่นเอง แล้วทำการ replace url ที่เป็น http ธรรมดาให้เป็น https ซะเพราะความขี้เกียจมาเขียน http หรือ https เพื่อทำการ download
mkdirp(`./download/${index + 1}`).then(made =>console.log(`made directories, starting with ${made}`))let pageURL = item.linkconst options = { waitUntil: 'load', timeout: 0 }await page.goto(pageURL, { waitUntil: 'networkidle0' })const coverSelector = '.img-manga'await page.waitForSelector(coverSelector, options)const imgURL = await page.evaluate(coverSelector => {const img = document.querySelector(coverSelector).getAttribute('src')return img}, coverSelector)const imageURL = imgURL.replace("http://", "https://")
พอเราได้ url สำหรับ download content ที่เราต้องการมาเรียบร้อยแล้ว เราก็จะมา implement function สำหรับการ download กัน โดยเราจะตัดชื่อ file ออกมาจาก url ที่เราได้มาก่อน เพราะว่าเราขี้เกียจตั้งชื่อไฟล์
const parsed = url.parse(imageURL)const filename = path.basename(parsed.pathname)await download(imageURL, `./download/${index + 1}/${filename}`)
ใน function download เราก็ทำการเขียน ไฟล์ลงในแต่ละ folder ของเล่มนั้นๆครับ
const download = async (imageURL, savePath) => {await new Promise((resolve, reject) => {const createFile = fs.createWriteStream(savePath)https.get(imageURL, (res) => {res.pipe(createFile)createFile.on('finish', () => {console.log(`Download ${savePath} is finished.`)createFile.close()resolve()})createFile.on('error', (error) => {reject(error)})})})}
เมื่อ download เสร็จแล้ว เราจะไปหา url ว่าหน้าต่อไปอยู่ตรงไหน จะได้วิ่งไป load เรื่อยๆจนกว่าจะครบด้วยการมองหา element .btn_next_page แล้วดึง url ภายใต้ attribute src ของ tag img ออกมา
const btn = '.btn_next_page'pageURL = await page.evaluate(btn => {const next = document.querySelector(btn).getAttribute('href')return next + '/'}, btn)
แล้วใช้คำสั่ง do while เพื่อวนทำแบบนี้ไปเรื่อยๆจนกว่าจะหมดทุกหน้าในเล่มนั้น แล้วค่อยไปยังเล่มถัดไปครับ
เป็นอันเรียบร้อยแล้ว เราก็ลอง run ทิ้งไว้ครับ แล้วเดินไปดู Serires Nexflix สักตอน แล้วค่อยกลับมาดูครับเป็นอันเสร็จสิ้น ในจริงจะให้ code แปลงทุกไฟล์รวมเป็น PDF ให้ด้วยเลย ก็ยังได้ แต่มันจะสบายเกินไปแล้ว ลองเอาไป implement กันต่อเองนะครับ
load https://www.mangathai.com/manga-hikaru-no-go/ch-1/ = 0made directories, starting with undefinedDownloading... https://1.bp.blogspot.com/-eYzObFaYGbs/UqQCWVUsS5I/AAAAAAAEefc/yjVpCVRkXLY/s0/v01_001.jpgDownload ./download/1/v01_001.jpg is finished.
code ทั้งหมด เอาขึ้น github ไว้ให้แล้วด้านล่างเลยครับ
บอกไว้ตรงนี้ก่อนว่า ผมไม่สนับสนุนสินค้าละเมิดลิขสิทธิ์นะครับ อันนี้แค่ทำให้ดูเป็นตัวอย่างเฉยๆ
Code Download ได้ที่นี่
สุขสั นต์วันแม่นะครับ บรัยยยยยยยยย
Quick Links
Legal Stuff