2018/07/16

非同步的爬蟲寫法 Python Node.js

PHP, Python, Node.js 都可以寫爬蟲,但如果你如果確定目標為一次多頁型的抓取,那一次併發會比一次抓一頁有效率許多,譬如說你拿到一個網址,透過這個網址可以解出有 20 頁的資料要抓,如果在單一 process 裡面寫迴圈跑 20,程式執行邏輯上是 送出請求 -> 拿到資料(對或錯) -> 送出請求 這樣的循環,假設一個頁面要抓 3 秒,這個 process 最起碼要花 60 秒完成,如果你一次併發 20 個請求,就是 3 秒完成,假設你為了資源一次併發限制為 10 個請求,也僅僅只需要 6 秒完成,跟 60 秒差距是很大的,PHP 原生除非裝其他的相關套件,否則沒有這種異步的寫法,聽說 PHP 7 已經內建有 Thread,在目前還不算普遍的情況下,我們就先不探討,以下示範 Python 跟 Node.js 的作法

job.php
<?php

$num = $_GET['num'];
$seconds = 3;

if (isset($_GET['seconds'])) {
    $seconds = $_GET['seconds'];
}

sleep($seconds);
echo $num.':'.$seconds;

我先在 server 上寫一段簡單的程式碼,他可以指定你回應延遲時間,這樣可以測出非同步的效果

crawler.py
# -*- coding: utf-8 -*-

from gevent import monkey
monkey.patch_all()
from gevent.pool import Pool
import urllib2


def download(url):
    response = urllib2.urlopen(url).read()
    print(response)
    return response

seconds = [1, 3, 2]
urls = [
    "http://chan15.info/job.php?num=%s&seconds=%s" % (i, seconds[i - 1]) for i in range(1, 4)
]
pool = Pool(10)
result = pool.map(download, urls)
print(result)

這是 python 的部份,我利用 gevent 的套件來實現 multithread,一次跑三次請求,number 順序為 1, 2, 3,而秒數延遲為 1, 3, 2,也就是說 number 2 跑最久,但我需要得到正確的 number 順序為 1, 2, 3,這樣的結果才是正確的

$ time python crawler.py
1:1
3:2
2:3
['1:1', '2:3', '3:2']

real    0m3.291s
user    0m0.248s
sys     0m0.040s

我們可以看出返回時間的確是 1, 3, 2,最後結果為 1, 2, 3,秒數為 0m3.291s,正確的順序以及併發時間

crawler.js
const util = require('util');
const request = require('request');

const getUrl = async (url) => {
    return new Promise((resolve, reject) => {
        request(url, (err, res, body) => {
            console.log(body);
            resolve(body);
        });
    })
};

const main = async () => {
    const url = 'http://chan15.info/job.php?num=%s&seconds=%s';
    const numbers = [1, 2, 3];
    const seconds = [1, 3, 2];
    const jobs = [];

    numbers.forEach((second, index) => {
        jobs.push(getUrl(util.format(url, second, seconds[index])));
    });

    Promise.all(jobs).then((result) => {
        console.log(result);
    });
};

main();

node.js 部份我使用了 request 去做請求,node.js 本來就是 async 的設計,所以搭配 async / await 跟 promise 的寫法即可

$ time node crawler.js 
1:1
3:2
2:3
[ '1:1', '2:3', '3:2' ]

real    0m3.320s
user    0m0.294s
sys     0m0.024s

執行結果跟 python 是一樣的

crawler.js 批次的寫法
const util = require('util');
const request = require('request');
const batch = require( 'batch-promise'  );

const url = 'http://chan15.info/job.php?num=%s&seconds=%s';
const numbers = [1, 2, 3];
const jobs = [];

numbers.forEach((second, index) => {
    jobs.push((resolve, reject) => {
        const target = util.format(url, second, 3)
        request(target, (err, res, body) => {
            console.log(body);
            resolve(body);
        });
    });
});

batch(jobs, 10).then((result) => {
    console.log(result);
});

多利用了 batch 這個套件

沒有留言: