Contents of this directory is archived and no longer updated.

Posts on this page:

Summer Scripting Games 2009 3-е задание на парсинг текста.

Beginner Division

Потребуется файл Shot Put.txt из Competitors Pack.

Задача:

  • В файле есть 2 абзаца. Нужно первый абзац записать в файл Shot Put A.txt, а второй абзац записать в Shot Put B.txt.
  • Переименовать файл Shot Put.txt в Shot Put.old.

Решение:

Если внимательно посмотреть на файл, то в нём можно найти одну уникальную последовательность - `r`n`r`n. Она разделяет 2 абзаца. Поэтому эту последовательность можно использовать в качестве разделителя для Get-Content:

[↓] [vPodans] $file = gc '.\Shot Put.txt' -Delimiter "`r`n`r`n"
[↓] [vPodans] $file[0]
This is the first paragraph of the shot put text file. It will
need to be placed into the first text file which you will create.
The shot put is an event involving putting and heavy metal ball.
The object of the event is to throw the put as far as possible,
or to put the shot : anyway, you get the idea.




[↓] [vPodans] $file[1]
This is the second paragraph of the shot put text file. it
contains information you will need to put into the second
file which you will also create.
[↓] [vPodans]

Если со вторым абзацем понятно, то у первого очень много пробелов и пустых строк. Их можно удалить при помощи метода Trim(), который использовался уже в Event1. Собственно, само решение:

$file = gc '.\Shot Put.txt' -Delimiter "`r`n`r`n" | %{$_.trim()}
$file[0] > 'Shot Put A.txt'
$file[1] > 'Shot Put B.txt'
ren '.\Shot Put.txt' 'Shot Put.old'

Advanced Division

потребуется файл Wordlist_ADV3.txt из Competitors Pack.

Задача:

  • распарсить текстовый файл и вытащить из них слова, в которых используется не более одной уникальной гласной буквы.
  • записать эти слова в новый текстовый файл.

Решение:

В принципе, тут есть только одно простое решение (считайте, что нагло стырено у Васи):

gc .\Wordlist_ADV3.txt | ?{$_ -replace '[^AEIOUY]' -match '^(.)\1*$'} > 'Wordlist_ADV3_new.txt'

суть решения заключается в том, чтобы из каждого слова убрать все согласные и проверить, что в слове используется только одна гласная 1 или более раз. Этот регексп описан в PowerShell Cookbook на странице 494.

Beginner Division

Summer Scripting Games 2009 Тут очень простое задание из серии фаллометрии :-)

Задача:

  • Показать данные своего процессора (процессоров) в соответствии с рисунком

Решение:

Тут нам потребуется WMI класс Win32_Processor. И из него выберем нужные данные. При этом я проверю, что значения не равны $null или нулю. Если это так, то в это поле впишу текст N/A для читабельности. Вот что получилось:

$Host.UI.RawUI.BackgroundColor = "Black"
cls
$processor = gwmi win32_processor | Select AddressWidth, L2CacheSize, L2CacheSpeed, L3CacheSize,
L3CacheSpeed, MaxClockSpeed, Name, NumberOfCores, NumberOfLogicalProcessors
$processor | gm -MemberType NoteProperty | %{
    if ($processor.$($_.name) -eq $null -or $processor.$($_.name) -eq 0) {$processor.$($_.name) = "N/A"}
}
Write-Host 'Strength evaluation for LocalHost' -ForegroundColor Green
Write-Host 'Speed ... ' $processor.MaxClockSpeed `
`nL2 Cache Size: $processor.L2CacheSize `
`nL2 Cache Speed: $processor.L2CacheSpeed `
`nL3 Cache Size: $processor.L3CacheSize `
`nL3 Cache Speed: $processor.L3CacheSpeed -ForegroundColor Yellow
Write-Host Strength ... `
`nNumber of Cores: $processor.NumberOfCores `
`nNumber of Logical Processors: $processor.NumberOfLogicalProcessors `
`nName: ```````````` $processor.Name -ForegroundColor Magenta
Write-Host Agility ... `
`nAddress Width: $processor.AddressWidth -ForegroundColor Cyan

