嗨,Scripting Guy!與您的烤麵包機保持連線

Microsoft Scripting Guy

程式碼下載:HeyScriptingGuy2008_09.exe(150 KB)

如果要用幾個字總括描述我們所處的現代世界,下面四個字可以說相當貼切:保持連線。拜手機問世之賜,您再也不必待在家中苦候別人的電話,因為他們隨時隨地都能聯絡到您 (是呀,真方便哪)。而要感謝無線運算,您就算不在辦公室,也可以隨時辦公事,因為您可以在家裡、在海灘,或是在您想得到的任何地方工作。

真人真事:Scripting Editor 的爸媽最近去了一趟露營之旅,可是卻無法連上營地的無線網路,只好被迫將就度日 (就像偉大的探險家 Lewis 和 Clark 一樣),幸好衛星電視還能用,真是謝天謝地!

不僅如此,GPS 裝置還可以讓您精確知道您所在的位置 (不出幾英呎的差距),有的裝置甚至可以讓別人精確知道您所在的位置 (所謂「跑得了一時,藏不了一世」這句古語,用來形容現今的情況,真是再貼切不過了)。如果撰寫這篇專欄的 Scripting Guy 想要的話,還可以設定在每次支票過戶時讓支票帳戶打電話通知他;同樣的,也可以設定讓他的車每月以電子郵件寄送進度報告給他。不只如此,烤麵包機還可以在他外出渡假時,提供蹓狗和澆花的服務。

好啦,最後那項服務也許太誇張了 — 未來可難說了。不過如果想要的話,Scripting Guy 可以購買一部能夠連接網際網路的烤麵包機,然後在回家的路上打電話給烤麵包機,請烤麵包機準備好熱騰騰的吐司等他進門。是不是真的會有人想要在進門時看到熱騰騰的吐司,我們不知道啦,不過,想要的話…

當然嘍,如果每個人都要保持連線的話,那麼當那些從不追逐流行的 Scripting Guy 主張多多斷線時,不必太過驚訝。難道這表示 Scripting Guy 建議您丟掉手機或膝上型電腦嗎?當然不;就算 Scripting Guy 比手機或膝上型電腦還聰明,他們也不會這麼說。他們是建議您在指令碼集合中加上離線式資料錄集。不過,如果您要丟掉手機或膝上型電腦,我們也不會阻止您的啦。

注意根據 Harris Interactive 所做的調查,43% 的美國人曾在度假時,使用膝上型電腦來查看並傳送與工作有關的電子郵件。而超過 50% 的美國人在度假時,會用手機查看電子郵件和/或語音郵件。這還不包括 40% 已經超過一年沒放假的美國人在內。

不用說也知道,一定有很多人非常樂意在指令碼集合中加入離線式資料錄集,但問題是:他們根本不知道什麼是離線式資料錄集。好吧,為了讓您熟悉這個概念,我就簡單介紹一下。離線式資料錄集可以說是一個資料庫資料表,但它並沒有連到真正的資料庫,而是由指令碼建立,只存在於記憶體中,指令碼一結束,它就跟著消失。換句話說,離線式資料錄集是一個虛構的資料結構,只存在幾分鐘就跟著資料一起消失了。嘩!聽起來真的很好用耶,Scripting Guy。多謝了!

好啦,我們承認啦,離線式資料錄集聽起來沒那麼吸引人,而且也真的不吸引人,不過有時候真的相當實用。經驗老道的 VBScript 編寫人員很清楚,VBScript 的資料排序功能並非世界無敵 (除非您認為沒有一個資料排序功能稱得上世界無敵)。同樣的,VBScript 處理大量資料的功能也很有限,它不能處理 Dictionary 物件 (它會限制您使用最多具有兩個屬性的項目) 或陣列 (主要限於單屬性資料清單),嗯 … 就這兩個。

