使用批处理/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 脚本设置自定义行分隔符的主要内容,如果未能解决你的问题,请参考以下文章

尤里卡中的自定​​义行

xlform 使用笔尖的自定义行

Leanback 创建不同的自定义行视图

如何在 PySpark 中使用自定义行分组来 reduceByKey?

WCF 自定义行为的依赖注入

列表视图自定义行