.

Такие объекты как WScript, WshNetwork и Scripting::Signer являются объектами Windows Script Host (WSH), что же касается Scripting::Dictionary, то он является одним из двух ключевых объектов библиотеки Microsoft Scripting Runtime Library (scrrun.dll). Другой базовый объект данной библиотеки - хорошо знакомый нам Scripting::FileSystemObject. Учитывая распространенность последнего из этих двух объектов, кажется довольно странным, что формирующий его объект не так широко известен. Возможно, это связано с названием объекта Dictionary (Словарь), которое вводит администраторов в заблуждение, и они не используют этот объект в процессе разработки инструментальных средств.

Структура данных, сохраняемых объектом Dictionary, подобна той, что применяется для хранения данных в массивах. Однако в Dictionary, в отличие от стандартного массива, индексы не обязательно должны быть целочисленными. Например, одномерный массив в VBScript имеет индексы 0, 1, 2, ... x, где x соответствует размерности массива, определяемой функцией UBound (имя массива). Что же касается Dictionary, то он помимо этого может иметь индексы (в терминологии данного объекта называемые ключами (key)) вида a, b, c либо "кухня34", "раковина87", "хомяк999".

Основное назначение Dictionary состоит в том, чтобы создавать коллекции - наборы однородных данных, в которых далее можно организовывать процедуры поиска или выполнять какие-то другие манипуляции с данными. Объект Dictionary имеет структуру, известную в языке Perl как ассоциативный массив (associative array). Каждый элемент данных в Dictionary ассоциируется с соответствующим ключом, эти связанные структуры получили название "пара ключ-значение" (key-value pair). В принципе можно привести массу аргументов в пользу применения Dictionary в качестве альтернативы обычному одномерному массиву, но самое главное, пожалуй, то, что при работе с Dictionary отпадает необходимость в использовании оператора ReDim при добавлении в него очередного элемента данных. Кроме того, следует отметить, что объект Dictionary имеет много полезных свойств и методов, недоступных для обычных массивов. В частности, здесь можно удалять элементы данных без образования пропусков в середине коллекции, а также задействовать в качестве ключей строковые значения.

Dictionary. Свойства и методы

Объект Dictionary имеет шесть методов и четыре свойства, которые описаны в статье MSDN, доступной по адресу: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/script56/html/jsobjdictionary.asp?frame=true. Создание объекта Dictionary осуществляется следующим образом:

Dim dicTest
Set dicTest = CreateObject _

  ("Scripting.Dictionary")

Имя объектной переменной можно выбирать по своему усмотрению. Лично я предпочитаю для обозначения переменных такого типа использовать префикс dic, но это не обязательно. Итак, теперь у нас есть объект Dictionary и мы можем записать в него элемент данных. Для этого служит метод Dictionary::Add, который использует два аргумента: ключ (key) и то значение, на которое должен ссылаться данный ключ (item). Ниже показано несколько примеров:

dicTest.Add "1", _
  "server43.europe.mycorp.com"
dicTest.Add "2", _
  "server82.oceania.mycorp.com"
dicTest.Add "servername", _
  "server43.europe.mycorp.com"
dicTest.Add "serverIP", _

  "10.123.123.123"

При желании любую из этих четырех строк можно использовать в своих сценариях. В данных примерах первый аргумент (т.е. "1", "2", "servername", и "serverIP") является ключом, а второй аргумент, соответственно, значением. Если попытаться использовать в методе Dictionary::Add ключ, который уже существует, система выдаст сообщение об ошибке. Но при этом существующему ключу можно присвоить новое значение, для этого используется свойство Dictionary::Item с именем требуемого ключа. Пример приведен ниже:

dicTest.Item("servername") = _

  "server77.europe.mycorp.com"

Проверить наличие в Dictionary того или иного ключа можно с помощью метода Dictionary::Exists, используя в качестве аргумента имя интересующего ключа:

