Страница 1 из 2

Прошу помочь с парсингом CSV

СообщениеДобавлено: 13.07.2018 12:53:04
SkinnerDE
Доброго времени суток.
Вкратце - есть устройство, генерирующее логи в формате CSV. Есть прога, которую я написал для чтения этих логов с FTP и построения графика (еще не готова, но основной функционал работает).
Проблема вот в чем: часть файлов читается, часть - нет. Кодировки проверял, всякие LF+CR тоже, еще что-то проверял, всего уже не помню.
Помогите пожалуйста определить где проблема - у меня в коде (я пока еще новичок), в самих CSV или может в сторонних модулях?

Lazarus 1.8.4, FPC 3.0.4., используются csvdocument и synapse.

Код: Выделить всё
unit mfr;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, TAGraph, TASeries, TATransformations, TATools,
  Forms, Controls, Graphics, Dialogs, StdCtrls, DBGrids, Grids, ComCtrls,
  ExtCtrls, FileCtrl, EditBtn, Buttons, Arrow, csvdocument, TAChartAxis,
  Ipfilebroker, RTTICtrls, laz_synapse, ftpsend;

type

  { TForm1 }

  TForm1 = class(TForm)
    Arrow1: TArrow;
    btDrawAll: TButton;
    btLoad: TButton;
    btCompare: TButton;
    btShift: TButton;
    CATransformLeftAutoScaleAxisTransform1: TAutoScaleAxisTransform;
    CATransformRifgtAutoScaleAxisTransform1: TAutoScaleAxisTransform;
    Chart1: TChart;
    CGChecker: TCheckGroup;
    CATransformLeft: TChartAxisTransformations;
    CATransformRifgt: TChartAxisTransformations;
    CATransformBar2: TChartAxisTransformations;
    CATransformBar2AutoScaleAxisTransform1: TAutoScaleAxisTransform;
    CATransformBar1: TChartAxisTransformations;
    CATransformBar1AutoScaleAxisTransform1: TAutoScaleAxisTransform;
    CheckBox1: TCheckBox;
    FileListBox1: TFileListBox;
    Label1: TLabel;
    ListBox1: TListBox;
    ListView1: TListView;
    Memo1: TMemo;
    StringGrid1: TStringGrid;
    TrackBar1: TTrackBar;
    procedure btCompareClick(Sender: TObject);
    procedure btDrawAllClick(Sender: TObject);
    procedure btLoadClick(Sender: TObject);
    procedure CheckBox1Change(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure ListView1DblClick(Sender: TObject);
    procedure TrackBar1Change(Sender: TObject);
    procedure UpdateFl;
  private
    { private declarations }
  public
    FTPSend: TFTPSend;
    { public declarations }
  end;

type csvLog=class(TCSVDocument)
  public
    Title,Date,Sample,Data:string;
    Combination: boolean;
    Units,Combi:array[0..16] of string;
    ur:integer;
    //загрузка данных и обработка шапки файла
    procedure FTPReadInfo(filename:string);
    //поиск № строки по тексту в первой колонке
    function RowSearch(text:string):Integer;
    //поиск № колонки по тексту и номеру строки
    function ColSearch(text:string;CRow:Integer):Integer;
end;

var
  Form1: TForm1;
  csv:csvLog;
  test,s0: string;
  syn1,syn2,syn9:integer;
  autoshift,combination:boolean;
  limits:array[1..10,1..2] of real;
  axisind:array[1..6] of Integer;
  const
  COLORS: array [0..15] of Integer =
    ($000080,$008000,$008080,$800000,$800080,$808000,$808080,$C0C0C0,$0000FF,$00FF00,$00FFFF,$FF0000,$FF00FF,$FFFF00,$000000,$F0CAA6);
implementation

{$R *.lfm}

{ TForm1 }

procedure csvLog.FTPReadInfo(filename:string);
var i,dr,cr: integer;
begin
  csv.Clear;
  //чтение из файла в режиме возможной докачки
  if Form1.FTPSend.RetrieveFile(filename,true) then Self.LoadFromStream(Form1.FTPSend.DataStream) else exit;
  //заголовок файла
  Title:='TITLE: '+Cells[1,RowSearch('TITLE')];
  //формирование даты
  dr:=RowSearch('TRG_TIME');
  Date:='TRG_TIME: '+Cells[3,dr]+'/'+Cells[2,dr]+'/20'+Cells[1,dr]+'   '+Cells[4,dr]+':'+Cells[5,dr]+':'+Cells[6,dr];
  //интервал замеров
  Sample:='TRG_SAMPLE: '+Cells[1,RowSearch('TRG_SAMPLE') ]+' ms';
  //массив заголовков данных
  ur:=RowSearch('UNIT');
  Combination:=false;
  Units[0]:='UNIT: ';
  for i:=1 to ColCount[ur] do
    begin
      Units[i]:=Cells[i,ur];
    end;
  //подсчет кол-ва строк с данными
  Data:='DATA: '+IntToStr(RowCount-ur-2)+' lines';
  //массив заголовков данных для комбинированного файла
  cr:=RowSearch('COMBI');
  if Cells[0,cr]='COMBI' then
  begin
    Combination:=true;
    Combi[0]:='COMBI: ';
    for i:=1 to ColCount[cr-1] do
      begin
        Combi[i]:=Cells[i,cr]+',';
      end;
  end;
end;

function csvLog.RowSearch(text:string):Integer;
var i:integer;
begin
  i:=0;
  while (Self.Cells[0,i]<>text) and (i<Self.RowCount-1) do inc(i);
    if i=Self.RowCount-2 then result:=-1;
   result:=i;
end;

function csvLog.ColSearch(text:string;CRow:Integer):Integer;
var i:integer;
begin
  i:=0;
  while (Self.Cells[i,CRow]<>text) and (i<Self.ColCount[CRow]-1) do inc(i);
    if i=(Self.ColCount[CRow]-2) then result:=-1;
    result:=i;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  //создаем соединение с FTP
  FTPSend:=TFTPSend.Create;
  FTPSend.TargetHost:='172.16.170.11';
  FTPSend.UserName:='anonymous';
  FTPSend.Password:='';
  FTPSend.PassiveMode:=True;
  if FTPSend.Login then
    begin
      //получаем информацию об объектах в директории
      FTPSend.List(FTPSend.GetCurrentDir, false);
      //обновляем список
      UpdateFL;
    end;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
  csv.Free;
  FTPSend.Logout;
  FTPSend.Free;
end;

//обновление списка файлов
procedure TForm1.UpdateFl;
var LI: TlistItem;
    I: Integer;
begin
  ListView1.Items.BeginUpdate;
  try
   ListView1.Items.Clear;
    //создаем элемент для перехода на один уровень вверх
    LI:=ListView1.Items.Add;
    LI.Caption:='[...]';
    //заполняем список информацией об объектах в текущей директории
    for I := 0 to FTPSend.FtpList.Count-1 do
      begin
        LI:=ListView1.Items.Add;
        LI.Caption:=FTPSend.FtpList[i].FileName;
        if FTPSend.FtpList[i].Directory then
          Li.SubItems.Add('Папка')
        else
        Li.SubItems.Add('Файл');
        LI.SubItems.Add(IntToStr(FTPSend.FtpList[i].FileSize));
        LI.SubItems.Add((DateToStr(FTPSend.FtpList[i].FileTime)));
      end;
  finally
    ListView1.Items.EndUpdate;
  end;
end;

//обработка двойного клика в списке файлов
procedure TForm1.ListView1DblClick(Sender: TObject);
var b:boolean;
    s,line:string;
    i:integer;
begin
  btLoad.Enabled:=false;
  if not Assigned(ListView1.Selected) then Exit;
  //переход на уровень вверх
  if ListView1.ItemIndex=0 then
    b:=FTPSend.ChangeToParentDir
  else
    //смена директории на выбранную
    b:=FTPSend.ChangeWorkingDir(ListView1.Selected.Caption);
if b then
   begin
     //получаем данные об объектах в текущей директории
     FTPSend.List(EmptyStr,False);
     //обновляем список
     UpdateFL;
   end
else
   begin
     s:=FTPSend.FtpList[ListView1.Selected.Index-1].FileName;
     //если файл нужного формата (csv), выводим информацию
     if Pos('.csv',s)>0 then
       begin
         //активация кнопки "Загрузить"
         btLoad.Enabled:=true;
         csv:=csvLog.Create;
         csv.FTPReadInfo(s);
         Memo1.Clear;
         Memo1.Lines.Add(csv.Title);
         Memo1.Lines.Add(csv.Date);
         Memo1.Lines.Add(csv.Sample);
         //создание списка заголовков в зависимости от типа файла
         if Combination then
            for i:=0 to 16 do line:=line+csv.Combi[i]
         else
            for i:=0 to 16 do line:=line+csv.Units[i];
         Memo1.Lines.Add(line);
         Memo1.Lines.Add(csv.Data);
       end;
   end;
end;

//загрузка данных в таблицу
procedure LoadGrid(Grid:TStringGrid);
var col,row:integer;
begin
  Grid.BeginUpdate;
  Grid.Clear;
  Grid.RowCount:=csv.RowCount;
  Grid.ColCount:=csv.ColCount[csv.ur+1];
  for col:=0 to Grid.ColCount-1 do
    // "row+csv.ur" задает сдвиг по строкам для вывода только данных с первой строки таблицы
    for row:=0 to Grid.RowCount-1 do Grid.Cells[col,row]:=csv.Cells[col,row+csv.ur];
  Grid.EndUpdate;
end;

//замена точки на запятую, вычисление максимумов и точки синхронизации
procedure ReplGrid(Grid: TStringGrid);
var i,y,c,x:integer;
    curr: real;
begin
  syn1:=0; syn2:=0;
  for x:=1 to Grid.ColCount-1 do
    for y:=1 to 2 do limits[x,y]:=0.0; //обнуление массива пределов
  Grid.BeginUpdate;
  for i:=1 to Grid.RowCount-1 do begin
    for c:=1 to Grid.ColCount-1 do
        begin
          if Grid.Cells[c,i]='' then break;
          Grid.Cells[c,i]:=StringReplace(Grid.Cells[c,i],'.',',',[rfReplaceAll, rfIgnoreCase]);  //замена
          curr:=StrToFloat(Grid.Cells[c,i]);
          if curr>limits[c,1] then limits[c,1]:=curr; //максимум
          if curr<limits[c,2] then limits[c,2]:=curr; //минимум
          if (Grid.Col=8) and (curr<>0) and (syn1<>0) then syn1:=Grid.Row;   //точка синхронизации
        end;
  end;
  Form1.Memo1.Lines.Add(FloatToStr(limits[2,1])+'/'+FloatToStr(limits[2,2])+', '+FloatToStr(limits[3,1])+'/'+FloatToStr(limits[3,2]));
  Grid.EndUpdate;
end;

//отображение позиции трекбара
procedure TForm1.TrackBar1Change(Sender: TObject);
begin
  Label1.Caption:=IntToStr(TrackBar1.Position);
end;

//отрисовка графика по таблице
procedure TForm1.btDrawAllClick(Sender: TObject);
var i,j:integer;
    ls: TLineSeries;
    trl,trr: TChartAxisTransformations;
begin
  Chart1.ClearSeries;                                       //все серии удаляются
  Chart1.AxisList[2].Visible:=false;
  Chart1.AxisList[0].Transformations.Destroy;
  trl:= TChartAxisTransformations.Create(Self);
  TAutoScaleAxisTransform.Create(Self).Transformations:=trl;//трансформация для левой оси
  Chart1.AxisList[0].Transformations:=trl;
  Chart1.AxisList[2].Transformations.Destroy;
  trr:= TChartAxisTransformations.Create(Self);
  TAutoScaleAxisTransform.Create(Self).Transformations:=trr;//трансформация для правой оси
  Chart1.AxisList[2].Transformations:=trr;
  for i := 1 to 6 do                       //создание новых серий
    begin
      ls := TLineSeries.Create(Self);
      Chart1.AddSeries(ls);
      if CGChecker.Checked[i-1] then     //проверяем, отмечена ли серия для отрисовки
        begin
          ls.SeriesColor := COLORS[i];
          ls.LinePen.Width := 2;
          ls.Legend.Visible:=true;
          ls.Title:=StringGrid1.Cells[i+1,0];
          //серии с большими максимумами привязываются к правой оси
          if (limits[i+1,1]>100) or (limits[i+1,2]<-100) then
            begin
              ls.AxisIndexY:=2;
              Chart1.AxisList[2].Visible:=true;
            end
          else ls.AxisIndexY:=0;
          //флаговые серии привязываются к 2-м нижним осям
    //      if i=7 then ls.AxisIndexY:=3;
    //      if i=8 then ls.AxisIndexY:=4;
          axisind[i]:=ls.AxisIndexY;   //запись в массив индексов осей
            for j := 1 to StringGrid1.RowCount-1 do
              begin
                if (StringGrid1.Cells[1,j]='') or (StringGrid1.Cells[i+1,j]='') then break;
                ls.AddXY(StrToFloat(StringGrid1.Cells[1,j]),StrToFloat(StringGrid1.Cells[i+1,j]));
              end;
        end;
    end;
  end;

//отрисовка вторичных серий и синхронизация
procedure TForm1.btCompareClick(Sender: TObject);
var csv2:csvLog;
    ccol,crow,i,c,x,urow,shift:Integer;
    ls2: TLineSeries;
begin
  shift:=0;
  x:=Chart1.SeriesCount;
  while x>6 do
    begin
      Chart1.DeleteSeries(Chart1.Series.Items[x-1]);  //удаление старых вторичных серий если есть
      x:=Chart1.SeriesCount;
    end;
  for c:=1 to 6 do
    begin
      ls2:=TLineSeries.Create(Self);        //создание новых серий
      Chart1.AddSeries(ls2);
      if CGChecker.Checked[c-1] then        //проверка на сравнение отмеченных серий
        begin
          ls2.SeriesColor:=COLORS[c];
          ls2.Legend.Visible:=false;
          ls2.LinePen.Width:=1;
          ls2.AxisIndexY:=axisind[c];
          if not(FileExists(FileListBox1.FileName)) then exit;    //TODO: FTP
          csv2:=csvLog.Create;
          csv2.LoadFromFile(FileListBox1.FileName);
          urow:=csv2.RowSearch('UNIT');
          //поиск нужной колонки по заголовку в соответствующем чекере
          ccol:=csv2.ColSearch(CGChecker.Items.Strings[c-1],urow);
          crow:=csv2.RowSearch('DATA 1');
          //если отмечена автосинхронизация, сравниваются точки синхронизации
          if autoshift then
          begin
            //поиск совпадающих меток
            for i:=crow to csv2.RowCount-1 do
              begin
                if (StrToInt(csv2.Cells[8,i])<>0) and (syn2<>0) then syn2:=i;
              end;
            if syn1<>syn2 then shift:=abs(syn1-syn2);
          end
          //при ручной синхронизации задается сдвиг по позиции трекбара
          else if abs(TrackBar1.Position)<(csv2.RowCount-urow+1) then shift:=abs(TrackBar1.Position);
          for i:=crow to StringGrid1.RowCount-1-shift do
            begin
              csv2.Cells[1,i]:=StringReplace(csv2.Cells[1,i],'.',',',[rfReplaceAll, rfIgnoreCase]);
              csv2.Cells[ccol,i+shift]:=StringReplace(csv2.Cells[ccol,i+shift],'.',',',[rfReplaceAll, rfIgnoreCase]);
              //обнуление пустых ячеек, если встречаются
              if csv2.Cells[1,i]='' then csv2.Cells[1,i]:='0';
              //сдвиг идет только по колонке с данными
              if csv2.Cells[ccol,i+shift]='' then csv2.Cells[ccol,i+shift]:='0';
              ls2.AddXY(StrToFloat(csv2.Cells[1,i]),StrToFloat(csv2.Cells[ccol,i+shift]));
            end;

        end;
    end;
  csv2.Free;
end;

//основная функция: чтение из файла, загрузка в таблицу, очистка, замена знака
//TODO: FTP
procedure TForm1.btLoadClick(Sender: TObject);
var i:integer;
begin
  btCompare.Enabled:=true;
  LoadGrid(StringGrid1);
  ReplGrid(StringGrid1);
  CGChecker.Caption:=s0;
  btDrawAll.Enabled:=true;
  CGChecker.Items.Clear;
  for i:=1 to 6 do
      begin
        CGChecker.Items.Add(StringGrid1.Cells[i+1,0]);
        if not Combination then CGChecker.Checked[i-1]:=true;
      end;
end;

//интерфейсная часть синхронизации
procedure TForm1.CheckBox1Change(Sender: TObject);
begin
  if CheckBox1.Checked then
    begin
      autoshift:=true;
      btShift.Enabled:=false;
      TrackBar1.Position:=0;
      TrackBar1.Enabled:=false;
      Label1.Caption:='';
    end
  else
    begin
      autoshift:=false;
      btShift.Enabled:=true;
      TrackBar1.Enabled:=true;
    end;
end;

end.


CSV файлики:
http://s000.tinyupload.com/index.php?file_id=56053932908693594650 - этот парсится нормально
http://s000.tinyupload.com/index.php?file_id=03757174214884548185 - парсится частично, только некоторые ячейки
http://s000.tinyupload.com/index.php?file_id=69652745427712141833 - вообще 0

Изображение

Если кто-то всерьез заинтересуется, вышлю проект целиком.

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 16.07.2018 09:03:45
Vadim
SkinnerDE
Правильно ли я понял, что стока UNIT является своего рода заголовком для колонок строк DATA?

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 16.07.2018 09:27:57
SkinnerDE
Vadim писал(а):SkinnerDE
Правильно ли я понял, что стока UNIT является своего рода заголовком для колонок строк DATA?


Да, так и есть.

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 16.07.2018 13:31:43
Vadim
SkinnerDE
В таком случае удалите из файлов все строки кроме тех, что начинаются с "UNIT" и "DATA". Их будет отлично и без малейших проблем открывать стандартный компонент TSdfDataSet (вкладка DataAccess).

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 16.07.2018 13:55:03
SkinnerDE
Да я бы и рад, но в этих строках важная служебная информация, в том числе время и дата лога, время сэмлирования, тип данных (токи, напряжения, скорости), айдишник контроллера и это нужно показывать пользователю. Кроме того, в дальнейшем нужен будет функционал для сравнения и анализа логов разных дат. И без этих данных не обойтись.

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 16.07.2018 15:30:56
Vadim
А кто Вам мешает эту важную информацию перебросить в другой файл?

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 17.07.2018 08:26:10
Снег Север
SkinnerDE писал(а):Да я бы и рад, но в этих строках важная служебная информация

Странное возражение, вас же никто не заставляет корежить исходник файла, речь идет о разделении частей при обработке.

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 17.07.2018 08:59:53
SkinnerDE
Vadim писал(а):А кто Вам мешает эту важную информацию перебросить в другой файл?


Ну, хотя бы то, что я её не могу получить. В первом посте я всё это описывал.

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 17.07.2018 12:26:06
pupsik
SkinnerDE структура у файлов одинакова?

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 17.07.2018 12:33:05
SkinnerDE
pupsik писал(а):структура у файлов одинакова?


Да, меняться могут лишь типы данных и порядок колонок.

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 17.07.2018 13:03:44
Vadim
SkinnerDE писал(а):Ну, хотя бы то, что я её не могу получить.

Дружище, Вы это серьёзно? Вы не шутите? :-D
Это ведь обычный текстовый файл, причем все типы информации этого файла расположены по строкам и однозначно идентифицируются.
Вот пример решения этой "глобальной" проблемы буквально за пару секунд:
Код: Выделить всё
Uses Classes;

Var
  st1, st2, st3: TStringList;
  i: integer;
 
Begin
  st1: TStringList.Create;
  st2: TStringList.Create;
  st4: TStringList.Create;
 
  st1.LoadFromFile('zero.csv');
 
  For i:=0 To st1.Count-1 Do
    If (Pos('UNIT', st1[i])>0)) or (Pos('DATA', st1[i])>0) Then
      st2.Add(st1[i])
    Else
      st3.Add(st1[i]); 
 
  st2.SaveToFile('zero_data.csv');
  st3.SaveToFile('zero_остально.csv');
  st1.Free;
  st2.Free;
  st3.Free;
