XSLT для преобразования структуры + Ruby для преобразования значений? - PullRequest
3 голосов
/ 22 декабря 2009

У нас есть довольно большие (~ 200 МБ) XML-файлы из разных источников, которые мы хотим преобразовать в общий формат.

Для структурных преобразований (имен элементов, вложенности и т. Д.) Мы решили использовать XSLT (1.0). Поскольку он должен быть быстрым (мы получаем много таких файлов), мы выбрали Apache Xalan в качестве движка. Структурные преобразования могут быть довольно сложными (не только <tag a> -> <tag b>) и различаться для файлов XML из разных источников.

Однако нам также необходимо преобразовать значения элементов. Преобразования могут быть довольно сложными (например, некоторые требуют доступа к API Карт Google, другие требуют доступа к нашей базе данных и т. Д.), Поэтому мы решили использовать простой DSL на основе Ruby, который представляет собой список «xpath selector» => объекты трансформатора, то есть:

{"rss/channel/item" => {:class => 'ItemMutators', :method => :guess_location}

Однако сохранение преобразований элементов отдельно от преобразований значений выглядит скорее как хак. Есть ли лучшие решения?


Например, в Java вы можете написать расширения для xalan и использовать их для преобразования значений. Есть ли что-то похожее, кроме рубина?


Спасибо, ребята! Все ответы были очень ценными. Я сейчас думаю:)

Ответы [ 3 ]

2 голосов
/ 19 февраля 2010

Вы должны иметь возможность использовать расширения XSLT. Поиск в Интернете показывает, что Xalan поддерживает Java для выполнения расширений: http://xml.apache.org/xalan-j/extensions.html

Цитата из связанной страницы:

Для тех ситуаций, когда вы бы хотел бы расширить функциональность XSLT с вызовами в процедурный язык, Xalan-Java поддерживает создание и использование элементов расширения и функции расширения. Xalan-Java также предоставляет растущие расширения библиотека доступна для вашего использования.

Также, очевидно, кто-то написал пакет на Ruby, который может предоставить расширения xslt: http://greg.rubyfr.net/pub/packages/ruby-xslt/classes/XML/XSLT.html

2 голосов
/ 19 февраля 2010

Я бы сделал все это на Ruby, написав модуль, который может выполнять две задачи:

1) выполнить SAX-анализ в Ruby различных входных XML-форматов, вывести XML-документ промежуточного формата и список ошибок проверки / нарушения ключа

2) создать дерево DOM из файла XML промежуточного формата, изменить его на месте, дополнить его импортированными данными и вывести измененное дерево DOM в стандартный формат

Первый шаг с использованием SAX позволяет извлекать избыточные данные из файла (а не загружаться в модель DOM!) И быстро создавать необязательные желаемые группы данных в тегах с одинаковыми именами. Для максимальной скорости группы данных никоим образом не должны сортироваться перед выходом в промежуточный формат XML, а промежуточный формат XML должен использовать короткие имена тегов.

Второй шаг с использованием DOM позволяет быстро сортировать и обрабатывать теги для промежуточного формата XML, где не было обнаружено ошибок проверки.

Под ошибкой проверки здесь я подразумеваю ряд вещей, таких как отсутствующие поля или недопустимые форматы ключей, диапазоны номеров и т. Д. Он также обнаруживает объекты, на которые ссылаются ключи, отсутствующие в файле; для этого он создает два хэша, один из ссылочных ключей и один из существующих ключей, и проверяет ссылочные ключи на соответствие существующим ключам как один из последних шагов перед завершением. Несмотря на то, что вы могли бы провести некоторую проверку с помощью XSD или DTD, Ruby предоставляет вам большую гибкость, и на практике многие проблемы проверки являются «более мягкими» ошибками, для которых можно внести некоторые ограниченные исправления.

Модуль должен ограничивать количество выполняемых задач параллельно во избежание нехватки ЦП или ОЗУ в системе.

Суть моей рекомендации - сделать все это в Ruby, но разделить работу на два этапа - первый этап, задачи, которые можно быстро выполнить с помощью SAX, и второй этап, задачи, которые можно быстро выполнить с помощью DOM.

EDIT

> Как мы делаем структурные преобразования с SAX?

Что ж, вы не можете сделать любое удобное переупорядочение или, тем более, вы больше не получаете преимущества использования памяти при последовательном разборе XML, но вот иллюстрация того подхода, который я имею в виду для первого этапа, с использованием Java ( извините, но не Ruby, но должно быть довольно легко переводимым - думайте об этом как псевдокод!):

class MySAXHandler implements org.xml.sax.ContentHandler extends Object {
  final static int MAX_DEPTH=512;
  final static int FILETYPE_A=1;
  final static int FILETYPE_B=2;
  String[] qualifiedNames = new String[MAX_DEPTH];
  String[] localNames = new String[MAX_DEPTH];
  String[] namespaceURIs = new String[MAX_DEPTH];
  int[] meaning = new int[MAX_DEPTH];
  int pathPos=0;
  public java.io.Writer destination;
  ArrayList errorList=new ArrayList();
  org.xml.sax.Locator locator;
  public int inputFileSchemaType;

