C/C++ call by value傳值, call by pointer傳址, call by reference傳參考 的差別

這篇主要想講參數傳遞的種類,分別有 call by value 傳值, call by pointer 傳址, call by reference 傳參考
這三大種類,也可以叫做 pass by value、pass by address、pass by reference。這幾種種類一定要搞清楚,是寫程式常見的狀況也是常見考題。

主要是 call by pointer 和 call by reference 的本質上有沒有差異, 差在寫法上的差異與使用上方便性。

傳值 call by value

這種參數傳遞方式是最簡單的方式,就是把每個參數都複製一份到函式裡運算,

1
2
3
int add(int x, int y) {
return x + y;
}

但上述這種方式在某些情形變得不適用,例如要修改傳遞進來的參數,或者不想複製一份參數浪費效能,
這時就會採用傳址 call by pointer 或傳參考 call by reference,也就是下面會介紹的內容。

傳址 call by pointer

Call by pointer 又稱 Call by address,但為了跟後面的方式作區分,用 Call by pointer 這名子比較明確,
這邊以最簡單的swap作為範例,
其中在傳址時相當於 int *x = &a; int *y = &b; 這樣的寫法

1
2
3
4
5
6
7
8
9
void swap(int *x, int *y) {
int tmp = *x;
*x = *y;
*y = tmp;
}

int a = 3;
int b = 5;
swap(&a, &b);

傳參考 call by reference

這邊也以最簡單的swap作為範例,可以跟上個範例作比較看看寫法上的差異,
其中在傳參考時相當於 int &x = a; int &y = b; 這樣的寫法,
這樣的好處在於之後在函式裡的變數寫法如同一般的變數寫法,

1
2
3
4
5
6
7
8
9
void swap(int &x, int &y) {
int tmp = x;
x = y;
y = tmp;
}

int a = 3;
int b = 5;
swap(a, b);

其他參考
[1] 什麼是傳值call by value、傳址call by address、傳參考call by reference
http://wp.mlab.tw/?p=176
[2]【教學】call by value, call by address, call by reference 差別在哪?
https://wayne265265.pixnet.net/blog/post/112556555
[3]C/C++之指標 (pointer),參考 (reference) 觀念整理與常見問題
https://dotblogs.com.tw/brian/2012/10/18/77588

其它相關文章推薦
C/C++ 新手入門教學懶人包
C/C++ 函式回傳參考用法 function return by reference
C++ std::sort 排序用法與範例完整介紹
std::queue 用法與範例
std::thread 用法與範例
C++ virtual 的兩種用法
C/C++ 判斷檔案是否存在
C++ 設計模式 - 單例模式 Singleton Pattern

Android systrace 基本用法教學

本篇介紹如何在 Android 開發使用 systrace 指令,Android 開發工具包內提供一個 systrace.py 供開發者使用,systrace 是開發 Android 時常用到的性能分析工具,使用 systrace 工具可對 android 程式進行錄製 trace 資料並轉換成可視覺化的 html 格式,以便後續分析,以下內容為如何安裝 Android systrace 與 systrace 基本使用教學。

Google 官方的 SDK Platform Tools release notes 有各版本的釋出說明。

Windows 安裝方式

從官方下載 Windows 最新版
目前最新版為 platform-tools_r34.0.5-windows.zip

Mac 安裝方式

從官方下載 Mac 最新版
目前最新版為 platform-tools_r34.0.5-darwin.zip

Ubuntu 安裝方式

從官方下載 Linux 最新版
目前最新版為 platform-tools_r34.0.5-linux.zip

下載好後 systrace.py 的路徑位於 android-sdk/platform-tools/systrace 目錄下,
systrace.py 是用 python2 寫成的,目前還不支援 Python3,所以在執行上要先有 Python 2 的運行環境。

systrace 指令基本教學

安裝好 systrace 後將 Android 裝置插上電腦的 USB 孔,執行要分析的 App 或程式後,再輸入 systrace 指令開始錄製,
systrace 指令的語法如下,

1
python systrace.py [options] [categories]

最基本最簡單的範例如下,什麼參數也沒帶的話 systrace 程式會需要你按下任何按鍵才會停止錄製,
預設輸出檔名為 trace.html。

1
$ python systrace

指定輸出檔名為 systrace.html

1
$ python systrace -o systrace.html

指定錄製 7 秒,但是如果提前使用完預設的 buffer 也會提前結束錄製

1
$ python systrace -t 7

指定錄製 7 秒,並加大 buffer,我自己是常用 1024000KB,官方文件的範例是 96000KB,

1
$ python systrace -t 7 -b 2048000

只想關注 CPU 排程的話,可以像下面這樣下,CPU 排程(sched)算是最常用到且實用的功能了,要看 CPU Frequency 的話可以用 freq,
預設不下的話會錄製出很多類別是自己不想關注的,二來錄製出來的東西會顯示很多同時檔案也較肥大,
指定自己想關注的類別的話,這樣其他類別的就不會錄製到,將能儲存更多的資料,檔案也會小很多,算是個小技巧,

1
$ python systrace -t 7 -b 96000 sched freq

要看還支援其它什麼類別的話可以用下列指令查看。

1
2
3
$ python systrace -l

$ python systrace.py --list-categories

以下列出 category 與說明,
gfx: Graphics
input: Input
view: View System
webview: WebView
wm: Window Manager
am: Activity Manager
sm: Sync Manager
audio: Audio
video: Video
camera: Camera
hal: Hardware Modules
res: Resource Loading
dalvik: Dalvik VM
rs: RenderScript
bionic: Bionic C Library
power: Power Management
pm: Package Manager
ss: System Server
database: Database
network: Network
adb: ADB
vibrator: Vibrator
aidl: AIDL calls
nnapi: NNAPI
rro: Runtime Resource Overlay
core_services: Core services
pdx: PDX services
sched: CPU Scheduling
freq: CPU Frequency
idle: CPU Idle
disk: Disk I/O
sync: Synchronization
memreclaim: Kernel Memory Reclaim
binder_driver: Binder Kernel driver
binder_lock: Binder global lock trace
memory: Memory
gfx: Graphics (HAL)
ion: ION allocation (HAL)

