本週 Windows PowerShell 秘訣

Office Space

這是使用 Windows PowerShell 的快速秘訣。只要有新的秘訣,我們每週就會在這裡發佈。如果您想要分享任何秘訣,或有任何疑問,請讓我們知道

您可以在本週 Windows PowerShell 秘訣封存找到更多秘訣。

處理自訂物件

當指令碼為您執行所有工作時,編寫指令碼總是非常有樂趣。例如,假設您想要取得資料夾 C:\Scripts 中所有檔案的清單,然後依大小 (Length) 將檔案排序。沒問題;您要做的只是讓 Get-ChildItemSort-Object cmdlet 為您完成所有工作:

Get-ChildItem C:\Scripts | Sort-Object Length

生命非常美好,不是嗎?

嗯,有時候是沒錯啦!很不幸地,事情並不是一直都這麼有趣,或是這麼簡單。例如,假設您擁有下列文字檔 (C:\Scripts\Test.txt),這是包含棒球統計資料的檔案:

Name,AtBats,Hits
Ken Myer,43,13
Pilar Ackerman,28,11
Jonathan Haas,37,17
Syed Abbas,41,20
Luisa Cazzaniga,22,6
Andrew Cencini,35,11
Baris Cetinok,19,4

您需要對這個檔案做的就是計算每一位打者的打擊率 (您可以將每一位打者的命中數除以其打擊數),然後將這些平均值依降冪順序排序。那聽起來有點簡單;我們不是只要讀取資料後,並計算每一位打者的打擊率就好了:

$colStats = Import-CSV C:\Scripts\Test.txt
foreach ($objBatter in $colStats)
  {
$objBatter.Name + " {0:N3}" -f ([int] $objBatter.Hits / $objBatter.AtBats)
  }

嗯!可以的,大致上是這樣。那個方式將明確提供我們每一位打者的打擊率;不過,它「不能」為我們將那些平均值排序。我們會得到下面的結果:

Ken Myer 0.302
Pilar Ackerman 0.393
Jonathan Haas 0.459
Syed Abbas 0.488
Luisa Cazzaniga 0.273
Andrew Cencini 0.314
Baris Cetinok 0.211

真好用,但結果出乎意料!

問題當然很明顯:在擁有「所有的」平均值之前,我們無法將平均值排序。那是個問題,因為我們的指令碼無法處理整個集合;它每次只能處理單一打擊率。那不只是個問題,還是個大問題。

當然,有很多方法可以解決這個困境,這些方法都包含某種「次要」資料儲存機制,例如一個陣列、雜湊表或者可能是中斷連接資料錄集。這個觀念很簡單:我們要做的是計算所有打擊率,將它們儲存在次要資料儲存機制中,只要我們有「所有的」平均值,就立即將該資料儲存機制排序。如同我們所說,這個觀念很簡單,但是執行起來並不直接;因為所有這些次要資料儲存機制都有它們自己的怪異的習慣,不一定很容易將資料放入其中。(有時候也會很難將資料從其中「取出」。)

其中首要的是,嘗試將資料硬塞到陣列或雜湊表似乎有一點詭異,至少對 Windows PowerShell 來說很奇怪。畢竟,PowerShell 的主要功能不是讓您處理「物件」。如果我們將此資訊儲存在一組物件中,然後處理這些物件而不是某種次要資料儲存機制,是否會好一點呢?

沒錯,就是這樣:

$colAverages = @()

$colStats = Import-CSV C:\Scripts\Test.txt

foreach ($objBatter in $colStats)
  {
$objAverage = New-Object System.Object
$objAverage | Add-Member -type NoteProperty -name Name -value $objBatter.Name
$objAverage | Add-Member -type NoteProperty -name BattingAverage -value ("{0:N3}" -f ([int] $objBatter.Hits / $objBatter.AtBats))
$colAverages += $objAverage
  }

$colAverages | Sort-Object BattingAverage -descending

