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 這個套件

2018/07/13

pyenv

pyenv 安裝網址

curl -L https://github.com/pyenv/pyenv-installer/raw/master/bin/pyenv-installer | bash

# 在 .bashrc 加入這些內容
export PATH="~/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"

常用指令:

  • pyenv update 更新軟體
  • pyenv install --list 列出可安裝的版本
  • pyenv install VERSION 安裝該版本
  • pyenv version 看現在使用的是那一個版本
  • pyenv versions 查看有哪些版本可以使用
  • pyenv local VERSION 指定現在用的版本或虛擬環境
  • pyenv virtualenv VERSION VIRTUAL_NAME 指定 python 版本開啟虛擬環境

2018/07/11

Map 與字串的應用

寫程式的時候常常會追求一行處理掉一串事情,會覺得這樣比較優雅,當然還是要視情況而定啦,易讀性跟錯誤處理也是需要考慮的,今天的題目是有一個陣列,如何一行搞定內容加上雙引號並且用逗號組成的字串呢

PHP
<?php

$datas = [
    'xxx@gmail.com',
    'yyy@gmail.com'
];
$string = implode(', ', array_map(function($item) {
    return "'{$item}'";
}, $datas));
echo $string; // 'xxx@gmail.com', 'yyy@gmail.com'
python
# -*- coding: utf-8 -*-

datas = [
    'xxx@gmail.com',
    'yyy@gmail.com'
]

string = ', '.join(["'%s'" % item for item in datas])
print(string) # 'xxx@gmail.com', 'yyy@gmail.com'

string = ', '.join(map(lambda item: "'%s'" % item, datas))
print(string) # 'xxx@gmail.com', 'yyy@gmail.com'
node.js
const datas = [
    'xxx@gmail.com',
    'yyy@gmail.com'
];

string = datas.map(item => `'${item}'`).join(', ')
console.log(string); // 'xxx@gmail.com', 'yyy@gmail.com'

2018/07/10

logrotate

之前就很好奇為什麼只要指定到 var log 下的檔案就會自己拆分,後來才知道原來是 server 固定時間會跑 logrotate 這個指令,理解這個指令對管理自己的 log 檔案有非常大的幫助。

常用參數

  • daily | weekly | monthy 更新頻率
  • dateext 檔案補上 rotate 的日期
  • missingok 如果找不到 log 檔沒關係
  • rotate 4 表示保留 4 份檔案
  • compress 表示壓縮起來,預設用 gzip
  • delaycompress 表示延後壓縮直到下一次 rotate
  • ifempty 即使是空文件也轉儲,這個是 logrotate 的預設值
  • notifempty 如果是空文件的話不做動
  • copytruncate 採用複製一份再清空原本的 Log File,滿足某些服務無法直接更名 Log File 來實現 Rotate 功能
  • size size 當日誌文件到達指定的大小時才做動,bytes(預設值)及 KB 或 MB
test.conf
/var/www/test/log/*.log {
    daily
    rotate 4
    missingok
    dateext
    size 10
}

我在 /var/www/test/log 裡面建立了一個 a.log,檔案大小為 43k,然後執行 logrotate -v test.conf,得到了 a.log-20180709 這個檔案,表示切割已經成功了。

可以把配置放到 /etc/logrotate.d/ 目錄下,crontab 會每天執行 logrotate 的工作,該工作會掃描這個資料夾,因此你的配置就會被執行,相關執行時間可以查 /etc/crontab,如果你想要特定時間獨立執行這個配置的話,就是放在一個指定的位置,然後自己加到 crontab 裡面。

詳細的使用參數可以參考 logrotate 的設定翻譯

注意

若要使用 script 做後續處理,有設定壓縮檔案的話建議使用 lastaction/endscript,才會真正等到檔案壓縮完成後處理內容。

2018/07/04

sh 分析 Apache access log

最近開始學習 server 管理,嘗試學習各種跟 server 有關的工具跟 knowhow,有個網站的簡易 shell script 語法可以快速做一些事情,把他寫成一個小工具來使用

apache_logger.sh
#!/bin/bash

command=$1
log=$2
commands=(
ip
access
loading
long
flow
404
)
commands_description=(
'ip 取得前十名 access 最多的 IP 位址'
'access 取得前十名 access 最多的網頁'
'loading 取得前十名 Loading 最大的頁面'
'long 取得前十名 User access 最久的頁面'
'flow 取得 access log 平均流量 (GB)'
'404 取得所有 404 Link'
)

function show_commands() {
    printf "用法 - sh loger.sh 命令 log位置\n\n"
    for (( i = 0; i < ${#commands_description[@]}; i++ )); do
        echo ${commands_description[$i]}
    done
    exit
}

if [[ -z $command && -z $log ]]; then
    show_commands
fi

if [[ -z $command ]]; then
    echo "Please input command"
    exit
fi

if [[ -z $log ]]; then
    echo "Please input log path"
    exit
fi

if [[ ! -f $log ]]; then
    echo "Log file is not exist"
    exit
fi

in_command=false
for (( i = 0; i < ${#commands[@]}; i++ )); do
    if [[ $command == ${commands[$i]} ]]; then
        in_command=true
    fi
done

if [[ $in_command = false ]]; then
    printf "Invalid command\n\n"
    show_commands
fi

case $command in
    'ip')
        cat $log |awk '{print $1}'|sort|uniq -c|sort -nr|head -10
        ;;
    'access')
        cat $log |awk '{print $11}'|sort|uniq -c|sort -nr|head -10
        ;;
    'loading')
        cat $log |awk '($NF > 60 && $7~/\.php/){print $7}'|sort -n|uniq -c|sort -nr|head -10
        ;;
    'long')
        cat $log |awk  '($7~/\.php/){print $NF " " $1 " " $4 " " $7}'|sort -nr|head -10
        ;;
    'flow')
        cat $log |awk '{sum+=$10} END {print sum/1024/1024/1024}'
        ;;
    '404')
        awk '($9 ~/404/)' $log | awk '{print $9,$7}' | sort
        ;;
esac

參考網站:快速分析 Apache 的 access log,抓出前十大網站流量兇手