End.

;-)

Вы, конечно, извините, но у меня сложилось впечатление, что Вы просто таки пришли в религиозный экстаз от компонента "csvdocument" и более ничего не видите. А это не более чем инструмент. Инструменты подбираются по удобству в работе...

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 17.07.2018 13:25:48
SkinnerDE
Выглядит так просто, что даже не верится.
Я не говорил, что еще новичок?
Спасибо Вам огромное.

Добавлено спустя 9 минут 31 секунду:
SkinnerDE писал(а): Вы просто таки пришли в религиозный экстаз от компонента "csvdocument" и более ничего не видите


Что-то вроде того :)

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 17.07.2018 16:16:29
Vadim
SkinnerDE писал(а):Я не говорил, что еще новичок?

Судя по теме за которую Вы взялись, на новичка совершенно не похожи. Новички обычно парятся над квадратными уравнениями. ;-) А некоторые приходят в неописуемое изумление узнав, что у квадратного уравнение есть решение даже при дискриминанте меньше нуля. :-D

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 17.07.2018 19:27:43
pupsik
Это ведь обычный текстовый файл
угу :D

Да, меняться могут лишь типы данных и порядок колонок.
класс выбрали верно. Подход не верен. Правда, лучше пользоваться классом "ниже"...

Хотя: если размеры файлов будут = пару десятков кб.... Тогда и TStringList + Pos сойдут. Иначе: иной подход нужен.

Re: Прошу помочь с парсингом CSV

СообщениеДобавлено: 18.07.2018 09:22:29
SkinnerDE
Vadim писал(а):Судя по теме за которую Вы взялись, на новичка совершенно не похожи.

Работа такая, раньше Паскалем занимался только в универе, да и то не выходя за рамки учебной программы. Потом немного Джавы, Питона, Джаваскрипт - просто для общего развития.
А так я больше программированием контроллеров занимаюсь. Можно сказать что это моя первая более-менее серьезная программа на Паскале.
pupsik писал(а):если размеры файлов будут = пару десятков кб...

максимальный размер лога 64 кБ - у контроллера просто памяти больше нет.

Добавлено спустя 14 минут 57 секунд:
И кстати, по поводу кода. Мне кажется можно обойтись только двумя TStringList.
Из первого (исходный файл) вычитать заголовки и освободить, а второй (только с данными) сохранить и передать в TSDFDataSet.