HomeArtTechHackBlockchain

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

By Khomkrid Lerdprasert
Published in Technology
August 12, 2020
2 min read
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

Share

Previous Article
Line Notify with firebase cloud function
Khomkrid Lerdprasert

Khomkrid Lerdprasert

Full Stack Life

Related Posts

สร้าง Key pair เพื่อทำการ signing document signature ด้วย Go lang
March 13, 2024
1 min
© 2024, All Rights Reserved.
Powered By

Quick Links

Author

Social Media