摘要:说明脚本编写者检索存储于 Microsoft Excel 电子表格中的信息以及利用该信息在 Microsoft Active Directory 中创建新的用户账户的方法。
系统管理员(或我们通常听说的具有相同职能的人员)是最后的执著于晦涩的人机交互界面的人。他们不使用鼠标和下拉列表,而是在命令行键入命令;他们不屑于图形用户界面,而喜欢在命令窗口中显示数据。系统管理员不会将车停在快餐店的送餐窗口(以便店员将餐点直接送入车内),他们会下车自行料理。但他们仍然不得不做某些必要的事情。我要说的是,您可以在命令提示符处运行脚本,并传递若干命令行参数,此外,您还可以将数据输出到命令窗口中或文本文件中。还可能需要完成其他什么功能?
实际情况是,在相当多的场合下,您不会再需要其他任何功能。另一方面,有时(请注意,仅仅是有时)在面对诸如 Microsoft® Excel 之类的应用程序时,系统管理员会发出这样的感慨,如果能够在自己的工作中使用 Excel 的某些功能该多好啊。例如,要在 Microsoft® Active Directory® 中创建大量新的用户账户。能够通过向脚本传递命令行参数达到这一目的吗?当然可以,如果您不在意键入与下面内容类似的命令字符串的话:
cscript new_user.vbs /cn="Myer Ken" /sAMAccountName="kenmyer" /givenName="Ken" /SN="Myer"看起来这不是一个好的解决方法。能不能从文本文件中读取所有信息呢?肯定可以。是的,比如可以这样安排文本文件的内容:
"CN","sAMAccountName","givenName","SN""Myer Ken","kenmyer","Ken","Myer""Jones TiAnna","TiAnnajones","TiAnna","Jones""Smith Ben","Bensmith","Ben","Smith"
这样做肯定有效,但您需要具有足够好的运气才能期望人力资源部向您提供这样的文本文件。您具备这些技术吗?只要您希望,就肯定能够具备。但对于人力资源部(或其他任何人)来说,在 Excel 中键入所有信息要简单得多,实际上这对提升他们所完成的工作的质量也是一个好机会。是的,Excel 是图形界面的应用程序,并且您可能习惯于使用晦涩的字符界面。但您只需在这里使用一次。试想一下,如果能够在一个步骤中就获得一个 Excel 电子表格,并针对它运行脚本程序,然后使用从此电子表格中收集的信息创建全部的用户账户,这不是很美妙的事情吗?
好了,对您可能不是这样。但假如您有一个朋友,他不象您这样热衷于晦涩的字符界面,那您就可以告诉他,他很幸运。本月我们要向您,不,我们要向您的朋友们介绍使用脚本直接读取 Excel 电子表格中的数据,然后使用这些数据在 Active Directory 中创建全部的新用户账户的方法。而下个月我们将向他们说明执行与此相反的操作的方法:如何从 Active Directory(或其他位置)获取数据,并将其显示在预先安排好格式的电子表格中。请注意,如果您决定尝试使用这些脚本,我们保证不会向您的系统管理员同事透露此事,真的。
我们从最简单的脚本开始介绍,这个脚本创建一个 Microsoft Excel 的实例,并向该实例添加一个新的工作簿:
Set objExcel = CreateObject("Excel.Application") objExcel.Workbooks.Add这很简单,是吧?将该脚本保存为 .vbs 文件并运行,运行后,您的桌面应与下图相似(当然,如果使用的墙纸不同,桌面也会不同):
图 1:创建可见的 Excel 实例
试想一下,您刚刚开始使用脚本,且已经编写了一个脚本,只是这个脚本什么也不做。但您已经是一个合格的 Microsoft 脚本编程人员了!
别着急,先等一下,现在看来,您在编写 Microsoft® Office 应用程序脚本时,您所见到的(或未见到的)并不是您得到的。前面的脚本是起了作用,您也确实创建了 Microsoft Excel 的新的实例。(如果您不相信,可以按下 Ctrl-Alt-Del 组合键,然后查看 Microsoft® Windows® 任务管理器中的“进程”选项卡。应该能够看到 Excel.exe 的一个实例。)这可以看作是您的第一节正式的 Microsoft Office 脚本编程课程:默认情况下,在任何时候使用脚本创建 Microsoft Office 应用程序的实例,该应用程序都将在屏幕上不可见的窗口中运行。Excel 其实仍然存在,只是您无法看到它。(这一点有点像他们说的您获得的最后一个)
现在,无论您相信与否,奇妙的事确实发生了。这是一个真正的、活生生的 Microsoft Excel 实例。您马上就会看到,可以以编程的方式读取其中的数据,从这个角度讲,您还可以执行许多在 Excel 中无法完成的操作。当 Excel 在可见的窗口中运行时,您唯一的损失就是失去了通过键盘键入内容,从而使应用程序响应击键操作这一功能。而这正是默认行为起作用的地方。假设您正在运行一个脚本程序,该脚本程序将使用 Excel 创建一个报表,我们还假设在脚本运行期间,Excel 始终处于可见状态。用户(甚至您本人)可能会无意间按下键盘上的某一个按键,从而毁掉整个报表。用户(甚至您本人)可以仅通过关闭 Excel 就达到不仅毁掉报表,而且使脚本崩溃的目的。(因为脚本会尝试向已经不存在的 Excel 实例发送命令。)以可见的方式运行 Excel 就可以避免这类问题的发生。所以,在 Microsoft Excel 中运行脚本与照看自己的孩子无异:即眼不见为净。
提示:既然如此,那么如果希望 Excel 在屏幕上可见,应采取什么措施?很简单,将 Visible 属性设置为 True 即可。下面的小脚本将创建 Microsoft Excel 的一个实例,并且如做魔术一般将其显示在屏幕上:
Set objExcel = CreateObject("Excel.Application") objExcel.Workbooks.Add objExcel.Visible = True作为介绍 Microsoft Office 脚本编程的第 2 课,我们对此脚本进行修改,使它能够创建一个 Excel 实例,并在屏幕上显示此实例,在暂停 10 秒钟后退出。(在某些方面,这与我典型的工作日中的操作极其相像。)下面是经过修改的脚本:
Set objExcel = CreateObject("Excel.Application") objExcel.Workbooks.Add objExcel.Visible = True Wscript.Sleep 10000 Wscript.Echo "脚本执行完毕。"这个脚本在运行时进行了什么操作呢?该脚本将创建一个 Excel 实例并显示在屏幕中。在经过 10 秒钟的暂停之后,会出现一条消息,提示您脚本已执行完毕。单击“确定”后,脚本将立即终止(Microsoft® VBScript 执行到脚本末尾后,脚本将终止)。
您的屏幕将显示下图中的内容:
图 2:脚本终止后的 Excel
你可能已经注意到,脚本终止后 Microsoft Excel 仍在运行,也就是说,脚本终止后 Excel 并未终止。这就是 Microsoft Office 脚本编程正式课程的第二课:Microsoft Office 应用程序的“进程外”运行。这是什么含义?是这样,有些 COM 对象(比如,FileSystemObject)与脚本在同一个进程中运行。也就是说,脚本所在进程终止后,在该进程中运行的 COM 对象也将终止运行(这就是在同一个进程中运行的含义)。脚本进程终止后,FileSystemObject 也将终止。
您不相信吗?下面我将为您证实。编写一段脚本,用来创建 FileSystemObject 的一个实例,打开任务管理器后,运行该脚本。脚本运行后,您会注意到只创建了一个新进程(Cscript.exe 或 Wscript.exe,取决于所使用的脚本宿主)。这是因为脚本和 FileSystemObject 在同一个进程中运行。
请停止运行,我们下面编写一段 FileSystemObject 脚本:
Set objFSO = CreateObject("Scripting.FileSystemObject") Set objFolder = objFSO.GetFolder("C:\") Wscript.Sleep 10000 Wscript.Echo "脚本已运行完毕。"现在编写一段用来创建 Microsoft Excel 的一个实例的脚本,打开任务管理器,然后运行此脚本。这时应该能够看到新增了两个进程:Wscript.exe(或 Cscript.exe)和 Excel.exe。这是因为 Excel 在自己的进程中运行。脚本运行结束后,脚本进程(Wscript.exe 或 CScript.exe)将消失,但 Microsoft Excel 进程(Excel.exe)将继续存在。
这很重要:如果您在 Excel 中不专门执行退出操作, Excel.exe 将持续运行(并继续占用内存)。如果运行 5 个脚本,每个脚本都创建一个 Excel 实例,那会是什么情况?在这种情况下,任务管理器应与下图类似:
图 3:任务管理器中的多个 Excel 实例
看起来有些不妙,不过这倒是快速耗尽计算机所有可用资源的好办法。请注意:终止脚本的运行不会使 Microsoft Office 应用程序自动终止。
看样子要束手无策了,是吗?别失望。您要终止一个 Excel 实例吗?只需要确保在脚本中的某处执行 Quit 命令就可以终止此实例。例如,下面的脚本创建将创建一个 Excel 实例,暂停 10 秒钟后,使用 Quit 命令关闭 Excel,再暂停 10秒钟后,自动终止运行。如果在打开任务管理的情况下运行此脚本,您将会看到系统创建了两个新进程,即 Wscript.exe(或 Cscript.exe)和 Excel.exe,经过短时间的暂停之后,将会看到 Excel.exe 和脚本宿主进程先后消失。
Set objExcel = CreateObject("Excel.Application") objExcel.Workbooks.Add objExcel.Visible = True Wscript.Sleep 10000 objExcel.Quit Wscript.Sleep 10000提示:有时您会发现脚本编写者将对象引用设置为 Nothing,就象下面这样:
Set objExcel = CreateObject("Excel.Application") objExcel.Workbooks.Add objExcel.Visible = True Wscript.Sleep 10000 Set objExcel = Nothing该语句用来释放对象引用(即 objExcel 将不再指向 Microsoft Excel 的实例),但它不会终止 Excel 的运行,实际上,Excel.exe 将继续运行,就好像任何事情都没有发生,因为确实没有发生任何事情。如果希望关闭 Microsoft Excel(这里指在脚本中),就必须使用 Quit 命令。
现在我们已经了解了创建 Microsoft Excel 实例以及立即终止此实例的方法。可能已经能够满足部分读者的要求。如果是这样,您完全可以拿起衣服直接回家。多谢您一直参与我们的讨论。但另外一些读者可能还希望执行一些更出奇的操作,比如说,读取电子表格中的数据。
要达到这一目的,需要做三件事:
1. |
创建 Microsoft Excel 实例。我们已经掌握了这部分内容。 |
2. |
打开要操作的电子表格。 |
3. |
读取数据。 |
现在转到步骤 2,看一看我们能否打开电子表格。在 Excel 对象模型(有关详细信息,请参阅 MSDN 中的 Excel Object Model Overview(英文))中,电子表格包含在 Workbooks 对象中。无论您是否相信这点,通过演示的第一个脚本,您对此已有所了解。您使用下面这行代码向集合中添加了一个新工作簿:
objExcel.Workbooks.Add要打开电子表格,我们需要创建一个 Workbooks 集合的实例,然后使用 Open 方法打开电子表格。听起来很复杂,但具体到能够创建 Excel 实例的 CreateObject 的调用,只需要写以下区区两行代码:
Set objExcel = CreateObject("Excel.Application") Set objWorkbook = objExcel.Workbooks.Open("C:\Scripts\New_users.xls")下面是具体的步骤:首先创建一个 Excel 实例,然后以黑体显示的那行代码将打开 C:\Scripts\New_users.xls 文件。如果要打开其他的 .xls 文件怎么办?那也没有问题。只需将 C:\Scripts\New_users.xls 替换为相应的文件路径。当然有一点是不言自明的,那就是如果在 C:\Scripts 文件夹中并不存在名为 New_users.xls 的文件,那么脚本就不会起作用。如果您的计算机中没有安装 Excel,那么脚本也不会起作用。(如果您认为仅仅通过运行这个脚本就可以避免购买 Microsoft Office,那么很抱歉,让您失望了。)
注意:有没有打开电子表格的其他的编程方法?有。实际上,我们这个专题中的许多操作都可以用其他的方法完成。但出于时间和版面的考虑,我们将以最容易被初学者接受的方法来编写 Excel 脚本。如果您希望了解能够完成相同任务的其他的方法,请转到 Excel object model documentation(英文)。
现在我们了解了打开电子表格的方法,在尝试检索电子表格中的数据之前,我们先讨论一下电子表格的布局。对 Excel 编程可以完成一些相当令人惊奇的工作。例如,您可以打开对其一无所知的电子表格,根据自己的需要对其操作,并随时得到其中的任何信息。这样当然好,但还需要编写很多代码才能实现。所以在创建电子表格时,我们将采取一种变通的方式,我也建议您在创建自己的新用户电子表格时,采取同样的方法:
• |
我们假设第 1 行用来存放指示用户各方面信息的列标题。 |
• |
假设个人用户的有关数据存放在第二行以后的区域。 |
• |
我们还假设数据中不存在空白行。也就是说,如果在第 2 行存放了用户 A 的有关数据,我们将不越过第 3 行以在第 4 行存放用户 B 的有关数据,而是将用户 B 的有关数据存放在第 3 行。 |
• |
我们将确保 mandatory 属性在第 1 列。mandatory 属性是每个新建的对象必须具备的,对于用户账户对象来说,它表示 CN 和 sAMAccountName。如果这样做(也就是说,CN 在第 1 列),那么每个用户在这一列都一定有相应的信息。如果第 13 行的第 1 列没有任何内容,就可以认为已经到达数据的末尾。(或意味着在输入数据时出现错误。但这样的事比较少见) 反过来,如果在第 1 列放置可选的属性(比如,telephoneNumber),那么第 13 行的第 1 列为空并不能说明什么:它可能表示已经到达数据末尾,但也可能意味着相应的用户没有电话号码。 |
以下是经过精简的用于创建用户账户的电子表格的布局:
图 4:用于创建用户账户的电子表格的示例
仔细查看这个示例电子表格,会发现其中包含四行四列。还会发现新用户的 CN 数据存放在第 1 列,SAM Account Name 存放在第 2 列,名字存放在第 3 列,人名中的姓存放在第 4 列。
注意:与前面的例子类似,在电子表格中,可能会将列标记为 A、B、C 和 D,而不是 1、2、3 和 4。这没有关系,脚本在运行时会忽略这一差别。但如果认为以数字标记列标题更方便,则可以执行以下操作:在 Excel 中单击“工具”,然后单击“选项”。在“选项”对话框的“常规”选项卡中,选中“R1C1 引用样式”,然后单击“确定”。请看,列标题的显示现在由名称变为了数字。至少在 Office 2003 和 Office XP 中是这样。如果使用的是以前版本的 Office,操作可能会有些不同。
那么我们的第一个用户的有关数据在哪里呢?可以利用 RF1C1 标记列出如下:
CN | SAM Account Name | First Name | Last Name |
R2C1 |
R2C2 |
R2C3 |
R2C4 |
请注意其中的格式。行编号相同,列编号顺次改变。如果是电子表格中的头两个用户,那又会怎样?
CN | SAM Account Name | First Name | Last Name |
R2C1 |
R2C2 |
[R2C3 |
R2C4 |
R3C1 |
R3C2 |
R3C3 |
R3C4 |
唯一的不同在于用户 1 的数据显示在第 2 行,用户 2 的数据显示在第 2 行的下一行。这表明我们可以通过读取 R2C1 到 R2C4 的单元格,获取所有用户 1 的有关数据。如果希望得到下一个用户的信息,应该怎样做呢?我们需要做的就是下移一行,读取该行中第 1 列到第 4 列单元格中的信息。要继续获取下一个用户的信息,只需继续下移到其他行。确实很简单,是吧。
到目前为止,我们已了解了获取电子表格中所有数据的方法,但还不够完全,因为还要掌握一些内部信息。我们还不知道电子表格的行数,但已经知道以下内容:
• |
第一个数据行是第 2 行(第 1 行是标题行) |
• |
数据只存在于第 1 列到第 4 列的单元格中。(我们还知道每一列所包含的数据的类别。) |
• |
如果我们在第一列遇到空的单元格,那我们肯定位于数据末尾。 |
如何能够不写一行代码就获得电子表格中的数据?可以这样做:
以第 2 行第 1 列为起点,获取用户的 CN 数据。然后分别获取第 2 行第 2 列 (SAM Account Name)、第 2 行第 3 列 (first name) 和第 2 行第 4 列 (last name) 中的数据。
然后下移到第 3 行第 1 列,获取下一个用户的 CN 数据。如果下一个用户没有 CN 数据,那又会如何(即该单元格为空时怎样办)?如果是这样,那么我们的工作就完成了,我们已经取得了全部数据,可以进行下一步了。如果该单元格中有数据,就读取它,然后检索第 2 列、第 3 列和第 4 列的数据,之后下移到第 4 行,再次重复执行此过程。
看起来很简单,是吧?更令人惊奇的是,实际的代码也很简单。以下是一个用来读取 Excel 电子表格中全部数据的脚本,它还能够显示从每个单元格中读出的值。先看一下这段代码,然后再讨论它执行的操作:
Set objExcel = CreateObject("Excel.Application") Set objWorkbook = objExcel.Workbooks.Open _ ("C:\Scripts\New_users.xls") intRow = 2 Do Until objExcel.Cells(intRow,1).Value = "" Wscript.Echo "CN: " & objExcel.Cells(intRow, 1).Value Wscript.Echo "sAMAccountName: " & objExcel.Cells(intRow, 2).Value Wscript.Echo "GivenName: " & objExcel.Cells(intRow, 3).Value Wscript.Echo "LastName: " & objExcel.Cells(intRow, 4).Value intRow = intRow + 1 Loop objExcel.Quit前面已经讨论过头两行代码,所以请看下面这行:intRow = 2。正如您所想, intRow 是一个变量,它代表电子表格中的当前行。为什么将 intRow 设置为 2,而不是设置为 1?很简单,因为第一列中的内容是列标题。数据实际上从第 2 行开始。
再往下我们创建了一个 Do Loop 循环结构,它将一直运行到单元格 (intRow, 1) 中的值为空为止。这是什么意思呢?当我们启动脚本时,intRow 等于 2,之后的操作是检查第 2 行第 1 列位置处的单元格中的值,如果为空,即未输入 CN 数据,那么程序将退出此循环结构。这时我们认为已经到达数据集的末尾。但如果第 2 行第 1 列位置处的单元格中有数据,那会怎样?毫无疑问,程序将进入循环结构,并运行其中的代码。
很不巧,本样例脚本所进行的操作只是回显从第 1、第 2、第 3 和第 4 列的单元格中读取的数据,稍后将演示执行其他更有趣的操作的方法。由于这是第一次向您剖析 Excel 脚本编程,还是越简单越好。
再往下会怎样呢?(我知道您已经急不可耐了,是吗?)是这样,首先使 intRow 加 1,为什么这样做呢?请记住,最初进入循环结构时,intRow 被设置为 2,用来确保获取第 2 行的所有数据。当这一操作执行完毕后,我们希望获取第 3 行的所有数据,intRow 变量加 1 后,其值将由 2 变为 3,为读取第 3 行的数据做好了准备。第 3 行的数据读取完毕后,intRow = intRow + 1 语句将变量 intRow 的值变为 4,程序进入下一次循环,读取第 4 行的数据。很有意思吧?
然后程序回到 Do Until 语句,检查 (intRow, 1)(目前代表第 3 行,第 1 列)位置处的单元格中的内容是否为空。如果不为空,程序将再次进入循环,读取第 3 行的数据;如果为空,程序将退出循环,转到最后一行代码,这行代码将终止Excel 的运行。
如果针对前面的电子表格示例运行此脚本,将得到与下面的内容相似的输出结果:
CN: Myer Ken sAMAccountName: KenMyer GivenName: Ken LastName: Myer CN: Jones TiAnna sAMAccountName: TiAnnaJones GivenName: TiAnna LastName: Jones CN: Smith Ben sAMAccountName: BenSmith GivenName: Ben LastName: Smith还能够执行什么有趣的操作?
既然问题已经提出,我姑且认为使用这些数据创建用户账户是一个满意的答案,您认为怎样?好,我们就从电子表格中读取部分数据,然后使用这些数据创建一些新的用户账户。听起来有些复杂,但您可能不相信,我们已经完成了其中难度最大的部分。下面只需要再添加几行用于创建账户的 ADSI 代码即可。
本专栏不打算讨论 ADSI。有关 ADSI 的详细信息,请参阅 ADSI SDK(英文)或 Windows 2000 Scripting Guide(英文)。下面只需按照我说的操作,以下代码将在 fabrikam.com 的 Finance OU 中创建用户账户:
Set objOU = GetObject("ou=Finance, dc=fabrikam, dc=com") Set objUser = objOU.Create("User", "cn=Myer Ken") objUser.sAMAccountName = "myerken" objUser.GivenName = "Ken" objUser.SN = "Myer" objUser.AccountDisabled = FALSE objUser.SetInfo下面要做的就是将这些代码与用于读取 Excel 电子表格中的数据的脚本结合起来。那该怎么做呢?对于初学者,只需用从电子表格中读取的相应数据替换脚本中 ADSI 部分的硬编码值。以下面这行 ADSI 代码为例:
objUser.GivenName = "Ken"我们知道可以在所查找的每一行的第 3 列获取用户的名(GivenName 属性),那么我们要做的就是用电子表格中相应位置处的值替换硬编码值 "Ken":
objUser.GivenName = objExcel.Cells(intRow, 3).Value然后使用上面设置用户账户属性值的语句替换原来的脚本中用于将内容回显到屏幕的语句。现在将 Excel 脚本与 ADSI 脚本结合起来,看一下执行的结果:
Set objExcel = CreateObject("Excel.Application") Set objWorkbook = objExcel.Workbooks.Open _ ("C:\Scripts\New_users.xls") intRow = 2 Do Until objExcel.Cells(intRow,1).Value = "" Set objOU = GetObject("ou=Finance, dc=fabrikam, dc=com") Set objUser = objOU.Create _ ("User", "cn=" & objExcel.Cells(intRow, 1).Value) objUser.sAMAccountName = objExcel.Cells(intRow, 2).Value objUser.GivenName = objExcel.Cells(intRow, 3).Value objUser.SN = objExcel.Cells(intRow, 4).Value objUser.AccountDisabled = FALSE objUser.SetInfo intRow = intRow + 1 Loop objExcel.Quit这种方法太简单了!前面的 Excel 脚本用于向屏幕回显数值:
Wscript.Echo "LastName: " & objExcel.Cells(intRow, 4).Value现在我们所做的就是用相应的 ADSI 命令替换 Wscript.Echo:
objUser.SN = objExcel.Cells(intRow, 4).Value就这么简单!
说实话,上面只是举了一个相当简单的例子:只有四个属性,并且在同一个 OU 中创建所有用户账户。如果还要包括其他属性(部门、电话号码、主目录,等等),那么只需在电子表格中创建相应的列,然后添加相应的代码行即可。例如,如果在第 5 列的单元格中存放用户的电话号码,那么可以向脚本中添加以下代码行:
objUser.telephoneNumber = objExcel.Cells(intRow, 5).Value如果要在 Finance OU 以外的位置创建用户账户,又该怎样呢?同样很容易。将 OU 名称置于某一列的单元格中(比如,置于第 6 列的单元格中),然后修改用于连接到希望的 OU 的代码行:
Set objOU = GetObject _ ("ou=" & objExcel.Cells(intRow, 6).Value & _ ", dc=fabrikam, dc=com")还有什么比这更加神奇的呢?
可事情总会这么顺利吗?总的来说是这样。但也有例外。毕竟 ADSI 确实还存在一些问题(比如,在处理多属性值和位屏蔽值方面)。如果需要设置多值属性或位屏蔽属性,那么这些特定代码行不会仅仅像如下所示那么简单:
objUser.otherTelephone = objExcel.Cells(intRow, 10).Value此外您还必须添加以下代码:
objUser.PutEx ADS_PROPERTY_UPDATE, "otherTelephone", Array(objExcel.Cells(intRow, 10).Value)但这不会太麻烦。有关详细信息,请参阅ADSI SDK(英文)和Windows 2000 Scripting Guide(英文)。
下个月我们将向您说明向 Excel 电子表格中写入数据的方法。如果您有相关的问题或看法,请发送邮件至 scripter@microsoft.com。不必担心,因为在脚本中使用 Excel 并不意味着您必须开始饮用 Chai 茶或使用带花边的桌布,您仍然可以选择以前简朴的方式。虽然 Chai 茶的味道可能要好一些...