И вот вывод:

[↓] [vPodans] $processor = gwmi win32_processor | Select AddressWidth, L2CacheSize, L2CacheSpeed, L3CacheSize,
>> L3CacheSpeed, MaxClockSpeed, Name, NumberOfCores, NumberOfLogicalProcessors
>> $processor | gm -MemberType NoteProperty | %{
>>  if ($processor.$($_.name) -eq $null -or $processor.$($_.name) -eq 0) {$processor.$($_.name) = "N/A"}
>> }
>> Write-Host 'Strength evaluation for LocalHost' -ForegroundColor Green
>> Write-Host 'Speed ... ' $processor.MaxClockSpeed `
>> `n'L2 Cache Size:' $processor.L2CacheSize `
>> `n'L2 Cache Speed:' $processor.L2CacheSpeed `
>> `n'L3 Cache Size:' $processor.L3CacheSize `
>> `n'L3 Cache Speed:' $processor.L3CacheSpeed -ForegroundColor Yellow
>> Write-Host Strength ... `
>> `n'Number of Cores:' $processor.NumberOfCores `
>> `n'Number of Logical Processors:' $processor.NumberOfLogicalProcessors `
>> `n'Name:  $processor.Name -ForegroundColor Magenta
>> Write-Host Agility ... `
>> `n"Address Width: `t`t`t`t" $processor.AddressWidth -ForegroundColor Cyan
>>
Strength evaluation for LocalHost
Speed ...  2834
L2 Cache Size: 6144
L2 Cache Speed: 2000
L3 Cache Size: N/A
L3 Cache Speed: N/A
Strength ...
Number of Cores: 4
Number of Logical Processors: 4
Name:                Intel(R) Core(TM)2 Quad CPU    Q9550  @ 2.83GHz
Agility ...
Address Width: 64
[↓] [vPodans]

По всей видимости оно получилось как и требовалось.

Advanced Division

И тут нас снова ждут соревнования. Здесь нам потребуется файл LongJump_Adv2.xls из Competitors Pack. Файл содержит данные об участниках прыжков в длину и результаты их 3-х попыток.

Задача:

  • На основании результатов 3-х попыток в колонку Result записать лучший результат из 3-х попыток
  • На основании результатов третей попытки для каждого участника, выбрать лучший результат из них и вывести на экран
  • Сравнить лучший результат из 3-х попыток для каждого участника с их средним результатом в сезоне. Такой графы я не нашёл, есть только лучший результат в сезоне. Вот с ним я и сравнивал. Если прыгун превзошёл свой личный рекорд сезона, то в графу Exceed,Achieve,Under Perform вписать Exceed. Если повторил, то Achieve или Under Perform, если лучший результат сегодня был хуже результата по сезону.
  • Отсортировать всю таблицу по графе Exceed,Achieve,Under Perform именно в таком порядке. Т.е. сперва идут те, кто улучшил свой показатель, потом – кто повторил и в самом конце те, кто не смог повторить свой результат в сезоне. И сохранить в файл с таким же именем.

Решение:

Вася Гусев решил окончательно сломать всем мозг разбором XLS в CSV, обработать его и собрать обратно в CSV – тиснуть сюда. Когда я это увидел – стало как-то не очень хорошо. Я решил работать прямо с XLS. И вот что у меня получилось:

