PowerShell: улучшаю преобразование данных из хеш-таблицы в XML-RPC

Feb 17, 2023 03:39

Ранее в этой серии постов:
...
16. PowerShell и ЖЖ: аутентификация challenge-response, ч.3 (практика через интерфейс «XML-RPC»)
17. PowerShell и ЖЖ: аутентификация с «cookie», интерфейс «flat»
18. PowerShell: улучшаю преобразование данных из XML-RPC в хеш-таблицу

Ранее я написал функцию toXML для преобразования хеш-таблицы в формат XML-RPC и использовал эту функцию при описании аутентификации «challenge-response» на веб-сервере ЖЖ («Живого Журнала») через интерфейс «XML-RPC». Вот код этой функции на языке PowerShell (я использую программу-оболочку «PowerShell» версии 7 в операционной системе «Windows 10»):

function toXML($hashT) {
$xml = ''
$xml += ""
$xml += "LJ.XMLRPC." + $hashT["mode"] + ""
$xml += "

"
foreach ($key in $hashT.Keys) {
if ($key -ne "mode") {
$xml += ""
$xml += "" + $key + ""
$type = ($hashT[$key]).GetType().FullName
if ($type -eq "System.Int32") {$type = "int"} else {$type = "string"}
$xml += "<$type>" + $hashT[$key] + ""
$xml += ""
}
}
$xml += ""
$xml += ""
return $xml
}

Понятно, что эту функцию я написал на скорую руку для демонстрации аутентификации «challenge-response» на веб-сервере ЖЖ. Мне сразу было понятно, что в будущем ее понадобится расширить и переписать. Я написал вопрос по этому поводу на веб-сервисе для задавания вопросов в области программирования «Stack Overflow»: получилась интересная дискуссия. Там мне сделали несколько важных замечаний по коду этой функции и посоветовали несколько подходов. Вдохновившись этими замечаниями, я переписал эту функцию.

Проблема множественного использования операторов + и +=

Началось всё с того, что один из пользователей сайта «Stack Overflow» отметил, что строка (объект класса «System.String») в программе-оболочке «PowerShell» (как и во многих других языках программирования) является неизменяемым (по-английски «immutable») объектом. Поэтому множественное применение для строк операции конкатенации (сложения) инициирует чрезмерное использование памяти компьютера и занимает в десятки раз больше времени, чем альтернативные способы.

Это замечание касается применения операторов + и += для строк в моем коде. Из-за того, что существующая строка в языке PowerShell не может быть изменена, при сложении строк создается новая строка-результат, в которую переписывается содержимое строк-слагаемых. Эта проблема, которая может приводить к уменьшению производительности программы, описана в документации на сайте компании «Microsoft».

Существует множество альтернативных способов сложения строк, которые считаются более производительными. По ссылке на документацию, приведенную в предыдущем абзаце, рекомендуют рассмотреть способ сложения строк с помощью оператора -join или с помощью класса «System.Text.StringBuilder», который реализует версию изменяемой (mutable) строки (кроме этих способов существует еще целый ряд других).

Надо ли бездумно заменять в программах операторы + и += сложения строк на альтернативы?

Многие начинающие программисты, увидев в учебнике или какой-нибудь статье описание очередного прогрессивного принципа, сразу бросаются применять этот принцип везде и всюду. Однако, у каждого принципа обычно имеется своя ограниченная область применения. Микроскоп может быть прекрасен для наблюдения за микромиром, но забивать им гвозди хоть и можно, но нецелесообразно.

В той области, для которой я собираюсь применять описанную выше функцию toXML, не потребуется использование слишком большого количества сложений строк. По уже приведенной ранее ссылке на документацию при иллюстративных замерах производительности разных способов сложения строк используется число операций сложения в 10000 (десять тысяч). Это явно не мой случай. Поэтому я спокойно могу пользоваться описанным выше кодом, на практике его «ущербность» у меня никак не проявляется: для используемых мною небольших хеш-таблиц число операций сложения строк не идет даже на сотни.