但是離線式資料錄集可以解決這兩個問題 (還有其他問題)。您需要排列資料,尤其是排列多屬性資料嗎?沒問題,就像我們說的,離線式資料錄集是虛擬的資料庫資料表,沒有什麼比排列資料庫資料表更容易的事了 (好吧,如果您還想雞蛋裡挑骨頭的話,那麼不排列資料庫資料表,當然是比排列資料庫資料表容易啦)。要不然,您需要追蹤一大組多屬性的項目嗎?沒問題,我們不是說過離線式資料錄集是虛擬的資料庫資料表嗎?您需要以某種方式篩選那些資訊,或者在那些資料中搜尋某個特定值嗎?啊,要是有辦法可以使用虛擬資料庫資料表就好了 …

說得好,也許該是示範給您看的時候了 (如果我們知道該示範什麼就好了)。好,假設 [圖 1] 是我們從 MLB.com 網站搜尋到的棒球統計資料,它們是儲存在以 Tab 鍵分隔各值的 C:\Scripts\Test.txt 中。

[圖 1] 儲存在以 Tab 鍵分隔各值的檔案中的統計資料

球員 全壘打數 RBI 平均值
D Pedroia 4 28 .276
K Kouzmanoff 8 25 .269
J Francouer 7 35 .254
C Guzman 5 20 .299
F Sanchez 2 25 .238
I Suzuki 3 15 .287
J Hamilton 17 67 .329
I Kinsler 7 35 .309
M Ramirez 12 39 .295
A Gonzalez 17 55 .299

看起來很不錯。不過,如果我們要的是用全壘打數排列的球員清單呢?離線式資料錄集能夠幫我們做到這樣的事嗎?答案馬上就揭曉;請見 [圖 2]。看到一大堆程式碼了嗎?不過別擔心,等一下就知道它們其實沒這麼恐怖。

[圖 2] 離線式資料錄集

Const ForReading = 1
Const adVarChar = 200
Const MaxCharacters = 255
Const adDouble = 5

Set DataList = CreateObject("ADOR.Recordset")
DataList.Fields.Append "Player", _
  adVarChar, MaxCharacters
DataList.Fields.Append "HomeRuns", adDouble
DataList.Open

Set objFSO = _
  CreateObject("Scripting.FileSystemObject")
Set objFile = objFSO.OpenTextFile _
  ("C:\Scripts\Test.txt", ForReading)

objFile.SkipLine

Do Until objFile.AtEndOfStream
    strStats = objFile.ReadLine
    arrStats = Split(strStats, vbTab)

    DataList.AddNew
    DataList("Player") = arrStats(0)
    DataList("HomeRuns") = arrStats(1)
    DataList.Update
Loop

objFile.Close

DataList.MoveFirst

Do Until DataList.EOF
    Wscript.Echo _
        DataList.Fields.Item("Player") & _
        vbTab & _
        DataList.Fields.Item("HomeRuns")
    DataList.MoveNext
Loop

我們先定義四個常數:

  • ForReading。這個變數是用來開啟文字檔,以及從文字檔讀取。
  • adVarChar。這是標準的 ADO 常數,用來建立使用 Variant 資料型別的欄位。
  • MaxCharacters。這是標準的 ADO 常數,用來指出 [變數] 欄位能容納的字元數上限 (此處是 255)。
  • adDouble。最後一個 ADO 常數,用來建立使用 double (數值) 資料型別的欄位。

定義常數之後,就會看到下面這個程式碼區塊:

Set DataList = CreateObject _
    ("ADOR.Recordset")
DataList.Fields.Append "Player", _
    adVarChar, MaxCharacters
DataList.Fields.Append "HomeRuns", _
    adDouble
DataList.Open

這是我們真正設定離線式資料錄集的指令碼部分。首先我們必須建立一個 ADOR.Recordset 物件的執行個體;不用說,此舉會建立一個虛擬資料庫資料表 (也就是離線式資料錄集)。

接著再使用這行程式碼 (以及 Append 方法),在資料錄集加入一個新欄位:

DataList.Fields.Append "Player", adVarChar, MaxCharacters

