4

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

我从供应商那里得到了一堆数据文件。它采用自称是 CSV 的自定义平面文件格式,但它不是逗号分隔的,并且值没有被引用。所以,根本不是真正的 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");

但是,这在大于 1 GB 左右的文件上会失败。(此外,我还没有对其进行分析,但我怀疑它也很慢)。

我可以使用的工具是:

  • cygwin 工具(sed、grep 等)
  • 。网

做这个的最好方式是什么?

4

4 回答 4

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;
        }
    }
}
于 2012-10-30T19:03:30.380 回答
2

做这么简单的事情需要大量的代码。

试试这个。

tr -d '\n' <dirtyfile >cleanfile
于 2012-12-13T03:42:24.777 回答
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;
        }
    }
}
于 2012-10-30T19:54:12.803 回答
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.txt从二进制转换file.txt为十六进制,每行一个字节。
  • xxd -r -p恢复转换。
  • 将前面没有(0d in HEX)sed的 (0a in HEX) 替换为空。\n\r

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

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

可能更容易理解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
于 2012-10-30T22:11:47.410 回答