# массив, который будет содержать результаты последней попытки прыжков, на основании
# которых нужно вывести лучший результат
$lastattempt = @()
# создаём COM объект и прочую рутину.
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $false
$excel.DisplayAlerts = $false
$excel.Workbooks.Open('C:\Users\vPodans\LongJump_Adv2.xls') | Out-Null
# у нас есть известное кол-во участников - 18. 19-я строка - это заголовки.
# поэтому я исходя из этого решил работать с колонкой Result (8-я по счёту). Т.к.
# некоторые попытки были провалены и в таблице стояли крестики. Чтобы вручную не парсить
# это всё, я в ячейку воткнул экселовскую формулу MAX, которая всё за меня сделает.
2..19 | %{$excel.Cells.Item($_,8).Value2 = "=max(e$_`:g$_)"
    # заодно в $lastattempt запишем результат последней попытки для текущего прыгуна
    $lastattempt += $excel.Cells.Item($_,7).Value2
    # сравниваем лучший результат за сегодня с результатом за сезон
    if ($excel.Cells.Item($_,8).Value2 -lt $excel.Cells.Item($_,4).Value2) {
    # и в соответствующую графу пишем результат
        $excel.Cells.Item($_,9).Value2 = "Under Perform"
    # вот тут пришлось использовать читы. Я не очень силён в custom sort в экселе, поэтому
    # рядышком я для каждого статуса Exceed/Achieve/.. я приписываю число. Вот по этому числу
    # я потом буду сортировать таблицу
        $excel.Cells.Item($_,10).Value2 = "3"
    } elseif ($excel.Cells.Item($_,8).Value2 -eq $excel.Cells.Item($_,4).Value2) {
        $excel.Cells.Item($_,9).Value2 = "Achieve"
        $excel.Cells.Item($_,10).Value2 = "2"
    } else {
        $excel.Cells.Item($_,9).Value2 = "Exceed"
        $excel.Cells.Item($_,10).Value2 = "1"
    }
}
# выбираем из LastAttempt лучший результат, попутно заменяя крестики на нолики (лол) и выводя лучший результат
$lastattempt | %{if ($_ -eq "x") {$_ = 0};$_} | sort | select -last 1
# выбираем диапазон изменения таблицы при сортировке. Заголовки не трогаем, поэтому начинаем с 2
$selection = $excel.Range("a2:j19")
# указываем колонку, по которой будем сортировать
$range = $excel.Range("j2:j19")
# сортируем
[void]$selection.Sort($range,"1")
# удаляем читерские числа, которые использовались для сортировки.
2..19 | %{$excel.Cells.Item($_,10).Value2 = ""}
# готово.
$excel.Save()
$excel.Quit()

Не скажу, что получилось коротко и идеально, но задачу вполне решает.

Summer Scripting Games 2009 И оно свершилось! Хотя официально Scripting Games начинаются только 15-го июня, но уже со вчерашнего дня стали известны уже 2 задания. Для начала немного информации, что и как:

В принципе, вы можете решать задания как угодно, т.к. засчитываются все, которые отвечают условиям задания.

Итак, Event1 и оба дивизиона:

Beginner Division

Из Competitors Pack нам потребуется файл 100 Meter Event.txt

Задача:

  • Выяснить, как читать файл?
  • Как прочитать из каждой строки поля Name, Country, Time?
  • Как отсортировать содержимое файла по полю Name?
  • Выбрать 3-х победителей из списка. Победителем считается тот, у кого самое маленькое время в колонке Time
  • Показать поля Name, Country и Time для первых 3-х победителей.

Решение:

Побегав курсором по файлу я обнаружил тот факт, что разделителем между столбцом Name и Country является табулятор ([Tab]), а между Country и Time стоит один пробел во второй строке, а в остальных строках стоит [Tab]. Следовательно, варианты использования командлетов с параметром –Delimeter не прокатят и нужно будет разбирать всё регулярными выражениями. Первый вопрос наталкивает на необходимость чтения файла не через Get-Content, а через [system.io.file]::ReadAllText(). Но я не вижу особого криминала в использовании родного для PowerShell командлета Get-Content. Это будет несложно. Основная проблема в том, как составить регулярное выражение. Я решил разобрать имя на First Name и LastName (т.к. правильней, всё же, будет, когда сначала идёт имя, а потом фамилия), далее выбрать Country от табуляции до первого числа. Причём, тут потребуется каждую часть сделать именованной (named capture). Если открыть PowerShell In Action на 111 странице, то там можно найти описание, как делать named captures. У меня регулярное выражение получилось вот такое:

^(?<ln>\w+)..(?<fn>.*)\t(?<country>.+)(?<time>\d\.\d+)