您瞧,一點都不複雜:只不過呼叫 Append 方法,還有三個參數而已:

  • 欄位的名稱 (球員)。
  • 欄位的資料型別 (adVarChar)。
  • 欄位所能儲存的字元數上限 (MaxCharacters)。

加上 [球員] 欄位之後,可以再加上第二個欄位:[全壘打數],其資料型別是數值 (adDouble)。完成這個工作之後,接下來就是呼叫 Open 方法,宣告離線式資料錄集已經開啟,並且可以用於商業用途了。

接著再建立 Scripting.FileSystemObject 物件的執行個體,然後開啟檔案 C:\Scripts\Test.txt。這個部分的指令碼其實與離線式資料錄集無關;只不過因為我們需要從文字檔擷取資料,才會加入這個部分。文字檔的第一行包含標頭資訊:

Player     Home Runs     RBI        Average

離線式資料錄集並不需要這項資訊,所以開啟檔案之後的第一件事,就是呼叫 SkipLine 方法,跳過第一行:

objFile.SkipLine

移到真正資料的第一行之後,接下來就是設定 Do Until 迴圈,這個迴圈是設計成逐行讀取剩下的部分。每當我們讀取檔案的一行時,就會把該值儲存在一個名叫 strLine 的變數中,然後用 Split 函數將這一行轉換成數值陣列 (每遇到一個定位點,就會斷開那一行):

arrStats = Split(strStats, vbTab)

沒錯,我們只是簡單帶過,但是我們假設大部分看這篇文章的人,應該都知道如何從文字檔擷取資訊。長話短說,第一次執行迴圈時,陣列 arrStats 包含 [圖 3] 所示的項目。

[圖 3] 陣列的內容

項目號碼 項目名稱
0 D Pedroia
1 4
2 28
3 .276

現在我們已經準備好來點樂子了:

DataList.AddNew
DataList("Player") = arrStats(0)
DataList("HomeRuns") = arrStats(1)
DataList.Update

我們要在離線式資料錄集加上 1 號球員 (D Pedroia) 的相關資訊。要在資料錄集加上一筆記錄,必須先呼叫 AddNew 方法,以建立一筆新的空記錄供我們使用。接著再使用程式碼的下兩行,將這些值指派給兩個資料錄集欄位 ([球員] 和 [全壘打數]),再呼叫 Update 方法,正式將該筆記錄寫到資料錄集當中。最後再回到迴圈開頭,針對文字檔中的下一行 (球員) 重複執行這道程序。看到沒?也許還是有一大堆程式碼,不過現在看起來整齊清爽多了。

那麼,當所有球員都加入離線式資料錄集之後會怎麼樣呢?等我們關閉文字檔之後,再來執行這個程式碼區塊:

DataList.MoveFirst

Do Until DataList.EOF
  Wscript.Echo _
    DataList.Fields.Item("Player") & _
    vbTab & _
    DataList.Fields.Item("HomeRuns")
  DataList.MoveNext
Loop

我們在第 1 行使用 MoveFirst 方法,將游標定位在離線式資料錄集的開端;如果不這麼做,可能只會顯示離線式資料錄集的一部分資料。接著就是設定 Do Until 迴圈,這個迴圈會持續執行,直到用完資料為止 (也就是到達離線式資料錄集的檔案結尾為止 — 屬性為 True)。

