1. 程式人生 > >Express + Node 爬取網站資料

Express + Node 爬取網站資料

前言

因為自己寫的demo需要歷史天氣的統計資料,但是國內很難找到免費的api介面,很多都需要付費和稽核。而國外的網站雖然免費但需要提前知道觀測站,城市id等資訊。所以就有了這麼一篇文章的誕生。

準備工作

作用
superagent 傳送請求
superagent-charset 設定請求的編碼
cheerio 讓解析html文件像jquery一樣簡單

實現思路

  1. 找到目標網站
    東方天氣網(本文以此為例)
    天氣網
  2. 分析網站結構
    通過分析,可以繪製如下流程圖
Created with Raphaël 2.1.2
訪問基址(非同步),拿到城市的根地址按城市和年月拼成新的地址訪問新地址(非同步)拿到城市在指定月的資料處理資料
  1. 程式碼編寫思路
    從流程圖可以看出,需要先非同步請求基址拿到城市的根地址。
    拿到根地址後拼接時間和城市,接著非同步訪問才能拿到我們要的資料。
    這種巢狀非同步可以使用Promise來實現(eventProxy基本已不用)。
  2. 程式碼例項
var express = require('express');
var router = express.Router();
var superagent = require("superagent");
var charset = require
("superagent-charset"); //解決編碼問題 var cheerio = require("cheerio"); /* GET users listing. */ var mysql = require("mysql"); var responseJson = require("../util/responseJson"); //匯入mysql模組 var dbConfig = require("../db/DBconfig"); //使用DBConfig.js的配置資訊建立一個MySQL連線池 var pool = mysql.createPool(dbConfig.mysql); charset(superagent); //
需要遍歷的資訊 var BaseUrl = "http://tianqi.eastday.com"; var Cities = ["成都"]; //需要獲取的城市 var indexArr = ['cd']; var Years = ["2018"]; //年份,因為2018年以前dom結構不一樣,所以這裡只取2018 var Months = ["01", "02", "03", "04", "05", "06", "07", "08"]; //月份 function getCityUrl (city) { //返回Promise return new Promise((resolve, reject) => { superagent.get(BaseUrl + "/history.html") .charset("utf-8") .end((err, sres) => { if (err) { next(err); return; } let $ = cheerio.load(sres.text); //後續繼續遍歷的基址 let href = $(".letter-box").find("a[title='" + city + "']").attr("href"); resolve(href); }); }) } //獲取指定城市在指定時間的資料 function getData (href, city) { let year = Years[0]; return Months.map(month => { let url = BaseUrl + href.replace(".html", "_" + year + month + '.html' ); //獲取天氣資料 return new Promise((resolve, reject1) => { superagent.get(url) .charset("utf-8") .end((err1, sres1) => { if (err1) { reject1(err1); return; } let $ = cheerio.load(sres1.text); let arr = []; $("#weaDetailContainer").find(".weatherInfo-item").each((index, item) => { let $item = $(item); arr.push({ time: year + "-" + month + "-" + $item.find(".dateBox").text().substr(0, 2), wea: $item.find(".weather-name").text(), tempL: $item.find(".low-temp").text(), tempH: $item.find(".high-temp").text(), wind: $item.find(".wind").text(), }); }); resolve(arr); }); }); }); } function dispatch(groups) { var results = [] return (function () { var fun = arguments.callee , group = groups.shift() if (!group) { return Promise.resolve(results) } var promises = [] group.forEach(function (task) { promises.push( Promise.resolve(task) ) }) return Promise.all(promises).then(function (rets) { results.push(rets) return fun() }) }()) } function query (sql) { return new Promise((resolve, reject) => { pool.getConnection((err, conn) => { if(err){ reject(err); } else { conn.query(sql, (err1, rows, fields) => { conn.release(); if(err1){ reject(err1); } else { resolve({ rows: rows, fields: fields }); } }); } }); }); } function makeSql (item, index) { let sql = "INSERT INTO weather_" + indexArr[index] + " (time, wea, tempH, tempL, wind) values "; let arr = [].concat.apply([], item); arr.map(group => { sql += "('" + group.time + "', '" + group.wea + "', '" + group.tempH + "', '" + group.tempL + "', '" + group.wind + "'),"; }); return sql.substring(0, sql.length-1); } router.get('/', function(req, res, next) { let promiseArr = []; promiseArr = Cities.map(city => { //遍歷城市 return getCityUrl(city); }); Promise .all(promiseArr) .then(hrefArr => { return hrefArr.map(href => { return getData(href); }); }) .then(arr => { return dispatch(arr); }) .then(data => { let arr = data.map((item, index) => { return query(makeSql(item, index)) }); Promise .all(arr) .then(() => { res.json({ status: true, msg: 'success' }) }) .catch(e => { res.json({ status: false, msg: e.message }) }) }) .catch(e => { res.send(e.message); }); }); module.exports = router;

不足

雖然能夠實現需求,但是感覺我的Promise在這裡用著好像挺亂,沒有完全解決巢狀問題。後續會增進學習,對這一部分更加完善。也希望大家能夠給出寶貴意見~~