Версия для печати
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум на Исходниках.RU > Delphi: Базы данных > XML to SQL2000


Автор: Voice 27.07.17, 01:30
на входе есть XML-файл и данные из него нужно как-то запихнуть в плоскую таблицу на SQL2000.
На СКЛе есть OPENXML, но читать он может только из строковой переменной максимум в 8000 символов. Мне же нужно читать файлы 1-30Мб.

Подскажите, какие средства есть для этого?
В голову пока приходит только парсинг xml'я, что мне кажется изобретением велосипеда, т.к. наверняка не одному мне потребовалось это сделать.
Да и непонятно, как быть с вложенными объектами.

Замечательно с этим справляется ексель: открываешь в нем xml-ку и получаешь плоскую таблицу. Но большой минус в том, что ексель зачем-то по своему усмотрению преобразовывает числа и отключить это никак нельзя.
Например, длинный id может из вида 1234567890 преобразоваться в "1+10", а число 1.2 легко стать датой 2017.01.02 и превратиться в число 42375.

Есть ли идеи, чем можно быстро перенести данные из xml в sql, кроме парсинга и построчного добавления в таблицу?

Автор: Fr0sT 27.07.17, 06:32
Непонятно, чем не устроил парсинг - разумеется, не самописный, с нуля, а с помощью готовых классов, которых как грязи. Файлы мелкие, поэтому можно брать любую реализацию.
Если хочется совсем уж выёжиться, то можно подключить xml через clientdataset как БД. Вроде бы даже стандартными средствами этого можно достичь.

Автор: Voice 27.07.17, 14:19
Цитата Fr0sT @
Непонятно, чем не устроил парсинг

как из дерева получить плоскую таблицу?

Цитата Fr0sT @
Файлы мелкие, поэтому можно брать любую реализацию.

файлы до 30Мб, что мне кажется не особо мелким

Цитата Fr0sT @
Если хочется совсем уж выёжиться, то можно подключить xml через clientdataset как БД

провайдера какого-то задать? Какого, например?

Автор: Fr0sT 28.07.17, 07:40
Цитата Voice @
как из дерева получить плоскую таблицу?

Пробегать по узлам. Какие-то узлы это записи, какие-то - поля. Все зависит от схемы данных.
Цитата Voice @
файлы до 30Мб, что мне кажется не особо мелким

Надо проверять на конкретных движках, но по нынешним меркам это пыль. Нормальный движок должен проглотить и не поморщиться. Это я к тому, что не требуется SAX, а можно грузить файл целиком.
Цитата Voice @
провайдера какого-то задать? Какого, например?

Поищи в хелпе, я недавно натыкался прямо в стандартной справке по этому классу, как его натравить на XML

Автор: Voice 28.07.17, 12:56
Цитата Fr0sT @
Поищи в хелпе, я недавно натыкался прямо в стандартной справке по этому классу, как его натравить на XML

а натыкался где? У меня Д7


Цитата Fr0sT @
Пробегать по узлам. Какие-то узлы это записи, какие-то - поля. Все зависит от схемы данных.

так и делаю сейчас. Но схемы данных нет, есть голая xml-ка, формат которой заранее неизвестен. Ексель же тоже как-то получает на вход xml и делает из нее таблицу.
Проблема в том, чтобы понять, какая ветвь - это уже новая запись в таблице.
Например, есть такая xml-ка (СИЛЬНО упрощенный вид):
продажи от дистрибьюторов клиентам какой-то продукции
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    <distributor name="Ivanov">
       <client name="Petrov">
          <prod_info>
             <prod_code>1</prod_code>
             <prod_name>Пряник<prod_name>
             <sale_info>
                <amount>100</amoount>
                <summa>123.45</summa>
             </sale_info>
          </prod_info>
          <prod_info>
             <prod_code>2</prod_code>
             <prod_name>Печенька<prod_name>
             <sale_info>
                <amount>200</amoount>
                <summa>500.00</summa>
             </sale_info>
          </prod_info>
       </client>
       <client name="Sidorov">
          <prod_info>
             <prod_code>3</prod_code>
             <prod_name>Колбаса<prod_name>
             <sale_info>
                <amount>100</amoount>
                <summa>123.45</summa>
             </sale_info>
          </prod_info>
          <prod_info>
             <prod_code>4</prod_code>
             <prod_name>Сыр<prod_name>
             <sale_info>
                <amount>200</amoount>
                <summa>500.00</summa>
             </sale_info>
          </prod_info>
       </client>
    </distributor>


