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,抓出前十大網站流量兇手

2018/06/19

靜態變數

雖然現在提倡物件導向,但偶爾也會有一些特殊情況會重複性的呼叫一些 function,如果在一個數量為 10 的迴圈裡面呼叫一個 function,那該 function 就會被 call 十次,假設該 function 的某些結果,例如說撈取資料是同一件事的話,其實沒必要也呼叫十次,PHP 的 static 可以很有效率的解決這件事情

PHP
<?php

function getData($number)
{
    static $dataFromDatabase;

    if (!isset($dataFromDatabase)) {
        echo 'init', PHP_EOL;
        $dataFromDatabase = 'This is data';
    }

    return sprintf(
        '%s %s',
        $dataFromDatabase,
        $number
    );
}

for ($i = 0; $i < 10; $i++) {
    echo getData($i), PHP_EOL;
}

執行結果

init
This is data 0
This is data 1
This is data 2
This is data 3
This is data 4
This is data 5
This is data 6
This is data 7
This is data 8
This is data 9

假如把 static $dataFromDatabase; 拿掉就會看到十次 init,對固定資料來講的話就是很沒必要的資源浪費,python 以及 node.js 沒有這樣的宣告方式,但有轉彎的作法

python
# -*- coding: utf-8 -*-

def get_data(number):
    if not hasattr(get_data, 'data_from_database'):
        print('init')
        get_data.data_from_database = 'This is data'

    return '%s %s' % (get_data.data_from_database, number)

for i in range(10):
    print(get_data(i))
node.js
function getData(number) {
    if (getData.dataFromDatabase === undefined) {
        console.log('init');
        getData.dataFromDatabase = 'This is data';
    }

    return `${getData.dataFromDatabase} ${number}`;
}

for (let i = 0; i < 10; i++) {
    console.log(getData(i));
}

2018/05/08

Virtualenv

現代開發軟體幾乎都會安裝使用一些其他人寫好的套件,目前幾個主流語言也都有 package manager,python 大部分使用 easy_install 以及 pip,python 有別於其他語言,他的套件都裝在 global,因此如果你的 server 有多個專案在運行的話會看到一堆 package,這樣很不立於管理,我們可以使用 virtualenv 對專案進行隔離,另外使用 virtualenvwrapper 進行隔離管理

安裝套件
$ pip install virtualenv
$ pip install virtualenvwrapper
.bashrc
export WORKON_HOME=/envs
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python
export VIRTUALENVWRAPPER_VIRTUALENV=/usr/local/bin/virtualenv
source /usr/local/bin/virtualenvwrapper.sh

將相關設定放置於 ~/.bashrc,進入 server 後即可直接使用,這邊的路徑要視當時 server 狀況做修改

建立專案
$ mkvirtualenv -p /usr/bin/python --no-site-packages CHAN

-p 的部份為指定 python 版本的 alias 位置,所以可以指定 2 或 3 當執行用的版本

建立環境後應該就直接進入該虛擬環境了,此時可以用 pip list 看到這個環境只有簡單的幾個必要套件存在而已

其他重要指令
workon # 列出所有虛擬環境
lsvirtualenv # 列出所有虛擬環境
lssitepackages # 列出該虛擬環境已安裝套件
rmvirtualenv NAME # 移除虛擬環境
cpvirtualenv NAME NEW_NAME # 複製虛擬環境
workon NAME # 進入虛擬環境
deactivate # 脫離虛擬環境

2018/04/20

Date Time Control On PHP Python Node.js

寫程式時,時間的使用是頻率很高的事情,簡單的像是拿取現在的時間、timestamp,稍微複雜的像是計算兩個時間的差距,今天來探討一些常見的例子

  1. 現在的時間
  2. 現在的 timestamp
  3. 將時間轉換為 timestamp
  4. 從 timestamp 取得時間
  5. 格式化時間
  6. 時間位移
  7. 時間差異

PHP

<?php