承認吧!第一眼看到這個指令碼時,可能沒那麼令人印象深刻。但是請等一等;如同您即將看到的,它實際上是這個問題的某種精巧的解決方案。

自訂物件指令碼的一開始,建立稱為 $colAverages 的空白陣列:

$colAverages = @()

是的,我們「確實」說過使用陣列似乎有一點詭異。但是請不要擔心;我們不打算使用這個陣列來儲存打擊者的名稱與打擊率;也就是我們的陣列不是用來保存這類資訊,這類資訊在依降冪順序排序打擊率之前,必須先以某種方式分開:

Ken Myer;0.302

反而,我們將使用這個陣列來儲存一些自行建立的自訂物件。

看到沒?我們告訴您這是此問題的精巧解決方案。

在建立空白陣列之後,我們使用 Import-CSV cmdlet 來讀入文字檔 C:\Scripts\Test.txt,然後將其內容儲存在稱為 $colStats 的變數。順便一提,Import-CSV 是容易被忽視的 cmdlet。只要您的文字檔有標頭行 (我們的文字檔就有),Import-CSV 就會將逗號分隔值檔案中的每個項目匯入成個別物件,而物件也會有明確定義的屬性。看一下我們將 Import-CSV 傳回的資料,傳送到 Get-Member cmdlet 得到的結果:

Name        MemberType   Definition
----        ----------   ----------
Equals      Method       System.Boolean Equals(Object obj)
GetHashCode Method       System.Int32 GetHashCode()
GetType     Method       System.Type GetType()
ToString    Method       System.String ToString()
AtBats      NoteProperty System.String AtBats=43
Hits        NoteProperty System.String Hits=13
Name        NoteProperty System.String Name=Ken Myer

看一下清單底部:我們檔案標頭中參照的三個欄位 (AtBats、Hits 以及 Name) 都列為物件屬性。酷吧?

不論如何,那「真的」很酷。不過,不論是否很酷,它並沒有解決問題:我們甚至沒有每一位打者的打擊率,更別提將那些平均值依降冪順序排序的能力。但沒關係;我們只是要點出問題而已。

首先,我們設定帶我們完成 $colStats 中每個項目的 foreach 迴圈:

$objAverage = New-Object System.Object

那麼,$objAverage 是何種物件呢?嗯!那完全由我們決定;當 $objAverage 本來是空白物件時,並沒有任何已定義的屬性。所以要使用下一行程式碼:

$objAverage | Add-Member -type NoteProperty -name Name -value $objBatter.Name

我們在此將 $objAverage 傳送到 Add-Member cmdlet,這個 cmdlet 可以讓我們將屬性新增到物件。在此情況下,我們新增一個 NoteProperty,將此屬性命名為 Name,並為集合 $colStats 中的第一個項目新增代表 Name 屬性值。換句話說,因為 $colStats 中第一個物件的 Name 等於 Ken Myer,表示 $objAverage 的 Name 也等於 Ken Myer。

但這是「真正」酷的部分;在下一行程式碼中,我們建立稱為 BattingAverage 的屬性,然後指定該屬性為打者的打擊率:

$objAverage | Add-Member -type NoteProperty -name BattingAverage -value ("{0:N3}" -f ([int] $objBatter.Hits / $objBatter.AtBats))

無可否認地,那是某種看起來很沈重的程式碼;因為我們對打擊率套用了一些格式。尤其是,我們對打擊率做兩件事。首先,我們使用語法 [int],確定 PowerShell 以整數來處理打者的打擊數 ($objBatter.Hits) 以及打者的擊中數 ($objBatter.AtBats)。如果我們沒有明確將這些值宣告為整數,PowerShell 會將它們視為字串來處理,我們就會得到下列錯誤訊息:

Method invocation failed because [System.String] doesn't contain a method named 'op_Division'.
At C:\scripts\test.ps1:11 char:88
+     $objAverage | Add-Member -type NoteProperty -name BattingAverage -value ($objBatter.Hits / <<<<  $objBatter.AtBats)