Сейчас решил, что ищем самого последнего потомка (это и будет одна запись) и к нему подтягиваем св-ва всех его предков (это будет наполнение записи).
Проблема в том, что в разных ветвях могут отсутствовать какие-то разделы. Например, у второго покупателя может быть информация о продукте, но не будет информации о продаже.
Хотя сейчас вот подумал, что это не должно особо мешать. Просто найдем этого последнего потомка (в данном случае будет prod_info) и к нему подтянем св-ва всех его предков. А в полях sale_info:amount и sale_info:summa будет пусто.

Вот тут получается, что я изобретаю велосипед и хожу по граблям. Тема создавалась с вопросом, есть ли что-то готовое. Тем более, что задача не особо специфическая, а ИМХО довольно распространенная.

Автор: Fr0sT 28.07.17, 14:32
Цитата Voice @
а натыкался где? У меня Д7

Слушай, ну ты вроде не чайник по разговору, а спрашиваешь элементарные вещи. Справка Д7 по клиентдатасету, раздел Using provider components.

Что касается схемы XML - это уже другая тема для беседы. Но если есть хотя бы какая-то определенность в структуре - дело существенно упрощается. Например, ты говоришь, что разделов может не быть. Но максимально полный набор узлов же известен? Вот и разбирать на его основе, а отсутствующие узлы игнорировать.
Погонял Эксель. На первый взгляд ничего сложного.
1. Пробег по узлам рекурсивно
2. Если у узла есть атрибут - добавить его как столбец итоговой таблицы
3. Если узел конечный (элемент с текстом) - добавить его как столбец итоговой таблицы
4. Узел, содержащий дочерние конечные узлы, закончился - добавить строку таблицы со всеми заполненными ячейками и перейти на уровень выше

Автор: Voice 02.08.17, 01:16
по ClientDataSet так не доразобрался. Там нужно XMLTransform прицеплять. А ему в свою очередь нужен какой-то файл трансформации, который можно получить из xml с помощью дельфевой утилиты XML Mapping. Она у меня почему-то запускаться не хочет (критическую ошибку выдает).
Да и нафига мне эти сложности с посторонними утилитами.

Крче, решил вопрос определения новой строки так:
- идем рекурсивно по ветвям (пробегаем все дочерние ветви. И для каждой дочерней ветви все ее дочерние ветви. И т.д. вглубь дерева)
- если ПЕРЕД этой ветвью есть ветвь с таким же именем (while prevSibling <> nil), добавляем для текущей ветви строку
- строим полный путь к ветви: получаем строку вида "aaa.bbb.ccc"
- если в списке полей такого поля нет, добавляем и запоминаем индекс этого поля, т.к. следующая ветвь возможно будет значением.
- если встретилось значение ветви (nodeType <> ELEMENT_NODE), пишем в таблицу (в только что запомненное поле)

