2018年5月30日 星期三

C 語言學習筆記 : 字串函數測試

C 語言的字串處理需匯入 string.h 函式庫, 其中常用的函式如下 :

 函數 說明
 strlen(str) 傳回字串 str 的長度 (字元個數)
 strrev(str) 將字串 str 反轉後傳回新字串之位址
 strupr(str) 將字串 str 轉成大寫後傳回新字串之位址
 strlwr(str) 將字串 str 轉成小後傳回新字串之位址
 strchr(str, ch) 從字串 str 頭開始尋找字元 ch, 傳回第一次出現的位址
 strrchr(str, ch) 從字串 str 尾開始尋找字元 ch, 傳回第一次出現的位址
 strspn(s1, s2) 比對兩個字串, 傳回第一次字元不同位置之索引
 strcspn(s1, s2) 比對兩個字串, 傳回第一次字元相同位置之索引
 strstr(heystack, needle) 從字串 needle 頭開始尋找字串 heystack, 傳回第一次出現的位址
 strcpy(dest, source) 將字串 source 的內容複製到字串 dest, 傳回目的字串 dest 位址
 strncpy(dest, source, n) 將字串 source 的內容複製 n 個字元到字串 dest, 傳回字串 dest 位址
 strcat(dest, source) 將字串 source 的內容串接到字串 dest 後面並傳回 dest 的位址
 strcmp(s1, s2) 比較字串 s1 與 s2 的內容, 相等傳回 0, s1 > s2 傳回正數, 否則負數
 strtok(str, delim) 將字串 str 以 delim 字串為分界符遞迴切割為子字串傳回 
 memset(str, ch, n) 以 n 個 ch 字元填入字串 str 中, 傳回 str 位址 

注意, 傳回值為位址的函數如 strrev() 等必須宣告一個 char 指標來儲存新字串之位址, 不可用 char 陣列. 參考 :

https://zh.wikipedia.org/wiki/String.h


測試 1 : strlen(str) 

此函數會傳回傳入字串 s1 的字元個數 (整數), 下列程式分別使用 scanf() 與 gets() 讀取輸入字串, scanf() 遇到空格就會停止讀取, 但 gets() 則不會, 會一直讀到跳行為止 (含); 除此之外 scanf() 不會讀入跳行字元 \n, 但 gets() 則會. 程式如下 :

#include <stdio.h>
#include <string.h>

int main(void) {
   char str[256];       
   printf("請輸入字串\n");                //輸入 "Hello World"
   scanf("%s", str);                            //用 scanf() 讀取會被空格中斷
   printf("讀入字串=%s\n", str);       //輸出 "Hello"
   printf("字串長度=%d 個字元\n", strlen(str));   //輸出 5
   gets(str);                                         //用 gets() 讀取不會被空格中斷
   printf("讀入字串=%s\n", str);       //輸出 " World"
   printf("字串長度=%d 個字元\n", strlen(str));    //輸出 6 (含前面一空格)
   return 0;
   }

執行結果如下 :

請輸入字串
Hello World
讀入字串=Hello
字串長度=5 個字元
讀入字串= World          (注意, W 前面有一個空格)
字串長度=6 個字元

--------------------------------
Process exited after 9.066 seconds with return value 0
請按任意鍵繼續 . . .


測試 2 : strrev(str) 

此函數會將傳入字串反轉, 並將反轉後之新字串開頭位址傳回, 故須定義一個 char 指標來儲存此位址. 程式如下 :

#include <stdio.h>
#include <string.h>

int main(void) {
   char str[256], *nstr;                     //分別儲存原始字串與反轉後之字串
   printf("請輸入字串\n");
   gets(str);   
   printf("讀入字串=%s\n", str);
   nstr=strrev(str);                           //須用 char 指標來儲存位址
   printf("反轉字串=%s\n", nstr);
   return 0;
   }

執行結果 :

請輸入字串
Hello World
讀入字串=Hello World
反轉字串=dlroW olleH 

--------------------------------
Process exited after 11.87 seconds with return value 0
請按任意鍵繼續 . . .


測試 3 : strupr(str)

此函數會將傳入字串之全部字元轉成大寫, 傳回此新字串之開頭位址, 因此必須定義一個 char 指標來儲存. 範例如下 :

#include <stdio.h>
#include <string.h>

int main(void) {
   char str[256], *nstr;                      //分別儲存原始字串與大寫後之字串
   printf("請輸入字串\n");
   gets(str);   
   printf("讀入字串=%s\n", str);
   nstr=strupr(str);                           //須用 char 指標來儲存位址
   printf("大寫字串=%s\n", nstr);
   return 0;
   }

執行結果 :

請輸入字串
hello world
讀入字串=hello world
大寫字串=HELLO WORLD

--------------------------------
Process exited after 9.636 seconds with return value 0
請按任意鍵繼續 . . .


測試 4 : strlwr(str)

此函數會將傳入字串之全部字元轉成小寫, 傳回此新字串之開頭位址, 因此必須定義一個 char 指標來儲存. 範例如下 :

#include <stdio.h>
#include <string.h>

int main(void) {
   char str[256], *nstr;                      //分別儲存原始字串與大寫後之字串
   printf("請輸入字串\n");
   gets(str);   
   printf("讀入字串=%s\n", str);
   nstr=strlwr(str);                           //須用 char 指標來儲存位址
   printf("小寫字串=%s\n", nstr);
   return 0;
   }

執行結果 :

請輸入字串
HELLO WORLD
讀入字串=HELLO WORLD
小寫字串=hello world 

--------------------------------
Process exited after 7.496 seconds with return value 0
請按任意鍵繼續 . . . 


測試 5 : strchr(str, ch)

此函數會從字串 str 頭開始尋找字元 ch, 找到的話傳回第一次出現的位址, 否則傳回 NULL. 將傳回位址減掉字串 str 之位址即為第一次出現的 ch 在 str 之索引, 例如 :

#include <stdio.h>
#include <string.h>

int main(void) {
   char str[]="Hello World";
   char *ptr;
   printf("字串內容 %s\n", str);
   for (int i=0; i<strlen(str); i++) {
    printf("字元 '%c' 位址=%p\n", str[i], &str[i]);
       }
   ptr=strchr(str, 'o');
   printf("第一個 o 出現位置之位址=%p\n", ptr);
   printf("第一個 o 出現位置之索引=%d\n", ptr-str);
   return 0;
   }

執行結果 :

字串內容 Hello World
字元 'H' 位址=000000000062FE20
字元 'e' 位址=000000000062FE21
字元 'l' 位址=000000000062FE22
字元 'l' 位址=000000000062FE23
字元 'o' 位址=000000000062FE24
字元 ' ' 位址=000000000062FE25
字元 'W' 位址=000000000062FE26
字元 'o' 位址=000000000062FE27
字元 'r' 位址=000000000062FE28
字元 'l' 位址=000000000062FE29
字元 'd' 位址=000000000062FE2A
第一個 o 出現位置之位址=000000000062FE24   
第一個 o 出現位置之索引=4   

--------------------------------
Process exited after 0.07511 seconds with return value 0
請按任意鍵繼續 . . .


測試 6 : strrchr(str, ch)

此函數會從字串 str 開始尋找字元 ch, 找到的話傳回第一次出現的位址, 否則傳回 NULL. 將傳回位址減掉字串 str 之位址即為第一次出現的 ch 在 str 之索引, 例如 : 

#include <stdio.h>
#include <string.h>

int main(void) {
   char str[]="Hello World";
   char *ptr;
   printf("字串內容 %s\n", str);
   for (int i=0; i<strlen(str); i++) {
    printf("字元 '%c' 位址=%p\n", str[i], &str[i]);
       }
   ptr=strrchr(str, 'o');
   printf("第一個 o 出現位置之位址=%p\n", ptr);
   printf("第一個 o 出現位置之索引=%d\n", ptr-str);
   return 0;
   }

執行結果 :

字串內容 Hello World
字元 'H' 位址=000000000062FE20
字元 'e' 位址=000000000062FE21
字元 'l' 位址=000000000062FE22
字元 'l' 位址=000000000062FE23
字元 'o' 位址=000000000062FE24
字元 ' ' 位址=000000000062FE25
字元 'W' 位址=000000000062FE26
字元 'o' 位址=000000000062FE27
字元 'r' 位址=000000000062FE28
字元 'l' 位址=000000000062FE29
字元 'd' 位址=000000000062FE2A
第一個 o 出現位置之位址=000000000062FE27
第一個 o 出現位置之索引=7

