ดึงข้อมูลเว็บไซต์ด้วย Nodejs และ Cheerio

Published on
NodeJS
2016/03/scraping-web-with-nodejs
Discord

บทความนี้เป็นตัวอย่างการดึงข้อมูลเว็บไซต์ด้วยการใช้ Node.js และ Cheerio ซึ่งเทคนิคการดึงข้อมูลเว็บไซต์ต่างๆนี้ เราเรียกมันว่า "Web Scraping" หรือ "Web Crawler" ก็แล้วแต่ หลักการมันก็คล้ายๆกับเว็บไซต์ Google ที่จะเข้าไปเก็บข้อมูล index ทุกๆเว็บไซต์ไว้เพื่อทำ search engine นั่นเอง

สำหรับตัวอย่างบทความนี้ จะเป็นตัวอย่าง การดึงข้อมูลของแอพใน Google Play มาแสดง ซึ่งนอกจากในบทความนี้แล้ว ยังสามารถนำไปประยุกต์ใช้ได้หลากหลาย ไม่ว่าจะเป็น ดึงข้อมูล ราคาทอง ราคาน้ำมัน ตารางหนังเข้าฉาย ราคาเกมส์ ราคาสินค้า Amazon, Wallmart เยอะแยะไปหมด

Web Scraping จะมองว่าเป็นสายเทาก็ได้นะครับ หากเราใช้ดึงข้อมูลสำหรับ personal use คิดว่าไม่น่าจะมีปัญหาอะไร ทางที่ดีควรจะได้รับอนุญาตจากทางเจ้าของเว็บไซต์นั้นๆจะดีที่สุดครับ :)

Getting Started

ก่อนอื่นเลย สิ่งที่ต้องเตรียมตัวสำหรับโปรเจ็คนี้มีอะไรบ้าง

อ่านเพิ่มเติม

Step 1 : Create project

เริ่มต้นสร้างโปรเจ็คกันเลยครับด้วย npm init หรือใช้ไฟล์ package.json ตามข้างล่างนี้

{
  "name": "nodejs-google-play-information",
  "version": "1.0.0",
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },
  "engines": {
    "node": "^4.2.0"
  },
  "dependencies": {
    "cheerio": "^0.20.0",
    "hapi": "^13.0.0",
    "request": "^2.69.0"
  }
}

จัดการลง dependencies ให้เรียบร้อย

npm install

โดยเป้าหมายของเราคือดึงข้อมูลรายละเอียดของ App บน Google Play โดยใช้ชื่อของ applicationId หรือก็คือชื่อ Package Name เวลาทำ App Android ซึ่งมันจะเป็นชื่อที่ไม่ซ้ำกัน ทำให้เราสามารถใช้ชื่อนี้ในการเข้าดูรายละเอียดแอพแต่ละหน้าได้ เช่น

สิ่งที่เราจะทำคือ ทำ route สำหรับรับค่า appId เหล่านี้ คือ

  • GET /{appId}

Step 2 : Create Server with Hapi.js

จากนั้นสร้างไฟล์ index.js ขึ้นมาและสร้าง Server ขึ้นมาง่ายๆ ด้วย Hapi.js ดังนี้ (รายละเอียดของ Hapi.js จะไม่ขอพูดถึงมากนัก แนะนำให้อ่านจากเว็บ Hapi.js หรือบทความที่ผมเคยเขียนด้านบนครับ)

'use strict'

const Hapi = require('hapi')
const server = new Hapi.Server()

server.connection({
  host: 'localhost',
  port: 8088
})

server.route({
  method: 'GET',
  path: '/{appId}',
  handler: (req, reply) => {
    reply({ message: 'Hello World' })
  }
})

server.start(err => {
  console.log(`Server running at ${server.info.uri}`)
})

จากโค๊ดด้านบน เขียนด้วย ES6 ซึ่งมีใน Node v4.2.4 ที่ผมใช้ในบทความนี้ โดยต้องกำหนด use strict ให้มัน โดยไม่ต้องใช้ Babel ในการ compile เป็น ES5 เลย