systrace 指令選項

以下為常用的 systrace 指令,有想到在陸續增加吧!

-o: 設定輸出檔名(預設為trace.html)
-t: 秒數
-b: 設定緩衝區大小KB

chrome 開啟 systrace 的 HTML 常用操作

錄製完的 systrace.html 一定要用 chrome 瀏覽器才開得起來,
開啟 systrace.html 的方式有兩種,一種是直接對該 html 右鍵選擇用 chrome 開啟,或者預設瀏覽器就是 chrome 就雙擊開啟,
另一種是在 chrome 瀏覽器網址列輸入 chrome://tracing/ 再按 load 按鈕開啟該 html,
w/s:放大 zoom in、縮小 zoom out
a/d:向左捲動、向右捲動
m:高亮顯示當前所選的區塊
f:放大當前所選取的區塊
0:恢復縮放與平移,恢復到預設大小
1:鼠標模式
2
3
4:範圍選取模式
E:時間軸置於當前滑鼠游標位置的中心
G:從當前選取區塊的開始處顯示網格,網格間距為16.6ms,再按一次則取消顯示
Shift + G:從當前選取區塊的結束處顯示網格,網格間距為16.6ms,再按一次則取消顯示
<左方向鍵>:跳至上一個的相同選取區塊
<右方向鍵>:跳至下一個的相同選取區塊
/:搜尋字串
Enter:下一個搜尋結果
?:顯示幫助

systrace 名詞解釋

以下為常見 systrace 的資訊以及解釋,通常框選多個 slice 時會顯示更多統計數據,
Wall Duration:持續時間
CPU Duration:cpu耗時
Self Time:自身方法耗時(不包含其呼叫方法)
CPU Self Time:自身方法cpu執行時間
Average CPU Duration:平均cpu耗時
Occurrence:發生次數

我的使用心得

systrace 的操作就介紹到這邊,systrace 的操作並不難,
實際上從 systrace 中分析出有用的資訊,並改善系統效能才是最精髓最難的地方,同時這也是需要多方面實務經驗的累積。
這部份經驗及技巧未來有機會再來談談。

其他參考

了解 Systrace Android 開源項目 - Android Open Source Project
https://source.android.com/devices/tech/debug/systrace
Capture a system trace on the command line | Android Developers
https://developer.android.com/topic/performance/tracing/command-line
手把手教你使用Systrace(一)
https://zhuanlan.zhihu.com/p/27331842
[Android] 效能工具Systrace的使用- IT閱讀
https://www.itread01.com/content/1545024620.html
Android Systrace 使用详解
https://www.jianshu.com/p/75aa88d1b575
Android Systrace使用介绍
https://www.jianshu.com/p/f83d84dcd0b8
os-android:性能分析工具(systrace,starce,top,iotop)_智能多媒体-CSDN博客
https://blog.csdn.net/shareviews/article/details/84749549

開發用的文章參考

Define custom events | Android Developers
https://developer.android.com/topic/performance/tracing/custom-events
說明在 Managed code (Java/Kotlin) 怎麼使用 beginSection 與 endSection
說明在 Native code (C/C++) 怎麼使用 ATrace_beginSection 與 ATrace_endSection
說明在 Native code (C/C++) 怎麼使用 ATRACE_NAME (aka ScopedTrace)

systrace/trace.h at master · ganadist/systrace · GitHub
https://github.com/ganadist/systrace/blob/master/libs/c/trace.h
C 版本的 TRACE_BEGIN 和 TRACE_END
systrace/systrace.py at master · ganadist/systrace · GitHub
https://github.com/ganadist/systrace/blob/master/libs/python/systrace.py
pyhton 版本的 traceBegin 和 traceEnd

其他技巧推薦

如果你是常常在Android adb shell下做事情的話,尤其是需要使用到vi,建議安裝busybox,使用busybox附帶的vi會方便很多,
如果還想知道busybox支援哪些指令或busybox基本用法的話請看這篇
其他的 Android 系列文章可以看這篇
下一篇來介紹Android fastboot指令的安裝與用法教學吧~

Linux convert 圖片格式轉換工具用法與範例

本篇將介紹如何使用 Linux 下的 convert 轉換圖片格式,例如:轉jpeg、轉png、轉彩色、轉灰階等等功能。

Linux convert 指令說明如下,

1
$ convert [輸入選項] <輸入檔名> [輸出選項] <輸出檔名>

png 轉換為 jpg 格式

png 轉換為 jpg 格式,轉jpeg也是一樣的方式

1
$ convert in.png out.jpg

jpg 轉換為 png 格式

jpg 轉換為 png 格式,轉jpeg也是一樣的方式

1
$ convert in.jpg out.png

yuv 灰階轉 png 格式

1
$ convert -depth 8 interlace plane -size 640x480 gray:in.yuv out.png

yuv 灰階轉 jpg 格式

1
$ convert -depth 8 interlace plane -size 640x480 gray:in.yuv out.jpg

yuv 彩色轉 png 格式

1
$ convert -size 640x480 gray:in.yuv out.png

yuv 彩色轉 jpg 格式

1
$ convert -size 640x480 gray:in.yuv out.jpg

常用選項

-size mxn: 設定輸入影像大小
-depth 8: 灰階, 單通道
-interlace <type>: 像素排列方式
Ex:none(RGBRGBRGB…), line(RRR…GGG…BBB…RRR…GGG…BBB…), plane(RRRRRR…GGGGGG…BBBBBB…)
-quality <value>:JPEG/PNG compression level

其它相關文章推薦
Linux 常用指令教學懶人包
Linux sed 字串取代用法與範例
Linux find 尋找檔案/尋找資料夾用法與範例
Linux cut 字串處理用法與範例
Linux tail 持續監看檔案輸出用法與範例
Linux grep/ack/ag 搜尋字串用法與範例
Linux tee 同時螢幕標準輸出和輸出到檔案用法與範例
Linux xargs 參數列表轉換用法與範例
Linux du 查詢硬碟剩餘空間/資料夾容量用法與範例
Linux wget 下載檔案用法與範例

C/C++ 函式回傳參考用法 function return by reference