Кроме этого, следует учитывать улучшившийся за последние десятки лет интеллект компиляторов (или интерпретаторов). Перед компиляцией (или в случае интерпретаторов - запуском) кода компилятор (или интерпретатор) производит оптимизацию кода. Например, следующий фрагмент кода из нашей функции:

$xml = ''
$xml += ""

может быть преобразован (если компилятор/интерпретатор достаточно умён) перед компиляцией в следующий код:

$xml = ''

Таким образом, в нашей программе, запускаемой на выполнение, число сложений неизменяемых строк будет уменьшено (такая оптимизация возможна, если компилятор видит в коде множество примыкающих друг к другу операций сложения строк). То есть в моем коде программа остается читабельной (именно для читабельности кода я использую множество примыкающих друг к другу сложений строк), а умный компилятор этот код оптимизирует и поэтому нет необходимости использовать альтернативные способы сложения строк.

О необходимости осмотрительного использования класса «System.Text.StringBuilder», кстати, сказано в его документации, цитата (это только фрагмент, для полного пояснения смотрите оригинал):

Consider using the String class under these conditions:

- When the number of changes that your code will make to a string is small. In these cases, StringBuilder might offer negligible or no performance improvement over String.

- When you are performing a fixed number of concatenation operations, particularly with string literals. In this case, the compiler might combine the concatenation operations into a single operation.

Использование текстовых шаблонов

Когда я писал вышеприведенную версию функции toXML, я интуитивно понимал, что в данном случае можно как-то работать с текстовыми шаблонами, но я не знал, как на языке PowerShell можно организовать эту работу. В упомянутом выше обсуждении на сайте «Stack Overflow» мне показали, как это можно делать с помощью оператора форматирования -f (есть и другие способы). Например, вместо следующего кода:

$xml += "LJ.XMLRPC." + $hashT["mode"] + ""

можно написать следующее:

$xml += "LJ.XMLRPC.{0}" -f $hashT["mode"]

Оператор форматирования -f является бинарным оператором, то есть принимает два операнда (аргумента) - один слева от себя, другой - справа от себя. Операнд слева представляет собой текстовый шаблон (строку), в котором места вставки данных могут быть указаны с помощью последовательностей {0}, {1}, {2} и так далее. Операнд справа представляет собой список данных для вставки в шаблон; элементы данных отделяются друг от друга запятыми. Каждый элемент данных индексируется целым числом, начиная с нуля. В текстовом шаблоне индекс вставляемого элемента данных указывается в фигурных скобках. В примере выше у нас один элемент данных $hashT["mode"], который в текстовом шаблоне обозначается последовательностью {0}. Естественно, текстовый шаблон может быть многострочной строкой (multiline string).

Использование рекурсивности «дерева» и применение «философии Unix»

Главной причиной переписывания вышеприведенного кода стало то, что мне понадобилось, чтобы эта функция умела обрабатывать хеш-таблицы со значениями-массивами. В вышеописанном коде функция toXML умеет обрабатывать только хеш-таблицы с несоставными (scalar) значениями вроде целых чисел или строк.

Я переписал функцию toXML, научив ее работать со значениями-массивами, и заметил, что код, обрабатывающий несоставное (scalar) значение хеш-таблицы и несоставное (scalar) значение массива - это один и тот же код. Я вынес этот код в отдельную функцию toXMLScalar, после чего вспомнил, что структура данных вида «дерево» обладает свойством рекурсивности. То есть каждая из веток «дерева» сама по себе тоже является «деревом». (Я уже ранее подробно изучал структуру данных «дерево», когда изучал язык программирования JavaScript. Я посвятил разбору строения «деревьев» ряд постов.)

Свойство рекурсивности структуры данных «дерево» (в нашей задаче мы создаем XML-дерево, оно же в данном случае является XML-деревом в формате XML-RPC) дает возможность написать обработку (в том числе создание) «дерева» с помощью рекурсии.