  String currentFirstName=null;
  String currentLastName=null;

  puiblic void setDocumentLocator(org.xml.sax.Locator l) { this.locator=l; }

  public void startElement(String uri, String localName, String qName,
    org.xml.sax.Attributes atts) throws SAXException { 

    // record current tag in stack
    qualifiedNames[pathPos] = qName;
    localNames[pathPos] = localName;
    namespaceURIs[pathPos] = uri;
    int meaning;

    // what is the meaning of the current tag?
    meaning=0; pm=pathPos==0?0:meanings[pathPos-1];
    switch (inputFileSchemaType) {
           case FILETYPE_A:
      switch(pathPos) {
        // this checking can be as strict or as lenient as you like on case,
        // namespace URIs and tag prefixes
             case 0:
        if(localName.equals("document")&&uri.equals("http://xyz")) meaning=1;
      break; case 1: if (pm==1&&localName.equals("clients")) meaning=2;
      break; case 2: if (pm==2&&localName.equals("firstName")) meaning=3;
        else if (pm==2&&localName.equals("lastName")) meaning=4;
        else if (pm==2) meaning=5;
      }
      break; case FILETYPE_B:
      switch(pathPos) {
        // this checking can be as strict or as lenient as you like on case,
        // namespace URIs and tag prefixes
             case 0:
        if(localName.equals("DOC")&&uri.equals("http://abc")) meaning=1;
      break; case 1: if (pm==1&&localName.equals("CLS")) meaning=2;
      break; case 2: if (pm==2&&localName.equals("FN1")) meaning=3;
        else if (pm==2&&localName.equals("LN1")) meaning=4;
        else if (pm==2) meaning=5;
      }
    }

    meanings[pathPos]=meaning;

    // does the tag have unrecognised attributes?
    // does the tag have all required attributes?
    // record any keys in hashtables...
    // (TO BE DONE)

    // generate output
    switch (meaning) {
      case 0:errorList.add(new Object[]{locator.getPublicId(),
        locator.getSystemId(),
        locator.getLineNumber(),locator.getColumnNumber(),
        "Meaningless tag found: "+localName+" ("+qName+
        "; namespace: \""+uri+"\")});
      break;case 1:
      destination.write("<?xml version=\"1.0\" ?>\n");
      destination.write("<imdoc xmlns=\"http://someurl\" lang=\"xyz\">\n");
      destination.write("<!-- Copyright notice -->\n");
      destination.write("<!-- Generated by xyz -->\n");
      break;case 2: destination.write(" <cl>\n");
        currentFirstName="";currentLastName="";
    }
    pathPos++;
  }
  public void characters(char[] ch, int start, int length)
            throws SAXException {
    int meaning=meanings[pathPos-1]; switch (meaning) {
    case 1: case 2:
              errorList.add(new Object[]{locator.getPublicId(),
        locator.getSystemId(),
        locator.getLineNumber(),locator.getColumnNumber(),
        "Unexpected extra characters found"});
    break; case 3:
      // APPEND to currentFirstName IF WITHIN SIZE LIMITS
    break; case 4:
      // APPEND to currentLastName IF WITHIN SIZE LIMITS
    break; default: // ignore other characters
    }
  }
  public void endElement(String uri, String localName, String qName)
    throws SAXException {
    pathPos--;
    int meaning=meanings[pathPos]; switch (meaning) { case 1:
      destination.write("</imdoc>");
    break; case 2:
      destination.write("  <ln>"+currentLastName.trim()+"</ln>\n");
      destination.write("  <fn>"+currentFirstName.trim()+"</fn>\n");
      destination.write(" </cl>\n");
    break; case 3:
      if (currentFirstName==null||currentFirstName.equals(""))
              errorList.add(new Object[]{locator.getPublicId(),
        locator.getSystemId(),
        locator.getLineNumber(),locator.getColumnNumber(),
        "Invalid first name length"});
      // ADD FIELD FORMAT VALIDATION USING REGEXES / RANGE CHECKING
    break; case 4:
      if (currentLastName==null||currentLastName.equals(""))
              errorList.add(new Object[]{locator.getPublicId(),
        locator.getSystemId(),
        locator.getLineNumber(),locator.getColumnNumber(),
        "Invalid last name length"});
      // ADD FIELD FORMAT VALIDATION USING REGEXES / RANGE CHECKING
    }
  }
  public void endDocument() {
    // check for key violations
  }
}

Код первого этапа предназначен не для переупорядочения данных, а просто для стандартизации в единый промежуточный формат (который может быть разным в порядке расположения групп данных в зависимости от типа исходного файла, поскольку порядок групп данных будет отражать порядок исходного файла. ) и подтверждение.

Но написание обработчика SAX стоит делать только в том случае, если вы еще не довольны своим XSLT. Предположительно, вы не пишете этот вопрос ...?

OTOH, если вам нравится ваш XSLT, и он работает достаточно быстро, я говорю, зачем менять архитектуру. В этом случае вам может пригодиться { эта } статья, если вы еще не включили соответствующие вызовы Xalan в модуль Ruby. Возможно, вы захотите попробовать сделать его одностадийным процессом для пользователей (для случаев, когда ошибок в данных не обнаружено!).

EDIT

При таком подходе вам придётся вручную выводить XML-код, чтобы:

& становится & amp;

> становится & gt;

<становится & lt; </p>

Non-ascii становится сущностью персонажа, если необходимо, в противном случае последовательность UTF-8

и т.д.

Также стоит написать функцию, которая может принимать объект SAX Attributes и гибкую спецификацию проверки, относящуюся к значению входного тега и формату файла в виде массива объектов или аналогичных, и может сопоставлять и возвращать значения и помечать ошибки строго или мягко по мере необходимости.

И, наконец, у вас должна быть конфигурируемая концепция MAX_ERRORS со значением по умолчанию, скажем 1000, записывать ошибку «слишком много ошибок» на этом пределе и останавливать ошибки записи после того, как вы достигнете предела.

Если вам нужно увеличить количество XML-файлов, которые вы можете выполнять параллельно, и по-прежнему боретесь с пропускной способностью / производительностью, я предлагаю, чтобы шаг DOM только загружал, переупорядочивал и сохранял, так что можно делать один или два документа одновременно , но сравнительно быстро, делая это в пакетном режиме, а затем второй процессор SAX затем Google вызывает и последовательно обрабатывает XML для N документов.

НТН

EDIT

> У нас есть ~ 50 различных входящих форматов, так что

> переключатель / кейс FORMAT_X не подходит.

Это, конечно, общепринятая мудрость, но как насчет следующего:

// set meaning and attributesValidationRule (avr)
if (fileFormat>=GROUP10) switch (fileFormat) {
  case GROUP10_FORMAT1: 

    switch(pathPos) {
    case 0: if (...) { meaning=GROUP10_CUSTOMER; avr=AVR6_A; }
    break; case 1: if (...) { meaning=...; avr=...; }
    ...
    }

  break; case GROUP10_FORMAT2: ...

  break; case GROUP10_FORMAT3: ...
}
else if (fileFormat>=GROUP9) switch (fileFormat) {
  case GROUP9_FORMAT1: ... 
  break; case GROUP9_FORMAT2: ...
}
...
else if (fileFormat>=GROUP1) switch (fileFormat) {
  case GROUP1_FORMAT1: ... 
  break; case GROUP1_FORMAT2: ...
}

...

result = validateAttribute(atts,avr);

if (meaning >= MEANING_SET10) switch (meaning) {
case ...:  ...
break; case ...:  ...
}
else if (meaning >= MEANING_SET9) switch (meaning) {
}
etc

Может быть достаточно быстрым и намного легче читать, чем множество функций или классов.

> Меня не устраивает то, что я не могу структурировать

> и преобразования значений с использованием некоторого однородного процесса

> (как и в Java, я могу написать расширения для Xalan).

Звучит так, как будто вы достигли предела XSLT или вы просто говорите об очевидном пределе, что ввод данных из источников, отличных от исходного документа, является проблемой?

Другая идея состоит в том, чтобы иметь проверочную таблицу стилей, таблицу стилей, которая выводит список ключей для попытки в Google Maps, таблицу стилей, которая выводит список ключей для попытки в вашей базе данных, процессы, которые фактически выполняют Google / БД вызывает и выводит больше XML, функцию «конкатенации XML» и таблицу стилей, которая объединяет данные, принимая ввод как:

<?xml version="1.0" ?>
<myConsolidatedInputXmlDoc>
  <myOriginalOrIntermediateFormatDoc>
    ...
  </myOriginalOrIntermediateFormatDoc>
  <myFetchedRelatedDataFromGoogleMaps>
    ...
  </myFetchedRelatedDataFromGoogleMaps>
  <myFetchedDataFromSQL>
    ...
  </myFetchedDataFromSQL>
</myConsolidatedInputXmlDoc>

Таким образом, вы можете использовать XSLT в «многопроходном» сценарии без вызова расширений Xalan.

1 голос
/ 25 февраля 2010

Один из подходов - использовать Xalan-J с некоторыми расширениями, которые возвращают RPC-вызовы в ваш процесс Ruby. Возвращенные данные могут быть дополнительно обработаны XSLT.

Для более тесной интеграции вы можете связать Xalan-C ++ как Ruby библиотека. Вероятно, вам нужна только небольшая часть Xalan API, аналогичная той, которая используется в драйвере командной строки XalanExe. Когда Xalan работает в процессе, ваши расширения могут прямой доступ к вашей модели Ruby.

Ссылки:

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...