If dicTest.Exists("servername") _
Then
   ' Key exists.
Else
   ' Key doesn't exist.

End If

В данном примере с помощью метода Exists выполняется обычная проверка логического значения.

Удаление пары ключ-значение осуществляется с помощью метода Dictionary::Remove, где в качестве аргумента также используется имя соответствующего ключа:

dicTest.Remove("serverIP")

Если такого ключа нет, то генерируется сообщение об ошибке. В тех случаях, когда требуется очистить объект Dictionary от всех содержащихся в нем данных, используется метод Dictionary::RemoveAll. Пример приведен ниже:

dicTest.RemoveAll

Чтение данных из объекта Dictionary

Считывать данные из объекта Dictionary можно несколькими способами. Если требуется прочитать какое-либо конкретное значение, можно воспользоваться описанным выше свойством Item. Здесь следует отметить, что с помощью свойства Item можно как считывать, так и записывать данные в объект Dictionary. Если интересующее значение является отображаемым, то для его вывода можно воспользоваться следующей командой:

WScript.Echo _

  dicTest.Item("servername")

Как отмечалось выше, с помощью свойства Item можно как считывать, так и записывать данные, поэтому если окажется, что ключ, заданный в качестве аргумента в свойстве Item, не существует, то для него в Dictionary будет создана новая запись, в которой в поле item будет записано пустое значение. Сценарий, приведенный в Листинге 1, является примером того, как объект Dictionary реагирует на попытку считывания данных по несуществующему ключу "servername". В результате выполнения данного сценария последовательно выводятся три значения: 0, пустая строка и 1. Из данного примера видно, что перед попыткой считывания данных по несуществующему ключу объект не содержал никаких данных, а после попытки считывания в нем уже находился один элемент.

Нужно также иметь в виду, что если с помощью свойства Item требуется присвоить и ключу, и самой величине заданные значения, и окажется, что указанный ключ не существует, то в этом случае в Dictionary будет создана новая строка, содержащая как сам ключ, так и заданное значение. Подобная ситуация иллюстрируется сценарием, показанным в Листинге 2, где сначала выводится значение 0, затем в Dictionary добавляется пара ключ-значение, после чего выводится значение 1.

Использование свойства Item с соответствующим ключом удобно в тех случаях, когда требуется обратиться к одному элементу коллекции. Что касается доступа ко всей коллекции, то он может осуществляться двумя способами. Первый метод - Dictionary::Items, с его помощью можно за один шаг извлечь из Dictionary все элементы item и занести их в массив. Этот подход позволяет работать с данными полученного массива, не акцентируя внимание на ключах. В Листинге 3 показано, как можно добавить в объект Dictionary два элемента данных, а затем поместить их в массив arrDicItems. Перебор элементов полученного массива выполняется с помощью свойства Dictionary::Count, в котором содержится номер соответствующего элемента массива. Как мы знаем, массив, состоящий, скажем, из 12 элементов, имеет диапазон индексов от 0 до 11, поэтому в сценарии при переборе элементов массива из общего количества элементов должна вычитаться единица, чтобы не выйти за границы диапазона.

Второй вариант извлечения всех данных из Dictionary состоит в том, чтобы сначала извлечь в массив все ключи, а затем, перебирая их, извлекать значения, соответствующие данным ключам, с помощью свойства Dictionary::Item. В Листинге 4 показан пример извлечения ключей в массив arrDicKeys с помощью метода Dictionary::Keys. В данном сценарии отображается как сам ключ, так и соответствующее ему значение - это было сделано для того, чтобы подчеркнуть различия между двумя описанными подходами. Что касается сценария Листинга 3, то здесь отсутствуют какие-либо сведения о том, какой ключ соответствует данному значению.