這篇主要想講 C++ 函式回傳參考 function return by reference 用法與用途。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Point {
public:
Point(int _x, int _y) : x(_x), y(_y) {
}

int& x() {
return x;
}

int& y() {
return y;
}

private:
int x = 0;
int y = 0;
};

用法1. 回傳參考(左值參考 lvalue reference)

point回傳的x或y是回傳參考型別,所以可以直接對它進行修改。

1
2
point.x() = 10;
point.y() = 30;

用法2

通常使用這種寫法時,是因為回傳物件的資訊很大,改用回傳參考(reference)會比回傳複本(copy)更有效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Window {
Point& getPosition() { // return reference
return position;
}
private:
Point position;
}

int main() {
Window w;
Point& pos = w.getPosition();
//Point pos = w.getPosition(); ?? 這樣可以編譯的過?如果可以,那是複本還是參考?

return 0;
}

用法3

C++14 基本上已經支援自動推導型別,使用auto的話編譯器就會自動幫你推導要回傳的類型。

1
2
3
4
5
6
7
8
9
10
auto& getPosition() {
return position;
}

int main() {
Window w;
auto& pos = w.getPosition();

return 0;
}

常見問題

不行回傳區域變數

1
2
3
4
Point& getPosition() {
Point (3, 5);
return p; // compile error
}

驗證回傳值參考

這邊做個實驗看看回傳參考的那份,是不是就是本來的那一份
第一個範例
https://www.geeksforgeeks.org/return-by-reference-in-c-with-examples/

參考
Return by reference in C++ with Examples - GeeksforGeeks
https://www.geeksforgeeks.org/return-by-reference-in-c-with-examples/
Returning values by reference in C++ - Tutorialspoint
https://www.tutorialspoint.com/cplusplus/returning_values_by_reference.htm
參考類型函式傳回 | Microsoft Docs
https://docs.microsoft.com/zh-tw/cpp/cpp/reference-type-function-returns?view=vs-2019
傳回值型態
https://openhome.cc/Gossip/CppGossip/returnBy.html
Is the practice of returning a C++ reference variable evil? - Stack Overflow
https://stackoverflow.com/questions/752658/is-the-practice-of-returning-a-c-reference-variable-evil
How to return a class object by reference in C++? - Stack Overflow
https://stackoverflow.com/questions/8914509/how-to-return-a-class-object-by-reference-in-c
reference - How to “return an object” in C++? - Stack Overflow
https://stackoverflow.com/questions/3350385/how-to-return-an-object-in-c

其它相關文章推薦
C/C++ 新手入門教學懶人包
C/C++ call by value傳值, call by pointer傳址, call by reference傳參考 的差別
C++ std::sort 排序用法與範例完整介紹
std::queue 用法與範例
std::thread 用法與範例
C++ virtual 的兩種用法
C/C++ 判斷檔案是否存在
C++ 設計模式 - 單例模式 Singleton Pattern

Python tuple 用法與範例

本篇 ShengYu 要介紹 python tuple 用法與範例,tuple 元組可以用來存放一組資料(collection),存放資料的個數不限,資料型別也不用一樣,
tuple 是序列就像 list 一樣,tuple 與 list 的差別是 tuple 是不可改變的(immutable),一旦建立的 tuple 物件,就無法修改 tuple 物件的內容。
tuple 的元素與元素之間是以 , 逗號作為分隔,元素放在小括號()內。
tuple 裡面存放的元素不只侷限於整數,還可以放字串、或是混和整數跟字串。

以下 Python tuple 內容將分為這幾部份,

  • Python 建立 tuple
  • Python 讀取 tuple 的元素
  • Python 更新 tuple
  • Python tuple 索引值為 -1 或 -n
  • Python tuple 切片
  • for 迴圈遍歷巡訪 tuple 裡的元素
  • Python 建立空的 tuple
  • Python 建立只有一個元素的 tuple
  • Python tuple 實際應用範例. 網路 socket 程式

Python 建立 tuple

以下為 Python 建立 tuple 元組的範例,前三種寫法都是初始 tuple 的寫法,
第四種寫法表示不限制存放元素的型別,tuple 可以放不同型別的元素,例如放一個整數跟一個字串。

Python 元組可以使用 len() 來計算元組的長度,如下範例,

1
2
3
4
5
6
7
8
9
t = (1, 2, 3)
t = 1, 2, 3
t = tuple([1, 2, 3])
# 可以存放不同資料型別
t = 101, 'Amy'

print(t)
print(type(t))
print(len(t))

輸出結果如下,

1
2
3
(101, 'Amy')
<class 'tuple'>
2

Python 讀取 tuple 的元素

這邊介紹 Python 利用 tuple 索引值 index 來取得元素,索引值從 0 開始,第一個元素的索引值為 0,第二個元素的索引值為 1,依此類推,

1
2
3
4
t = (1, 2, 3 ,4 ,5)
print(t[0])
print(t[1])
print(t[2])

輸出結果如下,

1
2
3
1
2
3

Python 更新 tuple

tuple 一旦建立後就無法修改裡面的元素,tuple 雖然是不可改變的,但是你可以結合現存的多個 tuple 去建立一個新的 tuple,如下範例,

1
2
3
4
5
6
7
8
9
t1 = (1, 2, 3)
t2 = ('apple','banana','orange')
t3 = t1 + t2

# t1[0] = 4 # 不允許修改 tuple 的元素

print(t1)
print(t2)
print(t3)

輸出結果如下,

1
2
3
(1, 2, 3)
('apple', 'banana', 'orange')
(1, 2, 3, 'apple', 'banana', 'orange')

Python tuple 索引值為 -1 或 -n

tuple 索引值 -1 是表示 tuple 的最後一個元素,索引值是 -2 表示 tuple 的倒數第二個元素,索引值是 -n 表示 tuple 的倒數第 n 個元素,依此類推,

1
2
3
4
t = (1, 2, 3 ,4 ,5)
print(t[-1])
print(t[-2])
print(t[-3])

輸出結果如下,

1
2
3
5
4
3

Python tuple 切片

這邊介紹 Python tuple 使用切片 slicing 來取得指定範圍內元素,範例如下,

