2013年12月8日 星期日

[mosquitto]在 windows command 下,要傳送雙引號。

這應該算是 windows command prompt 的小技巧,只是我到現在要用 mosquitto 的 mosquitto_pub.exe 才遇到。要把雙引號送進批次檔或是執行檔送出雙引號,其跳脫符號就是雙引號。

因為每次執行 mosquitto_pub.exe 都要到安裝的目錄下,而且,topic 跟 host 每次都要手打,很想偷懶。

於是在順手的地方,例如 C:\User\user,放了一個叫做 send.bat 的批次檔。內容如下:

"C:\Program Files (x86)\mosquitto\mosquitto_pub.exe" -t test -h 192.168.1.111 -m %1

然後,只要我在 C:\User\user 底下執行

send test

就會得到

test

若是執行

send test test

卻只會得到

send test

若要得到 test test,就要執行

send "test test"

接下來,我想要送一個 json 物件,{"id":"ricky","msg":"hello world!"},若直接執行

send {"id":"ricky","msg":"hello world!"}

,會變成

{id:ricky

我們看一下實際執行的指令是:

C:\User\user>"C:\Program Files (x86)\mosquitto\mosquitto_pub.exe" -t test -h 192.168.1.111 -m {"id":"ricky"

原因是,參數有先傳進批次檔,雙引號算是控制符號之一,真的送出去的字串裡,雙引號都被拿掉了。

所以不論是執行檔或是批次檔,要傳送 json 這種東西,雙引號的部份要用雙引號跳脫,所以就長成這樣:

send.bat "{""id"":""ricky"",""msg"":""this is test""}"

跟 Visual Basic (VB) 一樣,是用雙引號來跳脫。我還試了很久的反斜線…,兩邊沒有要統一一下嗎?

[sublime] 在 mac 開啟 .hgrc

在開啟檔案的時候,按 cmd + shift + .

隱藏檔就會跑出來。

嗯,很搞笑。

2013年12月2日 星期一

[python] 基因演算法探討數字遮罩的最佳解

前言

在 facebook 上,python taiwan 出現一個討論

 

楊文誠
這是我學校作業,但是我真的看不懂,可以幫我翻譯一下嗎?拜託~~
我連我要幹嘛都不知道了......

1391532_552537671495061_279533194_n

圖1:一個學生來問作業的解答,引起廣泛的討論。[1]

題目是找出合乎一個範圍內的數字,以它跟某一特定的數字作 or 運算可得到的值越大越好,而其本身越小越好。這可以當成是一個尋找遮罩的問題。

雖然有留言說已經有各種解法出現在 ptt 上(可見不只一個人來討解答),我就愛自己試一試。題目有附註說,用蠻力法要扣30分,但是我很難界定蠻力法是什麼樣子?難道不能把 i in range(L, U+1) 都算過一遍嗎?

遇到這種問題,當我沒辦法直覺想到演算法,就會交給上天/上帝,也就是基因演算法。誰叫這題目正好命中基因演算法的解形式呢!

人工智慧演算法,通常應用在正規演算法找不到或是正規演算法要算很久的情況。如果把所有解都算過一遍以得到最佳解都來得及,為什麼要用人工智慧得到可能最佳解呢?所以現在人工智慧演算法已經不流行了。在運算力如此可怕的時代,想用人工智慧演算法的人,準確的說會使用基因演算法的人,大概就像我這樣把演算法丟給上帝的懶墮蟲吧。我猜 ptt 的各種解法應該不會有用基因演算法的,因為不保證答案會對喔!可以說根本是來搞笑的!(若說是做先期研究的開路先鋒,就好聽很多。)

在這次的文章裡,可以好好體會了一下基因演算法的眉眉角角,在實際應用上會增加不少經驗值。同時,我也在這次的研究中,學到用基因演算法的方法論來研究問題、優化演算法。

第一章 基因演算法的基礎

基因演算法是模仿生物演化的理論,留下好基因,求得最佳解的一種演算方法。它從基因的互換、突變,來得到新的基因,透過適應性的評估哪些是決定比較好的基因並留下,再進行基因的互換、突變以得到新的基因,不斷重復這過程,直到滿意為止。

以上是理論,那實務上真的要解決問題,要面對的是先回答另一些問題:

  1. 基因的形式為何?
  2. 基因的互換方式為何?
  3. 基因的突變方式為何?
  4. 適應性如何評估?
  5. 要留下多少基因?
  6. 要看幾代?

這些問題,也就是問題之前的問題,是演算法之外的事,一般是不會在教科書上教的。多數的教科書,多是只給了一兩個範例,但是沒有說為什麼要這麼問這些問題。多把文章重點放在解釋基因演算法的原理或實作的程式碼。反而上述的幾個問題該「如何回答這些問題」成為了實務上最難解的問題。有些時候上面的六個問題沒有答案,就不能用基因演算法了。而這些答案都是 case by case,領域不同、題目不同,則答案就不同。甚至答案可以有很多,得看實際執行後的結果及效率來決定答案好不好,是不是最佳。經驗也會影響答案的內容,而且答案的內容也會影響實作,在這麼交互影響之下,答案討論起來是很繁瑣又不具通用性,或許是教科書不寫的原因吧?但是重申一次在實務上,這很重要!

我這裡按順序說明我對於各個問題的初步答案,第一個問題,基因的形式,在這裡剛好就是 mask 的樣子,由一串0與1組合而成的字串,不過為了計算方便,把它轉成 32-bit 的正整數數字,也剛好合題目的需求。剩下的問題,答案先暫時決定如下:

  • 基因的互換方式:暫定為「最佳基因的前半部加本次最佳基因的後半部」。
  • 基因的突變方式:暫定為「隨機挑一個bit,0、1互換」
  • 適應性評估:正好為題目需求,與 mask 運算完的數字越大越好,而 mask 本身則是越小越好。那就是按運算完的數字排列,挑最大的,若運算完數字同樣大小,則按 mask 本身數字挑比較小的。
  • 留下多少基因:跟基因互換有關聯,暫定為「已知最佳基因,與本次獲得最佳基因」,因此只有兩個。
  • 暫定1000代

第二章 完全亂數與暴力法的差別

在決定前一章的六個問題後,我開始實作我的演算法。程式中有一些基礎函式要先做:

import random
string_length = 32

def _bin_string(num):
    # bin(63) => '0b111111'
    return bin(num)[2:].zfill(string_length)

def _invert_at(s, index):
    return bin(int(s, 2) ^ 2 ** (index))[2:].zfill(string_length)

def _int_from_bin(s):
    return int(s, 2)

def score(num):
    '''分數函數'''
    return num | N

def f1(num1, num2):
    '''交配'''
    s1 = _bin_string(num1)
    s2 = _bin_string(num2)
    return _int_from_bin(s1[0:16] + s2[16:32])

def f2(num):
    '''突變'''
    i = random.randint(0, 31)
    s = _bin_string(num)
    ss = _invert_at(s, i)
    return _int_from_bin(ss)

def is_acceptable(num):
    '''存活函數'''
    if num > U:
        return False
    elif num < L:
        return False
    else:
        return True

接下來才是依照問題寫好的基因演算法:

L = 201310
U = 3567891
N = 30951344

p1 = L
p2 = U

i = 0
for j in range(1000):

    m1 = f1(p1, p2))
    m2 = f2(m1)

    if not is_acceptable(m1):
        print 'skip m1', j, m1
        continue
    if not is_acceptable(m2):
        print 'skip m2', j, m2
        continue

    score0 = score(p1)
    score1 = score(m1)
    score2 = score(m2)
    if score1 > score2:
        d = m1
    elif score1 < score2:
        d = m2
    else:
        d = min(m1, m2)
    score3 = score(d)
    if score0 > score3:
        p2 = d
    elif score0 < score3:
        p2 = p1
        p1 = d
    else:
        p1 = min(p1, d)
        p2 = max(p1, d)

    i += 1
    print j, p1, p2

print i, p1, p2

 

第一次執行,咦!沒有輸出。經過檢查,m1 不在 LU 的範圍內,所以完全得不到存活的子代基因,遇到這麼不順利的情況,非改不可。把 m1 的產生方式改成 f2(f1(p1, p2)),經過 10 次(每次 1000 代)測試,發現平均有存活的子代基因約 430 次左右,當然答案都不一樣,10 次得到最好的解是 mask 2603087,分數是 33554431。

那麼暴力法呢?程式如下:

p1 = L

for j in range(L, U + 1):

    score0 = score(p1)
    score1 = score(j)
    if score0 > score1:
        pass
    elif score0 < score1:
        p1 = j
    else:
        p1 = min(p1, j)

    print p1, score(p1), j

超級短的程式,得到的結果是 mask 2603087,分數是 33554431。

在觀察基因演算法給出的 10 次答案的時候發現,在求適應性分數來說,幾乎 10 次有 9 次的分數是正解。但是求 mask 的部份,卻不一定會是正確答案。拿這個答案交出去可能不會得分。或許,我再多跑幾次,可能得到更好的 mask,可能得到更差的 mask,但實務上來說,若我不跑一次暴力法,我永遠不會知道 mask 是不是最小,score 是不是最大。能不能接受基因演算法的答案,真的是看情況。

另一方面,兩者的執行環境都在 windows 的 console 下跑,並且取消所有迴圈中的 print,若不取消,暴力法的執行時間被 print 拖累太多太多,我認為不公平。最後結果是暴力法的時間是0:00:01.95,而基因演算法的時間是 0:00:00.14,約為暴力法的 7% 的執行時間。

第三章 從基因演算法的角度思考優化

在第二章中雖然基因演算法有得到答案,但其實是不確定是否為最佳答案。假設答案是不能接受這種不確定性,那如何思考更好的演算法,可以在可忍容的資源下把所有可能的答案都算過一遍?從基因演算法的角度出發就是要問如何更有效率的演化?提高演化效率的方向有,(1)基因的存活率、(2)基因交配的方法、(3)基因突變的方法。這三個方向不完全是獨立的,它們之前會隨問題而有不同程度的某種關聯性。

隨機挑選子代

首先從提升存活率來看,之前的子代產生是經由交配、突變產生,有時子代會跑出規定的上下限而直接淘汰。是否能直接從解集合裡產生子代,以提升存活率,進而能更廣泛提取驗證子代呢?首先試試不經交配、突變,直接亂數選取子代的效果。

f4_i = range(L, U + 1)
def f4(num, restart=False):
    '''不經交配、突變,直接亂數選取子代'''
    global f4_i
    if len(f4_i) == 0:
        raise Exception('len(f4_i) == 0')
    if restart:
        f4_i = range(L, U + 1)
    i = random.randint(0, len(f4_i) - 1)
    si = f4_i[i]
    del f4_i[i]
    return si

改寫原先程式,把存活函數拿掉,避免產生重覆的子代:

from datetime import datetime
fnstart = datetime.now()
'''不經交配、突變,直接亂數選取子代'''
result = []
p1 = L
p2 = U
for k in range(10):
    i = 0
    for j in range(1000):

        m1 = f4(p1)
        m2 = f4(p1)

        score0 = score(p1)
        score1 = score(m1)
        score2 = score(m2)
        if score1 > score2:
            d = m1
        elif score1 < score2:
            d = m2
        else:
            d = min(m1, m2)
        score3 = score(d)
        if score0 > score3:
            p2 = d
        elif score0 < score3:
            p2 = p1
            p1 = d
        else:
            p1 = min(p1, d)
            p2 = max(p1, d)

        i += 1
        #print j, p1, p2

    #print i, p1, p2
    result.append([p1, score(p1)])

print 'execute time', datetime.now() - fnstart

def cmp_result(x, y):
    if x[1] != y[1]:
        return y[1] - x[1]
    else:
        return x[0] - y[0]
print sorted(result, cmp_result)

結果,執行時間變長了,都是 10 * 1000 的迴圈,用掉約 37 秒。得到的答案更不正確。

execute time 0:00:37.332000
[[2603343, 33554431], [2603343, 33554431], [2603343, 33554431], [2603343, 33554431], [2603343, 3554431], [2603343, 33554431], [2603343, 33554431], [2603343, 33554431], [3128447, 33554431], 2620006, 33554422]]