並非我們發生這種情況,而刻意引起您的注意。它是... 我們的... 朋友所發生的。

接著,我們還使用 .NET Framework 格式化語法 "{0:N3}" –f,將產生的值限制為小數點三位數;如果沒有此格式化,我們將取得如下的打擊率:

0.210526315789474

打擊率「一定」是使用小數點三位數來表示;因此,我們使用語法結構 {0:N3} 將打擊率限制為小數點三位數。

不用說,格式化字串中的 3,是用來將數值限制為小數點三位數。如果想要顯示五位小數點,只要用 5 取代 3 就可以了:{0:N5}。我們可能還應該注意到打擊率並不完美;它們前面都含有零,這絕「不是」傳統顯示打擊率的方式。不過,去除前面的零是改天的主題。

因此這代表什麼意思呢?這表示我們現在有個稱為 $objAverage 的物件,此物件包含下列屬性及屬性值:

屬性名稱

屬性值

Name

Ken Myer

BattingAverage

0.302

既然已經建立此物件,我們該如何處理它呢?實際上,我們現在無法對它做太多事,我們需要將平均值依降冪順序排序,但尚未計算所有平均值之前,卻無法完成這項任務。因此,我們稍後需要另外設定 $objAverage;一種簡單的方法是將物件新增至陣列:

$colAverages += $objAverage

然後回到迴圈最上方,對文字檔中的下一位打者重複此程序。

因此,一旦計算並排序所有打擊率之後,我們要做什麼呢?老實說,我們不必做太多事。事實上,我們只要取得打擊率的集合,將它們傳送到 Sort-Object cmdlet,要求 Sort-Object 將資料依 BattingAverage 屬性降冪順序排序即可:

$colAverages | Sort-Object BattingAverage -descending

那樣對我們有任何好處嗎?當然是有好處:

Name                                               BattingAverage
------                                                    -------
Syed Abbas                                                  0.488
Jonathan Haas                                               0.459
Pilar Ackerman                                              0.393
Andrew Cencini                                              0.314
Ken Myer                                                    0.302
Luisa Cazzaniga                                             0.273
Baris Cetinok                                               0.211

再者,這不是對付這種問題的唯一方法;不過,它似乎比其他多數方法簡單。真的,雜湊表「可能」更簡單,我們只要追蹤兩個項目:Name 與 BattingAverage。但是,我們為何要追蹤 Name、BattingAverage、At Bats 以及 Hits 呢?老實說,處理多個項目,對雜湊表來說相當容易。在此情況下,您可能需要使用中斷連接資料錄集,它需要的程式碼類似於下列程式碼,正好可以解決這件事:

$adVarChar = 200
$MaxCharacters = 255
$adFldIsNullable = 32
$adDouble = 5

$DataList = New-Object -com "ADOR.Recordset"
$DataList.Fields.Append("Name", $adVarChar, $MaxCharacters, $AdFldIsNullable)
$DataList.Fields.Append("BattingAverage", $adDouble, $Null, $AdFldIsNullable)
$DataList.Open()

瞭解我們為何決定使用自訂物件與 Add-Member cmdlet 可能比較簡單的原因了吧?

如果那還不夠,請記住,因為這些「是」物件,我們可以將它們搭配其他 Windows PowerShell cmdlet 使用。例如,我們可以將此資訊傳送到 Measure-Object cmdlet:

$colAverages | Measure-Object BattingAverage -minimum -maximum -average |
Select-Object Minimum, Maximum, Average

接著,Measure-Object 將提供我們如下的資訊:

Minimum                Maximum                        Average
          -------                -------                        -------
0.210526315789474       0.48780487804878              0.348569480651885

再者,雖然這不是世界上最漂亮的格式,但是您大概瞭解我們的意思了吧!

大伙兒下週見。