1
2
3
4
5
6
t = (1, 2, 3 ,4 ,5)
print(t[0:3]) # tuple 索引值0到索引值3
print(t[2:7]) # tuple 索引值2到索引值7
print(t[2:]) # tuple 索引值2到最後
print(t[:4]) # tuple 前4個元素
print(t[:]) # tuple 所有元素

輸出結果如下,

1
2
3
4
5
(1, 2, 3)
(3, 4, 5)
(3, 4, 5)
(1, 2, 3, 4)
(1, 2, 3, 4, 5)

更多 tuple 切片 slicing 的教學範例請看這篇

for 迴圈遍歷巡訪 tuple 裡的元素

這邊示範用 for 迴圈來遍歷巡訪 tuple 裡的所有元素並印出來,範例如下,

1
2
3
t = ('apple','banana','orange','lemon','tomato')
for i in t:
print(i)

輸出結果如下,

1
2
3
4
5
apple
banana
orange
lemon
tomato

更多 for 迴圈用法請看這篇

Python 建立空的 tuple

Python 建立空的 tuple 範例如下,

1
2
3
4
t = tuple()
print(t)
print(type(t))
print(len(t))

輸出結果如下,

1
2
3
()
<class 'tuple'>
0

Python 建立只有一個元素的 tuple

Python 要建立只有一個元素的 tuple 需要在該元素後加上一個逗號(comma),否則不會被辨識為 tuple,如下範例,

1
2
3
t = ('hello', )
print(type(t))
print(len(t))

輸出結果如下,

1
2
<class 'tuple'>
1

Python tuple 實際應用範例. 網路 socket 程式

在寫網路 socket 程式時,socket.bind()socket.connect() 的參數就是由 host 與 port 所組成的 tuple,

1
2
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 80))

下一篇將介紹 range 的用法

以上就是 Python tuple 用法與範例介紹,
如果你覺得我的文章寫得不錯、對你有幫助的話記得 Facebook 按讚支持一下!

其他參考
https://www.tutorialspoint.com/python/python_tuples.htm
https://openhome.cc/Gossip/Python/TupleType.html
https://www.w3schools.com/python/python_tuples.asp
https://sites.google.com/site/ezpythoncolorcourse/tuple

其它相關文章推薦
如果你想學習 Python 相關技術,可以參考看看下面的文章,
Python 元組切片
Python 檢查 tuple 元組是否為空
Python 新手入門教學懶人包
Python str 字串用法與範例
Python list 串列用法與範例
Python set 集合用法與範例
Python dict 字典用法與範例

Linux vim 文字編輯器用法與範例

本篇介紹 Linux 的 vim 文字編輯器操作使用,在 Linux 下使用 vim 是必要的技能,會使用 vim 將能替你省下不少麻煩,一起來學習如何使用 vim 吧!

1. 新建檔案與編輯檔案

以下示範新建檔案與開啟檔案編輯,以 tmp.txt 為例,如果目錄下沒有 tmp.txt 則會建立一個空白文件(但要存檔才會產生檔案),如果該目錄下已經有 tmp.txt 檔案,則會開啟 tmp.txt 來編輯。

1
$ vim tmp.txt

2. 編輯模式

進入編輯模式來輸入文字

輸入i鍵即可開始插入文字,即可開始輸入文字,文字輸入完後按 esc 回到一般模式
以下為其他種編輯模式,
i: insert 插入模式
a: append 添加模式
o: 插入新的一行

3. 命令模式

按下:符號即可進入命令模式,這時可以作存檔、離開、搜尋、取代等操作。

存檔與離開

以下為存檔與離開的範例。

1
2
3
4
:wq # 存檔並離開
:w # 存檔
:q # 離開
:q! # 強制離開

行數顯示

1
2
:set nu # 顯示行號
:set nu! # 不顯示行號

4. 一般模式

多行選擇多行刪除

d 是刪除。

1
ctrl+v, 游標上下左右移動選擇範圍, d

多行選擇多行複製

y 是複製, p 是貼上。

1
2
ctrl+v, 游標上下左右移動選擇範圍, y
p

多行編輯插入文字

1
ctrl+v, 游標上下左右移動選擇範圍, I, 輸入文字, esc

參考
https://mropengate.blogspot.com/2015/07/vim-ch1-vim.html
https://gitbook.tw/chapters/command-line/vim-introduction.html
https://www.twblogs.net/a/5bbd59502b71776bd30c39c6

https://leizediyi.weebly.com/vim-3034019977311812716924335214502085430456201142099925442.html
三種模式切換示意圖畫的不錯。

5. 實用技巧 Vim 的 Column Edit Mode

多行插入怎麼做

  • ctrl + v to 進入 column mode
  • 選擇行與列…
  • shift + i 進入多行插入模式 insert mode in column mode
  • 輸入完想要插入的文字後按 Esc 即可發現瞬間多行一起插入文字

多行刪除怎麼做

  • ctrl + v to 進入 column mode
  • 選擇行與列…
  • 按 d 即進行多行刪除

參考
https://coderwall.com/p/ouzshq/column-edit-mode-in-vi
https://stackoverflow.com/questions/6971903/how-to-do-column-editing-in-vim

其它相關文章推薦
Linux 常用指令教學懶人包
Linux sed 字串取代用法與範例
Linux find 尋找檔案/尋找資料夾用法與範例
Linux cut 字串處理用法與範例
Linux tail 持續監看檔案輸出用法與範例
Linux grep/ack/ag 搜尋字串用法與範例
Linux tee 同時螢幕標準輸出和輸出到檔案用法與範例
Linux xargs 參數列表轉換用法與範例
Linux du 查詢硬碟剩餘空間/資料夾容量用法與範例
Linux wget 下載檔案用法與範例

Python TCP Socket Server/Client 網路通訊教學

本篇 ShengYu 介紹如何寫 Python TCP Socket Server/Client 網路通訊程式,在這個網路盛行的時代,網路通訊已成為基礎,想要精通學習網路通訊必須先了解 TCP/IP 協定,其中又以 TCP 通訊最常被使用,TCP 通訊程式通常分成伺服器端與客戶端的兩部份程式,接下來教學內容將介紹如何使用 socket API 來搭建一個典型的 TCP 通訊程式,甚至可以寫出一個聊天室的程式,或者像 LINE 這樣的通訊程式。

