如何从文件中删除\ n字符?

时间:2012-10-30 19:00:48

标签: c# .net sed newline

我有一个问题应该让大多数人去“WTF?”,但我仍然拥有它。

我从供应商处获得了大量数据文件。它采用自定义平面文件格式,声称为CSV,但不以逗号分隔,并且不引用值。所以,根本不是真的。

foo,bar,baz
alice,bob,chris

等等,除了更长,更少有趣。问题是,有些记录嵌入了换行符(!!!):

foo,bar
rab,baz
alice,bob,chris

这应该是每个三个字段的两个记录。通常情况下,我只会说“不,这是愚蠢的。”但是我不经意地看得更近了,发现它实际上是一种与实际行结束序列不同的行尾:

foo,bar\n
rab,baz\r\n
alice,bob,chris\r\n

注意第一行上的\ n。我已经确定这适用于我发现嵌入式换行的所有情况。所以,我基本上需要做s/\n$//(我试过这个特定的命令,它没有做任何事情)。

注意:我实际上并不关心字段的内容,因此无需更换换行符。我只需要文件中的每一行都有相同数量的记录(理想情况下,在同一个地方)。

我在处理文件的工具中有一个现有的解决方案:

Guid g = Guid.NewGuid();

string data = File.ReadAllText(file, Encoding.GetEncoding("Latin1"));
data = data.Replace("\r\n", g.ToString()); //just so I have a unique placeholder
data = data.Replace("\n", "");
data = data.Replace(g.ToString(), "\r\n");

但是,对于大于千兆字节左右的文件,这会失败。 (另外,我还没有对它进行分析,但我怀疑它的狗也很慢)。

我可以使用的工具是:

  • cygwin工具(sed,grep等)
  • .NET

这样做的最佳方式是什么?

4 个答案:

答案 0 :(得分:5)

不要将整个内容作为一个大的(可能是巨大的)字符串读入内存,而是考虑基于流的方法。

打开输入流并一次读取一行,根据需要进行替换。打开输出流并将修改后的行写入其中。类似的东西:

static void Main( string[] args )
{
    using( var inFs = File.OpenRead( @"C:\input.txt" ) )
    using( var reader = new StreamReader( inFs ) )
    using( var outFs = File.Create( @"C:\output.txt" ) )
    using( var writer = new StreamWriter( outFs ) )
    {
        int cur;
        char last = '0';
        while( ( cur = reader.Read() ) != -1 )
        {
            char next = (char)reader.Peek();
            char c = (char)cur;
            if( c != '\n' || last == '\r' )
                writer.Write( c );

            last = c;
        }
    }
}

答案 1 :(得分:2)

这是一个非常简单的代码。

试试这个。

tr -d '\n' <dirtyfile >cleanfile

答案 2 :(得分:0)

这是一个StreamReader类似乎可以做我想要的。请注意,这可能是特定于域特定的,因此它可能有用也可能没用:

class BadEOLStreamReader : StreamReader {
    private int pushback = -1;

    public BadEOLStreamReader(string file, Encoding encoding) : base(file, encoding) {

    }

    public override int Peek() {
        if (pushback != -1) {
            var r = pushback;
            pushback = -1;
            return r;
        }

        return base.Peek();
    }

    public override int Read() {
        if (pushback != -1) {
            var r = pushback;
            pushback = -1;
            return r;
        }

        skip:
        var ret = base.Read();
        if (ret == 13) {
            var ret2 = base.Read();
            if (ret2 == 10) {
                //it's good, push back the 10
                pushback = ret2;
                return ret;
            }
            pushback = ret2;
            //skip it
            goto skip;
        } else if (ret == 10) {
            //skip it
            goto skip;
        } else {

            return ret;
        }
    }
}

答案 3 :(得分:0)

编辑:经过一些测试,awk解决方案在速度方面提供了更好的结果。

UNIX / Linux / Cygwin中的标准文件/输入过滤器很难处理二进制文件。要使用过滤器执行此操作,您需要将文件转换为十六进制,使用sed(或awk进行编辑,请参阅下面的第二个解决方案),然后将其转换回原始数据。这应该这样做:

xxd -c1 -p file.txt | 
  sed -n -e '1{h}' -e '${x;G;p;d}' \
      -e '2,${x;G;/^0d\n0a$/{P;b};/\n0a$/{P;s/.*//;x;b};P}' |
  xxd -r -p

好的,这不容易理解,让我们从简单的部分开始:

  • xxd -c1 -p file.txtfile.txt从二进制转换为HEX,每行一个字节。
  • xxd -r -p恢复转换。
  • sed\n(HEX中的0)替换为\r(HEX中为0d)之前没有任何内容。

sed部分的想法是将前一个字节存储在保留空间中,并处理前一个字节和当前字节:

  • 在第1行,将行(字节)存储在保留空间中。
  • 在最后一行,以正确的顺序(x;G;p)打印两个字节并停止脚本(d)。
  • 对于中间的行,在保留空间中的当前字节和模式空间(x;G)中的2个字节(前一个和当前)之后,有3种可能的情况:
    1. 如果是\r\n,则打印\r\n保留在保留空间中以进行下一个周期并停止此循环(b命令)。
    2. 否则,如果它以\n结尾(意味着它没有以\r开头)在保留空间中存储空字符串并停止此循环(b命令)
    3. 否则打印第1个字符。

awk中可能更容易理解:

xxd -c1 -p file.txt |
  awk 'NR > 1 && $0 == "0a" && p != "0d" {$0 = ""}
       NR > 1 {print p}
       {p = $0}
       END{print p}' |
  xxd -r -p

可以通过以下方式进行测试:

printf "foo,bar\nrab,baz\r\nalice,bob,chris\r\n" |
  xxd -c1 -p | 
  sed -n -e '1{h}' -e '${x;G;p;d}' \
      -e '2,${x;G;/^0d\n0a$/{P;b};/\n0a$/{P;s/.*//;x;b};P}' |
  xxd -r -p

printf "foo,bar\nrab,baz\r\nalice,bob,chris\r\n" |
  xxd -c1 -p |
  awk 'NR > 1 && $0 == "0a" && p != "0d" {$0 = ""}
       NR > 1 {print p}
       {p = $0}
       END{print p}' |
  xxd -r -p