// 現在的時間
// PHP 沒有函式可以直接印出現在的值,一般都是用 date 加上需要的格式
echo date('Y-m-d H:i:s'), PHP_EOL; // 2018-04-20 10:03:22

// 現在的 timestamp
echo time(), PHP_EOL; // 1524189866

// 將時間轉換為 timestamp
echo strtotime('2018-04-20'), PHP_EOL; // 1524153600

// 從 timestamp 取得時間
echo date('Y-m-d H:i:s', 1524153600), PHP_EOL; // 2018-04-20 00:00:00

// 格式化時間
// 基本上就是使用 date 再去指定內容,常見的就是 Ymd His
// 其他的部份可以參考 http://php.net/date

// 時間位移
// 範例為 2018-04-20 12:00:00,往前移動一天又一小時
$datetimeString = '2018-04-20 12:00:00';
$datetime = date('Y-m-d H:i:s', strtotime($datetimeString));
echo $datetime, PHP_EOL; // 2018-04-20 12:00:00

// strtotime 很強大,其他應用可以參考 http://php.net/strtotime
$datetime = date('Y-m-d H:i:s', strtotime('-1 day -1 hour', strtotime($datetimeString)));
echo $datetime, PHP_EOL; // 2018-04-19 11:00:00

// 時間差異
// 基本上就是轉 timestamp 再去換算
$dateBegin = '2018-04-19';
$dateEnd = '2018-04-20';
echo strtotime($dateEnd) - strtotime($dateBegin), PHP_EOL; // 86400

Python

python 常用的方法有 timedatetime,接下來的範例用 datetime 直接做掉

# -*- coding: utf-8 -*-

import datetime

# 現在的時間
print(datetime.datetime.now()) # 2018-04-20 10:18:19.916229

# 現在的 timestamp
print(datetime.datetime.now().strftime('%s')) # 1524190730

# 將時間轉換為 timestamp
print(datetime.datetime(2018, 4, 20).strftime('%s')) # 1524153600

# 從 timestamp 取得時間
print(datetime.datetime.fromtimestamp(1524153600)) # 2018-04-20 00:00:00