以下 Python TCP 內容將分為幾部分,分別為:

  • 常見的 Socket API 函式 Overview 總覽
  • Python Socket TCP Server/Client 通訊流程
  • Python TCP Server 伺服器端程式 (Echo Sever)
  • Python TCP Client 客戶端程式 (傳送使用者的輸入)
  • Python TCP Client 客戶端程式 (定時傳送資料)
  • Python TCP 常見問題

Python 提供了兩個基本的 socket 模組:
Socket:標準的 BSD Socket API
SocketServer:Python 封裝好的 class,簡化了網絡服務器的撰寫開發,共有這4種:TCPServer,UDPServer,UnixStreamServer,UnixDatagramServer
本篇先以 Socket 的標準 BSD Socket API 來介紹,下一篇再介紹 SocketServer 的部份。

常見的 Socket API 函式 Overview 總覽

Python 的 socket 模組它提供了標準的 BSD Socket API,主要的 socket API 函式如下:
socket():建立 socket 與設定使用哪種通訊協定
socket.bind(address):將 socket 綁定到地址,在 AF_INET 下,以 tuple(host, port) 的方式傳入,如 socket.bind((host, port))
socket.listen(backlog):開始監聽 TCP 傳入連接,backlog 指定在拒絕連線前,操作系統可以掛起的最大連接數,該值最少為1,通常設為5就夠用了
socket.accept():等待連線,接受到 TCP 連線後回傳(conn, address),回傳的 conn 是新的 socket 類型,可以用來接收和發送資料,address 是連線客戶端的地址。
socket.connect(address):連線到 address 處的 socket,一般 address 的格式為 tuple(host, port),如果連線出錯,則回傳 socket.error 錯誤
socket.connect_ex(address):功能與 socket.connect(address) 相同,但成功回傳0,失敗回傳 errno 的值
socket.recv(bufsize[, flag]):接收 TCP 資料,Python3 回傳資料為 byte 類型,Python2 回傳資料為 str 類型,buffsize 指定要接收的最大資料量,flag 提供有關消息的其他訊息,通常可以忽略
socket.send(string[, flag]):發送 TCP 資料,Python3 是將 byte 發送到已連線的 socket,Python2 是將 str 發送到連線的socket,回傳值是發送的 byte 數
socket.sendall(string[, flag]):完整發送 TCP 資料,將 str 中的資料發送到已連線的 socket,但在回傳之前嘗試發送所有資料。成功回傳 None,失敗則拋出異常
socket.close():關閉 socket

Python Socket TCP Server/Client 通訊流程

以下 ShengYu 講解 Python TCP Server 端與 TCP Client 端的程式流程以及會如何使用這些 socket API,
TCP Server 的流程分為以下幾大步驟:

  1. 建立socket:s = socket.socket(socket.AF_INET, socket.SOCK_STREAM),指定 socket.AF_INET (Internet Protocol) family 的通訊協定,類型使用 socket.SOCK_STREAM (Stream Socket) 也就是 TCP 傳輸方式
  2. 綁定 socket 到本地 IP 與 port:s.bind()
  3. 開始監聽:s.listen()
  4. 等待與接受客戶端的請求連線:s.accept()
  5. 接收客戶端傳來的資料:s.recv()
  6. 傳送給對方發送資料:s.send()s.sendall()
  7. 傳輸完畢後,關閉 socket:s.close()

TCP Client 的流程分為以下幾大步驟:

  1. 建立 socket:s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  2. 連線至遠端地址:s.connect()
  3. 傳送資料:s.send()s.sendall()
  4. 接收資料:s.recv()
  5. 傳輸完畢後,關閉 socket:s.close()

以上是 TCP Server/Client 通訊的重點流程,實際的 Python socket API 用法與範例詳見下列章節,接下來就來看看怎麼寫 TCP Server/Client 通訊程式吧!

Python TCP Server 伺服器端程式 (Echo Sever)

這邊 ShengYu 就開始介紹怎麼寫 Python TCP Server 程式,下列範例這是一個典型的 Echo Server,Echo Server 就是收到什麼資料就回覆什麼資料,很簡單吧!
跟網路上其他範例不同的是此範例建立連線後不是傳輸一次資料就關閉連線,而是使用迴圈可以一直傳輸資料直到客戶端不想傳關閉連線為止,並且伺服器端再次地等待新的客戶端連線來服務。

如下例所示,伺服器端一開始建立 socket,用 bind() 綁定,這裡是使用 0.0.0.0, port 為 7000
使用 listen() 開始監聽,上限連線數為5,之後進入主迴圈,accept() 等待接受客戶端的連線請求,
一旦有客戶端連線的話,就會從 accept() 繼續往下執行,
之後是另一個迴圈來服務這個連線,不斷地從這個連線 recv 接收資料與 send 傳送資料,
如果 recv() 收到的資料長度為0,表示客戶端已斷開連線,此時我們也關閉這個連線,
之後回到 accept() 等待新的客戶端連線,等到新的客戶端連線連上便跟之前的流程一樣,這樣便是一個完整的 Python TCP 伺服器程式。

本篇範例是以 Python 3 作為示範,另外補充一下 Python 2 與 Python 3 的差異處,
Python 2 的 recv()send() 傳入的參數類型為 str
到了 Python 3 的 recv()send() 傳入的參數類型改為 byte
所以 str 要轉成 byte 要透過 decode(),以及使用 decode() 轉回 str

python3-tcp-socket-server.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket

HOST = '0.0.0.0'
PORT = 7000

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen(5)

print('server start at: %s:%s' % (HOST, PORT))
print('wait for connection...')

while True:
conn, addr = s.accept()
print('connected by ' + str(addr))

while True:
indata = conn.recv(1024)
if len(indata) == 0: # connection closed
conn.close()
print('client closed connection.')
break
print('recv: ' + indata.decode())

outdata = 'echo ' + indata.decode()
conn.send(outdata.encode())
s.close()

如果 Server 伺服器端不正常關閉後再次啟動時可能會遇到 socket.error: [Errno 98] Address already in use 這種錯誤訊息的話,那麼你可以在 bind() 之前設定 REUSEADDR 可以解決這個問題,

