Как использовать powershell для изменения порядка столбцов CSV
Входной файл:
column1;column2;column3
data1a;data2a;data3a
data1b;data2b;data3b
Цель: выходной файл с переупорядоченными столбцами, скажем
column1;column3;column2
...
ОБНОВЛЕНО Вопрос:
Что является хорошим способом использования powershell для решения этой проблемы.
Я знаю о существовании связанных с CSV командлетов, но у них есть ограничения.
Обратите внимание, что порядок записей не нужно изменять, поэтому загрузка всего файла ввода/вывода в память не требуется.
Ответы
Ответ 1
Вот решение, подходящее для миллионов записей (при условии, что ваши данные не имеют встроенных ";" )
$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()
Ответ 2
Import-CSV C:\Path\To\Original.csv | Select-Object Column1, Column3, Column2 | Export-CSV C:\Path\To\Newfile.csv
Ответ 3
Изменить: информация о контрольных показателях ниже.
Я не использовал бы командлеты CMS, связанные с Powershell. Я бы использовал либо System.IO.StreamReader
, либо Microsoft.VisualBasic.FileIO.TextFieldParser
для чтения в файле по очереди, чтобы не загружать всю вещь в памяти, и я бы использовал System.IO.StreamWriter
, чтобы записать ее обратно. TextFieldParser
внутренне использует StreamReader
, но обрабатывает разделенные поля с разделителями, поэтому вам это не нужно, что делает его очень полезным, если формат CSV не является простым (например, имеет разделители в кавычках).
Я бы тоже не делал этого в Powershell вообще, а скорее в .NET-приложении, поскольку он будет намного быстрее, чем Powershell script, даже если они используют одни и те же объекты.
Здесь С# для простой версии, если не использовать кавычки и кодировку 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()
Я сравнивал оба этих файла с 3-столбным CSV файлом с 10 000 000 строк. Версия С# заняла 171.132 секунды (чуть менее 3 минут). Версия Powershell заняла 2,364,995 секунды (39 минут, 25 секунд).
Изменить. Почему моя так долго проталкивает.
Функция swap является огромным узким местом в моей версии Powershell. Заменяя его на '{0};{1};{2}'
-строчный вывод, как ответ Романа Кузмина, сократите его до менее чем 9 минут. Замена TextFieldParser
более чем наполовину уменьшила оставшуюся до 4 минут.
Однако версия консольного приложения .NET от Roman Kuzmin ответила на 20 секунд.
Ответ 4
Замечательно, что люди пришли со своими решениями на основе чистого .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
Я бы сделал это следующим образом:
$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