Два рассмотренных подхода иллюстрируют ключевой (в прямом смысле также) момент, касающийся работы с объектом Dictionary. Речь идет о том, что, зная ключ, мы можем найти значение, однако нет простого способа найти соответствующий ключ по известному значению. Таким образом, в сценарии Листинга 4, зная значение "10.123.123.123", нам не удастся найти соответствующий ключ "serverIP". В принципе, у объекта Dictionary есть свойство Key, но, в отличие от свойства Dictionary::Item, здесь интерфейс на чтение не предусмотрен. С помощью данного свойства можно только изменять значение заданного ключа. Это делается следующим образом:

dicTest.Key("servername") = _

  "hostname"

Если указанного ключа не существует, то в результате выполнения данного оператора в Dictionary будет добавлена строка с заданным значением ключа и пустым значением item. Учитывая все сказанное, можно сформулировать следующие практические рекомендации для работы с объектом Dictionary: если вас интересуют только значения данных, но не их ключи – используйте Dictionary::Items; если же требуются как сами данные, так и соответствующие им ключи, работайте с Dictionary::Keys.

Третий способ получения полного набора данных из объекта Dictionary заключается в применении к данному объекту цикла For Each...Next. Этот алгоритм можно применить и к коллекции для последовательного извлечения из Dictionary всех ключей. Данный подход иллюстрирует сценарий, показанный в Листинге 5.

Встроенные механизмы сравнения

Завершая изучение объекта Dictionary, давайте рассмотрим еще одно свойство, которое в ряде ситуаций может оказаться весьма полезным. Если в процессе разработки используется язык VBScript, то ключи будут являться строковыми значениями. При этом в некоторых случаях может возникнуть необходимость в использовании ключей, содержащих символы в разных регистрах, например "SRV" и "srv"; "File", "FILE" и "file"; или "voID" и "VOID". Для того чтобы в подобной ситуации избежать ошибок, необходимо сделать так, чтобы объект Dictionary умел различать регистры. А объект Dictionary как раз по умолчанию является чувствительным к регистру. Если же требуется установить нечувствительный к регистру режим сравнения строк, то это можно сделать с помощью свойства Dictionary::CompareMode, которое доступно как для чтения, так и для записи. По умолчанию это свойство имеет значение 0, что соответствует двоичному (binary) режиму сравнения. Если для данного свойства установить значение 1, будет активизирован текстовый (textual) режим сравнения данных. В принципе данное свойство допускает установку значения 2 и выше, но все эти значения используются только в Microsoft Access, в сценариях же они не актуальны. Изменение значения свойства CompareMode должно выполняться после того, как соответствующий объект создан, но до начала записи в него каких-либо данных. Пример показан ниже:

Dim dicTest
Set dicTest = CreateObject _
  ("Scripting.Dictionary")
dicTest.CompareMode = _

  vbTextCompare

Если вы попытаетесь изменить значение CompareMode для непустого объекта Dictionary, то получите сообщение об ошибке. Отмечу также, что поскольку используемые здесь константы являются встроенными в VBScript, вместо значений 1 и 0 можно использовать, соответственно, vbTextCompare и vbBinaryCompare, что позволит сделать код сценария более удобным для восприятия.

Сопоставление файлов

У системных администраторов часто возникает необходимость сравнить два файла. Допустим, имеется файл номер 1, содержащий уникальные имена пользователей и компьютеров, и файл номер 2, в котором находятся уникальные имена компьютеров и соответствующие IP-адреса. Требуется найти в файле 1 те имена компьютеров, для которых в файле 2 нет информации об IP-адресах, а также те компьютеры из файла 2, для которых в файле 1 нет данных об имени пользователя. Или, например, такая ситуация: имеется два файла, каждый из которых содержит сотни, или даже тысячи строк данных, состоящих из ключей и соответствующих им значений. При этом известно, что в принципе они содержат одинаковые значения, но данные никак не отсортированы, к тому же возможны пропуски данных, что еще более усложняет процедуру сравнения. Давайте посмотрим, какими средствами мы располагаем для решения подобных задач.