1
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

Python TCP Client 客戶端程式 (傳送使用者的輸入)

先用一個終端機來啟動前述的 TCP 伺服器端的程式,接著再用另一個終端機執行 TCP 客戶端的程式。Python TCP Client 範例如下,這邊要示範的是傳送使用者的輸入訊息,將使用者的輸入訊息傳送給伺服器端,通常應用於一般聊天軟體上,學習之後就可以寫一個簡單的聊天軟體了。

如下例所示,客戶端一開始建立 socket,之後 connect() 連線伺服器主機的 host 與 port,
之後進入主迴圈,不斷地傳送使用者的輸入,需要注意的是要取得使用者輸入的函式 Python 2 裡是使用 raw_input(),而 Python 3 是使用 input()
使用者輸入完後按下 Enter 便會將資料發送給伺服器端,接著等待伺服器端傳送資料,接收到來自伺服器端的資料就把它印出來,

python3-tcp-socket-client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket

HOST = '0.0.0.0'
PORT = 7000

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

while True:
outdata = input('please input message: ')
print('send: ' + outdata)
s.send(outdata.encode())

indata = s.recv(1024)
if len(indata) == 0: # connection closed
s.close()
print('server closed connection.')
break
print('recv: ' + indata.decode())

以下示範一下程式的啟動過程,過程中我在客戶端輸入了兩次的訊息,最後按 ctrl+c 結束了程式,
客戶端輸出結果如下,

client
1
2
3
4
5
6
7
8
9
10
11
$ python3 python3-tcp-socket-client.py 
please input message: hello
send: hello
recv: echo hello
please input message: hello tcp
send: hello tcp
recv: echo hello tcp
please input message: ^CTraceback (most recent call last):
File "python3-tcp-socket-client.py", line 12, in <module>
outdata = input('please input message: ')
KeyboardInterrupt

伺服器端輸出結果如下,

server
1
2
3
4
5
6
7
$ python3 python3-tcp-socket-server.py 
server start at: 0.0.0.0:7000
wait for connection...
connected by ('127.0.0.1', 57060)
recv: hello
recv: hello tcp
client closed connection.

Python TCP Client 客戶端程式 (定時傳送資料)

前一章節示範了 Echo Sever 與 Client 通訊程式,這時可以打鐵趁熱,除了前一章節 TCP Client 使用者手動輸入的情形之外,這邊也介紹另一種客戶端會定時地傳送資料給伺服器端,同時這也適用於各種通訊情形。

步驟跟前一章節 TCP Client 幾乎相同,傳輸字串為 'heartbeat',這邊傳送後使用 time.sleep(1) 來讓程式睡眠1秒,之後再繼續傳送資料,進而達成定時傳送的功能,

python3-tcp-socket-client-heartbeat.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
import time

HOST = '0.0.0.0'
PORT = 7000

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

while True:
outdata = 'heartbeat'
print('send: ' + outdata)
s.send(outdata.encode())

indata = s.recv(1024)
if len(indata) == 0: # connection closed
s.close()
print('server closed connection.')
break
print('recv: ' + indata.decode())

time.sleep(1)

客戶端輸出結果如下,

client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ python3 python3-tcp-socket-client-heartbeat.py 
send: heartbeat
recv: echo heartbeat
send: heartbeat
recv: echo heartbeat
send: heartbeat
recv: echo heartbeat
send: heartbeat
recv: echo heartbeat
send: heartbeat
recv: echo heartbeat
^CTraceback (most recent call last):
File "python3-tcp-socket-client-heartbeat.py", line 24, in <module>
time.sleep(1)
KeyboardInterrupt

伺服器端輸出結果如下,

server
1
2
3
4
5
6
7
8
9
10
$ python3 python3-tcp-socket-server.py 
server start at: 0.0.0.0:7000
wait for connection...
connected by ('127.0.0.1', 50130)
recv: heartbeat
recv: heartbeat
recv: heartbeat
recv: heartbeat
recv: heartbeat
client closed connection.

Python TCP 常見問題

在 TCP 的傳輸裡,為什麼伺服器還要回傳給客戶端?
因為這只是個示範用的通訊程式,讓你了解通訊的過程,就像打電話或者跟別人對話一樣,你一句我一句的來回互動,你可以根據實際的需求而修改程式,你也可以改成一直傳,例如客戶端一直傳送,伺服器一直接收。

為什麼 recv 還沒收到資料前會卡住一直等?
因為預設是 blocking 非阻塞模式,recv 還沒收到資料前會卡住一直等,沒法做其他事情,直到 recv 接收到資料才會從 recv 函式返回,解決辦法是改用 Non-blocking 非阻塞模式,Non-blocking 模式是這次沒接收到資料就會從 recv 函式返回,接著繼續往下執行;另一個解決方式是另外建立執行緒去做其他事情。

以上就是 Python TCP Socket Server/Client 網路通訊教學,
如果你覺得我的文章寫得不錯、對你有幫助的話記得 Facebook 按讚支持一下!

其他參考
socket — Low-level networking interface — Python 3 documentation
https://docs.python.org/3/library/socket.html
Python Socket 编程详细介绍
https://gist.github.com/kevinkindom/108ffd675cb9253f8f71
shengyu7697/PyReportMyIP: This is a program that automatically updates the IP addresses to server.
https://github.com/shengyu7697/PyReportMyIP
Socket Programming in Python (Guide) – Real Python
https://realpython.com/python-sockets/
Python3 网络编程 | 菜鸟教程
https://www.runoob.com/python3/python3-socket.html
Python中的TCP socket寫法示例 | 程式前沿
https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/356876/
Python Socket 编程:基于TCP套接字网络错位的梦寐的博客-CSDN博客
https://blog.csdn.net/HHG20171226/article/details/93519772
Python Socket 编程:基于TCP套接字(1)网络错位的梦寐的博客-CSDN博客
https://blog.csdn.net/HHG20171226/article/details/93524184
Python學習:socketserver實現並發網絡錯位的夢寐的博客-CSDN博客
https://blog.csdn.net/HHG20171226/article/details/93711478
Python Socket 编程–基于UDP的套接字网络错位的梦寐的博客-CSDN博客
https://blog.csdn.net/HHG20171226/article/details/93590850

