在 C# 中比较两个不同 CSV 文件的行值
compare rows values of two different CSV files in c#
我知道还有更多类似的问题,但我找不到我的答案。我有两个 CSV 文件。这两个文件都包含相同图像的图像元数据,但是,第一个文件图像 ID 已过时。所以我需要从第二个文件中获取 ID,并将过时的 ID 替换为新的 ID。我正在考虑比较图像的经度、纬度和海拔高度行值,以及它在两个文件中匹配的位置,我从第二个文件中获取图像 ID。这些 ID 将在新对象中使用。并且文件中的行顺序不同,第一个文件包含的行数比第二个文件多。
文件结构如下:
第一个文件:
ImgID,Longitude,Latitude,Altitude
01,44.7282372307,27.5786807185,14.1536407471
02,44.7287939869,27.5777060219,13.2340240479
03,44.7254687824,27.582636255,16.5887145996
04,44.7254294913,27.5826908925,16.5794525146
05,44.728785278,27.5777185252,13.2553100586
06,44.7282279311,27.5786933339,14.1576690674
07,44.7253847039,27.5827526969,16.6026000977
08,44.7287777782,27.5777295052,13.2788238525
09,44.7282196988,27.5787045314,14.1649169922
10,44.7253397041,27.5828151049,16.6300048828
11,44.728769439,27.5777417846,13.3072509766
第二个文件:
ImgID,Longitude,Latitude,Altitude
5702,44.7282372307,27.5786807185,14.1536407471
5703,44.7287939869,27.5777060219,13.2340240479
5704,44.7254687824,27.582636255,16.5887145996
5705,44.7254294913,27.5826908925,16.5794525146
5706,44.728785278,27.5777185252,13.2553100586
5707,44.7282279311,27.5786933339,14.1576690674
如何在 C# 中完成此操作?是否有一些方便的库可以使用?
我会为 CSV read/write 使用 CSVHelper
library,因为它是一个完整的好库。为此,您应该声明一个 class 来保存您的数据,并且其 属性 名称必须与您的 CSV 文件的列名称相匹配。
public class ImageData
{
public int ImgID { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public double Altitude { get; set; }
}
然后查看两行是否相等,您需要做的是查看一个文件中每一行中的每个 属性 是否与另一行匹配。您可以通过简单地比较属性来做到这一点,但我宁愿为此编写一个比较器,如下所示:
public class ImageDataComparer : IEqualityComparer<ImageData>
{
public bool Equals(ImageData x, ImageData y)
{
return (x.Altitude == y.Altitude && x.Latitude == y.Latitude && x.Longitude == y.Longitude);
}
public int GetHashCode(ImageData obj)
{
unchecked
{
int hash = (int)2166136261;
hash = (hash * 16777619) ^ obj.Altitude.GetHashCode();
hash = (hash * 16777619) ^ obj.Latitude.GetHashCode();
hash = (hash * 16777619) ^ obj.Longitude.GetHashCode();
return hash;
}
}
}
简单的解释是,我们重写了Equals()
方法,并规定如果三个属性 值匹配,ImageData
class 的两个实例就相等。我稍后会展示用法。
CSV read/write 部分非常简单(图书馆的帮助页面有一些很好的示例和提示,请阅读)。我可以像这样写两种读写方法:
public static List<ImageData> ReadCSVData(string filePath)
{
List<ImageData> records;
using (var reader = new StreamReader(filePath))
{
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csv.Configuration.HasHeaderRecord = true;
records = csv.GetRecords<ImageData>().ToList();
}
}
return records;
}
public static void WriteCSVData(string filePath, List<ImageData> records)
{
using (var writer = new StreamWriter(filePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(records);
}
}
}
您实际上可以编写通用 <T>
read/write 方法,这样这两种方法可用于不同的 classes,如果这对您有用的话。
接下来是关键部分。首先,使用我们刚刚定义的方法将这两个文件读入内存。
var oldData = ReadCSVData(Path.Combine(Directory.GetCurrentDirectory(), "OldFile.csv"));
var newData = ReadCSVData(Path.Combine(Directory.GetCurrentDirectory(), "NewFile.csv"));
现在,我可以遍历'old'数据中的每一行,看看'new'数据中是否有对应的记录。如果是这样,我从新数据中获取 ID 并用它替换旧数据的 ID。注意我们写的比较器的用法。
foreach (var line in oldData)
{
var replace = newData.FirstOrDefault(x => new ImageDataComparer().Equals(x, line));
if (replace != null && replace.ImgID != line.ImgID)
{
line.ImgID = replace.ImgID;
}
}
接下来,简单地覆盖旧的数据文件。
WriteCSVData(Path.Combine(Directory.GetCurrentDirectory(), "OldFile.csv"), oldData);
结果
我正在使用您数据的简化版本来轻松验证我们的结果。
旧数据
ImgID,Longitude,Latitude,Altitude
1,1,2,3
2,2,3,4
3,3,4,5
4,4,5,6
5,5,6,7
6,6,7,8
7,7,8,9
8,8,9,10
9,9,10,11
10,10,11,12
11,11,12,13
新数据
ImgID,Longitude,Latitude,Altitude
5702,1,2,3
5703,2,3,4
5704,3,4,5
5705,4,5,6
5706,5,6,7
5707,6,7,8
现在我们的预期结果应该是旧文件的前 6 行应该更新 ID,这就是我们得到的结果:
更新旧数据
ImgID,Longitude,Latitude,Altitude
5702,1,2,3
5703,2,3,4
5704,3,4,5
5705,4,5,6
5706,5,6,7
5707,6,7,8
7,7,8,9
8,8,9,10
9,9,10,11
10,10,11,12
11,11,12,13
如果出于某种原因您不想使用 CSVHelper,另一种方法是编写一个方法来比较两行数据并确定它们是否相等(通过忽略第一列数据):
public static bool DataLinesAreEqual(string first, string second)
{
if (first == null || second == null) return false;
var xParts = first.Split(',');
var yParts = second.Split(',');
if (xParts.Length != 4 || yParts.Length != 4) return false;
return xParts.Skip(1).SequenceEqual(yParts.Skip(1));
}
然后我们可以将两个文件中的所有行读入数组,然后如果我们的方法说它们相等,我们可以用第二个文件中的行更新我们的第一个文件行:
var csvPath1 = @"c:\temp\csvData1.csv";
var csvPath2 = @"c:\temp\csvData2.csv";
// Read lines from both files
var first = File.ReadAllLines(csvPath1);
var second = File.ReadAllLines(csvPath2);
// Select the updated line where necessary
var updated = first.Select(f => second.FirstOrDefault(s => DataLinesAreEqual(f, s)) ?? f);
// Write the updated result back to the first file
File.WriteAllLines(csvPath1, updated);
我知道还有更多类似的问题,但我找不到我的答案。我有两个 CSV 文件。这两个文件都包含相同图像的图像元数据,但是,第一个文件图像 ID 已过时。所以我需要从第二个文件中获取 ID,并将过时的 ID 替换为新的 ID。我正在考虑比较图像的经度、纬度和海拔高度行值,以及它在两个文件中匹配的位置,我从第二个文件中获取图像 ID。这些 ID 将在新对象中使用。并且文件中的行顺序不同,第一个文件包含的行数比第二个文件多。
文件结构如下:
第一个文件:
ImgID,Longitude,Latitude,Altitude
01,44.7282372307,27.5786807185,14.1536407471
02,44.7287939869,27.5777060219,13.2340240479
03,44.7254687824,27.582636255,16.5887145996
04,44.7254294913,27.5826908925,16.5794525146
05,44.728785278,27.5777185252,13.2553100586
06,44.7282279311,27.5786933339,14.1576690674
07,44.7253847039,27.5827526969,16.6026000977
08,44.7287777782,27.5777295052,13.2788238525
09,44.7282196988,27.5787045314,14.1649169922
10,44.7253397041,27.5828151049,16.6300048828
11,44.728769439,27.5777417846,13.3072509766
第二个文件:
ImgID,Longitude,Latitude,Altitude
5702,44.7282372307,27.5786807185,14.1536407471
5703,44.7287939869,27.5777060219,13.2340240479
5704,44.7254687824,27.582636255,16.5887145996
5705,44.7254294913,27.5826908925,16.5794525146
5706,44.728785278,27.5777185252,13.2553100586
5707,44.7282279311,27.5786933339,14.1576690674
如何在 C# 中完成此操作?是否有一些方便的库可以使用?
我会为 CSV read/write 使用 CSVHelper
library,因为它是一个完整的好库。为此,您应该声明一个 class 来保存您的数据,并且其 属性 名称必须与您的 CSV 文件的列名称相匹配。
public class ImageData
{
public int ImgID { get; set; }
public double Longitude { get; set; }
public double Latitude { get; set; }
public double Altitude { get; set; }
}
然后查看两行是否相等,您需要做的是查看一个文件中每一行中的每个 属性 是否与另一行匹配。您可以通过简单地比较属性来做到这一点,但我宁愿为此编写一个比较器,如下所示:
public class ImageDataComparer : IEqualityComparer<ImageData>
{
public bool Equals(ImageData x, ImageData y)
{
return (x.Altitude == y.Altitude && x.Latitude == y.Latitude && x.Longitude == y.Longitude);
}
public int GetHashCode(ImageData obj)
{
unchecked
{
int hash = (int)2166136261;
hash = (hash * 16777619) ^ obj.Altitude.GetHashCode();
hash = (hash * 16777619) ^ obj.Latitude.GetHashCode();
hash = (hash * 16777619) ^ obj.Longitude.GetHashCode();
return hash;
}
}
}
简单的解释是,我们重写了Equals()
方法,并规定如果三个属性 值匹配,ImageData
class 的两个实例就相等。我稍后会展示用法。
CSV read/write 部分非常简单(图书馆的帮助页面有一些很好的示例和提示,请阅读)。我可以像这样写两种读写方法:
public static List<ImageData> ReadCSVData(string filePath)
{
List<ImageData> records;
using (var reader = new StreamReader(filePath))
{
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csv.Configuration.HasHeaderRecord = true;
records = csv.GetRecords<ImageData>().ToList();
}
}
return records;
}
public static void WriteCSVData(string filePath, List<ImageData> records)
{
using (var writer = new StreamWriter(filePath))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(records);
}
}
}
您实际上可以编写通用 <T>
read/write 方法,这样这两种方法可用于不同的 classes,如果这对您有用的话。
接下来是关键部分。首先,使用我们刚刚定义的方法将这两个文件读入内存。
var oldData = ReadCSVData(Path.Combine(Directory.GetCurrentDirectory(), "OldFile.csv"));
var newData = ReadCSVData(Path.Combine(Directory.GetCurrentDirectory(), "NewFile.csv"));
现在,我可以遍历'old'数据中的每一行,看看'new'数据中是否有对应的记录。如果是这样,我从新数据中获取 ID 并用它替换旧数据的 ID。注意我们写的比较器的用法。
foreach (var line in oldData)
{
var replace = newData.FirstOrDefault(x => new ImageDataComparer().Equals(x, line));
if (replace != null && replace.ImgID != line.ImgID)
{
line.ImgID = replace.ImgID;
}
}
接下来,简单地覆盖旧的数据文件。
WriteCSVData(Path.Combine(Directory.GetCurrentDirectory(), "OldFile.csv"), oldData);
结果
我正在使用您数据的简化版本来轻松验证我们的结果。
旧数据
ImgID,Longitude,Latitude,Altitude
1,1,2,3
2,2,3,4
3,3,4,5
4,4,5,6
5,5,6,7
6,6,7,8
7,7,8,9
8,8,9,10
9,9,10,11
10,10,11,12
11,11,12,13
新数据
ImgID,Longitude,Latitude,Altitude
5702,1,2,3
5703,2,3,4
5704,3,4,5
5705,4,5,6
5706,5,6,7
5707,6,7,8
现在我们的预期结果应该是旧文件的前 6 行应该更新 ID,这就是我们得到的结果:
更新旧数据
ImgID,Longitude,Latitude,Altitude
5702,1,2,3
5703,2,3,4
5704,3,4,5
5705,4,5,6
5706,5,6,7
5707,6,7,8
7,7,8,9
8,8,9,10
9,9,10,11
10,10,11,12
11,11,12,13
如果出于某种原因您不想使用 CSVHelper,另一种方法是编写一个方法来比较两行数据并确定它们是否相等(通过忽略第一列数据):
public static bool DataLinesAreEqual(string first, string second)
{
if (first == null || second == null) return false;
var xParts = first.Split(',');
var yParts = second.Split(',');
if (xParts.Length != 4 || yParts.Length != 4) return false;
return xParts.Skip(1).SequenceEqual(yParts.Skip(1));
}
然后我们可以将两个文件中的所有行读入数组,然后如果我们的方法说它们相等,我们可以用第二个文件中的行更新我们的第一个文件行:
var csvPath1 = @"c:\temp\csvData1.csv";
var csvPath2 = @"c:\temp\csvData2.csv";
// Read lines from both files
var first = File.ReadAllLines(csvPath1);
var second = File.ReadAllLines(csvPath2);
// Select the updated line where necessary
var updated = first.Select(f => second.FirstOrDefault(s => DataLinesAreEqual(f, s)) ?? f);
// Write the updated result back to the first file
File.WriteAllLines(csvPath1, updated);