HomeArtTechHackBlockchain
ONLINE ·
Index
/
Technology
/
Article

Puppeteer Web Scrapping Download การ์ตูนมาอ่านเล่นวันหยุด

Operator
Khomkrid Lerdprasert
Filed
August 12, 2020
Channel
Technology
Read
~2 min
Puppeteer Web Scrapping Download การ์ตูนมาอ่านเล่นวันหยุด

Puppeteer Web Scrapping Download การ์ตูนมาอ่านเล่นวันหยุด

การทำ Web Scrapping ก็คือการดึงเฉพาะส่วนที่เราต้องการใช้งานจากเว็ปไซต์เป้าหมายที่ไม่มี API ให้ใช้งาน มาเก็บไว้ที่เครื่องของเรา หรือเตรียมนำไปใช้งานทางด้านอื่นๆ ต่อไป เช่นนำไปทำ Dataset เพื่อทำ ML ในอนาคต

บอกไว้ตรงนี้ก่อนว่า ผมไม่สนับสนุนสินค้าละเมิดลิขสิทธิ์นะครับ อันนี้แค่ทำให้ดูเป็นตัวอย่างเฉยๆ

ลองนึกภาพว่าเราอยากอ่านการ์ตูนเรื่อง hikaru-no-go จากเว็ป https://www.mangathai.com/manga-hikaru-no-go เราจะต้องทำอะไรบ้าง

  1. เริ่มจากเข้า URL https://www.mangathai.com/manga-hikaru-no-go
  2. เลือกตอนที่ต้องการอ่าน
  3. เปิดอ่านทีละหน้า
  4. Save ไว้ดูทีหลัง

ทำอย่างนี้วนไปเรื่อยๆ -_-’ กว่าจะอ่านจบ เมื่อยมั้ยล่ะ งั้นเดี๋ยวเราจะคิดใหม่ทำใหม่ เขียน โปรแกรมให้มันทำอะไรซ้ำๆแทนเราไป แล้วเราก็นั่งรอให้มันทำงาน 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 browser
let page
const 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’

element div.container
element div.container

const resultSelector = 'body > div.container'
await page.waitForSelector(resultSelector, options)

ต่อมาเราะจะทำการ ดึงทุก link ที่เป็นแต่ละเล่มหรือตอนของการ์ตูนมาเก็บไว้เป็น array 1 ชุด เพื่อเอาไว้เข้าไปดึงแต่ละหน้าของเล่มนั้นอีกที หลังจากเปิด inspector เราจะเห็นว่า มี class ๆ นึงที่คลุม link ของแต่ละเล่มไว้ นั่นก็คือ .chapter-name

element .chapter-name
element .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 undefined
connect 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

element .img-manga
element .img-manga

โดยทำการ 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.link
const 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 ออกมา

element .btn_next_page
element .btn_next_page

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/ = 0
made directories, starting with undefined
Downloading... https://1.bp.blogspot.com/-eYzObFaYGbs/UqQCWVUsS5I/AAAAAAAEefc/yjVpCVRkXLY/s0/v01_001.jpg
Download ./download/1/v01_001.jpg is finished.

code ทั้งหมด เอาขึ้น github ไว้ให้แล้วด้านล่างเลยครับ

บอกไว้ตรงนี้ก่อนว่า ผมไม่สนับสนุนสินค้าละเมิดลิขสิทธิ์นะครับ อันนี้แค่ทำให้ดูเป็นตัวอย่างเฉยๆ

Code Download ได้ที่นี่

สุขสันต์วันแม่นะครับ บรัยยยยยยยยย

◎ Tags

##nodejs##puppeteer##tester##webscrapping
Khomkrid Lerdprasert
Operator

Khomkrid Lerdprasert

Technical Lead — building AI-powered platforms, omni-channel chat systems, and telemedicine solutions with Go, Next.js & clean architecture. 20+ years shipping software from crypto wallets to e-learning systems. Bangkok-based. Writes code late at night, brews beer on weekends.

GithubInstagram
Previous · transmission
Line Notify with firebase cloud function
Next · transmission
6 Songs in 7 Minutes
Metadata
Channel
Technology
Filed
August 12, 2020
Read
~2 min
Language
TH / EN
Transmit

Related

Saxking Track — แอป iOS สำหรับฝึกซ้อมแซกโซโฟนกับ Backing Track
Khomkrid Lerdprasert
April 23, 2026
1 min
aofiee.dev
signal / noise / code · craft
© 2019 – 2026, Khomkrid Lerdprasert.
All transmissions logged.
No newsletter. No profiling. Cookies require consent.
PGP · 7F3D 2024 A21E B584 · 0x7F3D
Channels
  • Art & Culture
  • Technology
  • Hack 101
  • Blockchain 101
  • Archive / All posts
— END OF TRANSMISSION —
// powered by curiosity, coffee, & wuxia
BKK · 13°45′N · 100°30′E