python tcp socket client / server examples
https://gist.github.com/tuxfight3r/bfd95575ce34af6bd3317611dc04006c
bind_socket.py
socket_client.py
socket_server.py
socket_server_threads.py
socket_server_with_select.py

其他相關參考
c# - Why does my python TCP server need to bind to 0.0.0.0 and not localhost or it’s IP address? - Stack Overflow
https://stackoverflow.com/questions/38256851/why-does-my-python-tcp-server-need-to-bind-to-0-0-0-0-and-not-localhost-or-its
Python 3 TypeError: Can’t convert ‘bytes’ object to str implicitly - Mkyong.com
https://mkyong.com/python/python-3-typeerror-cant-convert-bytes-object-to-str-implicitly/

其它相關文章推薦
下一篇將會介紹如何用 Python 3 提供的 socketserver 來撰寫 server 端的程式,
Python socketserver 伺服器端網路通訊程式

Android adb logcat 基本用法教學

本篇教學介紹如何在 Android 下使用 adb logcat 指令,adb logcat command line tool 是開發 Android 時常常用到的工具,使用 adb logcat 指令會 dump 出系統的 log 日誌訊息,包含 stack traces,當 andorid 裝置噴出錯誤訊息時,你可以使用 adb logcat 即時地觀看系統訊息,以便輔助開發除錯。

adb 指令的安裝方式可以參考這篇

使用 adb logcat 前,先開啟 USB 偵錯模式

使用 adb logcat 功能時,通常要將 android 裝置開啟 USB 偵錯模式,中文介面的路徑如下:

1
設定 > 開發人員選項 > USB 偵錯

英文介面的路徑如下:

1
Settings > Advanced > Developer options > USB debugging

如果找不到開發人員選項需要另外開啟這功能,中文介面的路徑如下:

1
Settings > 關於手機 > 版本號碼 (連續快速7點次)

英文介面的路徑如下:

1
Settings > About Phone > Build number (連續快速7點次)

點完之後它會提示你已經是開發人員了,之後就會顯示開發人員選項
也可參考這篇

adb logcat 指令基本教學

使用 adb logcat 的方式有兩種,一種是直接執行 adb logcat,

1
$ adb logcat

另一種是先進入 android 的 shell 模式再輸入logcat。
通常是使用 adb logcat 發現 log 會遺失掉(使用無線傳輸),就會使用這種方式。

1
2
$ adb shell
$ logcat

adb logcat 指令選項

以下為常用的 adb logcat 指令,有想到在陸續增加吧!
adb logcat -c: 清除緩衝區,截至目前的log都會被清除
adb logcat -s: 設定過濾器,
例如,adb logcat -s MyActivity:W 表示顯示單一 tag MyActivity 且 log 等級不低於 Warning adb logcat -s MyActivity:V ActivityManager:V 表示顯示多重 tag MyActivity 且 log 等級不低於 Verbose 與 ActivityManager 且 log 等級不低於 Verbose,以下為各種 log 等級縮寫與全名,

1
2
3
4
5
6
7
V: Verbose (lowest priority)
D: Debug
I: Info
W: Warning
E: Error
F: Fatal
S: Silent (highest priority, on which nothing is ever printed)

adb logcat -f: 將log輸出到指定的檔案
adb logcat -D: 在不同的log之間加上分隔線
adb logcat -g: 取得目前緩衝區的大小
adb logcat -G <size>: 設定緩衝區的大小
adb logcat -t <count>:
adb logcat -t <time>:
adb logcat -T <count>:
adb logcat -T <time>:
adb logcat -v <format>: 設定日誌輸出格式,預設的是threadtime格式,可用的選項如下,

  • brief: 顯示優先順序,標籤和程式PID
  • long: 顯示所有的metadata欄位並且用空行分隔訊息內容
  • process: 只顯示程式PID
  • raw: 顯示原始的日誌資訊,沒有其他的metadata欄位
  • tag: 只顯示優先順序與標籤
  • thread: 顯示程式PID與執行緒TID
  • threadtime (預設): 顯示日期,時間,優先順序,標籤,程式PID, 執行緒TID
  • time: 顯示日期,時間,優先順序,標記,程式PID

例如: adb logcat -v time 表示使用 time 輸出格式,只能指定一種格式。

adb logcat 常用進階技巧

將 adb logcat 輸出重新導到指定的檔案

1
adb logcat > aaa.log

將 adb logcat 輸出導到 grep 來搜尋特定關鍵字,
詳細的 grep 用法可以參考這篇

1
adb logcat | grep xxx

Windows 下請改用 findstr

1
adb logcat | findstr xxx

將 adb logcat 輸出導到 grep 來搜尋多重關鍵字,
詳細的 grep 用法可以參考這篇

1
adb logcat | grep -E 'aaa|bbb|ccc'

將 adb logcat 輸出用 tee 導到指定的檔案也同時使用 grep 來搜尋特定關鍵字,
詳細的 tee 用法可以參考這篇

1
adb logcat | tee aaa.log | grep xxx

adb logcat lines missing 的原因

當 android adb logcat 出現 identical 5 lines 這種訊息時表示 adb logcat 發現相鄰的 log 內容完全相同,
為了增進 log 的效能,從 Android O 開始 Log 的 chatty 機制會把重複內容去掉換成是 identical 5 lines 這樣的 log,告訴開發者省略了 5 行內容。
如下所示,代表 AAA 行程的 log 被省略了 5 行。

1
2020-06-24 22:00:00.000 10123-10123 I/chatty: uid=10500 AAA identical 5 lines

關掉這個功能的方法:目前還沒找到方法可以關掉它。

參考
Logcat command-line tool | Android Developers
https://developer.android.com/studio/command-line/logcat
adb logcat 指北_敲鍵盤的呼倫貝爾人-CSDN博客_logcat -l
https://blog.csdn.net/kennethyo/article/details/76603228
Appdome | Gathering Android Device Logs Using ADB - Appdome
https://www.appdome.com/no-code-mobile-integration-knowledge-base/gathering-android-device-logs-using-adb/
西加加 Android: [Android] adb logcat使用方法
http://pianovv510.blogspot.com/2013/07/android-adb-logcat.html
Android logcat lines missing原因分析 - 庚拓天下 - 博客園
https://www.cnblogs.com/genggeng/p/10706379.html

