Windows PowerShell每周提示(36):处理自定义对象
当脚本能为你完成所有工作的时候编写脚本总是很有乐趣的。例如,假设你想要得到一份C:\Scripts文件夹内所有文件的列表,然后按照大小(Length)对这些文件进行排序。没问题,你要做的是使用Get-ChildItem及Sort-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 |
这的确有用,但不是我们心理想要的。
当然问题很明显:我们无法对打击率进行排序直到我们拥有了所有的打击率。这里的问题是我们的脚本不处理整个集合,而是每次只处理单个打击率。这不仅仅一个问题,而是一个大问题。
当然,有很多方法能解决这个窘境,这些方法都涉及到类似“二次”数据存储的概念,像数组(array),哈希表(hash table),或者也许是断开的记录集(disconnected recordset)。该想法很简单,我们需要做的是计算所有打击率,将他们存储在第二个数据存储中,随后,一旦我们拥有所有打击率,就对第二个数据存储进行排序。像我们所说的,想法很简单,但是执行起来并非总是一帆风顺的,这是因为所有这些二次数据存储都有他们自己的古怪的,反常的一面,所以将数据放入其中并不总是很简单。(有时从中取出数据也很困难。)
在此基础上,尝试将数据放入一个数组或者哈希表中似乎有点不合常理,至少在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数组对象开始:
是的,使用数组看上去是有点离经叛道。但是不要担心,我们并不打算使用这个数组来存放打击者的姓名和击球率。也就是说,该数组将不会用来存放此类信息,在我们能够以降序排列打击率之前,我们需要使用一些方法来处理信息:
取而代之的是,我们将使用这个数组来储存一些我们自行创建的自定义对象。
理解了?我们对你说过对我们的问题而言这是一个小巧且又美妙的解决方法。
在创建空数组之后,我们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)都以对象属性列出,很酷吧?
事实上也仅仅是酷。然而,无论是不是酷,这并没有解决我们的问题:我们还没有每一位选手的打击率,更不用说以降序排列这些数据。
但是这没什么,我们正打算处理这个问题。
首先,我们建立起foreach循环来遍历$colStats中的每一项:
|
foreach ($objBatter in $colStats) |
在循环内,我们使用New-Object cmdlet来创建一个崭新的名为$objAverage的对象:
|
$objAverage = New-Object System.Object |
那么$objAverage是哪种类型的对象呢?好的,这完全有我们决定,目前$objAverage实质上是一个空对象,没有任何已定义的属性。而下面这行代码将完成定义属性的工作:
|
$objAverage | Add-Member -type NoteProperty -name Name -value $objBatter.Name |
这里我们将$ObjAverage传递给Add-Member对象,该cmdlet允许我们向一个对象内添加属性。在本例中,我们添加了一个NoteProperty,并赋予属性名为Name的名称及代表Name属性的一个值,该值为$colStats集合中的第一项。换句话说,因为在$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:N3}结构来限制我们的结果值包含三位小数。
|
注意。无需多说,在格式化字符串中3是代表将值限制为三位小数。如果我们想要显示五位小数,我们只需用5替代3:{0:N5}。我们还应当注意的是,我们的打击率并不是完美的。他们都包含前导0,这不是显示打击率的传统方法。然而,摆脱前导0又是另一天的话题了。 |
那么所有这些都意味着什么?这意味着我们现在有了一个名为$objAverage的对象,该对象包含以下属性和属性值:
|
属性名 |
属性值 |
|
Name |
Ken Myer |
|
BattingAverage |
0.302 |
那么我们现在该对这个已创建的对象做些什么呢?事实上,现在我们还不能马上处理这个对象。记住,我们需要以降序排列打击率,在我们没有计算完所有的打击率之前我们还不能做排序这件事。因此我们还需要保留$objAverage一段时间。一种方法是将对象添加到一个数组中:
|
$colAverages += $objAverage |
然后回到循环顶部,我们将对文本文件中的下一位选手重复此过程。
那么一旦我们完成计算及储存所有的打击率之后呢?老实说,我们不需做太多事。事实上,我们要做的是将含有打击率的集合传递给Sort-Object cmdlet,让它来按照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有可能更简单的理由了么?不仅仅是这些,记住,因为这些是对象。我们可以使用其它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 |
再一次说明,这不是世界上最漂亮的格式,但是你已经知道如何去实现了。
下周见。
英文原文
http://www.microsoft.com/technet/scriptcenter/resources/pstips/apr08/pstip0411.mspx