--------------------------------
Process exited after 0.08958 seconds with return value 0
請按任意鍵繼續 . . .


測試 7 : strspn(s1, s2) 

#include <stdio.h>
#include <string.h>

int main(void) {
   char str1[]="Hello World";
   char str2[]="Hello Tony";
   size_t loc=strspn(str1, str2);
   printf("第一個字元不同位置之索引=%d\n", loc); 
   return 0;
   }

執行結果 :

第一個字元不同位置之索引=6

--------------------------------
Process exited after 0.09996 seconds with return value 0
請按任意鍵繼續 . . .


測試 8 : strcspn(s1, s2) 

#include <stdio.h>
#include <string.h>

int main(void) {
   char str1[]="7777B333";
   char str2[]="6666B222";
   size_t loc=strcspn(str1, str2);
   printf("第一個字元相同位置之索引=%d\n", loc);
   return 0;
   }

執行結果 :

第一個字元相同位置之索引=4

--------------------------------
Process exited after 0.08926 seconds with return value 0
請按任意鍵繼續 . . .


測試 9 : strstr(heystack, needle)

此函數會在 heystack 字串中從頭尋找第一個符合之子字串 needle, 找到的話將此起始位址傳回, 否則傳回 NULL. 顯示傳回位址所指內容即是此起始位置之後之子字串. 將傳回位址減掉 heystack 字串開頭位址即是符合字串在 heystack 中之索引.

#include <stdio.h>
#include <string.h>

int main(void) {
   char str[]="Hello World";
   char *ptr;
   printf("字串內容 %s\n", str);
   for (int i=0; i<strlen(str); i++) {    //顯示字串中各字元位址
       printf("字元 '%c' 位址=%p\n", str[i], &str[i]);
       }
   ptr=strstr(str, "o");
   printf("字串開頭位址=%p\n", str);
   printf("第一個 o 出現位置之子字串=%s\n", ptr);
   printf("第一個 o 出現位置之子字串位址=%p\n", ptr);
   printf("第一個 o 出現位置之索引=%d\n", ptr-str);
   return 0;
   }

執行結果 :

字串內容 Hello World
字元 'H' 位址=000000000062FE20
字元 'e' 位址=000000000062FE21
字元 'l' 位址=000000000062FE22
字元 'l' 位址=000000000062FE23
字元 'o' 位址=000000000062FE24
字元 ' ' 位址=000000000062FE25
字元 'W' 位址=000000000062FE26
字元 'o' 位址=000000000062FE27
字元 'r' 位址=000000000062FE28
字元 'l' 位址=000000000062FE29
字元 'd' 位址=000000000062FE2A
字串開頭位址=000000000062FE20
第一個 o 出現位置之子字串=o World
第一個 o 出現位置之子字串位址=000000000062FE24
第一個 o 出現位置之索引=4

--------------------------------
Process exited after 0.1082 seconds with return value 0
請按任意鍵繼續 . . .


測試 10 : strcpy(dest, source) 

此函數會將第二參數來源字串 source 的內容複製到第一參數目的字串 dest, 並傳回目的字串的位址.

#include <stdio.h>
#include <string.h>

int main(void) {
   char source[]="Hello World"; 
   char dest[20]={};   
   printf("來源字串 '%s'\n", source);
   printf("目的地字串 '%s'\n", dest);
   printf("來源字串位址 %p\n", source);   //顯示來源字串位址
   printf("目的地字串位址 %p\n", dest);   //顯示目的地字串位址
   strcpy(dest, source);     //傳回 dest 字串之位址
   printf("目的字串 '%s'\n", dest); 
   printf("目的地字串位址 %p\n", strcpy(dest, source));  //顯示目的地字串位址
   return 0;
   }

執行結果 :

來源字串 'Hello World'
目的地字串 ''
來源字串位址 000000000022FE40
目的地字串位址 000000000022FE20
目的字串 'Hello World'
目的地字串位址 000000000022FE20

--------------------------------
Process exited with return value 0
Press any key to continue . . .


測試 11 : strncpy(dest, source, n)

此函數會將第二參數來源字串 source 的內容複製 n 個字元到第一參數目的字串 dest, 並傳回目的字串 dest 的位址.

#include <stdio.h>
#include <string.h>

int main(void) {
   char source[]="Hello World";
   char dest[20]={}; 
   printf("來源字串 '%s'\n", source);
   printf("目的地字串 '%s'\n", dest);
   printf("來源字串位址 %p\n", source);   //顯示來源字串位址
   printf("目的地字串位址 %p\n", dest);   //顯示目的地字串位址
   strncpy(dest, source, 7);     //傳回 dest 字串之位址 (複製 7 個字元)
   printf("目的字串 '%s'\n", dest);
   printf("目的地字串位址 %p\n", strcpy(dest, source, n));  //顯示目的地字串位址
   return 0;
   }

執行結果 :

來源字串 'Hello World'
目的地字串 ''
來源字串位址 000000000062FE40
目的地字串位址 000000000062FE20
目的字串 'Hello W'
目的地字串位址 000000000062FE20

--------------------------------
Process exited after 0.09536 seconds with return value 0
請按任意鍵繼續 . . .


測試 12 : strcat(dest, source)

此函數會將字串 source 的內容串接到字串 dest 後面並傳回 dest 的位址 (指標), 例如 :

#include <stdio.h>
#include <string.h>

int main(void) {
   char str1[20]="Hello";
   char str2[20]=" World";
   printf("'Hello' 位址=%p\n", str1);       //顯示第一個字串位址
   printf("' World' 位址=%p\n", str2);     //顯示第二個字串位址
   printf("strcat('Hello', ' World')=%s\n", strcat(str1, str2));     //串接後以第一個字串為結果
   printf("strcat('Hello', ' World') 位址=%p\n", strcat(str1, str2));   //顯示第一個字串位址
   return 0;
   }

此程式中先顯示兩個串接字串在記憶體中之位址, strcat(dest, source) 函數以第一個字串作為結果字串, 因此傳回值為第一個字串之位址.

執行結果 :

'Hello' 位址=000000000062FE30 
' World' 位址=000000000062FE10
strcat('Hello', ' World')=Hello World 
strcat('Hello', ' World') 位址=000000000062FE30     //串接後傳回第一個字串位址

--------------------------------
Process exited after 0.08643 seconds with return value 0
請按任意鍵繼續 . . .


測試 13 : strcmp(s1, s2)

#include <stdio.h>
#include <string.h>

int main(void) {
   char str1[20]="Hello";
   char str2[20]="hello";
   printf("strcmp('Hello', 'Hello')=%d\n", strcmp(str1, str1));
   printf("strcmp('Hello', 'hello')=%d\n", strcmp(str1, str2));
   printf("strcmp('hello', 'Hello')=%d\n", strcmp(str2, str1));
   return 0;
   }

執行結果 :

strcmp('Hello', 'Hello')=0
strcmp('Hello', 'hello')=-1
strcmp('hello', 'Hello')=1

--------------------------------
Process exited after 0.09408 seconds with return value 0
請按任意鍵繼續 . . .

可見由於 H 的 ASCII 編碼數值 (72) 小於 h 的編碼 (104), 因此 strcmp('Hello', 'hello') 傳回 -1.


測試 14 : strtok(str, delim)

此函數會以字串 delim 為分界符號拆解字串 str, 傳回值為分界符前面之子字串, 若要全部拆解須用遞迴方式, 但遞迴時第一參數必須傳入 NULL, 例如 :

#include <stdio.h>
#include <string.h>

int main(void) {
   char str[]="11:22:33:44:55:66";      //待拆解字串
   char delim[]=":";      //分界符
   char *tok;                 //儲存分界符前之子字串
   printf("待拆解字串 %s\n", str);
   printf("分界字串 %s\n", delim);
   tok=strtok(str, delim);      //第一次拆解
   while (tok != NULL) {     //遞迴拆解
       printf("%s\n", tok);       //輸出分界符前之子字串
       tok=strtok(NULL, delim);    //第二次拆解傳入 NULL
       }
   return 0;
   }

執行結果 :

待拆解字串 11:22:33:44:55:66
分界字串 :
11
22
33
44
55
66