Так как вторую проблему решить проще, начнем с нее. К сожалению, нельзя просто взять и отсортировать содержимое файлов, а затем выполнить их построчное сравнение, потому что как только в одном из файлов обнаружится отсутствующая строка, при переходе к следующей строке будет сгенерировано сообщение об ошибке. В принципе данную ситуацию можно обойти, создав соответствующую процедуру, которая при обнаружении отсутствующей строки будет ее игнорировать и переходить к обработке следующей. Другой вариант состоит в том, чтобы считать содержимое обоих файлов и преобразовать их в соответствующие массивы, после чего, организовав циклические процедуры для каждого из массивов, выполнить для каждого элемента массива 1 проверку, существует ли такой элемент в массиве 2. Однако если мы воспользуемся возможностями объекта Dictionary, это позволит нам существенно упростить код.

Итак, в первую очередь необходимо создать два объекта Dictionary и заполнить их информацией, содержащейся в файлах, которые в рассматриваемом случае имеют формат данных, разделяемых запятой (CSV). После этого для каждого элемента данных (item) в Dictionary 1 выполняется проверка его наличия в Dictionary 2 и наоборот. Полученные в обоих случаях результаты стандартным образом записываются в выходной файл. Текст соответствующего сценария приведен в Листинге 5. В самом начале сценария объявляются три константы, две из которых описывают входные файлы .csv, а третья – формируемый текстовый файл результатов. Здесь же объявляются две стандартные константы для чтения и записи в файлы, которые используются при работе с объектом FileSystemObject. Далее объявляются необходимые переменные, после чего сценарий создает объект FileSystemObject и открывает три файла – два для чтения, один для записи. Затем по очереди создаются два объекта Dictionary и, соответственно, две циклических процедуры While...Wend, в ходе выполнения которых происходит построчное считывание содержимого каждого из входных файлов до тех пор, пока не будет достигнут его конец. Переменная strLine используется для временного хранения данных считанной строки. Разделение строки данных на ключ и значение выполняется с помощью оператора Split – встроенной функции VBScript, использующей в данном случае символ запятой в качестве разделителя. Полученные таким образом ключ и значение используются в качестве аргументов для метода Dictionary::Add. По завершении каждого из циклов сценарий закрывает соответствующий исходный файл. Теперь, когда оба объекта Dictionary заполнены необходимыми данными, можно приступать к сравнению их содержимого.

Сначала выполняется обработка объекта dicData1 циклом For Each...Next. В ходе его выполнения считывается значение каждого ключа, которое затем записывается в переменную strKey. После этого с помощью метода Dictionary::Exists выполняется проверка, существует ли данный ключ в dicData2. Если в dicData2 такой ключ найден не был, то в выходной файл результатов записывается строка, в которой указывается, что данный ключ существует только в dicData1. Если этот ключ есть и в dicData1, и в dicData2, внутри цикла выполняется сравнение значений полей item, для чего вызывается метод Dictionary::Item с переменной strKey в качестве аргумента. Если значения совпадают, то никаких действий не предпринимается, если же выясняется, что они различны, то этот факт также регистрируется в файле выходных данных.

После того как первый цикл выполнит для каждого ключа объекта dicData1 проверку, существует ли такой ключ в dicData2, запускается еще один цикл. Он проверяет, имеются ли в dicData2 такие ключи, которые отсутствуют в dicData1. Если выясняется, что ключ есть и в dicData1, и в dicData2, тогда для него внутри цикла выполняется сравнение значений полей item, для чего вызывается метод Dictionary::Item с переменной strKey в качестве аргумента. В конце сценария для каждого из входных файлов выполняется обращение к свойству Dictionary::Count для записи в выходной файл информации о количестве содержащихся в них строк, после чего этот файл закрывается. В дальнейшем выходной файл может обрабатываться любым способом в зависимости от необходимости – например, можно распечатать содержащиеся в нем данные. Для проверки работы описанного сценария можно воспользоваться двумя тестовыми входными файлами небольшого объема.