# 格式化時間
# 其他文字格式可以參考 http://tinyurl.com/ydz294lw
print(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')) #2018-04-20 10:23:30

# 時間位移
datetime_string = '2018-04-20 12:00:00'
convert_datetime = datetime.datetime.strptime(datetime_string, '%Y-%m-%d %H:%M:%S')
print(convert_datetime) # 2018-04-20 12:00:00

# timedelta 很強大,其他可以參考 http://tinyurl.com/yc83qt4h
convert_datetime = convert_datetime - datetime.timedelta(days=1, hours=1)
print(convert_datetime) # 2018-04-19 11:00:00

# 時間差異
date_begin = '2018-04-19'
date_end = '2018-04-20'
begin = int(datetime.datetime.strptime(date_begin, '%Y-%m-%d').strftime('%s'))
end = int(datetime.datetime.strptime(date_end, '%Y-%m-%d').strftime('%s'))
print(end - begin) # 86400

Node.js

坦白說,如果你從網路上查詢 nodejs 或 jsavascript 對時間的控制大部分的人都會推薦你去使用套件,像是 moment.js,因為 JS 對時間的控制沒有那麼直覺跟友善,要花很多功夫才能達成,但我們今天就是研究 native 的東西,我們就用 native 的方法來完成

// 現在的時間
let date = new Date();
console.log(date.toString()); // Fri Apr 20 2018 11:32:22 GMT+0800

// 現在的 timestamp
console.log(date.getTime()); // 1524192512090

// 將時間轉換為 timestamp
const dateString = '2018-04-20';
date = new Date(Date.parse(dateString));
console.log(date.toString()); // Fri Apr 20 2018 08:00:00 GMT+0800 (CST)

// 從 timestamp 取得時間
date = new Date(1524192512090);
console.log(date.toString()); // Fri Apr 20 2018 10:48:32 GMT+0800 (CST)

// 格式化時間
// 這個就真的沒招了,不用套件的話得自己取值組合
date = new Date(1524192512090);
formatDate = date.getFullYear() + '-' + ('0' + (date.getMonth() + 1)).slice(-2) +
    '-' + ('0' + date.getDate()).slice(-2) + ' ' + date.getHours() + ':' +
    date.getMinutes() + ':' + date.getSeconds();
console.log(formatDate); // 2018-04-20 10:48:32

// 時間位移
const datetimeString = '2018-04-20 12:00:00';
const convertToDate = new Date(Date.parse(datetimeString));
const otherDate = new Date(
    convertToDate.getFullYear(), convertToDate.getMonth(), convertToDate.getDate() - 1,
    convertToDate.getHours() - 1, convertToDate.getMinutes(), convertToDate.getSeconds()
);
console.log(convertToDate.toString()); // Fri Apr 20 2018 12:00:00 GMT+0800 (CST)
console.log(otherDate.toString()); // Thu Apr 19 2018 11:00:00 GMT+0800 (CST)

// 時間差異
const dateBegin = '2018-04-19';
const dateEnd = '2018-04-20';
const begin = new Date(Date.parse(dateBegin));
const end = new Date(Date.parse(dateEnd));
console.log((end.getTime() - begin.getTime()) / 1000); // 86400

相信看完上面的範例以後,大家應該都會去裝 moment.js 了 XDDDD

2018/04/16

How PHP Access Private Method And Property For Test

其實測試是不應該測 protected 以及 private method 的,但有時候為了重構,原生程式碼東西拆不夠細導致同一個 public method 執行超多的 inner method,你要在測試該 public method 中測試所有 private method 是相當痛苦的,今天來探討一下如何用 Reflection 去測試 private 的內容

<?php

class Chan
{
    private $privateName = 'A';
    private static $privateStaticName = 'B';

    private function privateFoo($name = null)
    {
        if ($name !== null) {
            return $name;
        }

        return $this->privateName;
    }

    private static function privateStaticFoo($name = null)
    {
        if ($name !== null) {
            return $name;
        }

        return self::$privateStaticName;
    }
}

這是一個叫 Chan 的 class,他包含了一般跟 static method 以及 property,現在逐步示範怎麼導出 private method 結果

使用 privateFoo
$chan = new Chan();

$reflection = new \ReflectionClass($chan);
$method = $reflection->getMethod('privateFoo');
$method->setAccessible(true);
echo $method->invoke($chan), PHP_EOL; // A
echo $method->invokeArgs($chan, array('C')), PHP_EOL; // C

這個範例中沒帶參數會返回 A,有帶參數會返回參數值

使用 privateFoo 並影響預設 property
$chan = new Chan();

$reflection = new \ReflectionClass($chan);
$method = $reflection->getMethod('privateFoo');
$method->setAccessible(true);

$property = $reflection->getProperty('privateName');
$property->setAccessible(true);
$property->setValue($chan, 'D');

echo $method->invoke($chan), PHP_EOL; // D

我們利用了 getPropery 這個方法變動了預設的 privateName 內容

使用 privateStaticFoo
$chan = new Chan();

$reflection = new \ReflectionClass($chan);

$method = $reflection->getMethod('privateStaticFoo');
$method->setAccessible(true);
echo $method->invoke($chan), PHP_EOL; // B
echo $method->invokeArgs($chan, array('E')), PHP_EOL; // E

$property = $reflection->getProperty('privateStaticName');
$property->setAccessible(true);
$property->setValue('F');
echo $method->invoke($chan), PHP_EOL; // F

基本上用法差不多,只有 invokeArgs 的时候不用傳物件進去,但測試過後其實傳也可以

2018/04/13

PHP, Python, Node.js CRUD On MySQL

紀錄一下這三個語言對 MySQL 做 CRUD 的方法當作筆記用,不會使用 ORM 或者其他功能強大複雜的套件,畢竟如果有使用 framework 通常都有附,這邊使用最簡潔的方法完成。

MySQL

先建立表單來做 CRUD 使用。

CREATE TABLE `examples` (
	`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(50) NOT NULL,
	`created_at` DATETIME NOT NULL,
	PRIMARY KEY (`id`)
)
COLLATE='utf8mb4_unicode_ci';

PHP - PDO

PHP 現在一定是使用 PDO,實做方式如下。

<?php

$username = 'root';
$password = 123456;

try {
    $name = 'Chan';
    $createdAt = date('Y-m-d H:i:s');

    // 設定
    $dbh = new \PDO('mysql:host=localhost;dbname=demo;charset=utf8', $username, $password);

    // 寫入
    $sql = "INSERT INTO `examples` (`name`, `created_at`) VALUES (:name, :created_at)";
    $sth = $dbh->prepare($sql);
    $sth->bindParam(':name', $name, \PDO::PARAM_STR);
    $sth->bindParam(':created_at', $createdAt, \PDO::PARAM_STR);
    $sth->execute();

    // 提取多筆
    $sql = "SELECT * FROM `examples`";
    $sth = $dbh->prepare($sql);
    $sth->execute();
    $rows = $sth->fetchAll(\PDO::FETCH_ASSOC);
    var_dump($sth->rowCount());   
    var_dump($rows);

    // 提取單筆
    $sql = "SELECT * FROM `examples`";
    $sth = $dbh->prepare($sql);
    $sth->execute();
    $rows = $sth->fetch(\PDO::FETCH_ASSOC);
    var_dump($rows);
} catch (Exception $e) {
    var_dump($e-getMessage());
} finally {
    $sth = null;
    $dbh = null;
}

其中 PDO::PARAM_STR 的部份是讓你確定傳入文字的型別,可以有效防止 SQL Injection,常使用的有:

  1. PDO::PARAM_STR
  2. PDO::PARAM_INT
  3. PDO::PARAM_BOOL

其他可以使用的部份可以看這邊,更新跟刪除的部份就是把 INSERT 的內容改成 UPDATEDELETE,就不多寫範例了。

Python - MySQLdb

Python 我們使用 MySQLdb 來操作,怎麼安裝網路上有很多教學,這邊也不示範了。

# -*- coding: utf-8 -*-

import MySQLdb
import datetime

username = 'root'
password = '123456'

try:
    # 設定
    db = MySQLdb.connect(host='localhost', db='demo', user=username, passwd=password)
    cursor = db.cursor()

    # 寫入
    values = (
        'Chan',
        datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    )
    sql = "INSERT INTO `examples` (`name`, `created_at`) VALUES (%s, %s)"
    cursor.execute(sql, values)
    db.commit()

    # 提取多筆
    sql = "SELECT * FROM `examples`"
    cursor.execute(sql)
    rows = cursor.fetchall()
    print(cursor.rowcount)
    print(rows)

    # 提取單筆
    sql = "SELECT * FROM `examples`"
    cursor.execute(sql)
    row = cursor.fetchone()
    print(row)
except Exception as e:
    print(str(e))
finally:
    db.close()
這個套件沒有回傳 key name,所以如果你想要對應欄位名稱的話可能要這樣使用

# 提取多筆
sql = "SELECT * FROM `examples`"
cursor.execute(sql)
rows = cursor.fetchall()

for row in rows:
    pk, name, date = row
    print(pk)
    print(name)
    print(date)

此套件傳遞參數的方法很多,可以參考 Python best practice and securest to connect to MySQL and execute queries 這篇。

Node.js - mysql2

node.js 部份我選用了 mysql2,他語法基本上跟 mysql 一樣,多了些功能跟據說效能有提昇,我是沒實測,但就用最新的。

const mysql = require('mysql2');
const datetime = require('node-datetime');

try {
    // 設定
    const connection = mysql.createConnection({
        host: 'localhost',
        user: 'root',
        password: '123456',
        database: 'demo'
    });

    // 寫入
    const name = 'Chan';
    const dt = datetime.create();
    const createdAt = dt.format('Y-m-d H:M:S');
    let sql = 'INSERT INTO `examples` (`name`, `created_at`) VALUES (?, ?)';
    connection.execute(sql, [name, createdAt], (err, rows, fields) => {
        if (err) {
            console.log(err);
        }
    });

    // 讀取
    sql = 'SELECT * FROM `examples`';
    connection.execute(sql, (err, rows, fields) => {
        console.log(rows);
    });

    connection.end();
} catch (e) {
    console.log(e.message);
}

這個套件沒有封裝多筆或單筆的功能,要取單筆就是拿取陣列第一筆資料,上面的程式碼是 sync 模式執行的,所以其實是有機會讀到沒寫入的資料,除非你使用 callback 或 promise,示範一下 promise 的作法。

const mysql = require('mysql2/promise');
const datetime = require('node-datetime');

async function main() {
    try {
        const connection = await mysql.createConnection({
            host: 'localhost',
            user: 'root',
            password: '123456',
            database: 'demo'
        });

        // 寫入
        const name = 'Chan';
        const dt = datetime.create();
        const createdAt = dt.format('Y-m-d H:M:S');
        let sql = 'INSERT INTO `examples` (`name`, `created_at`) VALUES (?, ?)';
        await connection.execute(sql, [name, createdAt]);

        // 讀取
        sql = 'SELECT * FROM `examples`';
        const [rows] = await connection.execute(sql);
        console.log(rows);

        connection.end();
    } catch (e) {
        console.log(e.message);
    }
}

main();

2018/04/12

MySQL 全文檢索

近期還挺常遇到客戶想要他的搜尋 bar 像 Google 那麼強大,可以多關鍵字查找網站內容,但要達到那樣的要求在中文是很難的,中文有語意以及斷字問題,英文每個字都會分開寫,中文是全部連在一起,所以要達成這樣的功能必須使用中文斷詞的套件,再餵給 solr 或 elasticsearch 那種全文檢索引擎,但客戶的 content 不算龐大,安裝 solr 或 es 並且調校要花不少的成本,幸好 MySQL 在 5.6.4 之後已經支援全文檢索了,以下示範使用方式

斷詞套件

首先是要選擇斷詞套件,目前網路最熱門的應該就屬於 jieba 了,這個套件有 PHP 的版本,使用 composer 安裝好以後就可以寫程式碼了

資料表

不過要使用功能之前當然要設定我們的 MySQL,先建立表單

CREATE TABLE `cuts` (
	`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`content` TEXT NULL COLLATE 'utf8mb4_unicode_ci',
	`full_text` TEXT NULL COLLATE 'utf8mb4_unicode_ci',
	PRIMARY KEY (`id`),
	FULLTEXT INDEX `full_text` (`full_text`)
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB;

content 部份是要存原來的資料,而 full_text 的欄位是要存斷詞之後的結果

斷詞

<?php

ini_set('memory_limit', '1024M');

include 'vendor/autoload.php';

use Fukuball\Jieba\Jieba;
use Fukuball\Jieba\Finalseg;

Jieba::init();
Finalseg::init();

$content = '我工作於掌中乾坤,預計今年去看 NBA,去看劉寶傑打球';
$cuts = Jieba::cut($content);

印出來的結果為

Array
(
    [0] => 我
    [1] => 工作
    [2] => 於
    [3] => 掌中
    [4] => 乾坤
    [5] => ,
    [6] => 預計
    [7] => 今年
    [8] => 去
    [9] => 看
    [10] => NBA
    [11] => ,
    [12] => 去
    [13] => 看
    [14] => 劉寶傑
    [15] => 打球
)

看起來斷的挺精準的,不過我的公司叫掌中乾坤,如果打這四個字會搜尋不到,我們可以用自己定義的詞庫

<?php

ini_set('memory_limit', '1024M');

include 'vendor/autoload.php';

use Fukuball\Jieba\Jieba;
use Fukuball\Jieba\Finalseg;

Jieba::init();
Jieba::loadUserDict('user.txt');
Finalseg::init();

$content = '我工作於掌中乾坤,預計今年去看 NBA,去看劉寶傑打球';
$cuts = Jieba::cut($content);
print_r($cuts);

會印出

Array
(
    [0] => 我
    [1] => 工作
    [2] => 於
    [3] => 掌中乾坤
    [4] => ,
    [5] => 預計
    [6] => 今年
    [7] => 去
    [8] => 看
    [9] => NBA
    [10] => ,
    [11] => 去
    [12] => 看
    [13] => 劉寶傑
    [14] => 打球
)

有一好沒兩好,乾坤這個詞洗掉了,可以修改斷詞為全模式

$cuts = Jieba::cut($content, true);

會印出

Array
(
    [0] => 我
    [1] => 工作
    [2] => 於
    [3] => 掌中乾坤
    [4] => 乾坤
    [5] => ,
    [6] => 預
    [7] => 今年
    [8] => 去
    [9] => 看
    [10] => N
    [11] => B
    [12] => A
    [13] => ,
    [14] => 去
    [15] => 看
    [16] => 寶
    [17] => 傑
    [18] => 打球
)

看起來反而對精準度有所影響,看自己怎麼取捨,接著我們把這些內容轉成 base64 存到資料庫,因為 MySQL 的全文檢索不認中文…

<?php

ini_set('memory_limit', '1024M');

include 'vendor/autoload.php';

use Fukuball\Jieba\Jieba;
use Fukuball\Jieba\Finalseg;

Jieba::init();
Jieba::loadUserDict('user.txt');
Finalseg::init();

$content = '我工作於掌中乾坤,預計今年去看 NBA,去看劉寶傑打球';
$cuts = Jieba::cut($content);
$cutStrings = implode(' ', array_map('base64_encode', $cuts));

try {
    $db = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root', 123456);
    $sql = "INSERT INTO `cuts`(content, full_text) VALUES(:content, :full_text)";
    $sth = $db->prepare($sql);
    $sth->bindParam(':content', $content, \PDO::PARAM_STR);
    $sth->bindParam(':full_text', $cutStrings, \PDO::PARAM_STR);
    $sth->execute();
} catch (Exception $e) {
    var_dump($e->getMessage());
}

搜尋的模擬程式碼如下:

<?php

$searchKey = '找 掌中乾坤';
$keys = explode(' ', $searchKey);
$trim = function($item) {
    return $item !== '';
};
$transkey = implode(' ', array_map('base64_encode', array_filter($keys, $trim)));

try {
    $db = new PDO('mysql:host=localhost;dbname=test;charset=utf8', 'root', 123456);
    $sql = "SELECT * FROM `cuts` WHERE MATCH(full_text) AGAINST(:full_text IN BOOLEAN MODE)";
    $sth = $db->prepare($sql);
    $sth->bindParam(':full_text', $transkey, \PDO::PARAM_STR);
    $sth->execute();
    $rows = $sth->fetchAll(\PDO::FETCH_ASSOC);

    var_dump($rows);
} catch (Exception $e) {
    var_dump($e->getMessage());
}

這樣就可以找到正確的結果了,不過 jieba 套件在 PHP 執行上算慢的,同樣的環境 PHP 7 要跑 2 秒,Python 版本是 0.2 秒…怕 PHP 存檔有延遲的人可以送 queue 後處理,或者是用排程跑 python 去搞定這件事情,也有大神寫了 extension 去呼叫 cpp 的版本,或者兩秒可以忍受的話就牙一咬吧 XDDDD

2018/04/01

Path Issue

今天來聊聊路徑,這幾天寫 python 要執行 cronjob 的時候發現一件有趣的事情,來看程式碼

# -*- coding: utf-8 -*-


def file_get_contents(file_path):
    with open(file_path, 'r') as the_file:
        return the_file.read().strip()


print(file_get_contents('logs/chan.log'))

我們在 logs 目錄下建一個檔案叫 chan.log,裡面只有 hello world 字串,接下來我們在同等目錄下執行程式

/var/www/python/ $ python path.py
hello world

我們得到正確的結果,但假設要寫 cronjob 不會這樣下指令,我們會下 /usr/bin/python /var/www/python/path.py,我們將目錄切換到 /tmp 再來看看結果

/var/www/python/ $ cd /tmp
/tmp $ /usr/bin/python /var/www/python/path.py
Traceback (most recent call last):
  File "/var/www/python/path.py", line 9, in <module>
    print(file_get_contents('logs/chan.log'))
  File "/var/www/python/path.py", line 5, in file_get_contents
    with open(file_path, 'r') as the_file:
IOError: [Errno 2] No such file or directory: 'logs/chan.log'

我們得到了找不到目錄或檔案的訊息,python 在執行時會因為不同的位置得不到相對路徑的結果,可以說好也可以說不好,引用的路徑本來就該相對嚴格些,不好在有時候一些簡單的東西想偷懶都不行了,來修正程式碼

# -*- coding: utf-8 -*-

import os


def file_get_contents(file_path):
    real_path = os.path.join(os.path.dirname(__file__), file_path)
    with open(real_path, 'r') as the_file:
        return the_file.read().strip()


print(file_get_contents('logs/chan.log'))

再次執行

/tmp $ /usr/bin/python /var/www/python/path.py 
hello world

得到正確的結果了,讓我們來看看 node.js

const fs = require('fs');

const content = fs.readFileSync('logs/chan.log').toString().trim();

console.log(content);
/var/www/nodejs $ node path.js
hello world
/var/www/nodejs $ cd /tmp
/tmp $ /usr/bin/node /var/www/nodejs/path.js 
fs.js:646
  return binding.open(pathModule._makeLong(path), stringToFlags(flags), mode);
                 ^

Error: ENOENT: no such file or directory, open 'logs/chan.log'
    at Object.fs.openSync (fs.js:646:18)
    at Object.fs.readFileSync (fs.js:551:33)
    at Object.<anonymous> (/var/www/nodejs/path.js:3:20)
    at Module._compile (module.js:652:30)
    at Object.Module._extensions..js (module.js:663:10)
    at Module.load (module.js:565:32)
    at tryModuleLoad (module.js:505:12)
    at Function.Module._load (module.js:497:3)
    at Function.Module.runMain (module.js:693:10)
    at startup (bootstrap_node.js:188:16)

沒想到 node.js 的認定跟 python 是一樣的呢,來修改程式碼

const fs = require('fs');
const path = require('path');

const fileGetContents = (filePath) => {
    const realPath = path.join(__dirname, filePath);

    return fs.readFileSync(realPath).toString().trim();
};

console.log(fileGetContents('logs/chan.log'));
/tmp $ /usr/bin/node /var/www/nodejs/path.js 
hello world

得到正確的結果了,我們來看看 PHP

<?php

echo trim(file_get_contents('logs/chan.log')), PHP_EOL;
/var/www/php $ php path.php 
hello world

PHP 一行就寫完了,難怪是這個世界上最好的語言(lol)

/tmp $ /usr/bin/php /var/www/php/path.php 
PHP Warning:  file_get_contents(logs/chan.log): failed to open stream: No such file or directory in /var/www/php/path.php on line 3

OK,事實上只要離開了相對位置,三個語言遇到的情況是一樣的,來修改程式碼

<?php

echo trim(file_get_contents(__DIR__.'/logs/chan.log')), PHP_EOL;
/tmp $ /usr/bin/php /var/www/php/path.php 
hello world

什麼,PHP 加一個 __DIR__ 就搞定了,你還在跟我爭 PHP 是不是世界上最好的語言 (lol)?

來總結一下,其實在這三個語言裡用類似 include 語法內部引入檔案時是不會有問題的,例如說 include ../file.php;,只是我自己習慣還是會 include __DIR__.'/../file.php'; 去引用,這樣會更萬無一失

2018/03/29

Generators

目前我學的三個語言都有支援 generators,一般來說我們會用 foreach 去跑 Iterator,假設我們創建了一個極大的 array variable,非常佔用記憶體,而 generators 的出現就是解決這個問題,當你真的要取值時才使用 yield 吐出資料,最常見的範例就是爬檔案了,一般寫法是把檔案內容整個讀進來 memory,然後你在對他進行 iterator 的訪問,假設該檔案有五萬行資料,你的東西在第十筆就找到了,那剩下的四萬多筆資料只是單純浪費記憶體空間而已,接下來實做三個語言的 generators 用法

PHP

<?php

function go($length)
{
    for ($i = 0; $i < $length; $i++) {
        yield $i;
    }
}

foreach (go(10) as $value) {
    echo $value, PHP_EOL;
}

node.js

function* go(length) {
    for (let i = 0; i < length; i++) {
        yield i;
    }
}

for (value of go(10)) {
    console.log(value);
}

python

def go(length):
    for value in range(10):
        yield value


for value in go(10):
    print(value)

上面的程式執行後都會得到 0-9,而 python 在這個部份有很多變化,像是可以用 xrange 這個 function 拿到一個 generator 的結果,寫法也比較多變化

x = [v for v in range(10)]
print(x)

# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

x = (v for v in range(10))
print(x)

# <generator object <genexpr> at 0x7fe2b60916e0>

for value in x:
    print(value)

# 0 1 2 3 4 5 6 7 8 9

2018/03/22

Async Await

ndoe.js 預設的功能都是非同步的,一般來說程式語言都是依序從上執行到下,而 node.js 的話不是,我們來看一下一個簡單的範例

const runTimeout = () => {
    setTimeout(() => {
        console.log('time is up');
    }, 2000);
};

runTimeout();
console.log('done');

以往我們的想像會是先拿到 time is up 在拿到 done,但這個 case 的執行結果會先拿到 done 再拿到 time is up,這樣的優點是你可以一次同時處理很多事情,拿到結果後進行下一步,之前要達到這樣的效果,要寫很多程式碼,然而現實生活中其實大多的功能都必須依序執行,所以有了 promise 來解決這件事情,網路上範例很多,我這邊會用最精簡的作法當作是個人的筆記。

以上方例子來說,修改方式如下:

const runTimeout = async (time) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(time);
        }, time);
    });
};