Что делает это выражение:

  • в секцию LN (LastName) попадают все буквы до первого небуквенного знака.
  • далее пропускаем 2 символа (запятую и пробел).
  • в секцию FN (FirstName) попадает всё до первой табуляции.
  • после табуляции идёт Country и в него запишем всё, вплоть до первого числового знака.
  • И числа записываем в секцию Time.

Давайте проверим его:

[↓] [vPodans] $file = gc '.\100 Meter Event.txt'
[↓] [vPodans] $file[1] -match "^(?<ln>\w+)..(?<fn>.*)\t(?<country>.+)(?<time>\d\.\d+)"
True
[↓] [vPodans] $matches

Name                           Value
----                           -----
country                        Australia
ln                             Aaberg
fn                             Jesper
time                           8.57
0                              Aaberg, Jesper    Australia 8.57


[↓] [vPodans] $file[11] -match "^(?<ln>\w+)..(?<fn>.*)\t(?<country>.+)(?<time>\d\.\d+)"
True
[↓] [vPodans] $matches

Name                           Value
----                           -----
country                        Japan
ln                             Hansen
fn                             Anne Grethe
time                           8.85
0                              Hansen, Anne Grethe    Japan    8.85


[↓] [vPodans]

и теперь соберём объект с нужными свойствами и отправим его на выход. На выходе отсортируем объекты по параметру Time и выберем первые 3 объекта:

gc "100 Meter Event.txt" | ?{$_ -match "^(?<ln>\w+)..(?<fn>.*)\t(?<country>.+)(?<time>\d\.\d+)"} |%{
    $obj = "" | Select @{n='Name';e={$matches.fn + " " + $matches.ln}},
    @{n='Country';e={$matches.country}},@{n='Time';e={$matches.time}}
    $obj
} | sort time | select -First 3 | ft -AutoSize

и вот вывод:



решил ещё отформатировать в Format-Table с ключом –AutoSize для красоты. Признаюсь, что это задание меня сильно озадачило. Для меня оно оказалось очень непростым (такие дела).

Advanced Division

Для этой задачи нам потребуется файл Personal Information Cards_ADV1.txt из Competitors Pack.

Задача:

  • найти самую короткую строку в файле, которая содержит какой-либо текст.
  • вывести на экран 3 самые короткие строчки

Решение:

В файле много пустых строк, но это не проблема. Итак, one-liner решение:

gc "Personal Information Cards_ADV1.txt" | ?{$_.trim().length -ne 0} | sort length | select -First 3
[↓] [vPodans] gc "Personal Information Cards_ADV1.txt" | ?{$_.trim().length -ne 0} | select –First 3
PPID
Claims
Street
[↓] [vPodans]

Странно, что в Advanced Division получилось такое простое задание.

Работая с функциями в PowerShell можно столкнуться с одной особенностью – в качестве передачи аргументов в функции или скрипты вы можете использовать почти всё, кроме ключей (данных типа Switch). Их передавать можно, но тут есть одна особенность. Обычно это ощущается, когда вы работаете с командлетами. Возьмём простой пример:

function Test ([string[]]$Path, [String]$Filter, [switch]$Force) {
Write-Host '$Path is:' $Path
Write-Host '$Filter is:' $Filter
Write-Host '$Force is:' $Force
}

и вызовем эту функцию:

[↓] [vPodans] function Test ([string[]]$Path, [String]$Filter, [switch]$Force) {
>> Write-Host '$Path is:' $Path
>> Write-Host '$Filter is:' $Filter
>> Write-Host '$Force is:' $Force
>> }
>>
[↓] [vPodans] Test C:\ * -force
$Path is: C:\
$Filter is: *
$Force is: True
[↓] [vPodans]

В принципе, всё как и ожидалось. Но если внимательно посмотреть на последний аргумент, то мы увидим лишь True, т.е. увидим факт, что ключ Force был передан. Однако, PowerShell не умеет подставять (биндить) переменную $Force (равно как и другие переменные) как именованный параметр в другую команду. Чтобы в этом убедиться, мы попробуем сымитировать нашу функцию как командлет Get-ChildItem:

[↓] [vPodans] function Test ([string[]]$Path, [String]$Filter, [switch]$Force) {
>> Get-ChildItem $Path $Filter $Force
>> }
>>
[↓] [vPodans] Test C:\ * -Force
Get-ChildItem : A positional parameter cannot be found that accepts argument 'True'.
At line:2 char:14
+ Get-ChildItem <<<<  $Path $Filter $Force
    + CategoryInfo          : InvalidArgument: (:) [Get-ChildItem], ParameterBindingException
    + FullyQualifiedErrorId : PositionalParameterNotFound,Microsoft.PowerShell.Commands.GetChildItemCommand

[↓] [vPodans]

Как видно, командлет Get-ChildItem не смог сопоставить последний аргумент ни с одним из своих параметров. Т.е. по факту выполнилась следующая строка:

Get-ChildItem -Path C:\ -Filter * True

PowerShell не смог сказать командлету, что мы указали ключ Force и хотим именно его передать в командлет. По факту в этом коде Get-ChildItem не знал, какие параметры ему были переданы и подставлял их на основе номера позиции параметра. А т.к. у Get-ChildItem нету параметра с порядковым номером 3 (с номером 1 идёт Path или LiteralPath, а с номером 2 идёт Filter. Остальные параметры именованные), то мы получили ошибку. Но всё же, как выкручиваться из этой ситуации? Вы можете как угодно пытаться подставить аргументы, но ничего не выйдет. Для этих целей в PowerShell V2 появилась специальная переменная - $PSBoundParameters. Эта переменная по сути представляет собой хэш-таблицу:

[↓] [vPodans] function Test ([string]$Path, [String]$Filter, [switch]$Force) {
>> $PSBoundParameters
>> }
>>
[↓] [vPodans] Test C:\ * -Force

Key                                                         Value
---                                                         -----
Force                                                       True
Path                                                        C:\
Filter                                                      *


[↓] [vPodans]

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

function Name ($arg1, $arg2, $arg3 ... $argN) {
Command @PSBoundParameters
}

и в результате будет исполняться вот такая команда:

Command –arg1 <значение $arg1> –arg2 <значение $arg2> –arg3 <значение $arg3> … –argN <значение $argN>

Т.е. будут подставляться именованные параметры и значения переменных соответствующих аргументов. Давайте проверим, как это подействует на наш пример с Get-ChildItem:

function Test ([string]$Path, [String]$Filter, [switch]$Force) {
Get-ChildItem $PSBoundParameters
}

[↓] [vPodans] function Test ([string]$Path, [String]$Filter, [switch]$Force) {
>> Get-ChildItem @PSBoundParameters
>> }
>>
[↓] [vPodans] Test C:\ * -Force


    Directory: C:\


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
d--hs       10.07.2007.     19:44            $Recycle.Bin
d--hs       09.03.2008.     17:53            Boot
d----       23.11.2008.     19:41            inetpub
d-rh-       26.06.2007.     22:27            MSOCache
d----       09.03.2008.     17:41            PerfLogs
<...>

И вы можете видеть, что у нас всё получилось в наилучшем виде! В сравнении с предыдущим примером фактически выполнилась следующая команда:

Get-ChildItem -Path C:\ -Filter * -Force

Для формирования аргументов для командлетов в функциях (так называемые wrapped cmdlets) это самый красивый и идеальный вариант. Если вы захотите изменить логику стандартных командлетов или добавить в них свой функционал, то $PSBoundParamters сделает за вас очень много лишней работы. Но это не единственное полезное применение для этой переменной. Она так же позволяет сократить возможность ошибки при вызове функции внутри скрипта или другой функции, которая принимает те же аргументы. Давайте посмотрим ещё один пример:

function Test ([string[]]$Path, [String]$Filter, [switch]$Force) {
    Write-Host '$Path in Test is:' $Path
    Write-Host '$Filter in Test is:' $Filter
    Write-Host '$Force in Test is:' $Force
    Write-Host --------------------------------
    function Test2 ([switch]$Force, [string[]]$Path, [String]$Filter) {
Write-Host '$Path in Test2 is:' $Path
Write-Host '$Filter in Test2 is:' $Filter Write-Host '$Force in Test2 is:' $Force } Test2 @PSBoundParameters }

