После внесения некоторых изменений в ваш код в демонстрационных целях ...
- Усечение регулярного выражения в соответствии с данными примера
- Изменение разделителя вывода (теперь
$delimiter
)до ,
, поэтому результаты легко увидеть - Использование
StringReader
и StringWriter
для ввода и вывода соответственно
... учитывая ...
$text = @'
ID FIRST_NAME LAST_NAME COLUMN_NM_TOO_LON5THCOLUMN
10000000001MINNIE MOUSE COLUMN VALUE LONGSTARTS
10000000002PLUTO COLUMN VALUE LONGSTARTS
'@
... способ, которым вы предложили настроить текст совпадения для определенных индексов, будет выглядеть следующим образом ...
$proc_yyyymm = '201912'
$match_regex = '^(.{11})(.{24})(.{19})(.{17})(.{9})'
$delimiter = ','
$indicesToNormalizeToZero = ,2
$stream_in = New-Object -TypeName 'System.IO.StringReader' -ArgumentList $text
$stream_out = New-Object -TypeName 'System.IO.StringWriter'
while ($line = $stream_in.ReadLine()) {
if ($line -match $match_regex) {
$trimmedMatches = $Matches[1..($Matches.Count-1)].Trim()
foreach ($index in $indicesToNormalizeToZero)
{
if ($trimmedMatches[$index] -eq '')
{
$trimmedMatches[$index] = '0'
}
}
$new_line = "$proc_yyyymm$delimiter" + ($trimmedMatches -join $delimiter)
$stream_out.WriteLine($new_line)
}
}
$stream_out.ToString()
Альтернативой может быть использование [Regex]::Replace()
метод . Это хорошо, когда вам нужно выполнить пользовательское преобразование для совпадения, которое не может быть выражено в подстановке регулярного выражения . По общему признанию, это может быть плохое соответствие, потому что вы сопоставляете целую строку вместо отдельных полей, поэтому в пределах соответствия вам нужно знать, какое поле какое.
$proc_yyyymm = '201912'
$match_regex = [Regex] '^(.{11})(.{24})(.{19})(.{17})(.{9})'
$match_evaluator = {
param($match)
# The first element of Groups contains the entire matched text; skip it
$fields = $match.Groups `
| Select-Object -Skip 1 `
| ForEach-Object -Process {
$field = $_.Value.Trim()
if ($groupsToNormalizeToZero -contains $_.Name -and $field -eq '')
{
$field = '0'
}
return $field
}
return "$proc_yyyymm$delimiter" + ($fields -join $delimiter)
}
$delimiter = ','
# Replace with a HashSet/Hashtable for better lookup performance
$groupsToNormalizeToZero = ,'3'
$stream_in = New-Object -TypeName 'System.IO.StringReader' -ArgumentList $text
$stream_out = New-Object -TypeName 'System.IO.StringWriter'
while ($line = $stream_in.ReadLine()) {
$new_line = $match_regex.Replace($line, $match_evaluator)
# The original input string is returned if there was no match
if (-not [Object]::ReferenceEquals($line, $new_line)) {
$stream_out.WriteLine($new_line)
}
}
$stream_out.ToString()
$match_evaluator
является MatchEvaluator
делегат , который вызывается для каждого успешного совпадения, найденного во входном тексте, в Replace()
и возвращает все, что вы хотите, чтобы текст замены был. Внутри я делаю то же самое специфичное для индекса преобразование, сравнивая имя группы (которое будет индексом [String]
) с известным списком ($groupsToNormalizeToZero
);вместо этого вы могли бы использовать именованные группы, хотя я обнаружил, что это меняет порядок $match.Groups
. Здесь могут быть более подходящие приложения [Regex]::Replace()
, которые сейчас мне не встречаются.
В качестве альтернативы использованию регулярных выражений, поскольку их длины известны, вы можете извлечь поля непосредственно из $line
, используя Substring()
method ...
$proc_yyyymm = '201912'
$delimiter = ','
$stream_in = New-Object -TypeName 'System.IO.StringReader' -ArgumentList $text
$stream_out = New-Object -TypeName 'System.IO.StringWriter'
while ($line = $stream_in.ReadLine()) {
$id = $line.Substring( 0, 11).Trim()
$firstName = $line.Substring(11, 24).Trim()
$lastName = $line.Substring(35, 19).Trim()
$columnNameTooLong = $line.Substring(54, 17).Trim()
$fifthColumn = $line.Substring(71, 9).Trim()
if ($lastName -eq '')
{
$lastName = '0'
}
$new_line = $proc_yyyymm,$id,$firstName,$lastName,$columnNameTooLong,$fifthColumn -join $delimiter
$stream_out.WriteLine($new_line)
}
$stream_out.ToString()
Еще лучше, поскольку длина каждой строки известна, вы можете избежать проверок новой строки ReadLine()
и последующего выделения String
, читаякаждая строка в виде блока Char
с и извлечения оттуда полей.
function ExtractField($chars, $startIndex, $length, $normalizeIfFirstCharWhitespace = $false)
{
# If the first character of a field is whitespace, assume the
# entire field is as well to avoid a String allocation and Trim()
if ($normalizeIfFirstCharWhitespace -and [Char]::IsWhiteSpace($chars[$startIndex])) {
return '0'
} else {
# Create a String from the span of Chars at known boundaries and trim it
return (New-Object -TypeName 'String' -ArgumentList ($chars, $startIndex, $length)).Trim()
}
}
$proc_yyyymm = '201912'
$delimiter = ','
$stream_in = New-Object -TypeName 'System.IO.StringReader' -ArgumentList $text
$stream_out = New-Object -TypeName 'System.IO.StringWriter'
$lineLength = 82 # Assumes the last line ends with an \r\n and not EOF
$lineChars = New-Object -TypeName 'Char[]' -ArgumentList $lineLength
while (($lastReadCount = $stream_in.ReadBlock($lineChars, 0, $lineLength)) -gt 0)
{
$id = ExtractField $lineChars 0 11
$firstName = ExtractField $lineChars 11 24
$lastName = ExtractField $lineChars 35 19 $true
$columnNameTooLong = ExtractField $lineChars 54 17
$fifthColumn = ExtractField $lineChars 71 9
# Are all these method calls better or worse than a single WriteLine() and object allocation(s)?
$stream_out.Write($proc_yyyymm)
$stream_out.Write($delimiter)
$stream_out.Write($id)
$stream_out.Write($delimiter)
$stream_out.Write($firstName)
$stream_out.Write($delimiter)
$stream_out.Write($lastName)
$stream_out.Write($delimiter)
$stream_out.Write($columnNameTooLong)
$stream_out.Write($delimiter)
$stream_out.WriteLine($fifthColumn)
}
$stream_out.ToString()
Поскольку ответ @ HAL9256 подтверждает, что функции PowerShell (очень!) медленные, способсделать то же самое без избыточного кода и без функций - определить набор дескрипторов полей и выполнить цикл по нему, чтобы извлечь каждое поле из соответствующего смещения ...
$proc_yyyymm = '201912'
$delimiter = ','
$stream_in = New-Object -TypeName 'System.IO.StringReader' -ArgumentList $text
$stream_out = New-Object -TypeName 'System.IO.StringWriter'
$lineLength = 82 # Assumes the last line ends with an \r\n and not EOF
$lineChars = New-Object -TypeName 'Char[]' -ArgumentList $lineLength
# This could also be done with 'Offset,Length,NormalizeIfEmpty' | ConvertFrom-Csv
# The Offset property could be omitted in favor of calculating it in the loop
# based on the Length, however this way A) avoids the extra variable/addition,
# B) allows fields to be ignored if desired, and C) allows fields to be output
# in a different order than the input.
$fieldDescriptors = @(
@{ Offset = 0; Length = 11; NormalizeIfEmpty = $false },
@{ Offset = 11; Length = 24; NormalizeIfEmpty = $false },
@{ Offset = 35; Length = 19; NormalizeIfEmpty = $true },
@{ Offset = 54; Length = 17; NormalizeIfEmpty = $false },
@{ Offset = 71; Length = 9; NormalizeIfEmpty = $false }
) | ForEach-Object -Process { [PSCustomObject] $_ }
while (($lastReadCount = $stream_in.ReadBlock($lineChars, 0, $lineLength)) -gt 0)
{
$stream_out.Write($proc_yyyymm)
foreach ($fieldDescriptor in $fieldDescriptors)
{
# If the first character of a field is whitespace, assume the
# entire field is as well to avoid a String allocation and Trim()
# If space is the only possible whitespace character,
# $lineChars[$fieldDescriptor.Offset] -eq [Char] ' ' may be faster than IsWhiteSpace()
$fieldText = if ($fieldDescriptor.NormalizeIfEmpty `
-and [Char]::IsWhiteSpace($lineChars[$fieldDescriptor.Offset])
) {
'0'
} else {
# Create a String from the span of Chars at known boundaries and trim it
(
New-Object -TypeName 'String' -ArgumentList (
$lineChars, $fieldDescriptor.Offset, $fieldDescriptor.Length
)
).Trim()
}
$stream_out.Write($delimiter)
$stream_out.Write($fieldText)
}
$stream_out.WriteLine()
}
$stream_out.ToString()
Я , предполагая что прямое извлечение строк будет быстрее, чем регулярное выражение, но я не знаю, что вообще должно быть $true
, не говоря уже о PowerShell;только тестирование покажет, что.
Все вышеперечисленные решения дают следующий результат ...
201912,ID,FIRST_NAME,LAST_NAME,COLUMN_NM_TOO_LON,5THCOLUMN
201912,10000000001,MINNIE,MOUSE,COLUMN VALUE LONG,STARTS
201912,10000000002,PLUTO,0,COLUMN VALUE LONG,STARTS