将基于行的记录表转换为基于列的记录(CSV)

时间:2014-09-03 18:54:52

标签: powershell batch-file csv

如果文本文件中的表包含基于行的数据,那么您建议使用哪种方法转换为基于列的表? (例如CSV)。

Input_data.txt:

Source =         X:\folder_abc
Destination =    Y:\Abc_folder
Total bytes =    208,731,021
MB per min =     256.5
Source =         X:\folder_def
Destination =    Y:\xyz_folder
Total bytes =    123,134,545
MB per min =     326
Source =         X:\folder_foo
Destination =    Y:\Baz_folder
Total bytes =    24,344
MB per min =     532
...etc.

所需结果(此处仅使用标签格式化,以便易读):

Source,             Destination,        Total bytes,    MB per min
"X:\folder_abc",    "Y:\Abc_folder",    "208,731,021",  "256.5"
"X:\folder_def",    "Y:\xyz_folder",    "123,134,545",  "326"
"X:\folder_foo",    "Y:\Baz_folder",    "24,344",       "532"
...

我可以使用的工具是Windows批处理文件和Powershell。更喜欢.bat解决方案,因为我在那里更舒服,但如果那太迂回或不透明,我们就可以解决它。

更新,根据评论

我已经想出如何将记录转换为名称& value 变量,但不知道如何从该点操纵它们以转置为列。

for /f "tokens=1,2 delims==" %%a in ('findstr /c:"=" "%logfile%"') do (
  @echo %%a %%b
  )

它刚刚发生在我身上我可以为每个文本文件做一列,然后将它们全部附加到Excel中。粗暴但可行(?)

for /f "tokens=1,2 delims==" %%a in ('findstr /c:"=" "%logfile%"') do (
  @echo %%b >>  %%a.csv
  )

UPDATE-2:引用所需结果中的所有值,因为dbenham指出不这样做会导致问题。

4 个答案:

答案 0 :(得分:2)

我意识到你不熟悉PowerShell,但它可能是你应该研究的东西。大约3年前我就在你的位置,现在90%的时间用它代替批处理文件。

在PowerShell中,这相对简单。您可以通过ForEach循环运行字符串数组,创建一个对象并为每个属性添加成员,然后当您到达新的Source行时输出上一个对象并开始一个新对象。它会自动为您创建一个数组,您可以将其传递给Export-CSV

我将具体做的是将变量$Record设置为空字符串。

然后我获取文件的内容,并将其传递给Where语句,该语句将匹配RegEx匹配的每一行。这将创建自动变量$Matches,它随线一起传递到管道。匹配将捕获第一个冒号之前的所有内容,然后捕获冒号后面的所有内容以及任何尾随空格。

这是通过一个ForEach循环传送的,它将为每一行执行一次。它检查$Matches[1](第一个冒号前的所有内容)='来源'。如果是,则输出$Record的当前内容,并创建一个新的$Record作为具有一个属性的自定义对象:'来源' = $Matches[2](第一个冒号和尾随空格后的所有内容)。如果$Matches[1]不等于'来源'然后,它会向$Record添加一个新属性,其中属性名称为$Matches[1],值为$Matches[2]。为清洁起见,我在.Trim()上执行了$Matches[2]方法,以确保没有前导或尾随空格或换行符或任何奇怪的内容。

在我处理完所有内容后,我再次通过Where语句运行它以删除空白记录(例如我事先设置的第一个记录)。然后我再次输出$Record。正如你所说的那样,你希望用CSV格式化,我已将整个循环和尾随$Record传递给Export-CSV

$Record = ""
$Output = @()
Get-Content Input_data.txt |     Where{$_ -match "([^:]*):\s*?(\S.*)"}|Foreach{
    if($Matches[1] -eq "Source"){
        $Output += $Record
        $Record = [PSCustomObject]@{'Source'=$Matches[2].trim()}
    }else{
        $Record | Add-Member $Matches[1] $Matches[2].trim()
    }
}|?{![string]::IsNullOrEmpty($_)} | Export-Csv Output.csv -NoTypeInformation
$Output += $Record
$Output | Export-Csv Output.csv -NoTypeInformation -Append

结果是包含以下内容的csv文件:

"Source","Destination","Total bytes","MB per min"
"X:\folder_abc","Y:\Abc_folder","208,731,021","256.5"
"X:\folder_def","Y:\xyz_folder","123,134,545","326"
"X:\folder_foo","Y:\Baz_folder","24,344","532"

或者,如果您不将其传输到Export-CSV,则只需将其显示在屏幕上:

Source                    Destination              Total bytes              MB per min              
------                    -----------              -----------              ----------              
X:\folder_abc             Y:\Abc_folder            208,731,021              256.5                   
X:\folder_def             Y:\xyz_folder            123,134,545              326                     
X:\folder_foo             Y:\Baz_folder            24,344                   532

编辑:好的,我使用Add-Member的方式出错了。这意味着您拥有旧版本的PowerShell。有2个解决方案。第一个,也是我的建议,更新PowerShell。有时这不是一个选择,所以没关系,我们可以使用它。

如果您使用的是PS v1或v2,我使用Add-Member的方式并不起作用。我如何使用它是如果你将一个对象传递给Add-Member然后指定2个字符串参数,它假设第一个是NotePropertyName,第二个是NotePropertyValue。你可以看到它上面的样子。因此,如果不起作用,该怎么办才能使用更详细的语法:

Add-Member -InputObject $TargetVariable -MemberType NoteProperty -Name Name -Value Value

在我们的例子中,它意味着我们替换Add-Member行:

Add-Member -InputObject $Record -MemberType NoteProperty -Name $Matches[1] -Value $Matches[2].trim()

然后你改变了输入。这很容易修复...将RegEx匹配从"([^:]*):\s*?(\S.*)"更改为"([^=]*)=\s*?(\S.*)"。所以把它们放在一起:

$Record = ""
$Output = @()
Get-Content Input_data.txt | Where{$_ -match "([^=]*)=\s*?(\S.*)"}|Foreach{
    if($Matches[1] -eq "Source"){
        If(![String]::IsNullOrEmpty($Record)){$Output += $Record}
        $Record = [PSCustomObject]@{'Source'=$Matches[2].trim()}
    }else{
        Add-Member -InputObject $Record -MemberType NoteProperty -Name $Matches[1] -Value $Matches[2].trim()
    }
}
$Output += $Record
$Output | Export-Csv C:\Temp\Output.csv -NoTypeInformation

Edit2:我想我已经忘记了 - 在旧版本的PowerShell中,对于Export-Csv来说,并不是一个选项。这可以通过收集所有数据并在最后输出一次来实现。我已经通过在顶部附近创建一个空数组$Output来更新我的答案中的最后一个脚本,然后在循环中而不是仅在完成一个输出$Record时将其添加到数组中。我还修改了该行以通过If语句以避免向数组添加空白记录。然后在ForEach循环之后,我将最后一条记录添加到数组中,最后将整个记录数组输出到CSV文件。

答案 1 :(得分:2)

使用创建多个数组的纯Batch文件可以轻松解决此问题,输出文件(字段)每列一个。当读取输入文件时,每次出现起始字段时,数组的索引都会递增(在这种情况下为“Source”),因此后续元素将存储在各自数组中的正确位置。输出只显示同一行中每个数组的一个元素。

@echo off
setlocal EnableDelayedExpansion

set "header="
set "output="
set i=0
for /F "tokens=1* delims==" %%a in (Input_data.txt) do (
   set "field=%%a"
   set "field=!field:~0,-1!"
   if "!field!" equ "Source" set /A i+=1
   if !i! equ 1 (
      set "header=!header!,"!field!""
      set "output=!output!,"^^!!field![%%i]^^!""
   )
   for /F %%c in ("%%b") do set "!field![!i!]=%%c"
)

(
echo %header:~1%
for /L %%i in (1,1,%i%) do echo %output:~1%
) > Result.csv

输出示例:

"Source","Destination","Total bytes","MB per min"
"X:\folder_abc","Y:\Abc_folder","208,731,021","256.5"
"X:\folder_def","Y:\xyz_folder","123,134,545","326"
"X:\folder_foo","Y:\Baz_folder","24,344","532"

您可以在以下位置查看批处理文件中的阵列管理:Arrays, linked lists and other data structures in cmd.exe (batch) script

编辑未添加数组的新方法

在我阅读dbenham的评论之后,我意识到在这个问题中使用数组是没有必要的,所以我相应地修改了我的原始解决方案;我还借用了dbenham使用%%~Na的技巧来消除字段名称末尾的空格:

@echo off
setlocal EnableDelayedExpansion

set "header=1"
set "row="
(for /F "tokens=1* delims==" %%a in (Input_data.txt) do (
   if defined header set "header=!header!,"%%~Na""
   for /F "tokens=*" %%c in ("%%b") do set "row=!row!,"%%c""
   if "%%a" equ "MB per min " (
      if defined header echo !header:~2!& set "header="
      echo !row:~1!
      set "row="
   )
)) > Result.csv