其他技巧推薦

如果你是常常在Android adb shell下做事情的話,尤其是需要使用到vi,建議安裝busybox,使用busybox附帶的vi會方便很多,
如果還想知道busybox支援哪些指令或busybox基本用法的話請看這篇
其他的 Android 系列文章可以看這篇
下一篇來介紹Android fastboot指令的安裝與用法教學吧~

開源專案-金庸群俠傳

今天介紹Github上開源專案的金庸群俠傳遊戲,

scarsty/kys-cpp

https://github.com/scarsty/kys-cpp
星星數:168stars
技術:Cpp,SDL,sqlite3,boost asio
使用 C++ 與 SDL2 的「金庸群俠傳」復刻版。
小時候常玩的金庸群俠傳,印象很深刻當時野球拳練得很辛苦,沒想到長大後竟然發現有網友的重製開源版本,
有機會再來分享這裡面的遊戲軟體架構吧~

其他參考
[1] 写一个《金庸群侠传》——完结开源
https://zhuanlan.zhihu.com/p/30449554
[2] [C++复刻] 《金庸群侠传》C++复刻版正式发布!
https://www.dawuxia.net/thread-1034211-1-1.html

其它相關文章推薦
開源專案CursorJail-滑鼠鎖定工具
開源專案-2048
開源專案-tetris俄羅斯方塊
開源專案-數獨sudoku
開源專案-仙劍奇俠傳

Python OpenCV 影像侵蝕 erode 與影像膨脹 dilate

本篇介紹如何用 Python 搭配 OpenCV 模組來作影像侵蝕 cv2.erode 與影像膨脹 cv2.dilate,影像的侵蝕 Erosion 與膨脹 Dilation 是型態學的兩種基本運算,形態學主要是用來處理二值化後的影像,趕緊來學習吧。

Erosion 影像侵蝕

這篇要介紹影像處理中的型態變換,首先先介紹 Erosion 影像侵蝕。
用途1:Erosion 影像侵蝕對於移除影像中的小白雜點很有幫助,可用來去噪,例如影像中的小雜點,雜訊。
用途2:細化影像,消除毛刺。

影像侵蝕的概念就是將影像中白色區域(或高亮)進行細化或縮減,運算完的結果圖比原圖的白色區域更小,也可想像成讓該物體瘦一圈,而這一圈的寬度是由捲積 kernel 的大小所決定的,
實際上捲積 kernel 沿著影樣滑動並計算,如果捲積 kernel m x n 範圍內所有像素值都是1,那麼新的像素值就保持原來的值,
否則新的像素值為0,這表示捲積 kernel 掃過的所有像素都會被腐蝕或侵蝕掉(變為0),所以整張影像的白色區域會變少。

cv2.erode() 的第一個參數為二值化的影像, 第二個參數為使用的捲積 kernel,第三個參數為迭代次數(預設為1),
範例中的 kernel 捲積大小為 3x3,可以改成 5x5 或 7x7 較為常見,預設值為 3x3

opencv-erode.py
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import cv2
import numpy as np

image = cv2.imread('LinuxLogo.jpg', 0)
kernel = np.ones((3,3), np.uint8)
erosion = cv2.erode(image, kernel, iterations = 1)

cv2.imshow('Input', image)
cv2.imshow('Result', erosion)
cv2.waitKey(0)

結果圖如下所示:
可以發現右邊影像的線條明顯地變細了

Dilation 影像膨脹

再來介紹 Dilation 影像膨脹,
用途1:Dilation 影像膨脹通常是配合著影像侵蝕 Erosion 使用,先使用侵蝕的方式使影像中的線條變窄,同時也去除雜訊,之後再透過 Dilation 將影像膨脹回來。
用途2:用來連接兩個很靠近但分開的物體。

影像膨脹的概念就是將影像中白色區域(或高亮)進行擴張,運算完的結果圖比原圖的白色區域更大,也可想像成讓該物體胖一圈,而這一圈的寬度是由捲積 kernel 的大小所決定的,
在實際上捲積 kernel 沿著影樣滑動並計算,如果捲積 kernel m x n 範圍內只要有一個像素值是1,那麼新的像素值就為1,
否則新的像素值保持原來的像素值,這表示捲積 kernel 掃過的所有像素都會被擴張或膨脹(變為1),所以整張影像的白色區域會變多。

cv2.dilate() 的第一個參數為二值化的影像, 第二個參數為使用的捲積 kernel,第三個參數為迭代次數(預設為1),
範例中的 kernel 捲積大小為 3x3,可以改成 5x5 或 7x7 較為常見,預設值為 3x3

opencv-dilate.py
1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import cv2
import numpy as np

image = cv2.imread('LinuxLogo.jpg', 0)
kernel = np.ones((3,3), np.uint8)
dilation = cv2.dilate(image, kernel, iterations = 1)

cv2.imshow('Input', image)
cv2.imshow('Result', dilation)
cv2.waitKey(0)

結果圖如下所示:

參考
Morphological Transformations — OpenCV-Python Tutorials beta documentation
https://opencv24-python-tutorials.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_morphological_ops/py_morphological_ops.html
opencv-python中的腐蝕與膨脹函數
https://blog.csdn.net/hjxu2016/article/details/77837765

其它相關文章推薦
Python OpenCV 彩色轉灰階(RGB/BGR to GRAY)
Python OpenCV 彩色轉HSV(RGB/BGR to HSV)
Python OpenCV 彩色轉YCbCr(RGB/BGR to YCbCr)
Python OpenCV 灰階轉彩色(Gray to RGB/BGR)
Python OpenCV 影像二值化 Image Thresholding
Python OpenCV 影像平滑模糊化 blur
Python OpenCV 影像邊緣偵測 Canny Edge Detection
Python OpenCV 垂直vconcat 和水平hconcat 影像拼接
Python OpenCV resize 圖片縮放
Python 新手入門教學懶人包
小專案 Python OpenCV 圖片轉字元圖畫