我正在开发一个应用程序,我必须将CSV文件中的数据上传到数据库表中。问题是,我没有CSV文件,但我有平面文本文件被转换成CSV。 另一个问题是,由于具有不同系统的几个客户使用该应用程序,因此我使用不同布局的平面文本文件。解析一个文本文件
我想要实现的是创建一个从特殊文件加载“规则”的应用程序;这些规则将与平面文本文件一起处理,以生成CSV文件。从平面文件转换为CSV的应用程序将是相同的,只是这组规则会有所不同。
我该如何做到这一点?你推荐的最佳做法是什么?
我正在开发一个应用程序,我必须将CSV文件中的数据上传到数据库表中。问题是,我没有CSV文件,但我有平面文本文件被转换成CSV。 另一个问题是,由于具有不同系统的几个客户使用该应用程序,因此我使用不同布局的平面文本文件。解析一个文本文件
我想要实现的是创建一个从特殊文件加载“规则”的应用程序;这些规则将与平面文本文件一起处理,以生成CSV文件。从平面文件转换为CSV的应用程序将是相同的,只是这组规则会有所不同。
我该如何做到这一点?你推荐的最佳做法是什么?
这取决于规则的复杂性。如果唯一不同的输入是所使用的列和分隔符的名称,那么这很容易,但是如果您希望能够解析完全不同的格式(如XML等),那么这是一个不同的故事。
我自己会选择为'记录'读取器实现一个基类,它从文件读取记录并将它们输出到数据集或CSV。 然后,您可以实现实现读取不同源格式的子类。
如果您愿意,您可以为这些格式添加特定的规则,以便您可以制作一个通用的XMLReader,它可以从BaseReader下载,但允许配置列名称。但是我会先从一些硬编码的阅读器开始,直到你更清楚你可能遇到的那些格式的哪些方言。
编辑:根据要求,它可能是一个例子。
注意,这个例子并不理想!它读取自定义格式,将其转换为一个特定的表格结构并将其保存为CSV文件。您可能需要进一步分割它,以便可以将代码重新用于不同的表结构。特别是字段defs,您可能希望能够设置后代类或者工厂类。 但是为了简单起见,我采取了更加严格的方法,并在单个基类中放入了太多的智能。
基类具有创建内存数据集所需的逻辑(我使用了TClientDataSet)。它可以'迁移'一个文件。实际上,这意味着它读取,验证和导出文件。
阅读是抽象的,必须在子类中实现。它应该将数据读取到内存数据集中。这允许您在客户端数据集中进行所有必要的验证。这允许您执行字段类型和大小,并在需要时执行任何其他检查,以数据库/文件格式不可知的方式。
使用数据集中的数据完成验证和写入。从源文件解析到数据集的那一刻起,就不再需要关于源文件格式的知识。
声明: 不要忘记使用DB, DBClient
。
type
TBaseMigrator = class
private
FData: TClientDataset;
protected
function CSVEscape(Str: string): string;
procedure ReadFile(AFileName: string); virtual; abstract;
procedure ValidateData;
procedure SaveData(AFileName: string);
public
constructor Create; virtual;
destructor Destroy; override;
procedure MigrateFile(ASourceFileName, ADestFileName: string); virtual;
end;
实现:
{ TBaseReader }
constructor TBaseMigrator.Create;
begin
inherited Create;
FData := TClientDataSet.Create(nil);
FData.FieldDefs.Add('ID', ftString, 20, True);
FData.FieldDefs.Add('Name', ftString, 60, True);
FData.FieldDefs.Add('Phone', ftString, 15, False);
// Etc
end;
function TBaseMigrator.CSVEscape(Str: string): string;
begin
// Escape the string to a CSV-safe format;
// Todo: Check if this is sufficient!
Result := '"' + StringReplace(Result, '"', '""', [rfReplaceAll]) + '"';
end;
destructor TBaseMigrator.Destroy;
begin
FData.Free;
inherited;
end;
procedure TBaseMigrator.MigrateFile(ASourceFileName, ADestFileName: string);
begin
// Read the file. Descendant classes need to override this method.
ReadFile(ASourceFileName);
// Validation. Implemented in base class.
ValidateData;
// Saving/exporting. For now implemented in base class.
SaveData(ADestFileName);
end;
procedure TBaseMigrator.SaveData(AFileName: string);
var
Output: TFileStream;
Writer: TStreamWriter;
FieldIndex: Integer;
begin
Output := TFileStream.Create(AFileName,fmCreate);
Writer := TStreamWriter.Create(Output);
try
// Write the CSV headers based on the fields in the dataset
for FieldIndex := 0 to FData.FieldCount - 1 do
begin
if FieldIndex > 0 then
Writer.Write(',');
// Column headers are escaped, but this may not be needed, since
// they likely don't contain quotes, commas or line breaks.
Writer.Write(CSVEscape(FData.Fields[FieldIndex].FieldName));
end;
Writer.WriteLine;
// Write each row
FData.First;
while not FData.Eof do
begin
for FieldIndex := 0 to FData.FieldCount - 1 do
begin
if FieldIndex > 0 then
Writer.Write(',');
// Escape each value
Writer.Write(CSVEscape(FData.Fields[FieldIndex].AsString));
end;
Writer.WriteLine;
FData.Next
end;
finally
Writer.Free;
Output.Free;
end;
end;
procedure TBaseMigrator.ValidateData;
begin
FData.First;
while not FData.Eof do
begin
// Validate the current row of FData
FData.Next
end;
end;
一个例子子类:TIniFileReader,就好像它们是数据库中的记录读取ini文件部分。正如你所看到的,你只需要实现读取文件的逻辑。
type
TIniFileReader = class(TBaseMigrator)
public
procedure ReadFile(AFileName: string); override;
end;
{ TIniFileReader }
procedure TIniFileReader.ReadFile(AFileName: string);
var
Source: TMemIniFile;
IDs: TStringList;
ID: string;
i: Integer;
begin
// Initialize an in-memory dataset.
FData.Close; // Be able to migrate multiple files with one instance.
FData.CreateDataSet;
// Parsing a weird custom format, where each section in an inifile is a
// row. Section name is the key, section contains the other fields.
Source := TMemIniFile.Create(AFileName);
IDs := TStringList.Create;
try
Source.ReadSections(IDs);
for i := 0 to IDs.Count - 1 do
begin
// The section name is the key/ID.
ID := IDs[i];
// Append a row.
FData.Append;
// Read the values.
FData['ID'] := ID;
FData['Name'] := Source.ReadString(ID, 'Name', '');
// Names don't need to match. The field 'telephone' in this propriety
// format maps to 'phone' in your CSV output.
// Later, you can make this customizable (configurable) if you need to,
// but it's unlikely that you encounter two different inifile-based
// formats, so it's a waste to implement that until you need it.
FData['Phone'] := Source.ReadString(ID, 'Telephone', '');
FData.Post;
end;
finally
IDs.Free;
Source.Free;
end;
end;
OP希望保持代码之外的变体。通过使用然后传递给解析器的规则。您的建议将规则烘焙成静态编译的代码。这有点不方便。 –
@ Golez。不幸的是,来源可能非常不同,所以规则也是如此。我想我可能需要解析不同的格式。告诉我更多关于您提到的用于将我的文件转换为CSV的基类,然后将CSV上载到数据库中非常容易 –
使每个阅读器在特定文件夹中实现一个DLL(或运行时包),并使主数据泵应用程序加载这些。它避免了重新编译基本应用程序,并使得创建新读取程序非常简单。如果记录定义非常复杂,那么配置文件将快速接近脚本语言,以至于OP将成为配置复杂度时钟的大部分方法。 – afrazier
这与“屏幕刮板”面临的问题非常相似。如果最终用户希望能够使用它,那么我会避免使用正则表达式(如果需要,则作为内部实现细节除外),并且不向最终用户公开原始正则表达式编辑。相反,我会让他们加载他们的数据文件的样本,并用拖拽和拖放样式直观地构造他们的规则。
单击“匹配文本”按钮,单击并拖动以在屏幕上选择一个矩形区域。如果格式不准确或不可重复,请选择此选项,以便可以向上或向下或向左或向右移动一定数量。限制您可以走多远到原始框外。
单击“抓取文本”按钮,单击并拖动到屏幕上的矩形或非矩形(流动)区域。用字段命名输出,并给它一个类型(整数,字符串[x]等)。
单击保存并将模板规则写入磁盘。加载不同的文件,看看规则是否仍然适用。
不错,但是要编程的工作量很大,而且如果我判断它是正确的,那么就可以限制OP。 – GolezTrol
在大多数语言中编写delphi程序的工作量可能并不多。可视化工具部分实际上会非常简单,因为有很多可视化控件可以处理大部分这种情况。 –
你的规则语言很复杂程度必须取决于你的输入文件多么复杂和变化的。一种明显的可能性是以正则表达式的形式提供规则。使用匹配组挑选出单个列。 –
首先 - 确定这些格式之间的差异是什么,因此规则可以有多不同。 Delphi源代码和XML和JSON都是文本文件 - 但是非常不同的文件。 也许MS Excel文本fiels向导就足够了。 –
借助Firebird,您可以将这些文件声明为外部表并在SQL中直接使用它们,就好像它们存在于数据库中一样。数据类型转换将它们导入“真实”表格可以在内置或自定义UDF的帮助下完成。 –