ส่วนโค๊ดอื่นๆ ก็เป็นการเริ่มกำหนด route โดย path /appId

ทดสอบสั่งรัน server

node index.js

และเมื่อเข้า http://localhost:8088/appId ก็จะได้ข้อความ

{
  "message": "Hello World"
}

ซึ่งตอนนี้ appId จะเป็นอะไรก็ได้ มันก็จะได้ผลลัพธ์เหมือนกันหมด สิ่งที่เราต้องทำต่อคือรับค่า appId มาจากนั้นก็ใช้ request module เพื่อเปิดหน้าเว็บของ Google Play ด้วย appId

ก็เลยเพิ่มโค๊ดด้านล่างเพิ่มเติม

const URL = 'https://play.google.com/store/apps/details?id='

server.route({
  method: 'GET',
  path: '/{appId}',
  handler: (req, reply) => {
    let appId = req.params.appId
    let lang = req.query.lang || 'en'
    let url = `${URL}${appId}&hl=${lang}`

    reply({
      url: url
    })
  }
})

Step 3 : Use Request

ต่อมา module ที่จะทำให้เราสามารถ call HTTP request ได้ก็คือ request นั่นเอง ซึ่งการใช้งาน Request แบบคร่าวๆ โดยมี syntax ดังนี้

  • request(URL, callback) : URL คือ url ที่เราต้องการ call ส่วน callback เป็น callback function ซึ่งมี (err, response, body) 3 ตัว
    • err : หากการ call HTTP มี error เกิดขึ้น
    • response : เป็นค่า response ที่ตอบกลับมาจาก server มีพวก header, statusCode
    • body : เป็นข้อมูล body ที่ server ส่งกลับมา เหมือนหน้า HTML ทั่วไปเวลาเราเปิดเว็บไซต์

ตัวอย่าง

const request = require('request');

request('https://devahoy.com', (err, response, body) {

  if (!err && response.statusCode === 200) {
      console.log('body : ' +  body);
  }
});

Step 4 : Cheerio

ต่อมา สิ่งที่เราจะทำ เมื่อเวลาที่เรา ได้ค่า body จากการ call HTTP ก็คือ เราจะใช้ cheerio เพื่อหา DOM element ในหน้า HTML นั้นๆ โดยมี syntax คร่าวๆ คล้ายๆ jQuery ทำให้ศึกษาเพิ่มเติมไม่ยาก ตัวอย่างเช่น

มี html ดังนี

<ul id="fruits">
  <li class="apple">Apple</li>
  <li class="orange">Orange</li>
  <li class="pear">Pear</li>
</ul>

การใช้ Cheerio และการ Selector

const cheerio = require('cheerio')

let $ = cheerio.load(html)

$('.apple', '#fruits').text()
//=> Apple

$('ul .pear').attr('class')
//=> pear

$('li[class=orange]').html()
//=> Orange

ลองนำมาประยุกต์ใช้กับโปรเจ็คที่กำลังทำอยู่ร่วมกับ request ก็จะได้โค๊ดตรงส่วนของ server.route() ดังนี้

const URL = 'https://play.google.com/store/apps/details?id='

server.route({
  method: 'GET',
  path: '/{appId}',
  handler: (req, reply) => {
    let appId = req.params.appId
    let lang = req.query.lang || 'en'
    let url = `${URL}${appId}&hl=${lang}`

    request(url, (err, response, body) => {
      if (!err && response.statusCode === 200) {
        let $ = cheerio.load(body)

        reply({})
      } else {
        reply({
          message: `We're sorry, the requested ${url} was not found on this server.`
        })
      }
    })
  }
})

Step 5 : Cheerio Selector

ต่อมา เราลองมาดูหน้าตัวอย่างที่เราต้องการดึงข้อมูลมา โดยเข้าเว็บ Facebook on Google Play

ขั้นตอนนี้เราจะใช้ Chrome Developer Tools เข้ามาช่วย ทำได้โดยการเลือก More Tools => Developer Tool Cheerio