С помощью сценария, показанного в Листинге 6, можно решить первую из двух задач, сформулированных в самом начале этого пункта. В данном примере файл Users.csv содержит пары "имя пользователя" и "имя компьютера", а в файле Clients.csv находятся пары, состоящие из имени компьютера и соответствующего IP-адреса. За исключением того, что здесь используются другие имена переменных (например, dicUsers вместо dicData1), в целом структура циклов For...Each похожа на ту, которую мы видим в сценарии Листинга 5. Но поскольку в данном случае ключевое поле dicClients является полем item для dicUsers, в циклы внесены изменения, учитывающие этот нюанс. В ходе выполнения первого цикла происходит последовательная запись каждого имени пользователя (strKey) и имени компьютера (strItem) в объект Users и выполняется проверка, существует ли данный элемент strItem в качестве ключа в объекте Clients. Если его там нет, то этот факт регистрируется в выходном файле. Второй цикл работает с объектом Clients. Здесь нужно выявить соответствие между каждым клиентом (strKey) и полем item в объекте Users. Процедура, проверяющая, соответствует ли данному ключу какой-либо элемент item в объекте Users, использует метод Dictionary::Items, с помощью которого формируется массив, содержащий все элементы item. После этого все элементы созданного массива просматриваются в цикле с целью обнаружения совпадения какого-либо из них со значением переменной strKey. Если совпадение найдено, то логической переменной bolFound присваивается значение TRUE (изначально ей было присвоено значение FALSE). Если же после того как цикл сопоставил все элементы из Clients с содержимым объекта Users, переменная bolFound по-прежнему имеет значение FALSE, в файл выходных данных будет сделана запись о том, что компьютер с этим именем имеется только в списке Clients.

В заключение приведу краткую статистику: по моим наблюдениям, использование объекта Dictionary дает примерно восьмикратный выигрыш во времени по сравнению с обработкой стандартных массивов. А если мы работаем с данными объемом в десятки тысяч строк, разница будет особенно заметна.


Листинг 1: DicItem1.vbs

Dim dicTest
Set dicTest = CreateObject("Scripting.Dictionary")
WScript.Echo dicTest.Count
WScript.Echo dicTest.Item("servername")
WScript.Echo dicTest.Count

Листинг 2. DicItem2.vbs

Dim dicTest
Set dicTest = CreateObject("Scripting.Dictionary")
WScript.Echo dicTest.Count
dicTest.Item("servername") = _
  "server43.europe.mycorp.com"
WScript.Echo dicTest.Count

Листинг 3. DicItems.vbs

 
Dim dicTest, arrDicItems, intCount
Set dicTest = CreateObject("Scripting.Dictionary")
 
dicTest.Add "servername", _
  "server43.europe.mycorp.com"
dicTest.Add "serverIP", "10.123.123.123"
 
arrDicItems = dicTest.Items
 
For intCount = 0 to dicTest.Count - 1
      WScript.Echo arrDicItems(intCount)
Next

Листинг 4. DicKeys.vbs

 
Dim dicTest, arrDicKeys, intCount
Set dicTest = CreateObject("Scripting.Dictionary")
 
dicTest.Add "servername", _
  "server43.europe.mycorp.com"
dicTest.Add "serverIP", "10.123.123.123"
 
arrDicKeys = dicTest.Keys
 
For intCount = 0 to dicTest.Count - 1
      WScript.Echo arrDicKeys(intCount) & _
  vbCrLf & dicTest.Item(arrDicKeys(intCount))
Next

Листинг 5. CompareFile.vbs

 
Option Explicit
 
Const INPUT_FILE_1 = "C:Scriptsinput1.csv"
Const INPUT_FILE_2 = "C:Scriptsinput2.csv"
Const OUTPUT_FILE = "C:Scripts
esults.txt"
 