const main = async () => {
    try {
        const r1 = await runTimeout(2000);
        const r2 = await runTimeout(1000);

        console.log(r1);
        console.log(r2);
        console.log('done');
    } catch (e) {
        console.error(e);
    }
};

main();

我將程式買改寫成可帶入參數,這樣可以驗證他的確是依序做完的,執行結果為:

2000
1000
done

另一個情境是,執行數量是動態的話要怎麼實現,範例如下:

const runTimeout = async (time) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(time);
        }, time);
    });
};

const times = [
    3000,
    1000,
    2000
];

const main = async () => {
    try {
        Promise.all(times.map(runTimeout)).then((msg) => {
            console.log(msg);
            console.log('done');
        });
    } catch (e) {
        console.error(e);
    }
};

main();

執行結果:

[ 3000, 1000, 2000 ]
done

第二種作法:

const runTimeout = async (time, name) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(`run ${name} in ${time} secs`);
        }, time);
    });
};

const times = [
    [3000, 'a'],
    [1000, 'b'],
    [2000, 'c']
];
const runners = times.map(async (item) => {
    const result = await runTimeout(item[0], item[1]);

    return result;
});
const main = async () => {
    try {
        Promise.all(runners)
            .then((msg) => {
                console.log(msg);
                console.log('done');
            });
    } catch (e) {
        console.error(e);
    }
};

main();

執行結果:

[ 'run a in 3000 secs',
  'run b in 1000 secs',
  'run c in 2000 secs' ]
done