如果文本文件中的表包含基于行的数据,那么您建议使用哪种方法转换为基于列的表? (例如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指出不这样做会导致问题。
答案 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"