Создание каждой ветки «дерева» я решил сделать в отдельной функции. Таким образом, у меня получился ряд маленьких и простых функций, которые вызывают друг друга и обмениваются текстом, решая одну общую задачу. Такой подход иногда называют «философией Unix» - это принципы, придуманные и использованные разработчиками операционной системы «Unix» в ходе ее разработки и сформулированные в конце 1970-х годов. Как я уже писал выше, любой принцип подходит не для всех случаев. В данном случае мне было любопытно попробовать и посмотреть, что получится.

Вот какие шесть функций у меня получились:

function toXMLScalar($val) {
$type = $val.GetType().Name
if ($type -eq "Int32") {$type = "int"} else {
$type = "string"
$val = $val -replace '&', '&'
$val = $val -replace '<', '<'
}
"<{0}>{1}" -f $type, $val
}
function toXMLValue($val) {
$val = if ($val -is [System.Array]) {
toXMLArray $val
} elseif ($val.GetType().Name -eq "Hashtable") {
toXMLStruct $val
} else {
toXMLScalar $val
}
"{0}" -f $val
}
function toXMLArray($arr) {
$values = foreach ($elem in $arr) { toXMLValue $elem }
$values = -join $values
"{0}" -f $values
}
function toXMLMember($key, $val) {
$val = toXMLValue $val
"{0}{1}" -f $key, $val
}
function toXMLStruct($hashT) {
$members = foreach ($key in $hashT.Keys) {
if ($key -ne "mode") {
toXMLMember $key $hashT[$key]
}
}
$members = -join $members
"{0}" -f $members
}
function toXML($hashT) {
$xml = -join @(
''
"LJ.XMLRPC.{0}"
"

{1}"
)
$value = toXMLValue $hashT
$xml -f $hashT["mode"], $value
}

Теперь функция toXML может обработать хеш-таблицу как с несоставными (scalar) значениями, так и с составными значениями. Согласно спецификации формата XML-RPC составных значений может быть два: хеш-таблица (struct) и массив (array). Несоставных (scalar) значений может быть шесть, но я реализовал только несоставные (scalar) значения целочисленного типа (int) и строкового типа (string).

Также я добавил при обработке строки замену (экранирование) специальных символов & и < на последовательности символов & и <. Это требование спецификации формата XML-RPC. Если эти символы не экранировать, полученный текст формата XML может быть невалидным (наличие указанных символов может привести к неправильной интерпретации данного текста в формате XML).

Также теперь функция toXML может обработать вложенные значения, то есть хеш-таблица может иметь значение в виде хеш-таблицы или массива, те, в свою очередь, тоже могут обладать вложенными значениями и так далее.

У приведенной реализации в виде шести функций есть недостаток: при очень глубокой вложенности значений исходной хеш-таблицы в какой-то момент работы приведенных функций число вызовов в стеке вызовов может привысить лимит, который обычно имеется в реализации используемого языка программирования. В интернетах пишут, что во второй версии программы-оболочки «PowerShell» такой лимит был равен 1000 вызовов. В любом случае, для моей задачи нет нужды в обработке хеш-таблиц с очень глубокой вложенностью. Поэтому для моей задачи я пока что решил оставить преобразование хеш-таблицы в XML в виде вышеописанных шести функций.

Конечно, как известно, любую рекурсивную реализацию можно реализовать в итеративном виде (без рекурсии, с помощью циклов). При итеративной реализации указанного недостатка с превышением лимита вызовов в стеке вызовов можно избежать. Однако, итеративная реализация обычно получается гораздо сложнее рекурсивной. Я пока не готов этим заняться.

Для запуска работы этой системы функций достаточно вызвать функцию toXML, задав ей входную хеш-таблицу с параметрами. Далее функция toXML вызовет функцию toXMLValue, та вызовет функцию toXMLStruct и так далее. В конце концов исполнение вернется к функции toXML, которая вернет строку с текстом в формате XML-RPC.

Инструмент, Образование, Программирование, Английский язык

Previous post Next post
Up