首先,維護解集合的代價太高,在這例子中,高過低存活率的計算代價。另外得到的答案變異數變大,暗示前次基因的交配策略,在選擇子代中有顯著的助益。這是個很重要的暗示。

找出基因固定段

由前段知道交配策略有顯著助益,因此朝向基因段交互關係研究。

L、U、N 三個數的2進位表示為:

L = 00000000000000110001001001011110
U = 00000000001101100111000100010011
N = 00000001110110000100011110110000

從問題的本質來看,N 與 mask 作用後要最大,最好是 mask 都為 1,但是 mask 又要最小,策略就是 mask 把 N 的 0 的地方換成 1 的是最有效率。

U = 00000000001101100111000100010011
M = 11111110001001111011100001001111 (第一步)

然而,因為有 L、U 作為存活函數的參數,是解集合的上下限,在比 U 最前面的 1 更前面的 0 是不可以變動。所以 M 要把比 U 最高位 1 之前的 1 全變成 0:

U = 00000000001101100111000100010011 (有點對不齊)
M = 00000000001001111011100001001111 (第二步)

因此,基因交配以最佳解的前半部為子代的前半部,是把比較小的 M 留下來,正好是提高演化效率的方法,算是誤打誤撞。不過,在了解 N 、 M 及 U 的關係之後,可以把 M 的基因分成兩段:

M[0:U_first_1_pos+1] 這一段是固定不動,會維持較小的 M,並且有合理的天然上限,交配、突變時會增加存活率。
M[U_first_1_pos+1:] 這一段就是可以改變的基因段。

在第二步這裡得到的 M 是可以得到最高分數的 M,如果它滿足 L <= M <= U,那麼答案就得到了。這裡,又不小心地得到了跟暴力演算法一樣的答案 M = 00000000001001111011100001001111 = 2603087。

如果情況不是呢?那就要在 M[U_first_1_pos+1:] 這一段基因裡變化。

在 M[U_first_1_pos+1:] 的優化

在變動段的基因段裡,依然照著最佳演化的目標前進,繼續觀察 N、M 的關係。

00000001110110000100011110110000 (N)
00000000001001111011100001001111 (M)

第二步得到的 M,在它不滿足 L <= M <= U 的條件下,必須要改變某些基因,讓它符合 L <= M <= U。在所有的基因的變化會有幾種可能,它的影響如下:

  1. N:0, M:0,把 M 的 0 變成 1 ==> 分數變大,mask 變大。
  2. N:1, M:1,把 M 的 1 變成 0 ==> 分數不變,mask 變小。
  3. N:0, M:1,把 M 的 1 變成 0 ==> 分數變小,mask 變小。
  4. N:1, M:0,把 M 的 0 變成 1 ==> 分數不變,mask 變大。

也因此,上面的情況裡,優先測試的是 2 (當 M > U) 或 4 (當 M < L)。若都無法找到滿足 L <= M <= U 的解,則再嘗試 1 (當 M < L) 或 3 (當 M > U) 。但是在第二步得到的 M,不會有第 1 種及第 2 種情況。也就是只剩下第 3 種(分數變小,mask 變小)、第 4 種(分數不變,mask 變大)。所以到了這裡可以明確的說:

當 M > U 的時候,採取「尋找 N:0, M:1 的 bit,把 M 那邊的 bit 反轉」的策略進行演化。

當 M < L 的時候,採取「尋找 N:1, M:0 的 bit,把 M 那邊的 bit 反轉」的策略進行演化。

這樣的基因組應該全部列舉出來了吧?仍然以例題為例,有13個 bit 是 N:0, M:1,所以有 2^13 = 8192 種變化。而 N:1, M:0 有 9 個 bit,所以有 2^9 = 512 種變化。如果仔細想想,又可以再使用 L, U 去掉不合適的基因。可以更準確。不論 L, U, N 怎麼變化,依照步驟到這裡的全列舉,已經會是很小的範圍了。

結尾

在這個題目中,用了基因演算法,一不小心就在短時間內得到不錯的答案。以找到答案為前提之下,基因演算法可以提供一個初步的探索。而以尋找問題解法為目的的時候,也可以從基因演算法的角度出發,思考基因的最佳演進方向及演進方法。在探求的過程中,把問題的解決方法思考出來,也算是一個很好的訓練解析問題的方法。人工智慧演算法不是沒用,只是式微。跟老兵一樣?

參考:

[1] [https://www.facebook.com/photo.php?fbid=552537671495061&set=gm.10152422989873438&type=1&relevant_count=1&ref=nf]

2013年11月16日 星期六

[osx] 軟體 raid 簡單操作 SOP 及軟體 raid 不支援 3T 硬碟?

最近興高彩烈買了 3T 硬碟一顆,準備要跟另外一顆 3T 硬碟合體變成 raid1,但每每在製作過程中會出現無法將硬碟加入 raid 這種錯誤。實在讓人想不通。2T 的硬碟都可以正常工作的,如果真的是軟體限制,希望新版的 osx 可以支援。

順便記下軟體 raid 的簡單操作。

組合成 raid:

  插入兩顆硬碟。

  打開磁碟工具程式

  先隨便點一顆硬碟,再點右邊 RAID 頁籤

  在 「RAID 設置名稱」那一欄填入一個名字,這會是之後掛載上來的磁碟名字。

  「卷宗格式」選 Mac OS擴充格式 (日誌式)

  「RAID 類型」選 映射的 RAID 設置

  按下該頁籤左下方的 + 號,會出現一個灰色的硬碟圖示。

  從左方的硬碟中,把你要的兩顆硬碟拖到灰色的硬碟圖示下方。按下製作按鈕。

  接下來等工具程式,它會把剩下工作做完,然後掛載一個新的磁碟到系統裡,就完成了。

遇到硬碟有問題:

  打開磁碟工具程式,當硬碟有問題的時候,會看到紅色的字。

  點到那紅色字,畫面右邊會顯示狀況,它會告訴你哪一顆硬碟有問題,整個 RAID 設置會進到已降級的狀態。

  在已降級的狀態下,無法新增硬碟。可以做的動作有幾個,結果非常不同,一、對有問題的硬碟做「重建」,重建若完成,RAID 會回到已連線狀態;二、對有問題的硬碟按下 - 號,再按「降級」。RAID 設置會少掉一個硬碟,但還是 RAID 設置,狀態會回到已連線,就可以新加硬碟;三、對有問題的硬碟按「刪除」,這會使用 RAID 設置被取消,會跑出兩個硬碟都是同個名字,但資料都還在。

  所以,最好是先試重建,若硬碟問題不大,就可以再撐一陣子。重建時間很久,2T要等3~5天,若途中還有對該硬碟做動作,會等更久。

  若硬碟真的該換,則是對有問題的硬碟按下 - 號,再按降級,讓 RAID 回到已連線,然後再加新的硬碟,維持 RAID 的設置。

  刪除的動作我以後應該不會再去按了。除非是要把硬碟轉成一般用途。

[python] bit 操作與易經

從沒有機會需要操作 bit,沒想到寫易經的程式非得用到不可。

第一個最需要的是,把數字轉成 0101 的字串。這個內建的 bin() 函數可用,只是它會在字串前多加0b。依實際需要,得把 0b 去掉。

>>> bin(13)
'0b1100'
>>> bin(13)[2:]
'1101'

第二個就是把 0101 的字串轉回數字,這可以用 eval,或是 int('0b1100', 2) 來達成。

>>> int('0b1101',2)
13
>>> int('1101',2)
13

第三個,易經是 6-bits 系統,而且非常需要補 0 在高位數。

>>> bin(13)[2:].rjust(6,'0')
'001101'

第四個,因為我手上的易經參考書,低位寫在前面,高位寫在後面,我得把字串反過來。從字串轉回數字也得一樣照辦。

>>> bin(13)[2:].rjust(6,'0')[::-1]
'101100'
>>> int('101100'[::-1], 2)
13

第五個,對某個 bit 做反轉運算,因為對 6-bits 的操作沒有把握再加上高低位已經反轉,已經夠亂了,所以採取直接對字串操作,但因為 python 的字串沒法單獨對第某個字元操作,所以寫了個函數代替。主要是拆開字串為 list,操作完再組回去。index 從 0 開始。

def _invert_at(s, index):
    ss = list(s)
    if ss[index] == '0':
        ss[index] = '1'
    else:
        ss[index] = '0'
    return ''.join(ss)

>>> _invert_at('101100', 1)
'111100'

更新:經過研究測試之後,相同的功能可以用 bin(int(s, 2) ^ 2 ** (5 - index))[2:].rjust(6, '0') 來解決。

基礎工程結束,接下來應用開始。因為用 bit 來表達了,原本常用的 1~8,都得用 0~7 這種以 0 開始的 index 系統。

一般卜卦是在得到一個卦(叫做「本卦」)之後,都會再求一個「變卦」及「互卦」。在一般簡易數字型卜卦中,本卦、變卦、互卦的求法是:

  • 本卦是得到兩個 0~7 之間的數字,一個代表上卦,一個代表下卦。例如,上卦 1、下卦 7,就是 101100,豐卦。(下卦在前面,低位在前面)
  • 變卦是從 0~5 之間得到一個數字,代表動爻的位置,然後把本卦的對應的 bit 反轉。例如,101100,我們得到 2 爻動,就是得到 111100,大壯卦。
  • 互卦的求法是把本卦234爻做為下卦,把345爻做為上卦。011110,大過卦。

互卦用程式來表示很清楚:

>>> s = '101100'
>>> s[1:4] + s[2:5]
'011110'

接下來,用程式表示八宮卦象變化大概就是這樣:

def get_8_change(s, step):
    if step == 0:
        return s
    elif step == 1:
        return _invert_at(s, 0)
    elif step == 2:
        return _invert_at(s, 1)
    elif step == 3:
        return _invert_at(s, 2)
    elif step == 4:
        return _invert_at(s, 3)
    elif step == 5:
        return _invert_at(s, 4)
    elif step == 6:
        return _invert_at(s, 3)
    elif step == 7:
        ss = _invert_at(s, 2)
        ss = _invert_at(ss, 1)
        ss = _invert_at(ss, 0)
        return ss

拿坎宮的八個卦的程式是

s = '010010'
for i in range(8):
    s = get_8_change(s, i)
    print s, _int_from_bin_r(s), money64[s]

執行結果是:

010010 18 坎
110010 19 節
100010 17 屯
101010 21 既濟
101110 29 革
101100 13 豐
101000 5 明夷
010000 2 師

八宮卦象變化的函式 get_8_change 的 step 0~6 跟京房 16 卦一樣,可以做為基礎繼續改造。

[python] named pipe on windows 7 using ctypes

同樣這篇也是誤入岐途。

原先使用 .Net 的 NamedPipeServer 及 NamePipeClient 好好的。不小心看到網路上有提到 python 也有支援。很開心地就拿來用,結果是失敗。(原po1原po2)

在沒有小心查證之下,又堅信 python 必定可使用 named pipe,嘗試了諸多的 post。都是被無情地打槍。(族繁不及備載) 最後就是看到使用 windows 內 kernel32 的一組 API 達成這個功能(原po):

  1. CreateNamedPipeA
  2. ConnectNamedPipe
  3. WaitNamedPipeA
  4. SetNamedPipeHandleState
  5. ReadFile
  6. WriteFile
  7. FlushFileBuffers
  8. DisconnectNamedPipe

因此,一個很不巧又很湊巧地,讓我必須要去學一下 ctypes 是怎麼回事。

Windows API?

ctypes 是 python 一個很重要的 module,可以用來跟 dll 或 shared libraries 溝通。它提供了一組與 C 語言相容的資料型別(data type),以此做為純 python 的溝通橋樑。也就是說,我這次不小心踩到 Windows API啦…。(我寫 VB6 時最討厭遇到 Windows API)

先從 Windows API 來看,一個 NamePipe 的範例包含兩個部份,一個我硬把它叫 Server,一個我硬把它叫 Client。這是從 .Net 的 NamedPipeServer 及 NamePipeClient 來推論的。

在 server 這邊的流程是(很可惜網路上現在的 MSDN 很少描述這種東西了,或者這些都會在舊的 MSDN 裡,這裡所寫的流程是由網路上搜尋及試誤後得到的可能的一種正確流程。):

  1. 呼叫 CreateNamedPipeA,拿到 handle。
  2. 呼叫 ConnectNamedPipe,等待另一方連上。
  3. 在連上之後,使用 ReadFile 讀取資料,或使用 WriteFile 傳送資料(呼叫 FlushFileBuffers 確定資料沒有留在 buffer)。
  4. 使用 DisconnectNamedPipe 關閉連結,使用 CloseHandle 釋放資源。

在 client 這邊的流程是:

  1. 使用 CreateFileA 得到 handle。handle 有可能是幾種數值。如果 server 已經在等待中,handle 會直接拿到。如果不是 handle 是 INVALID_HANDLE_VALUE,接下來可以從 GetLastError 得到是否為 ERROR_PIPE_BUSY,若是 pipe busy 可以呼叫 WaitNamedPipeA,等 server 開啟。
  2. 成功拿到 handle 之後,可能需要改變 pipe 的傳送方式為 byte。此時使用 SetNamedPipeHandleState。此步驟可省略。
  3. 在連上之後,使用 ReadFile 讀取資料,或使用 WriteFile 傳送資料(呼叫 FlushFileBuffers 確定資料沒有留在 buffer)。
  4. 使用 DisconnectNamedPipe 關閉連結,使用 CloseHandle 釋放資源。

相信老程式人一定會知道,這些 API 在尾巴有 A 的,也有同名但尾巴不帶字的,或者是尾巴帶 W 的。現在 MSDN 不太強調這些資訊,我想這些差異現在應該是被微軟處理掉,開發者可以不用去在乎了。(在 http://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx 最下面一行。不懂的人還是看不懂。這告訴我們 .Net 就對了,不要再用 Windows API?)

之所以要先介紹 Named Pipe 的 Windows API,是因為,用 ctypes,就是得按照 Windows API 的規矩來!ctypes 不是一個已經包好(wrapped)的轉接器(adapter),而是要拿來做轉接器的工具。已經打包好的是 pywin32,我自己誤很大。

現在來仔細看 CreateNamedPipe[5] 的定義,進一步了解該從 MSDN 查到什麼資訊,如何用 ctypes 與它溝通:

HANDLE WINAPI CreateNamedPipe(
_In_      LPCTSTR lpName,
_In_      DWORD dwOpenMode,
_In_      DWORD dwPipeMode,
_In_      DWORD nMaxInstances,
_In_      DWORD nOutBufferSize,
_In_      DWORD nInBufferSize,
_In_      DWORD nDefaultTimeOut,
_In_opt_  LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
第一行看到的是 HANDLE WINAPI CreateNamedPipe,知道它會回傳一個 HANDLE 型態的值。
因為我們很取巧地使用 python,所以可以很取巧地不用刻意準備對應的型別來承接。
再來第二行 _In_ LPCTSTR lpName,說明這是一個傳入參數,型別是 LPCTSTR,
這個型別的意義請參考網路上的好文,我這裡也很投機地使用python 的 str 型別來傳。


接下來的 DWORD,應該大家都熟悉地知道是 2-byte bytes 型別。


這些的對應,我也很投機地找到一個地方有記錄,是別人整理好的在 rpython 的專案中。
但是重要的是,dwOpenMode、dwPipeMode 要填什麼值?
這個就要回到 MSDN(參考[5])的網頁裡找到對應的數值。例如 dwOpenMode,有基本的三個數值:


  • PIPE_ACCESS_DUPLEX 0x00000003
  • PIPE_ACCESS_INBOUND 0x00000001
  • PIPE_ACCESS_OUTBOUND 0x00000002

同時可以有下列三個不同的 flag 可調整 pipe 對於資料寫讀時的行為:



  • FILE_FLAG_FIRST_PIPE_INSTANCE 0x00080000
  • FILE_FLAG_WRITE_THROUGH 0x80000000
  • FILE_FLAG_OVERLAPPED 0x40000000

又同時有以下三個 flag,可調整 security access 的模式:



  • WRITE_DAC 0x00040000L
  • WRITE_OWNER 0x00080000L
  • ACCESS_SYSTEM_SECURITY 0x01000000L

所以,從上述三大類中,挑選正確的數值。通常大家都是這麼做的:



PIPE_ACCESS_DUPLEX = 0x3  # 宣告常數
FILE_FLAG_WRITE_THROUGH = 0x80000000
dwOpenMode = PIPE_ACCESS_DUPLEX | FILE_FLAG_WRITE_THROUGH


然後把 dwOpenMode 放進呼叫裡。


假設像我一樣抄完別人的 code,要呼叫 CreateNamedPipe 這個 function 就是這樣:



hPipe = windll.kernel32.CreateNamedPipeA(path,
                                                 PIPE_ACCESS_DUPLEX,
                                                 PIPE_TYPE_BYTE |
                                                 PIPE_READMODE_BYTE |
                                                 PIPE_WAIT,
                                                 PIPE_UNLIMITED_INSTANCES,
                                                 BUFSIZE, BUFSIZE,
                                                 NMPWAIT_USE_DEFAULT_WAIT,
                                                 None
                                                )


在 python doc 的 15.17.1.1. 說到會自動代入兩個物件,windll, oledll,從這兩個物件開始操作。


標準的 c data type,有 python doc 15.17.1.4. Fundamental data types 可以查,
但是對於 Windows API 裡特別的型別(例如 LPCTSTR) 就要試,或著查看 pywin32 之類的程式碼對照。


Array, Pointer 怎辦?


接下來還會遇到的是陣列,指標的問題。通常是為了應付 out 的參數,像是 ReadFile:


BOOL WINAPI ReadFile(
_In_         HANDLE hFile,
_Out_        LPVOID lpBuffer,
_In_         DWORD nNumberOfBytesToRead,
_Out_opt_    LPDWORD lpNumberOfBytesRead,
_Inout_opt_  LPOVERLAPPED lpOverlapped
);

第三行的 _Out_        LPVOID lpBuffer,我們得準備個陣列給它,而且還要是個指標。


首先,我在 python 從來沒想過要宣告陣列。據抄來的 code 是這樣的(我忘記哪抄來的):



buff = (c_ubyte * 4)()  # c_ubyte 型別,長度為 4


要讓它是個指標,就照官方文件給個 byref function 包著或建立一個 pointer 物件(byref 比較 lightweight)



lpbuff = byref(buff)
or
pbuff = pointer(buff)


在第五行的 _Out_opt_    LPDWORD lpNumberOfBytesRead,LPDWORD 的處理方式則是:



cbRead = c_ulong(0)
fSuccess = windll.kernel32.ReadFile(self.hpipe,
                                    byref(chBuf, offset),
                                    rsize,
                                    byref(cbRead),
                                    None)


offset += cbRead.value


如此,在 cbRead.value 就可以看到值。


指標還有個常用到的特色是指標移動,像是上面的第三行,byref(chBuf, offset),
得到的是 chBuf 再往後移動 offset 的指標。常寫 c 或 c++ 的人會了解。


為什麼直接用 ctypes ?


除了誤入岐途之外,也有一個好處,pywin32 真的不小。如果只是想要一個 named pipe,
我自己 285行(特化過的 named pipe server + client)就解決了。
程式碼長度約 12190 bytes。deploy 不用帶 pywin32 或叫 user 安裝。
目前我想到的是這個優點。不過網路上有很多人說,pywin32 解決很多轉換的步驟。
至少少掉很多查文件、設定常數的重覆工作。


 


參考:


[1] http://code.activestate.com/lists/python-list/446422/


[2] http://www.nullege.com/codes/search/ctypes.OleDLL


[3] http://stackoverflow.com/questions/3517159/ctypes-offset-into-a-buffer


[4] http://docs.python.org/2/library/ctypes.html#ctypes-arrays


[5] http://msdn.microsoft.com/en-us/library/windows/desktop/aa365150(v=vs.85).aspx


[6] http://stackoverflow.com/questions/1430446/create-a-temporary-fifo-named-pipe-in-python


[7] http://jonathonreinhart.blogspot.tw/2012/12/named-pipes-between-c-and-python.html

2013年11月5日 星期二

[android] emulator 以為地球在左邊

光看到標題一定不會知道我在說什麼。畫面不就是 portrait 跟 landscape 嗎?反正我就是遇到了像下圖一樣的情況。

 

1

不管怎麼轉都不如意

2

正常不就是要像下圖一樣的畫面。

3

老大說怎麼可以,一定是怎樣怎樣ooxx…。

設定檔東翻西找找不到。

因為英文不好,真的還不知道該怎麼查 google。這英文要怎麼描述?

終於,我在 stackoverflow 找到一篇 post,標題是「

Emulator in landscape mode, screen does not rotate」

http://stackoverflow.com/questions/7394447/android-emulator-in-landscape-mode-screen-does-not-rotate

我總算可以交差,那就是 bug。

後來我是按照底下留言把「Hardware keyboard present」關掉,就正常了。

[python] base64, base32, binascii

base64 的編碼方式,目的在於把 binary 的資料,用可見的 ascii 表示出來。每三個 byte 的資料,用四個 byte 的可見字元表示[1],所以資料會變大 1.33倍。老骨頭們應該有聽過 uuencode,那也是一樣的目的。通常會選擇 64 個可見字元,目前 base64 常見的應用在 MIME's Base64,它所選擇的字有 0-9, a-z, A-Z 再加上加號+ 及除號/ 組成 64 個字元。如果用在 url 裡面,會把 + 及 / 換成別的字,成為 base64 的變形。

在看過實例後,會發現有的 base64 後面會跟著等號 = 那是因為每次資料的 byte 數不一定是三的倍數。那麼少掉的 byte,就用等號來補。

那麼,base32,就是用 32 個字元來取代,也就是每 5 個 bit,就要用 1 個 byte 來表示。也就是 5 個 byte 的資料要 8 個 byte 來表示,資料變大成 1.6 倍,因此不常被使用。

那麼在 python 有兩個模組可以用 base64。一個是 base64 模組,一個是 binascii 模組。在 base64 模組提到它是遵照 RFC3548 來做的,演算法不同於 uuencode,所做出來的字串可以用在 e-mail 傳送任意資料,或是 http post 的一部份。在 html 內嵌圖片資源也是常常看見的。在這個模組也實做 base32 及 base16。

而在 binascii 這裡,有 a2b_base64(string) 及 b2a_base64(data) 兩個函式,非常簡單地就把該轉的轉完。

其實用哪個模組都可以,唯一要注意的是 binascii.b2a_base64(x) 的後面多了一個換行。所以轉成 base64 之後若拿來跟 base64.b64encode(s) 比對的話,要 strip 過才不會中招。

我自己常常遇到的應用是客戶把檔案轉成 base64 的字串放在 xml 裡(SOAP),我們再把它轉回來變檔案。

 

參考

[1] http://en.wikipedia.org/wiki/Base64

[webpy] webpy, django, web2py 的適用性研究

先來研究一下 webpy 、django 及 web2py 的差異。

雖然這三個都自稱 web framework,其中差異卻是很大。

差異的地方在於功能涵蓋率以及如何做到。

我研究的方法是從 hello world 來看它們的做法。

webpy 提到它有以下的功能,而這些都是 web framework 的基本功能:

  1. URL Handling
  2. Web server
  3. Templating
  4. Forms
  5. Database

而 django、web2py 的網站第一句話就說,「high-level Python Web framework」(django)、「full-stack framework」(web2py)。因此相較之下,webpy 顯得是比較手工(輕量級)的 framework。這怎麼說呢?

首先,webpy 最簡單的範例是:

import web

urls = (
    '/', 'index'
)

class index:
    def GET(self):
        return "Hello, world!"

if __name__ == "__main__":
    app = web.application(urls, globals())
    app.run()

執行這段程式

python code.py // 或者 python code.py 1234 以指定 port

之後就可以用 browser 來看執行結果。其他的事情,就自己手工來寫。

web2py 在安裝完之後,直接執行 web2py。進到預設網頁(http://127.0.0.1:8000)
在它的預設網頁先設定管理者密碼,再建立一個 Application,例如 myapp。
之後再進入 http://127.0.0.1:8000/myapp 的 edit mode,針對它提供的各大項目進行程式碼編輯。
這些項目包含:

  1. models
  2. controllers
  3. views
  4. languages
  5. modules
  6. static files
  7. plugins

以最簡單的 hello world 來說,就是在屬於 controller 的 default.py 裡,寫下

def index():
    return "Hello from MyApp"

如此瀏覽 http://127.0.0.1:8000/myapp/default/index 就會看見 hello world 了。

django 在安裝完後(這是一條漫長的道路),要建立網站架構,使用指令

django-admin.py startproject mysite

它會在你的工作目錄產生一個結構:

mysite/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        wsgi.py

然後再依照 django 規定修改新增相關檔案,啟動開發用 server,就可以看到結果。

由一個 hello world 範例,就可以看出這三個 web framework 的目標族群是不一樣的。
webpy 的確是簡單。若是只有幾頁網頁要展示,沒有複雜權限控管需要實作(例如與桌機整合的 single sign on),對於開發小型網站程式是很好的。不用裝一大包東西只為了幾種單純的網頁。
可以這樣思考,若是裝 apache 都嫌太大的專案,就可以思考用 webpy。類似的 framework 如 http://www.cherrypy.org/。然而是要完整功能,則免不了要自己打造很多功能。選用別的 framework 比較合適。
而 django、web2py,已經有相關許多模組做出權限控管,許多類型檔案支援,session 的支援等等。

這裡粗略的比較只是為了找出各個 web framework 適不適合自己所用的專案而已。 同樣都說是 framework,輕量級跟重量級可是差很多的。

我目前只要小小的 framework。

:p

 

參考:

http://www.cherrypy.org/

http://webpy.org/

https://www.djangoproject.com/

http://www.web2py.com/

2013年10月7日 星期一

[文字雲] 中文分詞的方法有哪些?

完全是誤打誤撞。

原先我的研究是依照某人喜愛的文章的標籤,推斷他有可能喜愛的文章。而其中,標籤的給定,是所謂的工人智慧,也就是使用者自行貼上所認定的標籤文字。前述的標籤文字,也就是在圖書領域或自然語言領域所謂的關鍵字。

原本我對於關鍵字是沒有需要去研究的,在工人智慧標籤的領域中,這是人的工作。但是在標籤的領域中,的確有著由電腦分析文章後給予關鍵字當做標籤的系統存在。也因此當我不小心誤入這個網站(http://blog.timc.idv.tw/posts/wordcloud/),我以為,我就可以提供自動給予標籤建議的功能,這在一個標籤系統中,是有效降低使用者心智負擔,提高使用者經驗的一種做法。

其中,文章詞頻分析,該「文字雲」的做法是用標準的 N-gram。因為中文很開心的是詞性變化與字無關,同時使用上,名詞、動詞、形容詞…等的字數也不會太長。我很開心的就拿來用了。當然我先把他的程式改寫成 python。我把我手邊有的財經資料文章試跑看看,我取到 8-gram,結果得到的是以下的結果,看來切的不是很好。

 

公司,839
新細明體,819
細明體,685
不適用,673
仟元,512
報導,507
億元,492
證交所重大訊息公,456
交所重大訊息公告,456
記者,447
精實新聞,447
事實發生日,286
本公司,197
美元,189
公司名稱,187
其他應敘明事項,174
發生緣由,166
與公司關係,163
個月,163
億美元,158
相互持股比例,153
不過,133
輸入本公司或子公,132
請輸入本公司或子,132
入本公司或子公司,132
科技股份有限公司,128

我很好奇,大家的自動詞頻統計的中文分詞是怎麼做到的?拜請 google 大神,得知台灣目前似乎只有中研院有公開研究成果可看(http://ckipsvr.iis.sinica.edu.tw/),另外有的是拿別人已經做好的詞庫或自己調整過的詞庫做比對。除了台灣的研究,在 google 找到的多是大陸的研究,而我的入門磚選擇的是一篇論文「中文分詞算法概述」(連結)。(我隱約地感覺到,台灣叫斷詞,大陸叫分詞。我不太確定兩邊的專有名詞用的是哪一個)。我最愛的就是這一種論文,它可以快速了解目前該領域的現況,對於初學者最好了!

一個題外話,文章中引言的第一句:

自然语言处理是人工智能的一个重要分支。中文分词是中文自然语言处理的一项基础性工作,也是中文信息处理的一个重要问题。

在我認為中文分詞若是採用詞庫為基礎,建詞庫除了用專人建出來之外,大可以使用搜尋引擎的輸入關鍵字或標籤系統的標籤來組合成詞庫,這是利用工人智慧的好範例。中文分詞是一個基礎工作,很可惜的這種基礎工作現在台灣很難 google 到(不在前幾頁)。

回主題,看完該論文之後快速整理中文分詞的做法如下:

  1. 基於字串比配的分詞方法:這種方法又叫「機械分詞方法」、「基於字典的分詞方法」。它是按照一定的策略將待分析的字串與足夠大的詞典進行比對。若在詞典中找到某個字串,就算比對成功(識別出一個詞)。這個方法有三個要素(1)分詞詞典、(2)文本掃描順序、(3)比對原則。文本掃描順序有:(1)正向順描、(2)逆向順描、(3)雙向掃描。比對原則有:(1)最大比對、(2)最小比對、(3)逐詞比對、(4)最佳比對。依照三個要素的排列組合,常常配對使用的方法是:(1)最大比對法(MM)、(2)逆向最大比對法(RMM)、(3)逐詞遍歷法、(4)設立切分標誌法、(5)最佳比對法。
  2. 基於理解的分詞方法:這種方法又叫「基於人工智慧的分詞方法」。在分詞的同時進行句法分析、語義分析,利用句法訊息和語義訊息處理岐義現象。通常包括三個部份:(1)分詞子系統、(2)句法語義子系統、(3)總控部份。基於理解的分詞方法主要有幾種實作:(1)專家系統分詞法、(2)神經網路分詞法、(3)神經網路專家系統整合式分詞法。
  3. 基於統計的分詞方法:這種方法的立論精神是:詞是穩定的組合,因此在文章中,相鄰的字同時出現的次數越高,越有可能構成一個詞。因此字與字相鄰出現的頻率可以反映成詞的可信度。可以從訓練的文章中,對於相鄰出現的字的組合的頻率進行統計,以決定是否為一個詞。這種方法又叫做無字典分詞法。這方法使用的統計模型有:(1)N元文法模型(N-gram)、(2)隱 Markov 模型、(3)最大熵模型。在實際應用上,常和「基於字典的分詞方法」結合,以獲得比對分詞法的高效率及無詞典分詞的可辨識生詞的優點。
  4. 基於語義的分詞方法:這個方法引入語義分析,對自然語言本身的語言訊息進行更多處理。如(1)擴充轉移網路法、(2)矩陣約束法。(這個方法在原文中說明就比較少,以致我看不太懂。第2大種的子項目中也有語義分析,不曉得指的是不是同一件事?)

根據以上的分類法,文字雲用的是 「3. 基於統計的分詞方法」中的 N-gram 方法。如果是拿以前的標籤系統,中文分詞不會有問題,因為整個系統的標籤就可以當做字典,拿來對文章做 N-gram 加上字典法,應該就很夠用了。但是,目前有收錄中文的標籤系統且資料(我只要標籤)有公開的,沒有!只好學文字雲的作者一下,自己用 N-gram 再加工。選擇 N-gram 只是因為隱 Markov 模型、最大熵模型我不曉得要怎麼把原理轉成實作。若有大德提點,則是非常感激不盡。

2013年10月1日 星期二

[vb.net] 變數初始值的指定

因為在 C# 用習慣了,換到 vb.net 有點卡卡的,原先用 vb6 又沒有這種語法。

基本型別是沒有什麼問題。

常常會忘記的就是物件的初始值指定、集合物件(collection, dictionary) 的內容值指定這兩件事。

所以寫下來備忘。

對於物件的初始值指定,是使用 With 關鍵字。在大括號裡給予初始值。

Class Apple
    Public Place As String
    Public weight As Integer
End Class

Dim a As Apple = New Apple With {.Place = "Taiwan", .weight = 1}

對於集合物件,則是使用 From 關鍵字。在大括號裡給予初始值。這東西要 VisualStudio 2010 之後才有支援(又多了一個使用 C# 的理由)。太長可以換行。

Dim PostLabel As Dictionary(Of String, String) = New Dictionary(Of String, String) From
    {
        {"Customer", "Forest"},
        {"Address", "Cali"},
        {"Code", "302"}
    }

Dim Box As Collection = New Collection From {"Apple", 2, {"Made", "HC, Taiwan"}, PostLabel, a}

不過,寫到這,我有的疑問,key / value pair 是用大括號加逗點,list 也是。

一下子,上面的 collection,不曉得 {"Made", "HC, Taiwan"} 這一項是怎麼解釋?

寫了個小程式測試一下

For Each obj In Box
    Console.WriteLine("Type:{0}", obj.GetType().Name)
Next

得到的結果是

Type:String
Type:Int32
Type:String
Type:Dictionary`2
Type:Apple

嗯,得到了一個 String 的型別?!

如果把該項換成 {"Made", "HC, Taiwan", "XX"},則會得到「型別初始設定式發生例外狀況」的錯誤。所以要記得這個特性才行。自己不要誤以為是正確的。

若是想要得到一個 string array,多出一組大括號,像是這樣

Dim Box As Collection = New Collection From {"Apple", 2, {{"Made", "HC Taiwan"}}, PostLabel, a}

得到的結果是

Type:String
Type:Int32
Type:String[]
Type:Dictionary`2
Type:Apple

但我想試著弄出 dictionary 來,試寫成

Dim Box As Collection = New Collection From {"Apple", 2, {{"Made", "HC Taiwan"}, {"Make", "TP Taiwan"}}, PostLabel, a}

得到錯誤

型別 '1-維陣列屬於 String' 的值無法轉換成 'String'。

所以目前還沒辦法隨心所欲呢。

[named pipe] 訊息傳送的協定 - 使用訊息長度

上次討論的是,使用分隔字元的訊息傳送協定的實作方式及需要注意的事項(http://www.dotblogs.com.tw/rickyteng/archive/2013/09/12/118266.aspx)(這一句好長!)。基本上使用分隔字元的方式,通常是用在文字訊息。因為把給人看的文字訊息中,比較不會使用到所謂控制字元(也就是那些看不到的字元),所以拿來用就比較不會有衝突。然而,若是傳送的是位元檔的內容,每一個 byte 可能的值都是 0~255,可以拿來當做分隔字元的 byte 等於是不存在。因此而這次要討論的是使用訊息長度為主的協定,通常就用來傳送非文字訊息。

使用訊息長度,意思就是每一次傳送,告訴對方要傳幾個位元組的資料。為了確保傳遞訊息長度的資訊一定送達,傳遞訊息長度的資料不能太長,而且最好是放在一開頭,這樣可以降低發生錯誤的機會。若是一開始訊息長度就失誤了,後面的訊息都會解譯錯誤。

使用訊息長度,最簡單的實作就是每次傳送的開頭 4 個位元組就是訊息長度。這次我們就來實作這種最簡易協定。

首先先改一下之前的範例,把 StreamReader / StreamWriter 這兩個方便的類別拿掉。

我們回歸到 NamedPipeServer / NamedPipeClient 原先提供的 Read / Write 方法。

Write 方法的改寫非常直覺,先把資料的 byte 陣列準備好,計算出所需長度。把長度轉換成 4 個位元組的表示,先寫出,再接著再將資料陣列寫出。

Sub SubHandleWrite(ByVal content As Byte())
    Dim Buffer As Byte()
    Dim BufferSize As Byte()
    ReDim Buffer(content.Length - 1)
    Array.Copy(content, Buffer, content.Length)
    BufferSize = BitConverter.GetBytes(Buffer.Length)
    PipeServerOut.Write(BufferSize, 0, 4)
    PipeServerOut.Write(Buffer, 0, Buffer.Length)
    TextBox3.Text = ""
    PipeServerOut.Flush()
End Sub

Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
    Try
        Dim ContentByte As Byte()
        ContentByte = System.Text.UTF8Encoding.UTF8.GetBytes(TextBox3.Text)
        SubHandleWrite(ContentByte)
    Catch ex As Exception
        CleanPipeServer()
        CleanThread()
        UpdateConnectionState(ConnectionState.Disconnected)
    End Try
End Sub

看一下 Read 方法的介面定義:

Public Overrides Function Read(ByVal buffer() As Byte, ByVal offset As Integer, ByVal count As Integer) As Integer

Read 方法回傳的值是讀取到的實際長度,傳入的參數則有預計要讀取的長度,用來放資料的陣列(buffer)。

對於接收方,讀到 byte 陣列之後,會要先判斷是資料或是長度資訊。雖然我們知道所有的訊息前 4 個位元組是長度資訊,但是每次 Read 方法都要先決定 buffer 的長度才能呼叫 Read。想想有點雞生蛋、雞生雞的問題。而大多數的人採用兩種策略來應付:

  1. 永遠先讀 4 個位元組,得知長度後,再準備足夠的 buffer,讀取訊息內容。若訊息內容較長,可以分次讀取。
  2. 永遠讀一定長度的位元組,再從最前面 4 個位元組算出長度,再算出需要讀取的次數,分次讀取。

Read 方法是阻塞式的跟之前的 ReadLine 一樣,行為就是讀到東西時,會馬上回傳,若讀不到東西時,會 Block 住,也就是卡住。Read 方法會回傳的另一種情況是 pipe 斷開。所以,此時要判斷是否為讀到東西而回傳,就是看 Read 方法的回傳值是否為 0。若是 Read 方法的回傳值為 0 就是 pipe 斷開;若是 Read 方法的回傳值不為 0,就是有讀到東西。

上述兩種策略說來都是一句,但是實際狀況可不是這麼簡單。問題就在讀取到的實際長度!最簡單的例子就是,你讀到的只有 2 個 byte 怎辦?來吧,讓我們用第一個策略來實作,捲起袖子見招拆招吧。

While True
    ' Read Length Step
    ReadCount = PipeServerIn.Read(ReadBuffer, 0, 4)
    Select Case ReadCount
        Case 0
            Exit While
        Case Is < 4
            ' Need Handle
        Case 4
            MsgLength = BitConverter.ToInt32(ReadBuffer, 0)
            While True
                ' Read Message Body
            End While
        Case Else
            ' God!
    End Select
End While

先讀取 4 個 byte,然後看看讀取到的數量,如果是 0,那就是斷掉了。如果小於 4,那就要另外處理(等下說明)。如果等於 4,就轉成訊息長度,然後進入接收訊息本體迴圈。如果都不是,那就看到神了!

讀到的數量小於 4,那就要等下一次讀取,所以要把這次讀到的保留下來吧!所以改寫成下面的樣子:

While True
    ' Read Length Step
    ReadCount = PipeServerIn.Read(ReadBuffer, 0, 4)
    Select Case ReadCount
        Case 0
            Exit While
        Case Is < 4
            Dim TmpByte As Byte()
            ReDim TmpByte(ReadCount - 1)
            Array.Copy(ReadBuffer, TmpByte, ReadCount)
            MsgLengthTmp.AddRange(TmpByte)
        Case 4
            MsgLength = BitConverter.ToInt32(ReadBuffer, 0)
            While True
                ' Read Message Body
            End While
        Case Else
            ' God!
    End Select
End While

眼尖的你有發現問題嗎?

當把這次讀到的保留下來後,讓程式做第二次讀取時,還是要讀 4 個位元組嗎?當然不是!所以就連在讀取訊息長度的階段 Read 方法,不是每次都讀 4 個位元組可以解決的。讓我們來修這個問題吧。所以就要改成下面的樣子:

Dim ReadBuffer As Byte()
ReDim ReadBuffer(3)
Dim MsgLengthTmp As List(Of Byte) = New List(Of Byte)
Dim TmpByte As Byte()

While True
    ' Read Length Step
    ReadCount = PipeServerIn.Read(ReadBuffer, 0, 4 - MsgLengthTmp.Count)
    If ReadCount = 0 Then
        Exit While
    End If
    ReDim TmpByte(ReadCount - 1)
    Array.Copy(ReadBuffer, TmpByte, ReadCount)
    MsgLengthTmp.AddRange(TmpByte)

    Select Case MsgLengthTmp.Count
        Case Is < 4
            ' Pass
        Case 4
            MsgLength = BitConverter.ToInt32(MsgLengthTmp.ToArray(), 0)
            MsgLengthTmp.Clear()
            While True
                ' Read Message Body
            End While
    End Select
End While

接下來要處理接受訊息本體,其考量與讀取訊息長度階段差不多。BUT,要多考量一個 buffer 長度的考量。因為訊息本體最長長度目前受到我們 4 個位元組的限制(Int32),只能有 256^4 / 2 = 2147483648 這麼長。一次讀取到的數量不一定是全部。所以分段接收還是要考慮進來。所以經過接收的部份如下:

MsgLength = BitConverter.ToInt32(MsgLengthTmp.ToArray(), 0)
MsgLengthTmp.Clear()
Dim ReadMsgBuffer As Byte()
Dim ReadMsgBufferSize As Integer = MsgLength
ReDim ReadMsgBuffer(ReadMsgBufferSize - 1)
Dim Offset As Integer = 0
While True
    ReadCount = PipeServerIn.Read(ReadMsgBuffer, Offset, MsgLength - Offset)
    UpdateTextBox2State(String.Format("Read {0} bytes", ReadCount))
    UpdateTextBox2State(BitConverter.ToString(ReadMsgBuffer, Offset, ReadCount))
    Offset += ReadCount
    If Offset = MsgLength Then
        UpdateTextBox2State("Recv a whole Msg")
        UpdateTextBox2State(System.Text.UTF8Encoding.UTF8.GetString(ReadMsgBuffer))
        Exit While
    End If
End While

目前這個程式是可以傳送文字,接收到之後更新在畫面上。

若是要傳送檔案,要怎麼修改。就留給大家吧。

image image

2013年9月23日 星期一

[named pipe] 訊息傳送的協定 - 使用分隔字元

在之前的例子中,在訊息傳送使用的是 StreamWriter 類別,訊息接收使用的是 StreamReader 類別。
接下來就是要討論幾種訊息傳送接收的方式。

在之前的例子中,使用了 WriteLine 方法傳送訊息,及 ReadLine 方法接收訊息。
這代表傳送端及接收端都「同意」使用「換行」這個字元當做一個訊息的結束。

在早期使用無線電通話機時,兩邊的人都必須要在說完話之後,加一個「over」,
讓對方知道自方說完一句話。使用 WriteLine、ReadLine 也是一樣的道理。

然後,這種方式,會有什麼問題呢?

首先,我們來看看在訊息中有換行字元會發生什麼事?

為了要看清楚接下來的範例,要改一下程式。
在 Server 端及 Client 端,接收到訊息後會將訊息更新到 TextBox2,負責處理的函式如下:

Sub UpdateTextBox2State(ByVal desc As String)
    If Me.InvokeRequired Then
        Me.Invoke(deleUpdateTextBox2State, New Object() {desc})
    Else
        TextBox2.Text += DateTime.Now.ToString() & desc & vbNewLine
    End If
End Sub

原本是「TextBox2.Text += desc & vbNewLine」,在 desc 前加上時間綽戳記。
如此每收到一個訊息,就會連同時間戳記一起寫下,就可觀察到不同次收到的訊息為何。

接下來,在 server 端的 TextBox2 裡,填入以下文字:

named pipe 測試1
分隔符號在訊息中

image

按下 Send 鈕之後,Client 端的畫面為:

image

原來預期一次傳送的訊息,被接收端認為是兩次訊息。

訊息斷裂後的兩次訊息,對於人類來說是還好,但對於程式來說,一定會造成無法辨識的結果。

所以收送的兩方一定要嚴守通訊的要求,才不會有意外發生。

但是,有些情況下,非得要傳送這種訊息中有換行字元的,例如一首詩。
那麼最常使用的方式,就是把訊息中的換行字元替換成其他的字元。
換成其他字元的方法,可以解決換行字元的問題,但會衍生出「若訊息中,有『其他字元』該怎麼辦?」的問題。畢竟很難保證,訊息中不會有你用來取代換行字元的字元。於是實務上會看到許多工程師,會用兩個以上的字元來替代訊息中的換行字元後傳送出去,在接收端再換回換行字元。
這樣就會把問題避開了嗎?不行!不管你用的替代字元或字串有多複雜,一個最簡單的例子就會突破了!那就是,若有人在訊息中問你,你的替代字串是什麼?你回應的時候,不就又被抓來當換行字元了嗎?不過,這種情況真的很少,只有工程師自己會玩這種把戲啦!一般使用者不會想來搞死工程師的。

還有另外一種方法,是把整個訊息編碼(encode),例如 base64,編碼完沒有換行字元,就可以用來傳送,接收端接到再解碼(decode)。兩種方式端看效率以及應用情況來選擇。有時,兩端的程式並非同一個人或同一公司撰寫,因此實務才會有許多解法出現,但不脫字元取代及訊息編碼(encode)這兩大類。

[python] unittest in IDLE

最近開始抄別人的 code 來學習。在 IDLE 試著要用 unittest 做測試,結果發生以下問題:

----------------------------------------------------------------------
Ran 1 test in 0.054s

OK

Traceback (most recent call last):
  File "D:\test.py", line 150, in <module>
    unittest.main()
  File "C:\Python27\lib\unittest\main.py", line 95, in __init__
    self.runTests()
  File "C:\Python27\lib\unittest\main.py", line 231, in runTests
    sys.exit(not self.result.wasSuccessful())
SystemExit: False

我找到網路上的解法,真的是可以用。

if __name__ == '__main__':
unittest.main(exit=False)

參考:http://stackoverflow.com/questions/2457068/using-idle-to-run-python-pyunit-unit-tests

2013年9月15日 星期日

[named pipe] .Net 的 named pipe,堪用的第一步。Client 端

經過 Server 端程式的字很多轟炸之後,再來看 Client 端程式會覺得很開心。不但 thread 變少,要考慮的狀況也少很多。復習一下,pipe name 與 pipe direction 的關係。

image

看圖可以比較清楚的知道,在 Client 端,<pipe name>_1 是 Out,屆時要連接的是 StreamWriter,而 <pipe name>_2 是 In,屆時要連接 StreamReader。正好跟 Server 端程式反過來。由此可以注意到,所謂的 In, Out 是對本身程式而言,而不是對 Pipe 的方向。同一個 pipe,對於 pipe 兩端的程式來說,in,out 就是剛好相反。所以為了自己也為了別人好,就不要把 pipe 取名 xxx_in 或 xxx_out。要是這樣取名字一定會害死人的。

接下來介紹與 server 端連接的函式。Client 端使用的是 NamedPipeClientStream 這個物件。因為 windows 的 pipe 是可以跨電腦的,所以 client 端的 pipe 在呼叫建構函式的時候,是要清楚告知是哪台電腦的 pipe。在這裡選擇三個參數的建構函式。第一個是 host name,輸入 "." 是告知使用本機。在實例化後,只要呼叫 Connect 即可與 server 端連接。在這裡一樣在處理 in 這個 pipe 需要用一個 thread 來呼叫 SubHandleRead。

Sub ConnectToServer()
    Dim pipename1 As String = TextBox1.Text & "_1"
    Dim pipename2 As String = TextBox1.Text & "_2"
    PipeClientIn = New NamedPipeClientStream(".", pipename2, PipeDirection.In)
    PipeClientOut = New NamedPipeClientStream(".", pipename1, PipeDirection.Out)

    Try
        UpdateConnectionState(ConnectionState.Waiting)
        PipeClientIn.Connect()
        PipeClientOut.Connect()
        ThreadIn = New Thread(AddressOf SubHandleRead)
        ThreadIn.Start()
        UpdateConnectionState(ConnectionState.Connected)
    Catch ex As Exception
        UpdateConnectionState(ConnectionState.Disconnected)
        PipeClientIn = Nothing
        PipeClientOut = Nothing
    End Try
End Sub

因為在 PipeClientIn 斷開之後,不需要自動重連,因此 SubHandleRead 非常簡單,斷開就離開。

Sub SubHandleRead()
    Dim RStream As StreamReader
    RStream = New StreamReader(PipeClientIn)
    While True
        Dim ReadStringLine As String = RStream.ReadLine
        If ReadStringLine Is Nothing Then
            Exit While
        End If
        UpdateTextBox2State(ReadStringLine)
    End While
    UpdateConnectionState(ConnectionState.Disconnected)
    CleanPipeClient()
End Sub

斷開的情境有兩個。一個是 Client 主動斷開,一個是 Server 主動斷開。

在 Server 主動斷開的情況下,SubHandleRead 裡 RStream.ReadLine 方法會傳回 Nothing,接下來離開迴圈。在更新完 GUI 的資訊後,最好把 PipeClientIn 及 PipeClientOut 都 Close 確保有釋放資源。SubHandleRead 離開後 ThreadIn 也就結束了。

在 Client 主動斷開的情況下,是由 PipeClientIn 執行 Close 方法,接下來 SubHandleRead 裡 RStream.ReadLine 方法會傳回 Nothing,然後離開迴圈。按照道理說,PipeClientIn 及 PipeClientOut 都執行過 Close 了,所以可不用執行 CleanPipeClient()。但因為執行也無妨。同時,離開了 SubHandleRead 後,ThreadIn 也停了,所以可放心不會有問題。

傳送資料的方式很簡單,只要在畫面下方的文字框,輸入文字,按下 Send 鈕就可以了。

image

Server 端程式跟 Client 端程式的寫法都一樣:

Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click
    Dim WStream As StreamWriter
    WStream = New StreamWriter(PipeServerOut)
    Try
        WStream.WriteLine(TextBox3.Text)
        TextBox3.Text = ""
        WStream.Flush()
    Catch ex As Exception
        CleanPipeServer()
        CleanThread()
        UpdateConnectionState(ConnectionState.Disconnected)
    End Try
End Sub

到此,基本的 Server 端程式與 Client 端程式的行為都已跟需求一樣。暫時休息一下。

 

參考:

http://msdn.microsoft.com/en-us/library/system.io.pipes.namedpipeserverstream.aspx

[named pipe] .Net 的 named pipe,堪用的第一步。Server 端

首先要先定義 pipe name 與 pipe in 及 out 的關係。

image

Server 與 Client 之間有兩條 pipe,所以會需要兩個名字,然而,為方便使用者輸入的簡化,當使用者輸入了<pipe name> 之後,實際建立的兩條 pipe 的名字是 <pipe name>_1 與 <pipe name>_2。假設使用者輸入的 pipe name 是 test,Server 端會建立 test_1 與 test_2 兩條 pipe 等待連接。

在這兩條 pipe 裡,Server 端以<pipe name>_1 為輸入端,<pipe name>_2 為輸出端。.Net 為我們準備好的 Server 端物件是 NamedPipeServerStream。設定 PipeServer 的方式僅需給予 pipe 的名字及方向。

當使用者輸入 pipe name 之後,按下 Open 鈕,會執行以下 sub:

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
    If TextBox1.Text <> "" Then
        IWantStop = False
        ServerSetup()
    End If
End Sub

Sub ServerSetup()
    Dim pipename1 As String = TextBox1.Text & "_1"
    Dim pipename2 As String = TextBox1.Text & "_2"
    PipeServerIn = New NamedPipeServerStream(pipename1, PipeDirection.In)
    PipeServerOut = New NamedPipeServerStream(pipename2, PipeDirection.Out)
    ThreadWaitConnectionIn = New Thread(AddressOf SubWaitConnectionIn)
    ThreadWaitConnectionOut = New Thread(AddressOf SubWaitConnectionOut)
    ThreadWaitConnectionIn.Start()
    ThreadWaitConnectionOut.Start()
    UpdateConnectionState(ConnectionState.Waiting)
End Sub

ServerSetup 前面 4 行就是在處理組合 pipe name 及設定 PipeServer。原本接下來應該是呼叫 WaitForConnection 等待連接,因為此呼叫會 block,所以另外用 thread 來做呼叫的處理。對於 PipeServerOut 的 WaitForConnection 比較單純,我僅使用一個 sub 來處理,在連接來時,若 PipeServerIn 也連接上時,才更新 GUI 上的狀況說明。

Sub SubWaitConnectionOut()
    PipeServerOut.WaitForConnection()
    If PipeServerIn.IsConnected Then
        UpdateConnectionState(ConnectionState.Connected)
    End If
End Sub

在 PipeServerOut 實例化後,執行以下動作,使用另一個 thread 來等待:

ThreadWaitConnectionOut = New Thread(AddressOf SubWaitConnectionOut)
ThreadWaitConnectionOut.Start()

在接收端 PipeServerIn 這個物件,在被連接後,要開始處理接收輸入的動作。然後,處理接收這件事,也要用一個 thread 來處理。於是 PipeServerIn 的 WaitForConnection 處理 sub 如下:

Sub SubWaitConnectionIn()
    PipeServerIn.WaitForConnection()
    ThreadIn = New Thread(AddressOf SubHandleRead)
    ThreadIn.Start()
    If PipeServerOut.IsConnected Then
        UpdateConnectionState(ConnectionState.Connected)
    End If
End Sub

在處理輸入這個 SubHandleRead,有一半是真的處理輸入,另一半是處理 PipeServer 重設的工作。不小心,又用了專用的 thread 來做事。因為 SubHandleRead 被 ThreadIn 所使用,在做重設定時 ThreadIn 必須先結束才能重設。我是不得已才這麼做才又多一個 thread 來呼叫 ReSetUp 的。

Sub SubHandleRead()
    Dim RStream As StreamReader
    RStream = New StreamReader(PipeServerIn)
    While True
        Dim ReadStringLine As String = RStream.ReadLine
        If ReadStringLine Is Nothing Then
            Exit While
        End If
        UpdateTextBox2State(ReadStringLine)
    End While
    UpdateConnectionState(ConnectionState.Disconnected)
    CleanPipeServer()
    If IWantStop = False Then
        If Not ThreadReSetup Is Nothing Then
            If ThreadReSetup.IsAlive Then
                ThreadReSetup.Abort()
            End If
        End If
        ThreadReSetup = New Thread(AddressOf ReSetup)
        ThreadReSetup.Start()
    End If
    CleanThread()
End Sub

Sub ReSetup()
    Thread.Sleep(500)
    ServerSetup()
End Sub

在處理輸入這一半,我使用了非常非常偷懶的 ReadLine 方法,在 StreamReader 的幫助下,只要讀到換行符號,就會從 ReadLine 方法得到資料,該資料不含換行符號。如果,Client 端的連接斷掉了,ReadLine 得到的是 Nothing。所以,得到 Nothing 的時候,就要離開處理迴圈。假如,Client 斷開的原因是故意的,那就不直接重建 Server。但,Client 斷開的原因是不小心的, IWantStop 會是 False,此時要開始執行重建 Server 的動作,回到等待連接的狀態。

而什麼時候是故意斷開的呢?就是按下 Close 鈕的時候:

Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
    IWantStop = True
    CleanPipeServer()
    CleanThread()
    UpdateConnectionState(ConnectionState.Disconnected)
End Sub

在清理 Thread 這件事,我用了很簡單的方法,就是用力地 Abort!為了保證每個 thread 一定都會執行 Abort,我用 Try Catch 把每行執行都包起來,就減少了一些 thread 物件是不是有實例化的檢查。

Sub CleanThread()
    Try
        ThreadWaitConnectionIn.Abort()
    Catch ex As Exception

    End Try
    Try
        ThreadWaitConnectionOut.Abort()
    Catch ex As Exception

    End Try
    Try
        ThreadIn.Abort()
    Catch ex As Exception

    End Try
    ThreadWaitConnectionIn = Nothing
    ThreadWaitConnectionOut = Nothing
    ThreadIn = Nothing
End Sub

然而在清理 PipeServerIn、PipeServerOut 這兩個物件時,比較複雜一點點。當有 Client 端連接上時,讓 PipeServerIn 直接呼叫 Close 方法,就可以斷開。雖然我很直覺地試過要先呼叫 Disconnect,結果就是 block 住,反而無法動彈。PipeServerOut 則需要呼叫 Disconnect,它可以保證所有寫出的字都被讀取之後才斷開。

另一個流程是,PipeServerIn、PipeServerOut 沒有連接上,也就是正在等待連接,而 Server 程式想要關掉,此時 WaitForConnection 卻無法取消!因為 WaitForConnection 的無法取消,導致 thread 的資源沒有釋放乾淨。為了要離開 WaitForConnection,網路上有人提出一個解法,就是自己連上馬上斷掉。而我也採用了這個解法。準備了 PipeClientIn、PipeClientOut 連接,然後 Close。

Sub CleanPipeServer()
    Try
        If PipeServerIn.IsConnected Then
            'PipeServerIn.Disconnect() ' no need
            PipeServerIn.Close()
        Else
            PipeServerIn.Close() 'It Needs to close first. Client_Close_First will need this line.
            Dim pipename1 As String = TextBox1.Text & "_1"
            Dim PipeClientOut = New NamedPipeClientStream(".", pipename1, PipeDirection.Out)
            PipeClientOut.Connect(500)
            PipeClientOut.Close()
            PipeClientOut = Nothing
        End If
    Catch ex As Exception

    End Try
    Try
        If PipeServerOut.IsConnected Then
            PipeServerOut.Disconnect()
            PipeServerOut.Close()
        Else
            Dim pipename2 As String = TextBox1.Text & "_2"
            Dim PipeClientIn = New NamedPipeClientStream(".", pipename2, PipeDirection.In)
            PipeClientIn.Connect(500)
            PipeClientIn.Close()
            PipeClientIn = Nothing
        End If
    Catch ex As Exception

    End Try
    PipeServerIn = Nothing
    PipeServerOut = Nothing
End Sub

到這裡,Server 端程式就說明完畢了。

[named pipe] .Net 的 named pipe,堪用的第一步。

NamedPipeServerStream 這個類別從 .Net 3.5 開始提供。使用者可以 io stream 的概念來操作資料的傳送。與它相對應的是 NamedPipeClientStream。由這兩個類別可以完成 named pipe 的範例。

在 msdn 的範例中,NamedPipeServerStream 是首先被啟用,以等待另一端來連接。程式宣告了一個 NamedPipeServerStream,名字叫 testpipe,pipe 的方向是 out。接下來便是等待對方來連接。這時,才是 NamedPipeClientStream 要來連接的時機。在 Client 連接之後,Server 只是寫一行字就準備關掉了。而 Client 也是讀一行字就準備關掉了。這樣的情境(Sceanrio),可以示範,但在實務上是不能用的。目前教育大概也是這樣吧,學校以為把東西教了,學生就可以用來工作了,學生也以為是這樣,公司也以為是這樣。結果,就整件事來說,這只是半套,另外半套得要有人教。但是全部的人都以為學生學了全套,到了公司發現不對,就怪學生或是學校,這情況就是亂象。雖然另外半套簡單到死,公司再不願意教的,就剩補習班要教了。雖然大家嫌補習班,證照班教的太基本、太無聊、太簡單。啊~套一句我最新的名言:「你不會的那一點,就是重點」。我離題了就此打住,希望有教學生的人可以幫幫學生們,不要被大題目嚇傻了,不要被大老闆嚇傻了。台灣的年輕人能力不輸人的。

接下來我這裡嘗試比較一般會使用的情境,會有幾個要求:

  1. Server 端要能等待連線,在 client 斷掉後回到等待下一次連線。
  2. Server 端要能傳送資料,也要能接收資料。Client 也是。
  3. 使用 GUI (也就是 windows form 程式)顯示接收到的訊息,以及輸入要傳送的訊息。

因為以上的要求,程式就變得複雜一點:

  1. 因為 .WaitForConnection 呼叫後會 block 住,等到 client 連接才回來,為避免 GUI 卡主,因此選擇用 thread 來處理。
  2. 同 1 的理由,不論 Server 或 Client 的 ReadLine 也會 block 住,也選擇用 thread 來處理。
  3. 為了第二點,Server 端及 Client 端中間,開兩個 pipe,一個 in,一個 out。

你會發現,不過多了三個要求,程式碼多了一大堆!例如,什麼叫做 client斷掉,怎麼判斷?MSDN 的範例中沒看到。收兩行以上的程式怎麼寫?MSDN 的範例中沒看到。多看別人寫的程式碼,尤其是 open source 的專案,是學寫程式的好範本!這讓我覺得以前的電腦書比較好,會一步步教,很仔細。但現在基礎的書大多跳掉細節只講觀念,反而又無法做出一個完整的東西。奇怪這一篇很常在抱怨哩…。

好,那先介紹 GUI 畫面,下次再看程式碼吧…。

image image

正常的操作情境(Scenario)是:

  1. Server 程式啟動,Label1 位置會是紅字底的文字 Disconnected image 說明程式是 Disconnected 狀態。
  2. 在 Server 畫面的 Open 右方的文字框,輸入 Pipe Name。
  3. 按下 Open 鈕後,Label1 位置會是黃字底的文字 Waiting image
  4. Client 程式啟動,Label1 位置會是紅字底的文字 Disconnected image
  5. 在 Client 畫面的 Open 右方的文字框,輸入 Pipe Name。
  6. 按下 Open 鈕後,兩隻程式的 Label1 位置會是綠字底的文字 Connected image
  7. 接下來就可以在畫面最下方的方塊輸入文字,按下 Send 鈕,另一方就會收到文字了。
  8. 按下 Client 畫面的 Close 鈕,Client 程式會變成 Disconnected 狀態。而 Server 程式會變成 Waiting 狀態。Client 程式關掉。
  9. 按下 Server 畫面的 Close 鈕,Server 程式會變成 Disconnected 狀態。Server 程式關掉。

2013年8月14日 星期三

[python] 使用 python 呼叫 console 執行檔,一個通用的 recipe

用 python 呼叫 console 執行檔有什麼稀奇呢?不就 import os, os.system 就好了嗎?
對,就這麼簡單。

import os
os.system('net')

但是要在程式中知道執行的成功或失敗,甚至得到執行檔的輸出,又該如何呢?

最近挖了一個 guiminer 的 python 專案的程式碼來看,學到一些東西。
該專案使用 wxpython 當做 gui。正好是 gui 程式與執行檔的整合範例。
這範例有以下特點:

1. 使用 subprocess 是目前 python 建議方式

subprocess.Popen(cmd, cwd=cwd,
  stdout=subprocess.PIPE,
  stderr=subprocess.STDOUT,
  universal_newlines=True,
  creationflags=flags,
  shell=(sys.platform != 'win32'))

2. 通常希望不要新開視窗,上述的 flags 可以控制視窗形式。其值是如下方式取得

try: import win32process

except ImportError: flags = 0

else: flags = win32process.CREATE_NO_WINDOW


3. 為了要監控程式的輸出,把 stdout 及 stderr 導至 PIPE 及 STDOUT (如 1.) 如此程式便需要不斷地讀取 stdout。


4. 使用 regular expression 針對輸出獲得自己想要的結果。


5. 也因為執行外部程式時需不斷地讀取 stdout,必定要另開 thread 來處理。使用 threading.Event() 來控制 run() 裡面的 while 迴圈何時離開。


觀察到以上 5 點,接下來便是改寫他的程式成為通用的 recipe。因為不想跟 gui 套件綁在一起,所以就把 gui 相關的拿掉,整理之後成為如下的 class。



import threading
import re
import subprocess
import sys

class CmdThreadListener(threading.Thread):
    '''
    Assign cmd and cwd
    '''

    LINES = []
    def __init__(self, cmd, cwd, callback=None, unhandle_callback=None):
        threading.Thread.__init__(self)
        self.shutdown_event = threading.Event()
        self.cmd = cmd
        self.cwd = cwd
        self.callback = callback
        self.unhandle_callback = unhandle_callback

    def run(self):
        try:
            import win32process
        except ImportError:
            flags = 0
        else:
            flags = win32process.CREATE_NO_WINDOW

        sub_proc = subprocess.Popen(self.cmd, cwd=self.cwd,
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.STDOUT,
                                      universal_newlines=True,
                                      creationflags=flags,
                                      shell=(sys.platform != 'win32'))

        self.proc = sub_proc
        while not self.shutdown_event.is_set():
            line = self.proc.stdout.readline().strip()
            if not line:
                continue
            if len(self.LINES) == 0:
                unhandleline = line
                if self.unhandle_callback is not None:
                    self.unhandle_callback(unhandleline)

            for s, event_func in self.LINES:
                match = re.search(s, line, flags=re.I)
                if match is not None:
                    event = event_func(match)
                    if event is not None:
                        if self.callback is not None:
                            self.callback(event)
                        break
                else:
                    unhandleline = line
                    if self.unhandle_callback is not None:
                        self.unhandle_callback(unhandleline)

    def stop(self):
        if self.proc is not None and self.proc.returncode is None:
            try:
                self.proc.terminate()
            except OSError:
                pass
        self.shutdown_event.set()

 


以下是它的使用方法:



    import time
    import os

    module_name = sys.executable if hasattr(sys, 'frozen') else __file__
    abs_path = os.path.abspath(module_name)
    cmd = 'net'
    cwd = os.path.dirname(abs_path)

    print cmd

    cmd = cmd.encode('cp950') # quick modify for windows console

    def p(x):
        print(x)

    listener_instance = CmdThreadListener(cmd, cwd, unhandle_callback=p)
    listener_instance.daemon = True
    listener_instance.start()

    time.sleep(5)

    listener_instance.stop()
    listener_instance.join()


它的輸出結果如下:



net
這個命令的語法是:
NET
[ ACCOUNTS | COMPUTER | CONFIG | CONTINUE | FILE | GROUP | HELP |
HELPMSG | LOCALGROUP | PAUSE | SESSION | SHARE | START |
STATISTICS | STOP | TIME | USE | USER | VIEW ]


在 windows 下,Popen 的 shell=False 時,cmd 不能夠是 shell 裡面的指令。所以 dir 之類的就不行用。若是想執行 shell 裡面的指令,Popen 的 shell 參數要設成 True。


註:若是照我的測試的程式碼放到 IDLE 直接執行,可能會出現 NameError: global name '__file__' is not defined。那是 IDLE 忘了 assign __file__ 的 bug。請到 cmd 底下執行 python 程式。

2013年8月12日 星期一

[sozi] Inkscape 的簡報外掛:使用心得

Sozi 的展示概念,可以想成有一張大海報掛在牆上,而我們是手拿攝影機的導演。導演選擇順序與角度,帶領觀眾跟著導演的鏡頭,觀賞海報的內容。

 

0opkSDo 
來源:http://prometheusis.blogspot.tw/2013/08/blog-post.html

 

因此,製作 sozi 簡報的時候,內容被放置的位置很重要,一但放置之後就不能動了。因為移動的是鏡頭,按順序移動時相近的主題放在附近,不會有轉場時看見雜物,也不會有轉場過久或過快的不適感。

如果,內容是用向量物件製作,那麼製作時不需在乎解析度。在鏡頭的 zoom in 或 zoom out 下,向量物件會保持很好的解析度。除非物件又多又移動快。尤其字體這物件是最吃運算,大家都建議把字體轉成 path 來提升效能。同時,當字體轉成路徑也比較平滑一點。

 

使用字型

 

使用路徑

 

在製作手順上,只要用長方形物件框住你要的東西,那個長方形,就是鏡頭的視角,在 sozi 裡叫做 frame。在製作時,最好讓每個 frame 長寬比都一致,觀眾會比較舒服,除非有特殊的目的。例如製造窄視角的狹隘感。直接使用你製作的物件當做 frame 也是可以,但是會有兩點不方便,一個是長寬比的掌握,另一個缺點是 sozi 預設 frame 是被隱藏的,這意味著每次新增 frame 都要記得把 hide 的勾勾拿掉,多了一個動作我不愛。另外,可以把所有的 frame 畫在一個新增的 layer 上,將會變得很好管理。

image

自動轉場可以做出動態影片的感覺。因為物件不能動,而是鏡頭動,在場景設計上要針對這特性做考量。一般 power point 物件飛來飛去的特效就難以做到,我想到的方法是 frame by frame 一張張製作,像是製作停格動畫,這太花工夫了。所以擺好場景最重要!

以下是個非常漂亮的展示

http://www.youtube.com/watch?v=pGfexUCiCEk

2013年8月11日 星期日

[python]處理給 shell 的參數

對我個人來說也困擾很久的問題。
我自己處理在傳參數給 os.system 的時候,若是有空隔或單引號的時候,
會特別用別的字取代,但如此也就把參數值也改掉了。
只有我自己用的程式還好,但是別人就是要用空隔跟單引號怎辦呢?

今天很巧的在 pycontw 2013 的影片裡,看到有人用以下解法。
馬上抄下來,以後慢慢用。
真是感恩。

def escapeShellArg(string):
  return "'"+string.replace("'", "'\\''")+"'"

args = ' '.join(map(escapeShellArg, argv))
os.system('ffmpeg -i '+args)

來源:

http://www.youtube.com/watch?v=RszFch-fjJc 15:50

pycontw 2013 郭學聰

2013年7月24日 星期三

[sozi] Inkscape 的簡報外掛:sozi - Windows 7 安裝及注意事項

不小心誤入進入貴哥的 sozi 介紹文[3]

一試很開心。發現中文介紹大多是 linux,於是我分享一下我在 windows 7 下的使用心得。

 

安裝

在官方介紹裡[1],目前只支援 32-bit,所以!

  1. 請安裝 inkscape 32-bit[6] 版本。在 windows 下,會被安裝在 C:\Program Files (x86)\Inkscape\
  2. 為了要更換 inkscape 的 python 版本到 python 2.7,請先安裝 python 2.7 for windows 32-bit[7] 到系統中。預設是安裝到 C:\Python27
  3. 安裝 LXML for Python 2.7 and Windows 32-bit[8] http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml
  4. 安裝 PyGTK 2.24 的 all-in-one 安裝檔 for Python 2.7 and Windows 32-bit[10]
  5. 把 C:\Python27 複製到 C:\Program Files (x86)\Inkscape
  6. 把 C:\Program Files (x86)\Inkscape\Python 改名為 C:\Program Files (x86)\Inkscape\Python26
  7. 把 C:\Program Files (x86)\Inkscape\Python27 改名為 C:\Program Files (x86)\Inkscape\Python
  8. 把 sozi-release-[...].zip[11] 解開,放到 C:\Program Files (x86)\Inkscape\share\extensions

搞定。啟動或重開 inkscape 就可以在 「擴充功能」 裡看到 sozi 了。

 

意外

如果發現有字型問題怎辦?

請到 C:\Program Files (x86)\Inkscape\Python\Lib\site-packages\gtk-2.0\runtime\etc\gtk-2.0

找到 gtkrc,加入以下文字:

style "user-font"
{
    font_name="mingliu 11"
}
widget_class "*" style "user-font"

font_name 可改成你的系統中,找得到的字型的「英文名字」。
(所以寫 細明體 會沒有用)
後面的 11 是預設點數。請隨意。

 

重要

sozi 的編輯畫面不能讓我改 title 怎辦?

其實,不只是 title 不能改,有許多文字輸入框,數字輸入框,遊標根本進不去。
image

只能按左下角的 New,然後按確定。難道只能這樣使用 sozi,沒有辦法了嗎?

不是!這時只要換個視窗,再換回 sozi 視窗,就會變得可以輸入了。
舉例:在上圖中,滑鼠先點到 Timeout (seconds) 的 1.00 的框框裡。此時看不到遊標。然後,按下工作列的顯示桌面,再按工作列的 Sozi 視窗。你會發現遊標出來了。

image

然後整個畫面就都可以正常輸入。

 

注意

sozi 的官網有提到,因為 Inkscape 的 plugin 系統的設計關係,以下的畫面千萬不要去按取消。

image

我是沒按過取消會怎樣。人家都說不要按了。

 

參考:

1. http://sozi.baierouge.fr/wiki/en:install#installation_on_windows

2. http://people.ofset.org/~ckhung/b/svg/sozi.php

3. http://newtoypia.blogspot.tw/2012/11/sozi.html

4. 貴哥的簡報 http://people.ofset.org/~ckhung/mm/

5. 貴哥的進階技巧 http://user.frdm.info/ckhung/b/svg/sozi.php

6. Inkscape download http://downloads.sourceforge.net/inkscape/inkscape-0.48.4-1-win32.exe

7. python 2.7 for windows 32-bit http://python.org/ftp/python/2.7.5/python-2.7.5.msi

8. lxml-3.2.1.win32-py2.7.exe http://www.lfd.uci.edu/~gohlke/pythonlibs/bm67sm2y/lxml-3.2.1.win32-py2.7.exe

9. lxml http://www.lfd.uci.edu/~gohlke/pythonlibs/#lxml

10. pygtk-all-in-one-2.24.0.win32-py2.7.msi http://ftp.gnome.org/pub/GNOME/binaries/win32/pygtk/2.24/pygtk-all-in-one-2.24.1.win32-py2.7.msi

11. sozi-release-13.05-21064303.zip http://sozi.baierouge.fr/wiki/_media/dl:sozi-release-13.05-21064303.zip

2013年7月18日 星期四

[git][dulwich]在 windows 才會有的權限認定問題

前一陣子試著使用 dulwich 來操作 .git 裡面的資料,例如上傳到 changeset 到 remote、下載 changeset 到 local。看起來都很正常。但是,把檔案從 .git 裡面拿出來放,就會判定某些檔案被改變,我去檢查內容,自己使用 sha,md5 對兩個檔案檢查,結果顯示兩個檔案內容完全一樣。我想,dulwich 這麼多人用,難道我又遇到「沒人會遇到」的問題嗎?

另外我使用 tortoisegit 來檢查其內容,一樣也是說相同,但是用 dulwich 拉回來的 .git,跟 tortoisegit 拉回來的 .git 對於相同檔案,卻是一個說相同,一個說有改變。這可搞死我了,難道要放棄使用 dulwich 嗎?

後來翻到一個舊的 tortoisegit 的 issue:https://code.google.com/p/tortoisegit/issues/detail?id=812

Allow to change git file permission mask / attributes (especially executable bit)

才知道,dulwich 可能也是中同一個招數。git 是有檢查 Read, Write, Execute vs User, Group, Other 的。於是就可以解釋,為何只要是 script 或是 linux 的執行檔才會出現內容相同,但被判定有改變。在 windows 的檔案權限是 666,跟原來的不一樣了,也沒辦法改成 755。不曉得 tortoisegit 怎麼解決這問題的…。我這裡暫時停止研究了。

[git]學習 TortoiseGit - revert

原本有在用 hg,還以為會很順利地就會使用 git。

結果,等到真正開始管專案,第一個遇到的就是,revert 呢?

因為原本很習慣地,只要有臨時改過某個檔,要回到原狀,就很簡單地「指」著那個檔按右鍵,選revert

image

在下圖按下 revert 就解決了。

image 

但是,在 tortoisegit 我眼花啦~~~~~!!

我找了兩天啊!可說是上天下海用 google 去找,怎麼沒有人會有這種問題呢?

大概沒有人跟我一樣眼睛被蚵仔擋到,就在這兒啊,怎麼會有人要問這種問題呢?

image

其實還真有…

http://stackoverflow.com/questions/8011538/git-pulling-only-certain-files

2013年7月16日 星期二

[javascript]javascript如何列舉dictionary所有的key?

繼上次對於 dictionary 的操作,還少了一件事。那就是列舉所有的 key 值。

這件事沒法子由 instance 的 method 來找,於是使用 Object.keys() 這個功能來處理。

var a = {'key1':'a','key2':'b','key3':'c'};

var b = Object.keys(a)

console.log(b)

//會得到 ["key1", "key2", "key3"]

參考:

http://stackoverflow.com/questions/10654992/how-to-get-collection-of-keys-in-javascript-dictionary

2013年7月10日 星期三

[webapi]微軟 ASP.NET Web API Self Host 的應用(2)

(本篇程式是用 vb.net 寫成)

前一篇留下一個問題,若是要提供服務 .html, .js, .css, .jpg, .png, .gif ... 等等,我想到的辦法是自行撰寫程式來處理。在參考了[1] 的 route 用法,我在 config.Routes.MapHttpRoute 多加 4 個 route。img、css、js 分別提供 圖檔、css、js 三大類檔案的服務。而 default 則是提供第一個頁面的服務。

config.Routes.MapHttpRoute("img", "img/{name}", New With {.controller = "Img", .action = "g"})
config.Routes.MapHttpRoute("css", "css/{name}", New With {.controller = "Css", .action = "g"})
config.Routes.MapHttpRoute("js", "js/{name}", New With {.controller = "Js", .action = "g"})
config.Routes.MapHttpRoute("API", "{controller}/{action}/{id}", New With {.id = RouteParameter.Optional}) ' vs2010 just can not IntelliSense....
config.Routes.MapHttpRoute("default", "", New With {.controller = "Home", .action = "g"})

接下來則是加入 ImgController, CssController, JsController, HomeController 這四個 class

Public Class ImgController
    Inherits ApiController
    Dim accept_type As Dictionary(Of String, String) = New Dictionary(Of String, String) From {{".jpg", "jpg"}, {".png", "png"}, {".gif", "gif"}}
    <HttpGet()> Public Function g(ByVal name As String) As HttpResponseMessage
        Dim content As New StreamContent(New System.IO.FileStream(Request.RequestUri.LocalPath.Substring(1), IO.FileMode.Open))
        Dim ext As String = System.IO.Path.GetExtension(Request.RequestUri.LocalPath).ToLower()
        If accept_type.ContainsKey(ext) Then
            content.Headers.ContentType = New MediaTypeHeaderValue("image/" & ext)
        End If
        Dim response = New HttpResponseMessage() With {.Content = content}
        'response.Headers.CacheControl = New CacheControlHeaderValue() With {.MaxAge = New TimeSpan(1, 0, 0)}
        Return response
    End Function
End Class

Public Class CssController
    Inherits ApiController
    <HttpGet()> Public Function g(ByVal name As String) As HttpResponseMessage
        Dim content As New StreamContent(New System.IO.FileStream(Request.RequestUri.LocalPath.Substring(1), IO.FileMode.Open))
        content.Headers.ContentType = New MediaTypeHeaderValue("text/css")
        Dim response = New HttpResponseMessage() With {.Content = content}
        'response.Headers.CacheControl = New CacheControlHeaderValue() With {.MaxAge = New TimeSpan(1, 0, 0)}
        Return response
    End Function
End Class

Public Class JsController
    Inherits ApiController
    <HttpGet()> Public Function g(ByVal name As String) As HttpResponseMessage
        Dim content As New StreamContent(New System.IO.FileStream(Request.RequestUri.LocalPath.Substring(1), IO.FileMode.Open))
        content.Headers.ContentType = New MediaTypeHeaderValue("text/javascript")
        Dim response = New HttpResponseMessage() With {.Content = content}
        'response.Headers.CacheControl = New CacheControlHeaderValue() With {.MaxAge = New TimeSpan(1, 0, 0)}
        Return response
    End Function
End Class

Public Class HomeController
    Inherits ApiController
    <HttpGet()> Public Function g() As HttpResponseMessage

        Dim content As New StreamContent(New System.IO.FileStream("html/first.html", IO.FileMode.Open))
        content.Headers.ContentType = New MediaTypeHeaderValue("text/html")
        Dim response As New HttpResponseMessage() With {.Content = content}
        Return response
    End Function
End Class

 

接下來,就是把測試用的 first.html, .css, .js, .png 放到 html, css, js, img 目錄去。

以下是 first.html

<!doctype html>
<html>
<head></head>
<title>web browser</title>
<body>
<link href="css/site.css" rel="stylesheet" />
<script src="js/jquery.js" type="text/javascript" ></script>

<script type="text/javascript">
$(document).ready(function(){
//alert("jquery alert works")
});
</script>
<p>
this is html test
</p>
<p>
<input type="text" id="txtboxTest" />
</p>
<p>
<button id="btnTest">click me</button>
</p>
<script type="text/javascript"> $(document).ready(function(){
$('#btnTest').click(function(){
$('#txtboxTest').val('jquery click works!');
})
}); </script>
<p>
<img class="imgstyle" src="img/glyphicons-halflings.png" />
</p>
</body>
</html>

以下是 site.css

body { background-color: #0000ff }
.imgstyle {border:2px solid #00ffff;background-color: #ffffff}

而 jquery 及 png 圖檔還請自行準備。執行後,以 IE 來看。

image

下圖是用 chrome 來開。

image

這樣的程度,應該可以處理小小應用程式的需求,使用 SPA (Single Page Application) 的觀念來做,也就不需要多個 html 檔。若有其他需要,只需新增 route 及 controller 即可。

但是,這樣的程式,拿給我爸這種老人家,要先點一個執行檔,再開瀏覽器,然後輸入網址,他一定會嫌。在黑大的文章[2]底下有個留言說:

# GATTACA said on 04 June, 2013 10:44 PM
SignalR也有SelfHost可用啊,

ip過濾也可以只限自己127.0.0.1連線,

我現在所有單機版的操作介面都只剩下一個填滿的WebBrowser,啟動後再去讀Html檔案進來;

美工依照原本的WinForm排版Html順便美化,

程式設計師再去填SignalR JavaScript Client與Knockout ViewModel,

如果有用到COM元件,其callback事件處理後透過SignalR更新到Html上,也沒有背景執行緒更新主執行緒WinForm畫面的問題;

雖然是較極端的做法,但是可以擺脫微軟悲情的UI更迭(MFC->WinForm->WPF...),

避免程式設計師將業務邏輯綁死到UI元件上,

Html美工又很好找,

可說是利大於弊啊

對啊,這樣就會跟 app.js 一樣,看起來像是原生桌面程式有什麼不好的呢?

原來的程式執行起來長這樣,看來就想把它關掉。

image

現在,請在 Form1 加上一個 WebBrowser。然後在 httpserver.OpenAsync.Wait() 這一句的後面,加上

WebBrowser1.Navigate(http://localhost:32767)

在程式裡調好 Form1 寬度及寬度。接著在 WebBrowser1_DocumentTitleChanged 加上

Private Sub WebBrowser1_DocumentTitleChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles WebBrowser1.DocumentTitleChanged
    Me.Text = WebBrowser1.DocumentTitle
End Sub

執行程式就得到:

image

嗯,我現在感覺滿意耶。

我猜想這概念已經存在這麼久,.NET 的社群裡也許已經有類似的程式。

我這隻程式應該只是個幼稚園。

如果想要程式的話,我最近會整理放上 github 讓大家下載來用。

更新,程式在此:https://github.com/rickyteng/washapp

現在要說有什麼缺點的話,應該就是想把 webbrowser 換成 chrome 或 firefox 的引擎,html5 支援比較高。還有用 vb.net 寫 web api 的 route 及 對應的 controller,好像不如 c# 的簡易,c# 可以用 Get 當 function 名字,看起來若不特別指定,action 就是呼叫 Get(),vb.net 的我找不到對應的寫法,所以寫起來有點醜醜的。

終於用三天把兩個月來研究的「用 web 技術寫桌面程式」,的主題弄出一個段落。雖然,其實是 windows 桌面,因為使用者都在 windows 上,我技術上最後也跨不出 windows 平台。接下來就該寫出些有用的東西吧?

 

 

參考:

1. https://github.com/darrelmiller/HypermediaApiSite

2. http://blog2.darkthread.net/post-2013-06-04-self-host-web-api.aspx