--------------------------------
Process exited with return value 0
Press any key to continue . . .

遞迴迴圈內的 strtok() 第一參數若傳入待拆解字串 str 會不斷地拆解第一個子字串而形成無窮迴圈, 務必要傳入 NULL, 參考 :

 # 切割字串函數:strtok, Network mac address 分割


測試 15 : memset(str, ch, n)

此函數會以 n 個 ch 字元填入 str 字串中, 傳回值為 str 字串之位址 (指標). 在宣告字元陣列時可用字串 literal 例如 "0000000000" 或 {'0', '0', .... '0'} 初始化, 但是如果陣列很大的話就必須用 memset() 了. 例如 :

#include <stdio.h>
#include <string.h>

int main(void) {
   char str[11];
   memset(str, '0', 10);     //填入 10 個 '0' 
   str[10]='\0';                  //字串結尾字元
   printf("%s\n", str);
   for (int i=0; i<strlen(str); i++) {
       printf("字元 '%c' 位址=%p\n", str[i], &str[i]);
       } 
   return 0;
   }

執行結果 :

0000000000
字元 '0' 位址=000000000022FE30
字元 '0' 位址=000000000022FE31
字元 '0' 位址=000000000022FE32
字元 '0' 位址=000000000022FE33
字元 '0' 位址=000000000022FE34
字元 '0' 位址=000000000022FE35
字元 '0' 位址=000000000022FE36
字元 '0' 位址=000000000022FE37
字元 '0' 位址=000000000022FE38
字元 '0' 位址=000000000022FE39

--------------------------------
Process exited with return value 0
Press any key to continue . . .

參考 :

http://www.cplusplus.com/reference/cstring/memset/
字串比較、搜尋
C語言字符串函數大全
[C&++] 字串整數轉換
C++字符串格式化 sprintf、printf
[C/C++] cstring (string.h) 搜尋函式:strstr, strchr
[C/C++] 切割字串函數:strtok, Network mac address 分割
C語言編譯器哪個好?6款好用的C語言編譯器推薦


單利複利計算機

今天找到一個很棒的線上單利複利計算機, 網址簡單又好記 :

http://rate.0123456789.tw

以投資 10000 元於殖利率 6%, 12%, 以及 18% 的價值股 20 年, 股息再投入為例分別計算複利本利和結果如下 :






20 年後 6% 的殖利率使原資產膨脹為 3.2 倍; 12% 膨脹為 9.6 倍; 18% 膨脹為 27 倍! 複利的威力真是超乎想像! 一個企業要長期維持殖利率 10% 以上不容易, 因此必須定期檢視持股與追蹤優質標的, 在殖利率因外部因素 (例如股災) 拉升時購入才能創造 18% 的超高利潤.

2018年5月29日 星期二

時空的解碼器

兩周前在鄉下的市圖分館找電腦書時, 轉身看到隔壁命理學書架上有一本 "圖解八字", 好奇翻閱之下覺得玄學真是神奇的學問, 透過簡單的八個字就能窺視人的未來? 由於我要看的書太多了, 估計就算借了也沒時間看, 就放回去了.

以前因為受訓時聽老師談論占星術, 曾買書鑽研了一陣子就因為其他研究而丟一邊. 近日或許機緣成熟, 想起書架上多年前買的兩本八字論命書籍, 但是翻讀之後覺得作者寫得太深奧無法契入, 因此上周去鄉下的市圖分館還書時特意再去命理書架尋找, 挑選了下面幾本我覺得應該可以看懂的入門書 :
  1. 八字真言 (知書房, 李如明)
  2. 圖解八字 : 一本可以邊看邊用的開運書
  3. 學學八字, 這本最好用 : 引爆命理學新革命
  4. 人人八字 : 為自己找出成功的密碼
  5. 八字學入門初階講義 : 從星座輕鬆學會看八字
我特別喜歡八字真言這本書, 文筆非常練達, 原來作者李如明為師大國文系畢業, 當過國中老師, 廣播電視編撰, 曾獲得多屆 (23, 31, 44 等) 金鐘獎最佳編劇肯定, 對命理易學等術數亦多有涉獵 (師從紅鳳鳥), 業餘為人批命. 此書為其八字研究經驗之作.

書中金玉良言節錄兩句如下 :
  1. 幸福的主宰不在於命, 而在於一顆會調適的心.
  2. 命裡沒有的, 現實一定得不到; 但命裡有的, 多半人會把它糟蹋掉.
進入五十歲之後, 是該讀些術數之書以知天命矣!

C 語言好站 : 美麗 C 世界

今天在解 CPE 的 C 語言字串習題時找到台中女中 (大概是資訊社團) 所建的 "美麗 C 世界" 網站, 整理的非常完整, 其中與字串有關的三篇如下 :

字元與字串
字串問題
字串問題二

看完此站全部文章, C 語言大致就複習完畢了.

2018年5月28日 星期一

大學程式能力檢定 (CPE) 試題

二哥 6 月份要再次參加 APCS 檢定, 前次的弱點是沒學過樹與圖, 所以實作題沒有全部答題. 除此之外, 字串也是實作題必考項目, 因此週日在鄉下的市圖分館借回 "大學程式能力檢定 (CPE) 密笈" 一書來複習字串處理題目. CPE 的首頁如下, 裡面有歷屆考題 :

https://cpe.cse.nsysu.edu.tw/index.php


6-1 字元與字串 :

例題 6.1.1 :

https://uva.onlinejudge.org/external/100/p10008.pdf

此題要求統計輸入的字串中英文字母不分大小寫出現的次數, 然後按次數由大至小列出字母與其出現之次數. 書上範例是用 C++ 寫的, 我將其改寫為 C 語言版如下 :

#include <stdio.h>
#include <string.h>

int main(void) {
   char str[256], *ustr;         //分別儲存原始輸入字串與轉成大寫後之字串
   int count[256]={0};         //一定要初始化為 0
   printf("請輸入字串\n");
   gets(str);     //不要用 scanf(), 會被空白終止讀取

   ustr=strupr(str);   //將字串內之英文字母全部轉成大寫 (傳回指標)
 
   for (int i=0; i<strlen(str); i++) {  //掃描每一個輸入字元
         count[*(ustr+i)]++;                  //將大寫字串中字母對應之 count 陣列元素增量
         } 
   for (int i=strlen(str); i>0; i--) {         //次數由大到小顯示
         for (char c='A'; c<='Z'; c++) {    //掃描 A~Z 對應之 count 元素是否為此 i
            if (count[c] == i) {printf("%c %d\n", c, count[c]);}  //是舊印出來
            }
         }
   return 0;
   }

上述程式主要使用了 string.h 中的 strupr() 來將字串轉成大寫, 並利用一個 count[256] 陣列來記錄英文字母出現之次數, 雖然配置了 256 個記憶體, 實際上有用到的部分是 A~Z (ASCII 碼 65~90) 共 26 個字母之統計數字. 此處 count 使用 ASCII 碼的 10 進位碼來當索引. 注意, strupr() 會傳回一個字元指標, 因此宣告了一個 ustr 來接收. 其次, 統計次數的陣列一定要初始化為 0, 否則記憶體內之殘存值會破壞統計結果. ASCII 碼參考 :

https://zh.wikipedia.org/wiki/ASCII

執行結果如下 :

請輸入字串
Hello World
L 3
O 2
D 1
E 1
H 1
R 1
W 1

--------------------------------
Process exited after 7.424 seconds with return value 0
請按任意鍵繼續 . . .

注意, 不要用 scanf() 來讀取輸入, 因為 scanf() 一遇到空白就會終止讀取, 因此輸入 Hello World 會只讀到 Hello 而已, gets() 則會一直讀到跳行.


例題 6.1.2 :

https://uva.onlinejudge.org/external/102/p10222.pdf