Const ForReading = 1
Const ForWriting = 2
 
Dim dicData1, dicData2, strKey, fso, ts
Dim filInput1, filInput2, filResults, strLine
 
Set fso = CreateObject("Scripting.FileSystemObject")
 
Set filInput1 = _
  fso.OpenTextFile(INPUT_FILE_1, ForReading)
Set filInput2 = _
  fso.OpenTextFile(INPUT_FILE_2, ForReading)
Set filResults = _
  fso.OpenTextFile(OUTPUT_FILE, ForWriting, True)
 
Set dicData1 = CreateObject("Scripting.Dictionary")
While Not filInput1.AtEndOfStream
      strLine = filInput1.ReadLine
      dicData1.Add Split(strLine,",")(0), Split(strLine,",")(1)
Wend
filInput1.Close
 
Set dicData2 = CreateObject("Scripting.Dictionary")
While Not filInput2.AtEndOfStream
      strLine = filInput2.ReadLine
      dicData2.Add Split(strLine,",")(0), Split(strLine,",")(1)
Wend
filInput2.Close
 
For Each strKey In dicData1
      If Not dicData2.Exists(strKey) Then
            filResults.WriteLine strKey & ": only in Dictionary1"
      Else
            If dicData2.Item(strKey) <> dicData1.Item(strKey) Then
                filResults.WriteLine strkey & _
                  ": is in both dictionaries but values differ"
            End If
      End If
Next
For Each strKey In dicData2
      If Not dicData1.Exists(strKey) Then
            filResults.WriteLine strKey & ": only in Dictionary2"
Next
 
filResults.WriteLine "Lines counted in file1 " & _
  "(" & dicData1.Count & ") and file2 (" & dicData2.Count & ")."
filResults.Close

Листинг 6. CompareUserClient.vbs

Option Explicit
 
Const INPUT_FILE_1 = "C:ScriptsUsers.csv"
Const INPUT_FILE_2 = "C:ScriptsClients.csv"
Const OUTPUT_FILE = "C:Scripts
esults.txt"
 
Const ForReading = 1
Const ForWriting = 2
 
Dim dicUsers, dicClients, strKey, fso, ts, strItem
Dim filInput1, filInput2, filResults, strLine, bolFound
 
Set fso = CreateObject("Scripting.FileSystemObject")
 
Set filInput1 = _
  fso.OpenTextFile(INPUT_FILE_1, ForReading)
Set filInput2 = _
  fso.OpenTextFile(INPUT_FILE_2, ForReading)
Set filResults = _
  fso.OpenTextFile(OUTPUT_FILE, ForWriting, True)
 
Set dicUsers = CreateObject("Scripting.Dictionary")
While Not filInput1.AtEndOfStream
      strLine = filInput1.ReadLine
      dicUsers.Add Split(strLine,",")(0), Split(strLine,",")(1)
Wend
filInput1.Close
 
Set dicClients = CreateObject("Scripting.Dictionary")
While Not filInput2.AtEndOfStream
      strLine = filInput2.ReadLine
      dicClients.Add Split(strLine,",")(0), Split(strLine,",")(1)
Wend
filInput2.Close
 
For Each strKey In dicUsers
      strItem = dicUsers.Item(strKey)
      If Not dicClients.Exists(strItem) Then
        filResults.WriteLine strKey & ": only in Users"
Next
For Each strKey In dicClients
      bolFound = FALSE
      For Each strItem in dicUsers.Items
            If strKey = strItem Then bolfound=TRUE
      Next
      If Not bolFound Then
        filResults.WriteLine strKey & ": only in Clients"
Next
 
filResults.WriteLine "Lines counted in users " & _
  "(" & dicUsers.Count & ") and clients " & _
  "(" & dicClients.Count & ")."
filResults.Close