答案 2 :(得分:1)

这类似于Aacini的原始答案,但我从未在内存中存储过多行。大型输入文件会消耗大量内存,这会降低脚本速度。只存储一行就可以避免这个问题。

另一个主要区别是我让代码发现开始新行的列名,而不是对值进行硬编码。

我还使用不同的方法从标题中的每个列名中去掉尾随空格。我假设列名不包含以下任何字符::.\/。我依赖文件名不能以空格结尾的事实,因此~n修饰符规范化“名称”以删除任何尾随空格。

当从值中去除前导空格时,我也使用"tokens=*",以防值包含空格。

@echo OFF
setlocal enableDelayedExpansion

set "input=test.txt"
set "output=result.csv"

set "row="
set "header="
set "begin="
set "first="
(
  for /f "usebackq tokens=1* delims==" %%A in ("%input%") do for /f "tokens=*" %%C in ("%%B") do (
    if "!begin!" equ "%%A" (
      if not defined first (
        set first=1
        echo !header:~1!
      )
      echo !row:~1!
      set "row="
    )
    set "row=!row!,"%%C""
    if not defined first for /f "delims=" %%H in ("%%A") do (
      if not defined begin set "begin=%%A"
      set "header=!header!,"%%~nH""
    )
  )
  echo !row:~1!
)>"%output%"


编辑2014-12-05

可以在VBS或JScript中更强大地实现相同的算法,并且它会更快。

或者你可以稍微启动并使用JREPL.BAT - 一个混合JScript /批处理实用程序,它执行正则表达式搜索和替换文本。它允许将用户定义的JScript代码片段合并到流程中,但在批处理上下文中执行。

整个命令可以放在一个lonnnnnggggggg线上,但那真的很难看。相反,我使用批处理行继续来定义具有大多数用户定义的JScript代码的变量,并使用/JBEG传递它。将双引号文字传递给CSCRIPT是不可能的,所以我改用'\x22'

脚本期望将源文件作为第一个也是唯一的参数传递,并使用扩展名为.csv的相同基本名称将输出写入同一位置。

@echo off
setlocal
set beg=^
var begin, header='.', line='', q='\x22';^
function writeLn(){^
  if (header) output.WriteLine(header.substr(2));^
  header='';^
  if (line) output.WriteLine(line.substr(1));^
  line='';^
}^
function repl($1,$2){^
  if ($1==begin) writeLn();^
  if (!begin) begin=$1;^
  if (header) header+=','+q+$1+q;^
  line+=','+q+$2+q;^
  return false;^
}
call jrepl "^(.+?) *= *(.*)" "repl($1,$2);" /jmatch /jbeg "%beg%" /jend "writeLn();" /f %1 /o "%~dpn1.csv"
exit /b

下面使用完全相同的JScript代码,但我使用/JLIB选项直接从文件而不是从变量加载它。该脚本使用标准的混合Jscript /批处理技术。这个选项允许我在代码中使用双引号文字。

@if (@X)==(@Y) @end /* harmless hybrid line that begins a JScript comment

::**** Batch code ********
@echo off
call jrepl "^(.+?) *= *(.*)" "repl($1,$2);" /jmatch /jlib "%~f0" /jend "writeLn();" /f %1 /o "%~dpn1.csv"
exit /b

****** Jscript code ******/

var begin, header='.', line='', q='"';

function writeLn(){
  if (header) output.WriteLine(header.substr(2));
  header='';
  if (line) output.WriteLine(line.substr(1));
  line='';
}

function repl($1,$2){
  if ($1==begin) writeLn();
  if (!begin) begin=$1;
  if (header) header+=','+q+$1+q;
  line+=','+q+$2+q;
  return false;
}

答案 3 :(得分:0)

这适用于源数据:

@echo off
(
 for /f "usebackq tokens=1,* delims==" %%a in ("input_data.txt") do (
   if not defined header echo Source,Destination,Total bytes,MB per min&set header=1
   for /f "tokens=*" %%c in ("%%b") do if "%%a"=="MB per min " (set/p=""%%c""<nul&echo() else (set/p=""%%c","<nul)
 )
)>"output_data.txt"

<强> “output_data.txt”

Source,Destination,Total bytes,MB per min
"X:\folder_abc","Y:\Abc_folder","208,731,021","256.5"
"X:\folder_def","Y:\xyz_folder","123,134,545","326"
"X:\folder_foo","Y:\Baz_folder","24,344","532"