此題為字元解碼問題, 鍵盤輸入 "k[r dyt I[o", 要將其解碼輸出為 "how are you". 觀察其編碼規則為字元在鍵盤位置往左兩格即為輸出之字元, 例如 k 往左兩鍵為 h, 而 [ 往左兩格為 o.

解題方法是建立一個鍵盤字元組成之字串 "`1234567890-=qwertyuiop[]\\asdfghjkl;'zxcvbnm,./", 然後掃描輸入的字元, 利用 strchr() 函數找出此字元在鍵盤字串中的位置, 將其索引減 2 即得到解碼字元. C 語言程式碼如下 :

#include <stdio.h>
#include <string.h>

int main(void) {
    char str[256];        //儲存輸入字串
    char *pstr;             //儲存轉成小寫的字串
    char kb[]="`1234567890-=qwertyuiop[]\\asdfghjkl;'zxcvbnm,./";
    printf("請輸入字串\n");
    gets(str);                //不要用 scanf(), 會被空白終止讀取
    pstr=strlwr(str);     //將輸入字串轉成小寫 (傳回指標)
    for (int i=0; i<strlen(str); i++) {  //掃描每一個輸入字元
         char *p=strchr(kb, *(pstr+i));   //在 kb 字串中搜尋字元, 傳回其位址
         if (p) {      //有找到 : 輸出其前兩格字元
             printf("%c", *(p-2));     //指標往前移兩格
             }
         else {        //沒找到 : 輸出原始字元
             printf("%c", str[i]);
             }
         }
    return 0; 
    }

此程式中使用 str[256] 陣列儲存原始輸入字串, 指標 pstr 指向將輸入字串以 strlwr() 函數轉成小寫後之字串, 陣列 kb 則儲存鍵盤字元依序排列組成之字串. 注意, 倒斜線必須用兩個 "\\" 來表示其本身, 否則後面的 a 字元會變成脫逸字元而找不到.

首先用 gets() 讀入字串存在 str 裡, 然後呼叫 strlwr() 將原始字串轉成小寫 (因為 kb 中全部都用小寫), 最後用 for 迴圈拜訪字串字串中的每一個字元, 並呼叫 strchr() 在 kb 字串中尋找此字元, 有找的話會傳回此字元在 kb 字串中的位址 (指標), 將指標往前移兩格即可找到解碼字元, 沒找到就輸出原始字元 : 

執行結果如下 :

請輸入字串
k[r dyt I[o
how are you
--------------------------------
Process exited with return value 0
Press any key to continue . . .
此題要求將輸入的整數 n (小於 2000000000) 每一位數遞迴相加, 直到變成個位數為止, 例如輸入 1234, 第一輪相加 1+2+3+4=10, 第二輪相加 1+0=1 即為答案, 主要是數字字元轉成整數問題. 依題義輸入整數小於 2000000000 為 10 位數, 可用一個 n[11] 陣列來儲存所輸入之數字字元 (ASCII 碼為 48~57), 只要將數字字元減掉 48 即可得到相對應之整數數字.


#include <stdio.h>
#include <string.h>

int main(void) {
   char n[11]={0};    //預設為 '0'
   printf("請輸入數字 (小於 2000000000)\n");
   scanf("%s", n);   
   int F=0;
 
   while (strlen(n) != 1) {   //還沒變個位數
       int F=0;
       for (int i=0; i<strlen(n); i++) {  //掃描每一個輸入字元
           F += n[i]-48;  //ASCII 字元轉成數字疊加
           } 
       memset(n, '\0', 11);
      sprintf(n, "%d", F);
       }
   printf("%s\n", n);
   return 0;
   }

執行結果如下 :

請輸入數字 (小於 2000000000)
47
2

--------------------------------
Process exited after 23.14 seconds with return value 0
請按任意鍵繼續 . . .


例題 6.1.4 :

https://uva.onlinejudge.org/external/102/p10252.pdf

此題要求輸入兩個字串, 比較兩字串中字元, 按照字母順序列出兩字串中同時出現之字元. 演算的方法與上面 6.1.1 類似, 即利用整數陣列來儲存字串中每個字元出現的次數, 並以 ASCII 碼作為陣列索引以利存取. 只要掃瞄兩個計數陣列中字元次數同時不為 0 者, 即為兩字串中同時都出現之字元. C 程式改寫如下 :

#include <stdio.h>
#include <string.h>

int main(void) {
    char a[1001], b[1001];    //儲存輸入的兩個字串
    counta[123]={0}, countb[123]={0};  //務必初始化為 0
    printf("請輸入兩行字串\n");
    gets(a);     //讀入第一個字串
    gets(b);     //讀入第二個字串
    for (int i=0; i<strlen(a); i++) {  //統計 a 字串裡字元出現之次數
        counta[a[i]]++;
        }
    for (int i=0; i<strlen(b); i++) {  //統計 b 字串裡字元出現之次數
        countb[b[i]]++;
        } 
    for (int i=97; i<123; i++) {  //掃描 a~z 字元
        if (counta[i] > 0 && countb[i] > 0) {   //列出兩者次數皆大於 0 者
            printf("%c", i);
            }
        }       
    return 0; 
    }


執行結果如下 :

請輸入兩行字串
retty
omen

-------------------------------
rocess exited with return value 0
ress any key to continue . . .

請輸入兩行字串
alking
own
w
-------------------------------
rocess exited with return value 0
ress any key to continue . . .

請輸入兩行字串
the
street
et
--------------------------------
Process exited with return value 0
Press any key to continue . . .

~未完待續

參考 :

[C]scanf字串空白錯誤
C語言字符串函數大全
C语言tolower()函数:将大写字母转换为小写字母
http://dhcp.tcgs.tc.edu.tw/c/p009.htm
經典c程式100例 91-100
C語言考古題 & C的解題 -- 程式設計學習入門

2018 年第 21 周記事

本周四爸與阿泉伯一家去澎湖玩 3 天, 週六回到家時已 9 點. 周六傍晚回到家只有我一個人, 還真的有點寂寞. 晚上檢視硬碟中以前小狐狸們小時候的照片與影片集, 過去那些時光都一去不復返, 真令人感慨. 換個角度看, 要感謝三隻小狐狸的到來在過去十幾年帶給爸媽的歡樂, 還好我忙歸忙還是有認真記錄生活點滴, 要不然真的忘記他們小時候各種搞笑模樣呢!

菁菁會考結束, 周六我將所有國中書籍都裝箱運回鄉下, 本周要來整理書櫥, 留下二哥高中書籍即可. 下周回鄉下要開始來整理雜物了.

週六下午大帥伉儷來訪, 我說家裡到處都是書. 很亂, 實在不好意思. 大帥應該是多年來唯一到訪的同學吧!

2018年5月27日 星期日

Python 學習筆記 : Selenium 模組瀏覽器自動化測試 (二)

Selenium 對我這個 "自動控" 來說可說是天上掉下來的禮物. 我的野心不僅僅是要把一切流程自動化, 還要能智慧化, 最好能創造一個可教育的人工意識來代理我們的思考與決策 ... 不過這都太遠也太充滿想像力了, 還是回過神來繼續測試 Selenium 吧.

以下測試參考了下面幾本書 :
  1.  Python 網路爬蟲實戰 (松崗, 胡松濤) 第 8 章
  2.  Python 自動化的樂趣 (碁峰, AL Sweigart) 第 11 章
  3.  Python 程式設計實務 (博碩, 何敏煌) 第 10-3 節
  4.  Python 初學特訓班 (碁峰, 文淵閣工作室) 第 6-3, 6.4 節
  5.  不只是測試-Python 網路爬蟲王者 Slenium (佳魁, 蟲師) 第 4 章
  6.  Learning Selenium Testing Tools With Python (Packt, Unmesh Gundecha) 第 4 章
本系列之前的文章參考 :

Python 學習筆記 : 安裝執行環境與 IDLE 基本操作
Python 學習筆記 : 檔案處理
Python 學習筆記 : 日誌 (logging) 模組測試
Python 學習筆記 : 資料庫存取測試 (一) SQLite
Python 學習筆記 : 資料庫存取測試 (二) MySQL
Python 學習筆記 : Selenium 模組瀏覽器自動化測試 (一)

除了上列書籍外, 我還參考了下列 Selenium 教學網站 :

Selenium with Python
Free Selenium Tutorials
Selenium Tutorial for Beginners
https://www.tutorialspoint.com/selenium/index.htm

從前一篇測試可知 Chrome 的 Web Driver 安裝最沒問題, 所以以下測試是以 Chrome 為對象, 理論上 Selenium 的所有函數在所支援的瀏覽器都是通用的.


1. 取得瀏覽器物件之屬性 : 

呼叫 webdriver.Chrome(), webdriver.Opera() 或 webdriver.Firfox() 等函數將傳回一個 WebDriver 物件 (瀏覽器物件), Selenium Webdriver API 在此物件中提供了許多屬性與方法以操控瀏覽器, 例如呼叫 get() 方法可在模擬瀏覽器中開啟指定網頁, 透過下列屬性可取得已開啟網頁之屬性 :

 屬性 說明
 name 瀏覽器名稱
 title 目前開啟網頁之標題
 current_url 目前開啟網頁之 URL
 page_source 目前開啟網頁之原始碼
 session_id 網頁連線 id
 capabilities 瀏覽器功能設定

以開啟 "Webbots, Spiders and Screen Scrapers" 這本書的測試網頁 hello_world.html 為例 :

>>> from selenium import webdriver         #匯入 webdriver 模組
>>> browser=webdriver.Chrome()              #開啟模擬瀏覽器
>>> browser.get("http://www.webbotsspidersscreenscrapers.com/hello_world.html") 
>>> browser.name                                          #瀏覽器名稱
'chrome'
>>> browser.title                                             #網頁標題
'Hello, world!'
>>> browser.current_url                                #網頁 URL
'http://www.webbotsspidersscreenscrapers.com/hello_world.html' 
>>> browser.page_source                                #網頁原始碼
'<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"><html xmlns="http://www.w3.org/1999/xhtml"><head>\n\t<title>Hello, world!</title>\n</head>\n\n<body>\nCongratulations! If you can read this, <br />\nyou successfully downloaded this file.\n\n\n</body></html>'
>>> browser.session_id                                    #連線 ID
'ba0f0e4f432eb1a2f0280736f20f2825'
>>> browser.capabilities                                  #瀏覽器功能設定
{'acceptInsecureCerts': False, 'acceptSslCerts': False, 'applicationCacheEnabled': False, 'browserConnectionEnabled': False, 'browserName': 'chrome', 'chrome': {'chromedriverVersion': '2.38.552522 (437e6fbedfa8762dec75e2c5b3ddb86763dc9dcb)', 'userDataDir': 'C:\\Users\\cht\\AppData\\Local\\Temp\\scoped_dir5572_4888'}, 'cssSelectorsEnabled': True, 'databaseEnabled': False, 'handlesAlerts': True, 'hasTouchScreen': False, 'javascriptEnabled': True, 'locationContextEnabled': True, 'mobileEmulationEnabled': False, 'nativeEvents': True, 'networkConnectionEnabled': False, 'pageLoadStrategy': 'normal', 'platform': 'Windows NT', 'rotatable': False, 'setWindowRect': True, 'takesHeapSnapshot': True, 'takesScreenshot': True, 'unexpectedAlertBehaviour': '', 'version': '66.0.3359.181', 'webStorageEnabled': True}


2. 操控瀏覽器之位置與大小 : 

瀏覽器物件的下列方法可操控瀏覽器之位置與大小 :

 方法 說明
 get_window_position() 取得瀏覽器視窗左上角位置
 set_window_position(x, y) 設定瀏覽器視窗左上角位置
 get_window_size() 取得瀏覽器視窗大小
 set_window_size(x, y) 設定瀏覽器視窗大小
 maximize_window() 將瀏覽器視窗最大化
 minimize_window() 將瀏覽器視窗最小化

例如 :

>>> browser.get_window_position() 
{'x': 10, 'y': 10}
>>> browser.set_window_position(50, 100) 
>>> browser.get_window_position()   
{'x': 50, 'y': 100}
>>> browser.get_window_size()   
{'width': 1050, 'height': 664}
>>> browser.set_window_size(1024, 768) 
>>> browser.get_window_size()   
{'width': 1024, 'height': 738}
>>> browser.maximize_window()   
>>> browser.get_window_size() 
{'width': 1296, 'height': 698}
>>> browser.minimize_window()   
Traceback (most recent call last):
  File "<pyshell#22>", line 1, in <module>
    browser.minimize_window()
  File "C:\Python36\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 740, in minimize_window
    self.execute(Command.MINIMIZE_WINDOW)
  File "C:\Python36\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 314, in execute
    self.error_handler.check_response(response)
  File "C:\Python36\lib\site-packages\selenium\webdriver\remote\errorhandler.py", line 208, in check_response
    raise exception_class(value)
selenium.common.exceptions.WebDriverException: Message: unknown command: session/ba0f0e4f432eb1a2f0280736f20f2825/window/minimize 

奇怪, 為何 minimize_window() 不動作呢? 錯誤訊息是 "unknown command", 難道 Chrome 沒有實作此方法? 搜尋網路發現有人也遇到此問題, 但看似無解, 參考 :

Python Selenium浏览器最小化方法minimize_window(self)抛异常

我在下列文章看到模擬 minimize_window() 的方法 :

Selenium Python minimize browser window

>>> browser.set_window_position(-3000, 0)     #模擬視窗最小化
>>> browser.set_window_position(0, 0)             #視窗復原


 3. 操控瀏覽器按鈕 : 

瀏覽器基本上有 "上一頁", "下一頁", "重新載入", 以及 "關閉" 四個按鈕, 可以透過瀏覽器物件的下列方法用程式操控 :

 方法 說明
 back() 按瀏覽器 "上一頁" 鈕
 forward() 按瀏覽器 "下一頁" 鈕
 refresh() 按瀏覽器 "更新" 鈕
 quit() 按瀏覽器 "關閉" 鈕, 同時關閉驅動程式
 close() 按瀏覽器 "關閉" 鈕

例如 :

>>> from selenium import webdriver         #匯入 webdriver 模組
>>> browser=webdriver.Chrome()              #開啟模擬瀏覽器
>>> urls=["http://www.google.com.tw",     #URL 串列
  "http://tw.yahoo.com", 
  "https://twitter.com"] 
>>> for url in urls:                                         #依序開啟網頁 (最後停在 Twitter)
browser.get(url)   

>>> browser.current_url                               #最後停在 Twitter
'https://twitter.com/'
>>> browser.back()                                        #回前一頁 (YAHOO)
>>> browser.current_url                             
'https://tw.yahoo.com/'
>>> browser.back()                                        #回前一頁 (Google)
>>> browser.current_url 
'https://www.google.com.tw/?gws_rd=ssl'
>>> browser.forward()                                   #回後一頁 (YAHOO)
>>> browser.current_url 
'https://tw.yahoo.com/'
>>> browser.forward()                                  #回後一頁 (Twitter)
>>> browser.current_url   
'https://twitter.com/'
>>> browser.refresh()                                    #重新載入

上例依序開啟 Google, YAHOO, Twitter 三個網頁, 最後網頁是載入 Twitter. 透過呼叫 back(), forward() 來操控載入上一頁與下一頁動作, 並透過 cuurent_url 屬性來檢查是否載入正確網頁.


4. 儲存網頁快照 (screenshot) : 

瀏覽器物件有一個 save_screenshot() 方法可將目前網頁儲存為 PNG 檔案下載至本機目錄下, 傳回值為 True 表示存檔成功, False 為失敗.

 方法 說明
 save_screenshot(filename) 將目前網頁儲存為 png 檔案下載

傳入參數若為單純檔名, 則 PNG 檔將儲存在 Python 安裝目錄下; 也可以傳入包含路徑之檔名, 這樣 PNG 檔就會存在指定目錄下, 但要注意在 Windows 下路徑分隔符是兩個倒斜線 (Escape), 例如 :

>>> from selenium import webdriver         #匯入 webdriver 模組
>>> browser=webdriver.Chrome()              #開啟模擬瀏覽器
>>> browser.get("http://tw.yahoo.com")    #開啟 YAHOO
>>> browser.save_screenshot("yahoo.png")      #儲存快照 PNG
True
>>> browser.save_screenshot("d:\Python\test\yahoo.png")   #須雙倒斜線
False
>>> browser.save_screenshot("d:\\Python\\test\\yahoo.png")   #指定路徑
True

注意, 所儲存是完整網頁的快照, 不受視窗大小的影響, 用 set_window_size()將瀏覽器視窗設成多小都能下載到完整網頁之快照.


5. 操控網頁元素 : 

這部分就是 Selenium Webdriver 作為網路爬蟲的核心功能, 在上面的測試中利用 page_source 屬性可取得所開啟網頁之原始網頁 HTML 內容, Webdriver 本身自帶了許多方法可從 HTML 原始碼中擷取資訊, 不需要用到 BeautifulSoup. 此外, 還可以透過下列方法來取得網頁元素 (標籤 tag), 以便進行網頁操控.

 方法 說明
 find_element(by, value) 使用 by 指定之方法取得第一個符合 value 的元素
 find_element_by_class_name(name) 傳回符合指定 class 名稱之元素
 find_elements_by_class_name(name) 傳回符合指定 class 名稱之元素串列
 find_element_by_css_selector(selector) 傳回符合指定 CSS 選擇器名稱之元素
 find_elements_by_css_selector(selector) 傳回符合指定 CSS 選擇器名稱之元素串列
 find_element_by_id(id) 傳回符合指定 id 之元素
 find_elements_by_id(id) 傳回符合指定 id 之元素串列
 find_element_by_link_text(text) 傳回符合指定超連結文字之元素
 find_elements_by_link_text(text) 傳回符合指定超連結文字之元素串列
 find_element_by_partial_link_text(text) 傳回符合部分指定超連結文字之元素
 find_elements_by_partial_link_text(text) 傳回符合部分指定超連結文字之元素串列
 find_element_by_name(name) 傳回符合指定元素名稱之元素
 find_elements_by_name(name) 傳回符合指定元素名稱之元素串列
 find_element_by_tag_name(tag) 傳回符合指定標籤名稱之元素
 find_elements_by_tag_name(tag) 傳回符合指定標籤名稱之元素串列

呼叫 find_element_ 方法會傳回一個代表網頁元素的 WebElement 物件, 如果是呼叫 find_elements_ 方法則會傳回 WebElement 物件組成的串列. 此 WebElement 物件有如下屬性與方法可取得網頁元素的內容或對元素進行操控, 例如按下按鈕或填入資料等.

 屬性或方法 說明
 tag_name 元素 (標籤) 名稱
 location 傳回元素對應螢幕左上角之座標 (x, y 字典)
 text 開頭標籤與結尾標籤之間的文字 (即 innerText)
 get_attribute(attr) 傳回屬性名稱 attr 之內容
 click()  觸發元素之 click 事件 (按鈕, 超連結, 表單提交等元素)
 send_keys(str) 對元素傳送文字 str (文字欄位或文字區域 textarea 元素)
 clear() 清除文字欄位或文字區域之內容
 is_displayed() 傳回 True (元素可見) 或 False (元素不可見)
 is_enabled() 傳回 True (元素可用) 或 False (元素不可用)
 is_selected() 傳回 True (元素有被勾選) 或 False (元素沒被勾選) : 核取方塊/選項圓鈕


Invent With Python 網站首頁為例, 在 Chrome 按 F12 或按 Ctrl + U 開啟 "檢視網頁原始碼", 可知有一個 id 屬性值為 navbarSupportedContent 的 div 元素  :

<body style="background-color: #caeab7;">

<nav class="navbar navbar-toggleable-md">
  <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon text-white">☰</span>
  </button>
  <a class="navbar-brand" href="/">Invent with Python</a>

  <div class="collapse navbar-collapse" id="navbarSupportedContent">
    <ul class="navbar-nav mr-auto">
      <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" id="navbarDropdownReadMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
          Read for Free
        </a>
        <div class="dropdown-menu" aria-labelledby="navbarDropdownReadMenuLink">
          <a class="dropdown-item" href="https://automatetheboringstuff.com">Automate the Boring Stuff with Python</a>
          <a class="dropdown-item" href="/cracking">Cracking Codes with Python</a>
          <a class="dropdown-item" href="/invent4thed">Invent Your Own Computer Games with Python</a>
          <a class="dropdown-item" href="/pygame">Making Games with Python & Pygame</a>
          <a class="dropdown-item" href="https://inventwithscratch.com">Scratch Programming Playground</a>
        </div>
      </li>

.... (略)

      <li class="nav-item">
        <a class="nav-link" href="https://www.youtube.com/user/Albert10110">YouTube</a>
      </li>

      <li class="nav-item">
        <a class="nav-link" href="https://www.reddit.com/r/inventwithpython">Forum</a>
      </li>

      <li class="nav-item">
        <a class="nav-link" href="/blog">Blog</a>
      </li>

      <li class="nav-item">
        <a class="nav-link" href="/#donate">Donate</a>
      </li>

先利用 find_element_by_id() 方法取得此 div 元素之 WebElement 物件, 就可以用上表中的屬性與方法取得此標籤之內容, 例如 :

>>> from selenium import webdriver 
>>> browser=webdriver.Chrome() 
>>> browser.get("http://inventwithpython.com/") 
>>> divEle=browser.find_element_by_id("navbarSupportedContent") 
>>> type(divEle)   
<class 'selenium.webdriver.remote.webelement.WebElement'>   
>>> divEle.tag_name 
'div'
>>> divEle.get_attribute("class") 
'collapse navbar-collapse'
>>> divEle.location 
{'x': 0, 'y': 0}
>>> divEle.text   
'' 

以 id 搜尋理論上應該只找到一個 WebElement 物件, 若以 class 或 tag 等來尋找則會找到多個 WebElement 物件, 這時就會將這些物件放在 list 中傳回, 例如搜尋 class 屬性為 dropdown-item 的元素會找到具有此 class 之多個超連結 a, 這些 WebElement 物件會放在串列中傳回, 可以用 for in 來迭代顯示 a 元素中的 href 屬性 (即網址), 例如 :

>>> classEles=browser.find_elements_by_class_name("nav-link")
>>> for ele in classEles: 
print(ele.get_attribute("href")) 


None
None
None
https://www.youtube.com/user/Albert10110
https://www.reddit.com/r/inventwithpython
http://inventwithpython.com/blog
http://inventwithpython.com/#donate

前三個 None 是因為找到三個 class="nav-link dropdown-toggle" 的無 href 屬性之 a 元素之故. 

2018年5月26日 星期六

代購悠康愛健康葉黃素

爸周四跟阿泉伯一家去澎湖玩三天, 晚上回到家剛好九點. 回來閒聊談到上回買的永信愛健康葉黃素, 說阿泉伯也想要買, 叫我上網代購. 上回是母親節有特價較便宜, 現在雖也有 85 折再 9 折優惠, 但終究比不上母親節時的 75 折再減 100.



優惠下來買四瓶共 5141 元, 平均一瓶 1285 元, 上回 6 瓶 6960, 平均一瓶 1160, 每瓶貴了 125 元.

2018年5月25日 星期五

Introduction to Machine Learning with Python 簡體電子書

今天在 "圖靈社區" 找到一本簡體中文機器學習線上電子書 :

Python机器学习基础教程

此書為 Sarah Guido & Andreas Müller 寫的 "Introduction to Machine Learning with Python - A Guide for Data Scientists (Oreilly)" 中譯版.




原來封面這隻動物叫做美洲大鲵, 我還以為是蜥蜴.

免費電子書 : Machine Learning for Humans

我在下列網頁找到作者 Vishal Maini 提供的機器學習免費電子書 :

Machine Learning for Humans

其線上電子書網址 :

https://www.dropbox.com/s/e38nil1dnl7481q/machine_learning.pdf?dl=0

也可以點右上角的 "Download" 下載 PDF 檔.

此電子書將機器學習的精隨濃縮在短短 97 頁的篇幅裡, 對此領域的基礎知識做了統整扼要的介紹, 是 ML 初學者非常棒的入門教材.

2018年5月24日 星期四

購買趙英傑的 "超圖解 Python 物聯網實作入門"

晚上去明儀想買剩下一本林大貴機器學習書籍, 結果已經被人買走, 殘念!  趙英傑寫的 "超圖解 Python 物聯網實作入門也只剩一本, 不過 85 折還是有點貴. 回來上 momo 查詢之前預訂的這本書, 發現已經上架了 (怎沒收到通知?), 所以就線上刷卡買了, 就是 79 折 :




雖然近期可能還沒空看, 但剛好 momo 有折扣, 先買來放著也好.

iPad 擷取螢幕快照的方法

今天在 iPad 上安裝 Line 後試著要將期刊中的不錯文章片段快照後以圖檔方式分享, 我在下列文章找到兩個方法, 一個是傳統的同時按 Home 鍵 + Power 鍵法, 另一個是用 "AssitiveTouch" 中的 "裝置/更多/螢幕快照", 參考 :

生活小竅門:ipad如何截圖 不使用HOME鍵進行截圖

我覺得第二個方法好, 因為第一個方法有時沒同時按結果關掉調螢幕. 螢幕截圖會存放在內建的 "照片" 這個 App 底下, 點選後按上箭頭按鈕選擇用 Line 分享即可.

iPad 2 升版 iOS 至 9.3.5 版 (13G36)

今天去上無聊的資安課時帶著 iPad 2 去看這期今周刊, 想要分享其中一篇文章給同學才發覺沒有安裝 Line, 上 App Store 下載發現我的 iPad 2 還是當初買來時的 iOS 7.1.2, 只能安裝較舊版的 Line. 下課後回到辦公室想說乾脆升版到最新的 iOS 9 好了, 之前 iOS 8 出來時我都不理會升版通知, 因為機器用得好好的幹嘛升版, 萬一有問題可是會妨礙到我每天看期刊的進度. 但安裝 Line 這件事讓我理解到, 有些新軟體要求較新版的 iOS, 不升版有時只能用舊版 App, 甚至有些 App 連舊版支援都沒有.

我這台 iPad 2 是 2012-04-20 趁赴台北向副總提出創新提案比賽第三次彩排的機會, 結束後叫阿龍表弟載我去 SOGO 忠孝電 8F 的德誼買的, 當時價格 $12500, 剛好把尾牙抽到的 5000 元 SOGO 禮券投入, 再自掏腰包 7500 元即可, 算是蠻划算的. 蘋果產品真的耐用, 到現在 6 年了機器都還很好用.

2018年5月22日 星期二

Python 學習筆記 : Selenium 模組瀏覽器自動化測試 (一)

最近從市圖借到下面這本書 :

# 不只是測試-Python 網路爬蟲王者 Slenium


Source : 金石堂


Selenium 是跨平台網站應用程式測試自動化工具, 但其用途不僅止於此, 它同時也是功能強大的瀏覽器自動化軟體, 可完全模擬人類開啟與操作瀏覽器的行為, 支援各種瀏覽器如 Chrome, Firefox, Opera, IE, Safari 等, 可實作以瀏覽器為基礎之網路爬蟲. Selenium 不僅跨平台, 跨瀏覽器, 還跨語言, 支援 Python, Java, C#, R, PHP, Javascript, Perl 等, 參考 Selenium 官網 :

https://www.seleniumhq.org
https://www.seleniumhq.org/about/platforms.jsp#browsers

讓我驚訝的是, 這本書的 4.12.2 節竟然也介紹了我最近在複習並重新整理筆記的 AutoIt, 這是以 BASIC 語法為基礎的自動化工具.

除了上面這本專著外, 下面幾本書也有介紹 Selenium 的章節 :
  1.  Python 網路爬蟲實戰 (松崗, 胡松濤) 第 8 章
  2.  Python 自動化的樂趣 (碁峰, AL Sweigart) 第 11 章
  3.  Python 程式設計實務 (博碩, 何敏煌) 第 10-3 節
  4.  Python 初學特訓班 (碁峰, 文淵閣工作室) 第 6-3, 6.4 節
本系列之前的文章參考 :

Python 學習筆記 : 安裝執行環境與 IDLE 基本操作
Python 學習筆記 : 檔案處理
Python 學習筆記 : 日誌 (logging) 模組測試
Python 學習筆記 : 資料庫存取測試 (一) SQLite
Python 學習筆記 : 資料庫存取測試 (二) MySQL

在網路爬蟲方面, Python 的第三方模組 requests, bs4 (BeautifulSoup), 以及 Scraper 的功能就已強大到可擷取絕大部分的網頁內容. 但是對於利用 Javascript 渲染 (Render) 產生的網頁而言, Python 的支援不足, 解決辦法就是利用第三方的 Selenium 套件建立一個模擬瀏覽器, 透過 Python 指令操控模擬瀏覽器來擷取網站資料.


1. 安裝 Selenium 套件 :

D:\Python\test>pip3 install selenium 
Collecting selenium
  Downloading https://files.pythonhosted.org/packages/57/bc/17164fd471ccdf0df3a992c710c0c3c47743462ff41ab72a02c6ede96e90/selenium-3.12.0-py2.py3-none-any.whl (946kB)
Installing collected packages: selenium
Successfully installed selenium-3.12.0

如果是離線安裝, 可先到 Pypi 網站下載 whl 檔後再用 pip3 安裝 :

https://pypi.org/project/selenium/#files


D:\Python\test>pip3 install selenium-3.12.0-py2.py3-none-any.whl 
Processing d:\python\test\selenium-3.12.0-py2.py3-none-any.whl
Installing collected packages: selenium
Successfully installed selenium-3.12.0


2. 下載 Chrome 驅動程式 :

上面第一步驟只是安裝 Selenium 模組而已, 要讓 Selenium 能操控瀏覽器還需要下載瀏覽器之驅動程式, 視需要操控的是 Chrome 或 FireFox 瀏覽器分別下載對應的驅動程式, Chrome 的驅動程式下載網址為 :

http://chromedriver.chromium.org/downloads
https://sites.google.com/a/chromium.org/chromedriver/downloads/

按 Latest Release 後面的超連結, 再點 chromedriver_win32.zip 超連結下載 :






將解壓縮後的 chromedriver.exe 檔複製到 Pyhton 安裝目錄下, 例如我的 Python 3 是安裝在 C:\Python36, 因此驅動程式應該放在 C:\Python36\Scripts 下 :




然後必須將 C:\Python36\Scripts 加入系統變數 Path 裡面 :




經過上面兩個程序後, 就可以用 Selenium 來操控瀏覽器了. 首先須從 Selenium 套件匯入 webdriver 模組, 然後呼叫 Chrome() 函數產生瀏覽器物件, 再呼叫瀏覽器物件的 get() 方法開啟網頁, 例如 :

Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "copyright", "credits" or "license()" for more information.
>>> from selenium import webdriver              #載入 webdriver 模組
>>> browser=webdriver.Chrome()                   #建立瀏覽器物件
>>> type(browser)                                              #傳回 WebDriver 物件
<class 'selenium.webdriver.chrome.webdriver.WebDriver'>      
>>> browser.get("http://www.google.com")    #開啟 Google 首頁

呼叫 webdriver.Chrome() 會開啟兩個視窗, 一個是執行驅動程式的 chromedriver 程式視窗, 另一個是模擬的 Chrome 瀏覽器視窗, IDLE 會停住, 等到網頁全部載入才顯示提示號 :




呼叫 browser 物件的 quit() 方法會同時關閉模擬之 Chrome 瀏覽器與驅動程式視窗 :

>>> browser.quit()                   #關閉模擬瀏覽器及其驅動程式

也可以呼叫 close() 方法, 但此方法只會關閉模擬瀏覽器而已, 驅動程式視窗則不會關掉, 因此最好還是呼叫 quit() 方法來關閉瀏覽器.

注意, 如果驅動程式 chromedriver.exe 不是放在 Scripts 子目錄下, 而是放在上一層的 Python 安裝目錄下的話, 雖然能正常開啟 Chrome, 但 chromedriver 視窗會出現如下 ERROR 訊息 :




[4152:4912:0520/201941.632:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.

DevTools listening on ws://127.0.0.1:12286/devtools/browser/bcd0ee1f-7636-45d1-b067-41729b6811e4
[4152:2044:0520/201948.345:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.
[4152:2044:0520/201949.483:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.
[4152:2044:0520/201949.923:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.
[4152:2044:0520/201950.667:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.
[4152:2044:0520/201950.955:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.
[4152:6920:0520/201951.128:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.
[4152:6920:0520/201951.363:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.
[4152:6920:0520/201951.615:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.
[4152:6920:0520/201952.443:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.
[4152:6920:0520/201952.687:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.
[4152:6920:0520/201952.858:ERROR:persistent_memory_allocator.cc(845)] Corruption detected in shared-memory segment.

只要把瀏覽器驅動程式移到在 Python 安裝目錄的 Scripts 子目錄下就不會出現 "Corruption detected in shared-memory segment" 了, 事實上所有瀏覽器驅動程式都應該放在 Scripts 目錄下, 參考 :

Chrome webdriver selenium memory error- python 3


Selenium 支援多種瀏覽器, 例如 Firefox, Chrome, Safari, Opera, IE, Edge 等等, 可用 help(webdriver) 指令查詢 :

>>>  help(webdriver)                               #查詢 Selenium 支援的瀏覽器
Help on package selenium.webdriver in selenium:

NAME
    selenium.webdriver

DESCRIPTION
    # Licensed to the Software Freedom Conservancy (SFC) under one
    # or more contributor license agreements.  See the NOTICE file
    # distributed with this work for additional information
    # regarding copyright ownership.  The SFC licenses this file
    # to you under the Apache License, Version 2.0 (the
    # "License"); you may not use this file except in compliance
    # with the License.  You may obtain a copy of the License at
    #
    #   http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing,
    # software distributed under the License is distributed on an
    # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    # KIND, either express or implied.  See the License for the
    # specific language governing permissions and limitations
    # under the License.

PACKAGE CONTENTS
    android (package)
    blackberry (package)
    chrome (package)
    common (package)
    edge (package)
    firefox (package)
    ie (package)
    opera (package)
    phantomjs (package)
    remote (package)
    safari (package)
    support (package)
    webkitgtk (package)

VERSION
    3.9.0

FILE
    c:\python36\lib\site-packages\selenium\webdriver\__init__.py

其中值得注意的是 PhantomJS, 這是一個基於 Webkit 的伺服端無使用者介面之 Javascript API, 全面支援 Web 標準 (DOM, CSS, JSON, Canvas, SVG 等), 主要用於網頁測試自動化, 網路爬蟲與網路監測, 完全不需要依靠瀏覽器. 由於無使用者介面, 故占用記憶體少, 執行速度快, 在爬行 Javascript 渲染的網頁時, PhantomJS 與 Selenium 是絕配, 參考 :

https://hiskio.com/courses/76/lectures/1883 (寫得好)

有空再來研究 PhantomJS, 以下針對常用的 Firefox, Edge, 以及 Opera 進行測試.


3. 下載 Firefox 驅動程式 :

Firefox 的驅動程式 geckodriver 下載網址如下 :

https://www.seleniumhq.org/download/
https://github.com/mozilla/geckodriver/releases




解壓縮後為 geckodriver.exe 檔, 同樣將此檔複製到 Python 安裝目錄的 Scripts 目錄下, 然後呼叫webdriver.Firefox() 方法建立瀏覽器物件, 它會同時開啟 geckodriver.exe 驅動程式與模擬的 Firefox 瀏覽器視窗, 再呼叫瀏覽器物件的 get() 方法開啟網頁, 例如 Yahoo 首頁 :

>>> from selenium import webdriver 
>>> browser=webdriver.Firefox() 
>>> browser.get("http://tw.yahoo.com") 




4. 下載 Opera 驅動程式 : 

Opera 的 WebDriver 驅動程式下載網址 :

https://github.com/operasoftware/operachromiumdriver/releases




我下載的是 64 位元版的 operadriver_win64.zip (for Opera 53 版), 解壓縮後得到 operadriver.exe, 複製到 Python 安裝目錄下的 Scripts 子目錄後呼叫 webdrive.Opera() 卻出現 "unknown error: cannot find Opera binary" 錯誤 :

>>> from selenium import webdriver 
>>> browser=webdriver.Opera() 
Traceback (most recent call last):
  File "<pyshell#28>", line 1, in <module>
    browser=webdriver.Opera()
  File "C:\Python36\lib\site-packages\selenium\webdriver\opera\webdriver.py", line 78, in __init__
    service_log_path=service_log_path)
  File "C:\Python36\lib\site-packages\selenium\webdriver\opera\webdriver.py", line 57, in __init__
    service_log_path=service_log_path)
  File "C:\Python36\lib\site-packages\selenium\webdriver\chrome\webdriver.py", line 75, in __init__
    desired_capabilities=desired_capabilities)
  File "C:\Python36\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 156, in __init__
    self.start_session(capabilities, browser_profile)
  File "C:\Python36\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 245, in start_session
    response = self.execute(Command.NEW_SESSION, parameters)
  File "C:\Python36\lib\site-packages\selenium\webdriver\remote\webdriver.py", line 314, in execute
    self.error_handler.check_response(response)
  File "C:\Python36\lib\site-packages\selenium\webdriver\remote\errorhandler.py", line 242, in check_response
    raise exception_class(message, screen, stacktrace)
selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Opera binary 
  (Driver info: OperaDriver=2.36 (bf69a2a4687a45dedaabd6155fd52bb0b7eb10c5),platform=Windows NT 6.1.7601 SP1 x86_64)

檢查 Opera 版本確實是 v53.0 :




後來在這篇文章中找到解方 (duanzhijie861229), 原來是要用 webdriver.ChromeOptions() 取得  options 物件後指定 Opera 程式的所在位置, 參考 :

"Could not find Opera binary" error in Python




>>> from selenium import webdriver 
>>> options=webdriver.ChromeOptions()   
>>> options.binary_location="C:\Program Files (x86)\Opera\launcher.exe" 
>>> driver=webdriver.Opera(options=options)       
>>> browser=webdriver.Opera(options=options)   
>>> browser.get("http://tw.yahoo.com")   

這樣就可以開啟網頁了 :




雖然可以成功開啟網頁了, 但奇怪的是, IDLE 卻停住了, 沒有顯示提示號, 所以沒辦法呼叫 quit() 來關閉瀏覽器物件, 必須等到手動關閉模擬瀏覽器視窗, 這時 IDLE 才會出現提示號, 呼叫 quit() 即關閉 Opera 驅動程式.

我覺得還是 Chrome 比較好用.


參考 :

https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/

2018年5月21日 星期一

2018 年第 20 周記事

本周 5/19, 5/20 是菁菁的國中會考, 我陪考到周日中午 12:30 考試結束, 吃過午飯才回鄉下, 順路載她去同學芸菁家. 由於爸下周四要跟阿泉伯他們家去澎湖玩, 所以也不用幫爸滷菜, 只煮了一道螞蟻上樹, 加上準備好大同無水料理鍋的材料, 如花椰菜, 高麗菜等切洗裝袋, 要用時直接取用, 約 22 分鐘即可熱食.

會考結束, 接下來就是傷腦筋的選填志願了. 菁菁的成績是校排 500 多名的中段班, 而且她很早就說不想讀大學, 不想唸一般高中, 高職畢業就想去工作了, 我深知她並非讀書的料, 因此也不強求, 只是盡量幫她複習功課而已. 但問題是她的興趣一變再變, 從最早的餐飲科, 改成觀光科, 考試這兩天又改變主意了, 說要念美容科. 因為查網路發現育英護專有美容科, 所以原先不考慮的五專現在也列入了. 但問題是學校集體報名好像結束了, 我叫她今天去學校找儀如老師問看看. 如果 OK 那育英也還蠻近的.   

菁菁的會考結束, 接下來就是二哥明年初的學測了. 好像去讀鳳中才不過是不久前的事, 一晃眼就要考大學了, 時光總是在不知不覺中滑過. 昨日下午回到鄉下, 開始整理書架上以前姊姊國中時幫她訂的 "侯老師周刊", 那是要上國一的暑假, 有人來推銷, 因我憎惡補習, 沒想要讓姊姊上升學補習班, 這個訴求在家自己複習的評量系統就這樣說服我了. 姊姊也還蠻認真地照進度做完大部分的評量, 成績也是普通, 數學, 理化比較差. 關於補習, 到了二哥與菁菁時, 我就舉手投降了. 這制度還真的不補習不行. 這是我在孩子教育上感到最抱歉的第一件事.

這套周刊我留下來準備給二哥與菁菁, 畢竟當時買了 30000 元左右. 事實上根本沒用到, 主要是教科書版本不同, 雖說內容都差不多, 但新學期都去買新的評量, 自然不會找舊的. 上個月本想找出侯老師周刊的會考評量來給菁菁做最後複習, 但沒找到 (夾在中間沒仔細找), 昨天整理時找到了, 頓時臉上三條線. 我有許多教育投資其實都買了沒用到 (套書, 小說), 真正有用的是那近 200 本的繪本故事, 地球公民 365, 以及中國歷史故事, 小鼴鼠妙妙系列等等. 有心買了卻沒時間用, 是我在孩子教育上感到最抱歉的第二件事.

我常常在想, 這種以考試為主導的記誦教學制度, 到底何時才會從國家的地平面上消失?