我正在開發一個應用程序,我必須將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的幫助下完成。 –