使用批处理/powershell 脚本设置自定义行分隔符
Posted
技术标签:
【中文标题】使用批处理/powershell 脚本设置自定义行分隔符【英文标题】:Set custom row delimiter using batch/powershell script 【发布时间】:2017-05-07 03:17:01 【问题描述】:我有一个 >1.5GB 的大文件,它有 '#@#@#' 作为行分隔符。在通过 Informatica 处理它之前,我将用 CRLF 字符替换它。问题是,我的文件中有 CR 、LF 字符,我需要在替换之前删除它们。我找到了几个选项可以做到这一点,但由于大小,我得到了 OutofMemory 异常。
param
(
[string]$Source,
[string]$Destination
)
echo $Source
echo $Destination
$Writer = New-Object IO.StreamWriter $Destination
$Writer.Write( [String]::Join("", $(Get-Content $Source)) )
$Writer.Close()
我的问题是,是否将我的行分隔符设置为“#@#@#”,然后逐行读取文件以删除 CR、LF 字符。
【问题讨论】:
听起来你需要更多内存。我知道我可以在我的计算机上加载 4GB 内存的 1.5GB 文件。我用来删除 CRLF 的大多数实用程序都要求它首先将整个文件加载到内存中,然后才能删除 CRLF。希望有人为您提出更好的解决方案。 谢谢壁球手,这是我要求额外内存的最后选择。我会看看是否有人提出不同的解决方案。谢谢。 您可以尝试不使用 StreamWriter 并且可能不会加载到内存中吗?类似:Get-Content "C:\source.txt" | Foreach 对象 $_.Replace(..) |设置内容 C:\..Output.txt 输入文件包含 CR 和 LF 字符,但它们也可以作为 CR+LF 序列出现吗?你想用什么来替换 CR 和 LF? 这听起来像是 sed or tr, etc 和 Enable true linux shell on windows 10 anniversary edition 的工作 【参考方案1】:好的,我的第一次尝试慢得令人难以忍受。这是一个很好的解决方案,它能够在 2 分 48 秒内处理一个 1.8 GB 的文件 :-)
我使用了混合批处理/JScript,因此它可以在 XP 以后的任何 Windows 机器上运行 - 不需要第 3 方 exe 文件,也不需要任何编译。
我读写大约 1 MB 的块。逻辑其实很简单。
我将所有 \r\n 替换为一个空格,并将 #@#@# 替换为 \r\n。您可以轻松更改代码中的字符串值以满足您的需要。
fixLines.bat
@if (@X)==(@Y) @end /* Harmless hybrid line that begins a JScript comment
::--- Batch section within JScript comment that calls the internal JScript ----
@echo off
setlocal disableDelayedExpansion
if "%~1" equ "" (
echo Error: missing input argument
exit /b 1
)
if "%~2" equ "" (
set "out=%~f1.new"
) else (
set "out=%~2"
)
<"%~1" >"%out%" cscript //nologo //E:JScript "%~f0"
if "%~2" equ "" move /y "%out%" "%~1" >nul
exit /b
----- End of JScript comment, beginning of normal JScript ------------------*/
var delim='#@#@#',
delimReplace='\r\n',
nl='\r\n',
nlReplace=' ',
pos=0,
str='';
var delimRegex=new RegExp(delim,"g"),
nlRegex=new RegExp(nl,"g");
while( !WScript.StdIn.AtEndOfStream )
str=str.substring(pos)+WScript.StdIn.Read(1000000);
pos=str.lastIndexOf(delim)
if (pos>=0)
pos+=delim.length;
WScript.StdOut.Write(str.substring(0,pos).replace(nlRegex,nlReplace).replace(delimRegex,delimReplace));
else
pos=0
if (str.length>pos) WScript.StdOut.Write(str.substring(pos).replace(nlRegex,nlReplace));
修复 input.txt 并将输出写入 output.txt:
fixLines input.txt output.txt
覆盖原文件test.txt
fixLines test.txt
只是为了好玩,我尝试使用 JREPL.BAT 处理 1.8 GB 文件。我不认为它会起作用,因为它必须将整个文件加载到内存中。计算机中安装了多少内存并不重要 - JScript 的最大字符串大小限制为 2GB。而且我认为还有其他限制因素起作用。
jrepl "\r?\n:#@#@#" " :\r\n" /m /x /t : /f input.txt /o output.txt
命令失败并出现“内存不足”错误需要 5 分钟。然后我的电脑花了很长时间才从严重的内存滥用中恢复过来。
以下是我最初的自定义批处理/JScript 解决方案,一次读取和写入一个字符。
slow.bat
@if (@X)==(@Y) @end /* Harmless hybrid line that begins a JScript comment
::--- Batch section within JScript comment that calls the internal JScript ----
@echo off
setlocal disableDelayedExpansion
if "%~1" equ "" (
echo Error: missing input argument
exit /b 1
)
if "%~2" equ "" (
set "out=%~f1.new"
) else (
set "out=%~2"
)
<"%~1" >"%out%" cscript //nologo //E:JScript "%~f0"
if "%~2" equ "" move /y "%out%" "%~1" >nul
exit /b
----- End of JScript comment, beginning of normal JScript ------------------*/
var delim='#@#@#',
delimReplace='\r\n',
nlReplace=' ',
read=1,
write=2,
pos=0,
char;
while( !WScript.StdIn.AtEndOfStream )
chr=WScript.StdIn.Read(1);
if (chr==delim.charAt(pos))
if (++pos==delim.length)
WScript.StdOut.Write(delimReplace);
pos=0;
else
if (pos)
WScript.StdOut.Write(delim.substring(0,pos));
pos=0;
if (chr=='\n')
WScript.StdOut.Write(nlReplace);
else if (chr!='\r')
WScript.StdOut.Write(chr);
if (pos) WScript.StdOut.Write(delim.substring(0,pos));
它有效,但它是一只狗。以下是处理 155 MB 文件的时间结果摘要:
slow.bat 3120 sec (52 min)
jrepl.bat 55 sec
fixLines.bat 15 sec
我验证了所有三种解决方案都给出了相同的结果。
【讨论】:
占用 1,500,000,000 个字节需要多长时间?如果她的计算机中有足够的内存,这似乎更适合 JREPL。 @Squashman - 大小是整个问题 - JREPL 需要/M
选项,这意味着整个文件必须适合内存,我不认为 JREPL 可以处理这么大的文件在记忆中。我想通过一次读取一个数据块(可能是 1mb
这看起来像很多过多的文件io,并且似乎容易出现内存泄漏。为什么不尝试一些更简单的方法,例如将文件分块成更大的块或使用自定义行结束的 csv 解析器?或者类似cc.davelozinski.com/c-sharp/…
@SamuelJackson - 内存泄漏?我想不是 !但是,是的,逐字节读取可能会减慢速度。我刚刚向 Squashman 承认,读取块可能会稍微提高性能。但我不认为表现会受到那么的糟糕。我不知道任何现有的实用程序(csv 或其他)可以有效地读取带有任意行终止符的行。任何解决方案都必须在逻辑上逐字节处理,但 IO 可以被缓冲。
@Squashman - 我已经确认 JREPL 使用 1.8 GB 文件失败。我最初的解决方案也是一只完整的狗。但是新的解决方案还不错!。【参考方案2】:
概念上简单且节省内存,但速度很慢 PowerShell 解决方案:
这个 PowerShell (v2+) 解决方案慢,但它在概念上很简单,您不应该耗尽内存,因为输入行一次处理一个,使用 #@#@#
作为行分隔符。
注意:此解决方案结合您的两个步骤:
它用一个空格替换原来的换行符,
它用换行符替换每个#@#@#
序列。
# Create sample input file.
@'
line 1 starts here
and
ends here#@#@#line 2 is all on one line#@#@#line 3 spans
two lines#@#@#
'@ > file
# Determine the input file.
$inFile = 'file'
# Create the output file.
$outFile = 'out'
$null = New-Item -Type File $outFile
Get-Content -Delimiter '#@#@#' $inFile | %
Add-Content -Value ($_.Replace("`r`n", " ").Replace($sep, '')) $outFile
注意:
当您使用-Delimiter
时,指定的分隔符在通过管道传递的每个项目中包含(与默认行为不同,其中默认分隔符(换行符)被剥离 )。
Add-Content
自动在其输出中添加尾随 CRLF(在 PSv5+ 中,这可以使用 -NoNewLine
抑制)。
该方法使用[string]
类型的.Replace()
方法,而不是PowerShell 灵活的、基于正则表达式的-replace
运算符,因为.Replace()
执行文字 替换,这更快(等效命令是Add-Content -Value (($_ -replace '\r\n', ' ') -replace '#@#@#') $outFile
.
也就是说,速度增益可以忽略不计。大部分时间是文件 I/O 部分)。
具有 C# 代码按需编译的更快的 PowerShell 解决方案
dbenham's clever and elegant batch + JScript solution 明显快于上述 PowerShell 解决方案。
这是他在按需编译的 PowerShell 脚本中使用 C# 代码的方法的改编。
编译速度快得惊人(在我 2012 年末的 iMac 上大约需要 0.3 秒),并且使用编译后的代码来处理文件可以显着提高性能。 另请注意,每个会话仅执行一次编译,因此后续调用不会支付此惩罚。
使用下面打印的脚本处理约 1 GB 的文件(通过重复上述示例文件的内容创建)会产生以下结果:
Compiling...
Processing file...
Completed:
Compilation time: 00:00:00.2343647
File-processing time: 00:00:26.0714467
Total: 00:00:26.3278546
实际应用程序中的执行时间会因许多因素而有所不同,但根据下面 cmets 中提到的@dbenham 的时间,按需编译解决方案的速度大约是批处理 + JScript 解决方案的两倍。
快速PowerShell解决方案源码:
# Determine the input and output files.
$inFile = 'file'
$outFile = 'out'
# Get current time stamp for measuring duration.
$dtStart = [datetimeoffset]::UtcNow
# How many characters to read at a time.
# !! Make sure that this at least as large as the max. input.line length.
$kCHUNK_SIZE = 1000000
Write-Host 'Compiling...'
# Note: This statement performs on-demand compilation, but only
# on *first* invocation in a given session.
$tsCompilation = Measure-Command
Add-Type @"
using System;
using System.IO;
namespace net.same2u.so
public static class Helper
public static void TransformFile(string inFile, string outFile, string sep)
char[] bufChars = new char[$kCHUNK_SIZE];
using (var sw = new StreamWriter(outFile))
using (var sr = new StreamReader(inFile))
int pos = 0; bool eof = false;
string bufStr, rest = String.Empty;
while (!(eof = sr.EndOfStream) || rest.Length > 0)
if (eof)
bufStr = rest;
else
int count = sr.Read(bufChars, 0, $kCHUNK_SIZE);
bufStr = rest.Length > 0 ? rest + new string(bufChars, 0, count) : new string(bufChars, 0, count);
if (-1 == (pos = bufStr.LastIndexOf(sep))) // should only happen at the very end
sw.Write(bufStr);
rest = String.Empty;
else
pos += sep.Length; rest = bufStr.Substring(pos);
sw.Write(bufStr.Substring(0, pos).Replace(Environment.NewLine, " ").Replace(sep, Environment.NewLine));
// class Helper
"@
if (-not $?) exit 1
Write-Host 'Processing file...'
# Make sure the .NET framework sees the same current dir. as PS.
[System.IO.Directory]::SetCurrentDirectory($PWD)
$tsFileProcessing = Measure-Command
[net.same2u.so.Helper]::TransformFile($inFile, $outFile, '#@#@#')
Write-Host @"
Completed:
Compilation time: $tsCompilation
File-processing time: $tsFileProcessing
Total: $([datetimeoffset]::UtcNow - $dtStart)
"@
【讨论】:
向该脚本添加一些并行处理,应该能够显着减少时间,同时保留内存:) - 赞成对系统资源的影响最小。仍然容易发生泄漏(就像大多数大文件操作一样),但在我看来总体上是一个可行的解决方案(只需要一些 PP) 谢谢@SamuelJackson。我假设您在开玩笑说并行处理(如果不是,请告诉我,以及您的想法)。可能有什么泄漏? 向下滚动on this site 以查看用于在 c# 中读取的并行处理示例——我相信 PowerShell 具有一些并行处理能力(但我可能是错的)。处理大文件的内存泄漏自然而然地伴随着缓冲数据(这是“寻找”用其他东西替换的东西所必需的。进行字节替换几乎会更快,因此将 '#@#@#' 替换为'\n '(5 个空格),因为这不需要初始指针移动。 呃,只比我原来的一次 1 字节批处理/JScript 解决方案稍微好一点:处理 155 MB 文件需要 39 分钟,而处理 155 MB 文件需要 52 分钟。对于 1.5 GB 的文件,这仍然可以扩展到 6.5 小时 - 不是很实用。将其与我的 latest batch/JScript solution 进行比较,时间为 2 分 48 秒,文件大小为 1.8 GB。我想您可以将我的 JScript 算法转换为 powershell 并获得更高的性能。 @dbenham:感谢您运行基准测试。我知道它不会很快,但我也没想到它会那么慢。 PowerShell 是抽象的拥护者,这很棒,但以牺牲性能为代价,有时使得它的使用变得不切实际。以上是关于使用批处理/powershell 脚本设置自定义行分隔符的主要内容,如果未能解决你的问题,请参考以下文章