Что мы делаем: мы создали функцию Test, которая принимает набор аргументов. Внутри этой функции есть другая функция Test2, которая принимает те же аргументы. Далее из функции Test вызываем функцию Test2 и с помощью $PSBoundParameters передаём в неё аргументы. Обратите внимание, что в функции Test2 я изменил порядок аргументов. Это сделано для того, чтобы показать, что $PSBoundParamters отсортирует наши аргументы. А теперь внимание на экран:

[↓] [vPodans] function Test ([string[]]$Path, [String]$Filter, [switch]$Force) {
>>     Write-Host '$Path in Test is:' $Path
>>     Write-Host '$Filter in Test is:' $Filter
>>     Write-Host '$Force in Test is:' $Force
>>     Write-Host --------------------------------
>>     function Test2 ([switch]$Force, [string[]]$Path, [String]$Filter) {
>>         Write-Host '$Path in Test2 is:' $Path
>>         Write-Host '$Filter in Test2 is:' $Filter
>>         Write-Host '$Force in Test2 is:' $Force
>>     }
>>     Test2 @PSBoundParameters
>> }
>>
[↓] [vPodans] Test C:\ * -Force
$Path in Test is: C:\
$Filter in Test is: *
$Force in Test is: True
--------------------------------
$Path in Test2 is: C:\
$Filter in Test2 is: *
$Force in Test2 is: True
[↓] [vPodans]

И смотрите, что у нас получилось. А у нас получилось, что функция Test2 получила тот же набор аргументов, причём они были подставлены в правильном соответствии. Фактически строка Test2 @PSBoundParameters была преобразована в:

Test2 -Path $Path -Filter $Filter -Force $true

Т.е. в вариант, который мы вынуждены использовать в PowerShell 1.0. Плюс, мы имеем возможность подставлять позиционно ключи (объекты типа Switch), что есть очень позитивно.

Иногда замечаю, что некоторые скриптописатели получают “поломанный” конвейер, когда данные из одной команды вышли, но никуда не пришли. Возьмём простой пример:

[↓] [vPodans] foreach ($n in 1..3) {$n}
1
2
3
[↓] [vPodans]

Тут всё просто, циклом foreach перебираются данные и выводятся на экран. Мы привыкли, что всё, что выводится на экран можно передать дальше на конвейер. В принципе, это правильно, за исключением ряда случаев, когда это не работает. Чтобы убедиться в этом – передадим вывод этой команды через конвейер на Set-Content:

[↓] [vPodans] foreach ($n in 1..3) {$n}
1
2
3
[↓] [vPodans] foreach ($n in 1..3) {$n} | Set-Content file.txt
An empty pipe element is not permitted.
At line:1 char:28
+ foreach ($n in 1..3) {$n} | <<<<  Set-Content file.txt
    + CategoryInfo          : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : EmptyPipeElement

[↓] [vPodans]

и мы получаем ошибку, что у нас пустой конвейер. Если написать простой проверочный фильтр:

filter Test-Input {'current $_ is: ' + $_}

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

  • If…Else
  • If…ElseIf
  • For
  • Foreach
  • While
  • Do…While
  • Do…Until
  • Switch

Чтобы при использовании этих конструкций вывод транслировался на конвейер их нужно заключить в подвыражение – $( ):

[↓] [vPodans] $(foreach ($n in 1..3) {$n}) | Set-Content file.txt
[↓] [vPodans] gc file.txt
1
2
3
[↓] [vPodans] $(foreach ($n in 1..3) {$n}) | Test-Input
current $_ is: 1
current $_ is: 2
current $_ is: 3
[↓] [vPodans]

я заключил цикл Foreach в подвыражение и теперь данные стали поступать на конвейер. Это хорошо видно по выводу фильтра Test-Input, который приведён выше. Вот таким нехитрым способом мы решили проблему пустых конвейеров :-)