Автор: Voice 16.08.17, 23:48
.... хотел было алгоритм скорректировать, потом решил просто код выложить. Код тренировочный, подчистил немного, но могло остаться много мусора (ненужные переменные, юниты и т.п.)
<{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}>
    unit Unit1;
     
    interface
     
    uses
      Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
      Dialogs, XMLDoc, XMLIntf, StdCtrls, XMLdom,
      TypInfo;
     
    type
      TForm1 = class(TForm)
        Button1: TButton;
        Memo1: TMemo;
        dlgOpen: TOpenDialog;
        procedure Button1Click(Sender: TObject);
        procedure FormCreate(Sender: TObject);
      private
        { Private declarations }
      public
        { Public declarations }
      end;
     
    type
       TCsvRow = record
          AFieldName, ACell: string;
       end;
     
       TObjInfo = array of TCsvRow;
     
     
    const
       cnstFileName = 'C:\test.xml';
       cnstFieldDelim = '.'; // разделитель xml-полей
     
    var
      Form1: TForm1;
      CsvTable: array of array of string; // таблица-массив [строка][столбец]
      ObjInfo: TObjInfo; // информация о текущем объекте (все поля с текущими значениями). При переходе на след.объект все потомки очищаются для заполнения новыми данными
      TmpFieldIndex: integer; // текущее поле в табл.CsvTable для заполнения
      TmpRecordIndex: integer; // текущая строка в табл.CsvTable для заполнения
     
    implementation
     
    {$R *.dfm}
     
    function FieldIndex(AName: string): integer; // ищем поле в таблице-массиве
    var
       n: integer;
    begin
    Result := -1;
    for n := 0 to high(CsvTable[0]) do
       if CsvTable[0][n] = AName then
          begin
          Result := n;
          exit;
          end;
    end;
     
    procedure AddField(AName: string); // добавляем новое поле (если нужно) в таблицу-массив и запоминаем индекс
    var
       m: integer;
    begin
    TmpFieldIndex := FieldIndex(AName);
    if TmpFieldIndex < 0 then // добавляем НОВОЕ поле
       begin
       for m := 0 to high(CsvTable) do // бежим по строкам и в каждую добавляем новое поле
          SetLength(CsvTable[m], length(CsvTable[m]) + 1);
       // записываем поле в конец
       TmpFieldIndex := high(CsvTable[0]);
       CsvTable[0][TmpFieldIndex] := AName;
       end;
    end;
     
    procedure SaveCsv(AFileName: string); // сохраняем таблицу-массив в csv-файл
    var
       n, m, i: integer;
       f: TextFile;
       TmpStr: string;
    begin
    try
       AssignFile(f, AFileName);
       Rewrite(f);
       for n := 0 to high(CsvTable) do // читаем строки
          begin
          TmpStr := '';
          for m := 0 to high(CsvTable[n]) do // читаем столбцы
             begin
             if n = 0 then // 0-я строка - названия столбцов (вырезаем имя поля из пути)
                begin
                i := LastDelimiter(cnstFieldDelim, cnstFieldDelim + CsvTable[n][m]); // на всякий добавляем в начало пустое поле, если строка пустая
                TmpStr := TmpStr + #9 + CsvTable[n][m]; //copy(CsvTable[n][m], i, length(CsvTable[n][m]) - i + 1);
                end else // значения (строка >= 1)
                TmpStr := TmpStr + #9 + CsvTable[n][m];
             end; // for m
          WriteLn(f, TmpStr);
          end; // for n
       finally
          CloseFile(f);
       end; // try
    end;
     
    function GetPath(ANode: IDOMNode): string; // получаем путь до заданного объекта
    begin
    if ANode = nil then
       exit;
    {
    if ANode.localName = '' then
       Result := GetPath(ANode.ParentNode)
       else Result := GetPath(ANode.ParentNode) + cnstFieldDelim + ANode.localName;
    }
    if ANode.localName = '' then
       Result := GetPath(ANode.ParentNode)
       else Result := ANode.localName + cnstFieldDelim + GetPath(ANode.ParentNode);
    end;
     
    procedure GetChildNodes(Node: IDOMNode; OffSet: String); // рекурсивно перебираем дерево объектов
     
       procedure SetParentFields; // заполняем поля текущей строки информацией по родительским объектам
       var
          n, m: integer;
       begin
       for n := 0 to high(ObjInfo) do
          begin
          m := FieldIndex(ObjInfo[n].AFieldName);
          if m >= 0 then
             CsvTable[TmpRecordIndex][m] := ObjInfo[n].ACell;
          end;
       end;
     
       procedure AddObjInfo(AField, AValue: string); // запоминаем информацию о родителе
       var
          n: integer;
       begin
       // ищем поле и обновляем значение
       for n := 0 to high(ObjInfo) do
          if ObjInfo[n].AFieldName = AField then
             begin
             ObjInfo[n].ACell := AValue;
             exit;
             end;
     
       // если поле не нашли, добавляем
       SetLength(ObjInfo, length(ObjInfo) + 1);
       ObjInfo[high(ObjInfo)].AFieldName := AField;
       ObjInfo[high(ObjInfo)].ACell := AValue;
       end;
     
       procedure ClearObjInfo(AParentPath: string); // чистим поля внутри заданного объекта (родителя)
       var
          n, m: integer;
       begin
       for n := 0 to high(ObjInfo) do
          begin
          {
          if copy(ObjInfo[n].AFieldName, 1, length(AParentPath) + length(cnstFieldDelim)) = AParentPath + cnstFieldDelim then
             ObjInfo[n].ACell := '';
          }
          m := length(AParentPath) + length(cnstFieldDelim);
          if copy(ObjInfo[n].AFieldName, length(ObjInfo[n].AFieldName) - m + 1, m) = cnstFieldDelim + AParentPath then
             ObjInfo[n].ACell := '';
          end;
       end;              
     
    var
      n, m: integer;
      TmpPath, TmpStr: string;
      TmpNode: IDOMNode;
      TmpObjInfo: TObjInfo;
      TmpFlag: boolean;
    begin
    // если ПЕРЕД этой ветвью есть ветвь с таким же именем, добавляем для текущей ветви строку
    TmpFlag := false;
    TmpNode := Node.previousSibling;
    while TmpNode <> nil do
       begin
       if TmpNode.nodeName = Node.nodeName then
          begin
          Inc(TmpRecordIndex);
          TmpFlag := true; // запоминаем, что уже добавили строку
          break;
          end;
       TmpNode := TmpNode.previousSibling;
       end;
    if not TmpFlag then // если строка еще добавлена не была, проверяем другое условие
       if (TmpNode <> nil) and (TmpNode.nextSibling = nil) then // если ПОСЛЕ этой ветви на ее уровне никого нет, добавляем строку
          begin
          Inc(TmpRecordIndex);
          end;
     
    // добавляем новую строку в таблицу-массив, если надо
    if TmpRecordIndex > high(CsvTable) then
       begin
       // добавляем новую строку в CsvTable
       SetLength(CsvTable, TmpRecordIndex + 1);
       SetLength(CsvTable[TmpRecordIndex], length(CsvTable[0])); // делаем у новой строки такое же кол-во полей, как и у 0-й строки (у заголовков)
       end;
     
    TmpPath := GetPath(Node);
    ClearObjInfo(TmpPath); // зайдя в новый объект, чистим вложенные объекты
     
    // добавляем поля
    AddField(TmpPath);
    CsvTable[TmpRecordIndex][TmpFieldIndex] := Node.nodeValue; // записываем значение в ячейку
    //Form1.Memo1.Lines.Add(IntToStr(TmpRecordIndex) + '_val ' + OffSet + TmpPath + ': ' + Node.nodeValue);
     
    AddObjInfo(TmpPath, Node.nodeValue); // добавляем прочитанную информацию об объекте
     
    if Node.attributes <> nil then // если есть атрибуты, обрабатываем, как значения
       for n := 0 to Node.attributes.length - 1 do
          begin
          AddField(Node.attributes[n].nodeName + ':' + TmpPath); // добавляем атрибут как поле (если такого поля еще нету)
          CsvTable[TmpRecordIndex][TmpFieldIndex] := Node.attributes[n].nodeValue; // записываем значение в ячейку
          Form1.Memo1.Lines.Add(IntToStr(TmpRecordIndex) + '_atr ' + Offset + Node.attributes[n].nodeName + ':' + TmpPath + ': ' + Node.attributes[n].nodeValue);
     
          AddObjInfo(Node.attributes[n].nodeName + ':' + TmpPath, Node.attributes[n].nodeValue);
          end;
     
    // идем дальше по дереву
    for n := 0 to Node.childNodes.length - 1 do
       GetChildNodes(Node.childNodes[n], OffSet + '  ');
     
    SetParentFields; // присваиваем прочитанные ранее св-ва объектов-родителей
    end;
     
    procedure TForm1.Button1Click(Sender: TObject);
    var
      xmlDocument: TXMLDocument;
      mainNode: IXMLNode;
      nodes: IXMLNodeList;
      n: Integer;
      TmpObjInfo: TObjInfo;
    begin
    FormCreate(Sender); // обнуляем массив CsvTable
    if not dlgOpen.Execute then
       exit;
     
    Memo1.Lines.LoadFromFile(dlgOpen.FileName);
     
    xmlDocument := TXMLDocument.Create(Owner);
    //xmlDocument.LoadFromFile(dlgOpen.FileName);
    xmlDocument.XML.Text := Memo1.Lines.Text;
     
    xmlDocument.Options := xmlDocument.Options + [doAttrNull];
    xmlDocument.Active := True;
    n := 0;
    Memo1.Lines.BeginUpdate;
    SetLength(ObjInfo, 0);
    GetChildNodes(xmlDocument.DocumentElement.DOMNode, '');
    Memo1.Lines.EndUpdate;
    SaveCsv(dlgOpen.FileName + '.txt');
    xmlDocument.Active := False;
    xmlDocument.Free;
    Memo1.Lines.Add('Готово');
    Memo1.Lines.Add(#13#10#13#10#13#10#13#10#13#10);
    end;
     
    procedure TForm1.FormCreate(Sender: TObject);
    var
       n: integer;
    begin
    dlgOpen.FileName := cnstFileName;
    // чистим массив
    Finalize(CsvTable);
    Finalize(ObjInfo);
     
    SetLength(CsvTable, 1); // добавляем первую строку (назв.столбцов)
     
    TmpFieldIndex := -1; // текущее поле в табл.CsvTable для заполнения
    TmpRecordIndex := 1; // текущая строка в табл.CsvTable для заполнения
    end;
     
    end.

Powered by Invision Power Board (https://www.invisionboard.com)
© Invision Power Services (https://www.invisionpower.com)