สิ่งแรกที่เราต้องการคือ Title ของแอพ ด้านบน จะเห็นว่าเราต้องการ .document-title selector จะเป็น $('.document-title').text()

Cheerio

ต่อมาด้านบน เราจะดึงข้อมูล Publisher และ Category แต่จะแตกต่างกับ Title คือมันจะแยกเป็น 2 กรณีคือ

  • class document-subtitle primary และ class document-subtitle cagetory
  • selector ของ cheerio จะเป็น $('.document-subtitle.primary').text() และ $('.document-subtitle.category').text()

Cheerio

สิ่งที่เราต้องการต่อมาคือ ข้อมูลส่วน Additional Information หากลองดู DOM Element เราจะเห็นว่าส่วนที่เราต้องการคือ meta-info > .content แต่ว่า meta-info มีหลาย element ทำให้เราจะได้ข้อมูลเป็น list กลับมา สิ่งที่ต้องการ ผมแค่ต้องการ version และ จำนวนครั้งในการ Install ซึ่งมันอยู่ในตำแหน่งที่ 2 และ 3 เพราะฉะนั้น cheerio selector จะได้เป็น

  • $('.meta-info > .content').eq(2).text() และ $('.meta-info > .content').eq(3).text()

สุดท้ายเมื่อได้ข้อมูลที่เราต้องการแล้ว ก็แค่สั่ง reply() ด้วยข้อมูลที่เราได้มา

โค๊ด index.js สุดท้ายเป็นดังนี้

'use strict'

const Hapi = require('hapi')
const request = require('request')
const cheerio = require('cheerio')

const URL = 'https://play.google.com/store/apps/details?id='

const server = new Hapi.Server()

server.connection({
  host: 'localhost',
  port: 8088
})

server.route({
  method: 'GET',
  path: '/{appId}',
  handler: (req, reply) => {
    let appId = req.params.appId
    let lang = req.query.lang || 'en'
    let url = `${URL}${appId}&hl=${lang}`

    request(url, (err, response, body) => {
      if (!err && response.statusCode === 200) {
        let $ = cheerio.load(body)

        let title = $('.document-title').text().trim()
        let publisher = $('.document-subtitle.primary').text().trim()
        let category = $('.document-subtitle.category').text().trim()
        let score = $('.score-container > .score').text().trim()
        let install = $('.meta-info > .content').eq(2).text().trim()
        let version = $('.meta-info > .content').eq(3).text().trim()

        reply({
          data: {
            title: title,
            publisher: publisher,
            category: category,
            score: score,
            install: install,
            version: version
          }
        })
      } else {
        reply({
          message: `We're sorry, the requested ${url} was not found on this server.`
        })
      }
    })
  }
})

server.start(err => {
  console.log(`Server running at ${server.info.uri}`)
})

ทดสอบรันเว็บของเรา node index.js และลองเข้าหน้าเว็บ โดยใส่ appId เช่น http://localhost:8088/app.akexorcist.mobileinternetsetting ก็จะได้ข้อมูลประมาณนี้

{
  "data": {
    "title": "Mobile Internet Setting [TH]",
    "publisher": "Akexorcist",
    "category": "Communication",
    "score": "4.4",
    "install": "50,000 - 100,000",
    "version": "1.5.1"
  }
}

ซึ่งเมื่อได้ข้อมูลมาแล้ว ก็จะเอาไปทำอะไรก็แล้วแต่ ขั้นตอนต่อไปก็ไม่ยากแล้ว :)

สรุป

บทความนี้เป็นแค่ตัวอย่างง่ายๆในการใช้ Cheerio ในการดึงข้อมูลเว็บไซต์เท่านั้น ไม่ได้ลงรายละเอียดเชิงลึก ซึ่ง Cheerio มันสามารถทำอะไรได้อีกเยอะ ซึ่งหวังว่าจะเป็นแค่ตัวอย่างให้ผู้สนใจได้นำไปศึกษาเพิ่มเติมดูครับ

ส่วนใครสนใจ Source Code ก็ดูได้จาก Github เลยครับ

Buy Me A Coffee
Authors
Discord