在迴圈裡面,我們只要回應 [球員] 和 [全壘打數] 欄位的值 (請注意,用來指出特定欄位的語法有點奇特:DataList.Fields.Item("Player")。最後再呼叫 Move­Next 方法,進入資料錄集的下一筆記錄就行了。

不說也知道,相當簡單吧。等該做的都做了之後,就會得到下面這個結果:

D Pedroia       4
K Kouzmanoff    8
J Francouer     7
C Guzman        5
F Sanchez       2
I Suzuki        3
J Hamilton      17
I Kinsler       7
M Ramirez       12
A Gonzalez      17

您瞧,很不錯吧!好啦,仔細想想,其實也沒那麼棒啦。就算我們取得球員姓名和全壘打總數,還是沒有把全壘打總數排好順序。真是的,離線式資料錄集怎麼沒幫我們排好資料順序啊?

其實原因是:我們根本沒告訴資料錄集我們想要依照哪一個欄位來排序。不過這很容易改正:只要修改指令碼,先加入排序資訊,接著再呼叫 MoveFirst 方法就行了。換句話說,讓那一段程式碼看起來像這樣:

DataList.Sort = "HomeRuns"
DataList.MoveFirst

很清楚,沒什麼花招,只不過是把 [全壘打數] 欄位指派給 Sort 屬性罷了。讓我們看看執行程式碼的結果:

F Sanchez       2
I Suzuki        3
D Pedroia       4
C Guzman        5
J Francouer     7
I Kinsler       7
K Kouzmanoff    8
M Ramirez       12
J Hamilton      17
A Gonzalez      17

好多了。不過,還有一個問題:通常全壘打總數是以遞增順序列示,全壘打數最多的球員排在第一個。有沒有什麼辦法可以用遞增順序排列離線式資料錄集呢?

當然有,只要加上好用的 DESC 參數就行了,請看下面:

DataList.Sort = "HomeRuns DESC"

那 DESC 參數能幫我們做什麼呢?說得好:

A Gonzalez      17
J Hamilton      17
M Ramirez       12
K Kouzmanoff    8
I Kinsler       7
J Francouer     7
C Guzman        5
D Pedroia       4
I Suzuki        3
F Sanchez       2

順帶一提,您可以依照多個屬性排序;只要指派每一個屬性給排列順序就行了。舉個例說,假設您想要先依照全壘打數,再依照 RBI 排序。沒問題,下面這個命令就可以幫您做到:

DataList.Sort = "HomeRuns DESC, RBI DESC"

您不妨試試看,親身體驗一下。雖然沒有度假時查看電子郵件那麼好玩,不過也不會差太多啦。

注意請記住,還沒加到資料錄集的欄位,不可以用來排列順序。這是什麼意思呢?這意思是,您在加入像 RBI 這樣的屬性到 Sort 陳述式之前,必須在指令碼的適當位置中加入下列幾行:

DataList.Fields.Append "RBI", adDouble

DataList("RBI") = arrStats(2)

如果要查看結果,也必須修改 Wscript.Echo 陳述式:

Wscript.Echo _
  DataList.Fields.Item("Player") & _
  vbTab & _
  DataList.Fields.Item("HomeRuns") & _
  vbTab & DataList.Fields.Item("RBI")

讓我們看看,離線式資料錄集還能做什麼?喔,有了:假設我們擷取所有球員的所有資訊,然後依照打擊率來排序 (除了別的意思之外,這表示我們必須修改原始指令碼,建立名叫 [RBI] 和 [打擊率] 的欄位)。結果如下所示:

J Hamilton      17      67      0.329
I Kinsler       7       35      0.309
A Gonzalez      17      55      0.304
C Guzman        5       20      0.299
M Ramirez       12      39      0.295
I Suzuki        3       15      0.287
D Pedroia       4       28      0.276
K Kouzmanoff    8       25      0.269
J Francouer     7       35      0.254
F Sanchez       2       25      0.238

很好,但萬一我們只是要安打數 .300 或 .300 以上的球員名單呢?我們該如何做到只顯示符合某特定條件的球員呢?這個嘛,其中一個方法就是在資料錄集指定篩選器:

DataList.Filter = "BattingAverage >= .300"

資料錄集篩選器的功能相當於資料庫查詢:它提供一個機制,規定只傳回離線式資料錄集所有記錄的其中一部分。在這個案例中,我們只要規定篩選器只保留 [打擊率] 欄位值大於等於 .300 的記錄,其他全部去掉就行了。您猜結果如何?篩選器的表現完全符合我們的要求:

J Hamilton      17      67      0.329
I Kinsler       7       35      0.309
A Gonzalez      17      55      0.304

要是我們家小朋友也能這麼聽話就好了。

對了,您可以在一個篩選器內使用多項條件。比方說,下面這個命令就規定只傳回 [打擊率] 欄位值大於等於 .300,而 [全壘打數] 欄位值大於 10 的記錄:

DataList.Filter = _
  "BattingAverage >= .300 AND HomeRuns > 10"

相較之下,這個篩選器則是規定只傳回 [打擊率] 欄位值大於等於 .300,或 [全壘打] 欄位值大於 10 的記錄:

DataList.Filter = "BattingAverage >= .300 OR HomeRuns > 10"

試試看,您就會明白哪裡不一樣。哎呀,我在鬼扯什麼:您也可以試試下面這個篩選器,就當作好玩吧:

DataList.Filter = "Player LIKE 'I*'"

結果顯示,您也可以在篩選器裡使用萬用字元。方法是使用 LIKE 運算子 (而不是等號),然後再使用星號,就像您在執行一個像 dir C:\Scripts\*.txt 的 MS-DOS® 命令時一樣。在前面的範例中,我們會得出一份姓名以 I 開頭的球員名單;那是因為我們所用的語法是:「請列出一份 [球員] 欄位值以 I 開頭,後面是什麼都無所謂的所有記錄」。不妨試 — 好啦,您想也知道我要說什麼了。

對了,您也沒有陷入像 0.309 這種打擊率的陷阱 (通常表示打擊率的數字都會省略前面的 0,例如 .309)。不過無所謂,您可以只用 FormatNumber 函數,以您要的任何古老方法設定打擊率的格式:

FormatNumber (DataList.Fields.Item("BattingAverage"), 3, 0)

顯示這個數字時,只要在 Wscript.Echo 陳述式中加入這個函數即可 (或者,也可以把輸出指派給一個變數,再將該變數置於您的 Echo 陳述式中):

Wscript.Echo _
  DataList.Fields.Item("Player") & _
  vbTab & _
  DataList.Fields.Item("HomeRuns") & _
  vbTab & DataList.Fields.Item("RBI") & _
  vbTab & _
  FormatNumber _
  (DataList.Fields.Item("BattingAverage"), _
   3, 0)

好玩吧?

不過,看來這個月已經沒時間了。簡單說就是 — 抱歉,電話響了。

不管怎樣,我們想提的是 — 喔,太好了,連手機也響了。我們剛剛收到一封從烤麵包機傳來的電子郵件。重要事項:熱騰騰的吐司已經準備好了,要奶油還是果醬呢?好啦,我們該走了,不過下個月還會再見!

Dr. Scripto 的指令碼謎題 (Scripting Perplexer)

這個每月一次的挑戰不僅測試您的解謎功力,更要測試您的指令碼技巧。

2008 年 9 月:指令碼搜尋

下面是一個簡單 (也許沒那麼簡單啦) 的字詞搜尋。請從清單中找出所有的 VBScript 函數和陳述式。但有個地方要注意:其餘的字母會拼出一個隱藏字詞,而那個字詞剛好就是 — 您猜猜看 — 一個 Windows PowerShell™ 指令程式!

字詞清單:Abs、Array、Atn、CCur、CLng、CInt、DateValue、Day、Dim、Else、Exp、Fix、InStr、IsEmpty、IsObject、Join、Len、Log、Loop、LTrim、Mid、Month、MsgBox、Now、Oct、Replace、Set、Sin、Space、Split、Sqr、StrComp、String、Timer、TimeValue、WeekdayName。

fig08.gif

答案是:

Dr. Scripto 的指令碼謎題 (Scripting Perplexer)

答案是:2008 年 9 月:指令碼搜尋

puzzle_answer.gif

The Scripting Guy 為 Microsoft 做事,也就是受雇於 Microsoft。他們在不玩、不教或不看棒球 (以及其他各種活動) 的時候,就負責管理 TechNet 指令碼中心。請造訪他們的網站:www.scriptingguys.com