Как использовать powershell для изменения порядка столбцов CSV - PullRequest
11 голосов
/ 17 июня 2011

Входной файл:

column1;column2;column3
data1a;data2a;data3a
data1b;data2b;data3b

Цель: выходной файл с переупорядоченными столбцами, скажем,

column1;column3;column2
...

ОБНОВЛЕНО Вопрос: Что является хорошим способом использования powershell для решения этой проблемы. Я знаю о существовании командлетов, связанных с CSV, но у них есть ограничения. Обратите внимание, что порядок записей изменять не нужно, поэтому загрузка всего файла ввода-вывода в память не требуется.

Ответы [ 5 ]

17 голосов
/ 17 июня 2011

Вот решение, подходящее для миллионов записей (при условии, что ваши данные не имеют встроенных ';')

$reader = [System.IO.File]::OpenText('data1.csv')
$writer = New-Object System.IO.StreamWriter 'data2.csv'
for(;;) {
    $line = $reader.ReadLine()
    if ($null -eq $line) {
        break
    }
    $data = $line.Split(";")
    $writer.WriteLine('{0};{1};{2}', $data[0], $data[2], $data[1])
}
$reader.Close()
$writer.Close()
15 голосов
/ 17 июня 2011
Import-CSV C:\Path\To\Original.csv | Select-Object Column1, Column3, Column2 | Export-CSV C:\Path\To\Newfile.csv
5 голосов
/ 17 июня 2011

Здорово, что люди пришли со своими решениями, основанными на чистом .NET.Однако я бы боролся за простоту, если это возможно.Вот почему я всех вас проголосовал;)

Почему?Я попытался сгенерировать 1.000.000 записей и сохранить их в CSV, а затем изменить порядок столбцов.Создание CSV было в моем случае гораздо более сложным, чем переупорядочение.Посмотри на результаты.

Потребовалось всего 1,8 минуты, чтобы изменить порядок столбцов.Для меня это довольно приличный результат. Это нормально для меня?-> Да, мне не нужно пытаться найти более быстрое решение, это достаточно хорошо -> сэкономил мое время на другие интересные вещи ;)

# generate some csv; objects have several properties
measure-command { 
    1..1mb | 
    % { 
        $date = get-date
        New-Object PsObject -Property @{
            Column1=$date
            Column2=$_
            Column3=$date.Ticks/$_ 
            Hour = $date.Hour
            Minute = $date.Minute
            Second = $date.Second
            ReadableTime = $date.ToLongTimeString()
            ReadableDate = $date.ToLongDateString()
        }} | 
    Export-Csv d:\temp\exported.csv 
}

TotalMinutes      : 6,100025295

# reorder the columns
measure-command { 
    Import-Csv d:\temp\exported.csv | 
        Select ReadableTime, ReadableDate, Hour, Minute, Second, Column1, Column2, Column3 | 
        Export-Csv d:\temp\exported2.csv 
}

TotalMinutes      : 2,33151559833333
5 голосов
/ 17 июня 2011

Редактировать : информация о сравнительном тестировании ниже.

Я бы не использовал командлеты Powershell csv.Я бы использовал System.IO.StreamReader или Microsoft.VisualBasic.FileIO.TextFieldParser для чтения в файле построчно, чтобы избежать загрузки всего объекта в память, и я бы использовал System.IO.StreamWriter, чтобы выписать его обратно.TextFieldParser внутренне использует StreamReader, но обрабатывает поля с разделителями, поэтому вам не нужно это делать, что очень полезно, если формат CSV не прост (например, содержит символы-разделители в полях в кавычках).

Я бы вообще не делал этого в Powershell, а скорее в приложении .NET, так как он будет намного быстрее, чем скрипт Powershell, даже если они используют одни и те же объекты.

Вот простой C # для простоговерсия, при условии отсутствия полей в кавычках и кодировки ASCII:

static void Main(){
    string source = @"D:\test.csv";
    string dest = @"D:\test2.csv";

    using ( var reader = new Microsoft.VisualBasic.FileIO.TextFieldParser( source, Encoding.ASCII ) ) {
        using ( var writer = new System.IO.StreamWriter( dest, false, Encoding.ASCII ) ) {
            reader.SetDelimiters( ";" );
            while ( !reader.EndOfData ) {
                var fields = reader.ReadFields();
                swap(fields, 1, 2);
                writer.WriteLine( string.Join( ";", fields ) );
            }
        }
    }
}

static void swap( string[] arr, int a, int b ) {
    string t = arr[ a ];
    arr[ a ] = arr[ b ];
    arr[ b ] = t;
}

Вот версия Powershell:

[void][reflection.assembly]::loadwithpartialname("Microsoft.VisualBasic")

$source = 'D:\test.csv'
$dest = 'D:\test2.csv'

$reader = new-object Microsoft.VisualBasic.FileIO.TextFieldParser $source
$writer = new-object System.IO.StreamWriter $dest

function swap($f,$a,$b){ $t = $f[$a]; $f[$a] = $f[$b]; $f[$b] = $t}

$reader.SetDelimiters(';')
while ( !$reader.EndOfData ) {
    $fields = $reader.ReadFields()
    swap $fields 1 2
    $writer.WriteLine([string]::join(';', $fields))
}

$reader.close()
$writer.close()

Я сравнил их оба с CSV-файлом с 3 столбцами и 10 000 000 строк.Версия C # заняла 171,132 секунды (чуть менее 3 минут).Версия Powershell заняла 2 364,995 секунды (39 минут 25 секунд).

Редактировать : почему мой черт возьми так долго.

Функция свопинга - огромное узкое место в моемВерсия Powershell.Замена его выводом в стиле '{0};{1};{2}', как в ответе Романа Кузьмина, сократило его до менее чем 9 минут.Замена TextFieldParser более чем вдвое оставшимися до менее чем 4 минут.

Однако, версия консольного приложения .NET ответа Романа Кузьмина заняла 20 секунд.

1 голос
/ 17 июня 2011

Я бы сделал это так:

$new_csv = new-object system.collections.ArrayList
get-content mycsv.csv |% {
$new_csv.add((($_ -split ";")[0,2,1]) -join ";") > $nul
}
$new_csv | out-file myreordered.csv
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...