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'; 去引用,這樣會更萬無一失