我有一个问题应该让大多数人去“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");
但是,对于大于千兆字节左右的文件,这会失败。 (另外,我还没有对它进行分析,但我怀疑它的狗也很慢)。
我可以使用的工具是:
这样做的最佳方式是什么?
答案 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.txt
将file.txt
从二进制转换为HEX,每行一个字节。xxd -r -p
恢复转换。sed
将\n
(HEX中的0)替换为\r
(HEX中为0d)之前没有任何内容。 sed
部分的想法是将前一个字节存储在保留空间中,并处理前一个字节和当前字节:
x;G;p
)打印两个字节并停止脚本(d
)。x;G
)中的2个字节(前一个和当前)之后,有3种可能的情况:
\r\n
,则打印\r
将\n
保留在保留空间中以进行下一个周期并停止此循环(b
命令)。\n
结尾(意味着它没有以\r
开头)在保留空间中存储空字符串并停止此循环(b
命令)在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