Быстрая естественная сортировка больших файлов XML в браузере? - PullRequest
5 голосов
/ 02 декабря 2011

У меня сейчас проблема, связанная с текущими ограничениями на сервер, который наша команда не контролирует.

У нас есть работа, которую должна выполнять база данных, но мы вынуждены использовать файл XML и анализировать его с помощью Javascript / jQuery. У нас даже нет прав на запись для наших сценариев (только через нашу учетную запись FTP) ... нам не нравится говорить об этом, но это то, что мы получили.

Проблема, связанная с этими ограничениями, заключается в том, что нам необходимо проанализировать большой XML-файл размером около 500 КБ с 1700-кратными записями имени документа / номера / URL-адреса.

Число довольно сложное, например, "31-2b-1029E", смешанное с такими вещами, как "T2315342".

Итак, я решил, что мне нужно использовать что-то под названием «Natural Sort» (спасибо stackoverflow).

В любом случае я попытался использовать этот скрипт здесь:

/*
 * Reference: http://www.overset.com/2008/09/01/javascript-natural-sort-algorithm/
 * Natural Sort algorithm for Javascript - Version 0.6 - Released under MIT license
 * Author: Jim Palmer (based on chunking idea from Dave Koelle)
 * Contributors: Mike Grier (mgrier.com), Clint Priest, Kyle Adams, guillermo
 */
function naturalSort (a, b) {
    var re = /(^-?[0-9]+(\.?[0-9]*)[df]?e?[0-9]?$|^0x[0-9a-f]+$|[0-9]+)/gi,
        sre = /(^[ ]*|[ ]*$)/g,
        dre = /(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,
        hre = /^0x[0-9a-f]+$/i,
        ore = /^0/,
        // convert all to strings and trim()
        x = a.toString().replace(sre, '') || '',
        y = b.toString().replace(sre, '') || '',
        // chunk/tokenize
        xN = x.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'),
        yN = y.replace(re, '\0$1\0').replace(/\0$/,'').replace(/^\0/,'').split('\0'),
        // numeric, hex or date detection
        xD = parseInt(x.match(hre)) || (xN.length != 1 && x.match(dre) && Date.parse(x)),
        yD = parseInt(y.match(hre)) || xD && y.match(dre) && Date.parse(y) || null;
    // first try and sort Hex codes or Dates
    if (yD)
        if ( xD < yD ) return -1;
        else if ( xD > yD ) return 1;
    // natural sorting through split numeric strings and default strings
    for(var cLoc=0, numS=Math.max(xN.length, yN.length); cLoc < numS; cLoc++) {
        // find floats not starting with '0', string or 0 if not defined (Clint Priest)
        oFxNcL = !(xN[cLoc] || '').match(ore) && parseFloat(xN[cLoc]) || xN[cLoc] || 0;
        oFyNcL = !(yN[cLoc] || '').match(ore) && parseFloat(yN[cLoc]) || yN[cLoc] || 0;
        // handle numeric vs string comparison - number < string - (Kyle Adams)
        if (isNaN(oFxNcL) !== isNaN(oFyNcL)) return (isNaN(oFxNcL)) ? 1 : -1; 
        // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2'
        else if (typeof oFxNcL !== typeof oFyNcL) {
            oFxNcL += ''; 
            oFyNcL += ''; 
        }
        if (oFxNcL < oFyNcL) return -1;
        if (oFxNcL > oFyNcL) return 1;
    }
    return 0;
}

И применяется с использованием:

// Natural Sort (disabled because it is super freaking slow.... need xsl transform sorting instead)
var sortedSet = $(data).children("documents").children("document").sort(function(a, b) {
    return naturalSort($(a).children('index').text(), $(b).children('index').text());
});

Это отлично работает с другим нашим меньшим XML-файлом, но для гигантского 500-килобайтного файла Safari (v4) просто зависает на несколько минут, чтобы разобраться в этом, в то время как Firefox (последний) занимает около 10 секунд, чтобы процесс (все еще не хорошо, но по крайней мере в здравом уме).

Я также нашел этот другой меньший / более легкий скрипт под названием Alphanum :

function alphanum(a, b) {
  function chunkify(t) {
    var tz = [], x = 0, y = -1, n = 0, i, j;

    while (i = (j = t.charAt(x++)).charCodeAt(0)) {
      var m = (i == 46 || (i >=48 && i <= 57));
      if (m !== n) {
        tz[++y] = "";
        n = m;
      }
      tz[y] += j;
    }
    return tz;
  }

  var aa = chunkify(a);
  var bb = chunkify(b);

  for (x = 0; aa[x] && bb[x]; x++) {
    if (aa[x] !== bb[x]) {
      var c = Number(aa[x]), d = Number(bb[x]);
      if (c == aa[x] && d == bb[x]) {
        return c - d;
      } else return (aa[x] > bb[x]) ? 1 : -1;
    }
  }
  return aa.length - bb.length;
}

Это работает быстрее для Safari, но все еще блокирует браузер на минуту или около того.

Я провел некоторое исследование, и, похоже, несколько человек рекомендовали использовать XSL для сортировки записей XML, что, по-видимому, намного быстрее из-за того, что он встроен в браузер, а не работает поверх JavaScript.

По-видимому, существует несколько различных реализаций, когда Сарисса упоминается несколько раз, а на странице sourceforge, по-видимому, указано, что последнее обновление произошло в 2011-06-22.

Есть и другие варианты, такие как xslt.js

Мой вопрос:

  1. Является ли XSL лучшим вариантом сортировки для этой конкретной проблемы?
  2. Если так, как я могу использовать XSL для естественной сортировки? (URL к ресурсам?)
  3. Если да на оба вопроса, какую библиотеку мне следует использовать для лучшей совместимости и скорости?
  4. Если XSL не лучший выбор, то какой?

Спасибо, что посмотрели на мою проблему.

Ответы [ 3 ]

5 голосов
/ 02 декабря 2011

Хороший вопрос, + 1.

Вот решение XSLT 1.0 (есть решение XSLT 2.0, которое намного проще и проще в написании и, вероятно, более эффективно, однако ни один из 5 основных браузеров не оснащен процессором XSLT 2.0) :

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="http://exslt.org/common" exclude-result-prefixes="xml">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:variable name="vDigits" select="'0123456789'"/>

 <xsl:variable name="vPadding" select=
 "'                    '"/>

 <xsl:variable name="vMaxNumLength"
      select="string-length($vPadding)"/>

 <xsl:template match="/">
  <xsl:variable name="vrtfPass1">
   <t>
    <xsl:apply-templates/>
   </t>
  </xsl:variable>

  <xsl:variable name="vPass1" select="ext:node-set($vrtfPass1)"/>

  <t>
    <xsl:for-each select="$vPass1/*/*">
     <xsl:sort select="@sortMe"/>

     <xsl:copy>
      <xsl:value-of select="."/>
     </xsl:copy>
    </xsl:for-each>
  </t>
 </xsl:template>

 <xsl:template match="str">
   <str>
    <xsl:apply-templates select="text()" mode="normalize"/>
    <xsl:copy-of select="text()"/>
   </str>
 </xsl:template>

 <xsl:template match="text()" mode="normalize" name="normalize">
  <xsl:param name="pText" select="."/>
  <xsl:param name="pAccum" select="''"/>

  <xsl:choose>
   <xsl:when test="not(string-length($pText) >0)">
     <xsl:attribute name="sortMe">
       <xsl:value-of select="$pAccum"/>
     </xsl:attribute>
   </xsl:when>
   <xsl:otherwise>
    <xsl:variable name="vChar1" select="substring($pText,1,1)"/>

    <xsl:choose>
     <xsl:when test="not(contains($vDigits,$vChar1))">
       <xsl:variable name="vDig1" select=
       "substring(translate($pText,
                            translate($pText, $vDigits, ''),
                            ''
                            ),
                  1,1)"/>
       <xsl:variable name="vDig">
        <xsl:choose>
         <xsl:when test="string-length($vDig1)">
          <xsl:value-of select="$vDig1"/>
         </xsl:when>
         <xsl:otherwise>0</xsl:otherwise>
        </xsl:choose>
       </xsl:variable>

       <xsl:variable name="vNewText" select=
        "substring-before(concat($pText,$vDig), $vDig)"/>

       <xsl:call-template name="normalize">
        <xsl:with-param name="pText" select=
         "substring($pText, string-length($vNewText)+1)"/>
        <xsl:with-param name="pAccum" select=
        "concat($pAccum, $vNewText)"/>
       </xsl:call-template>
     </xsl:when>

     <xsl:otherwise>
      <xsl:variable name="vNonDig1" select=
      "substring(translate($pText, $vDigits, ''),1,1)"/>

      <xsl:variable name="vNonDig">
        <xsl:choose>
         <xsl:when test="string-length($vNonDig1)">
          <xsl:value-of select="$vNonDig1"/>
         </xsl:when>
         <xsl:otherwise>Z</xsl:otherwise>
        </xsl:choose>
      </xsl:variable>

      <xsl:variable name="vNum" select=
           "substring-before(concat($pText,'Z'),$vNonDig)"/>

      <xsl:variable name="vNumLength" select=
       "string-length($vNum)"/>

      <xsl:variable name="vNewText" select=
       "concat(substring($vPadding,
                         1,
                         $vMaxNumLength -$vNumLength),
               $vNum
               )"/>

       <xsl:call-template name="normalize">
        <xsl:with-param name="pText" select=
         "substring($pText, $vNumLength +1)"/>
        <xsl:with-param name="pAccum" select=
        "concat($pAccum, $vNewText)"/>
       </xsl:call-template>
     </xsl:otherwise>
    </xsl:choose>
   </xsl:otherwise>
  </xsl:choose>
 </xsl:template>
</xsl:stylesheet>

когда это преобразование применяется к XML-документу ниже :

<t>
 <str>Allegia 6R Clasteron</str>
 <str>200X Radonius</str>
 <str>Xiph Xlater 10000</str>
 <str>1000X Radonius Maximus</str>
 <str>Callisto Morphamax 6000 SE</str>
 <str>10X Radonius</str>
 <str>20X Radonius</str>
 <str>30X Radonius</str>
 <str>20X Radonius Prime</str>
 <str>40X Radonius</str>
 <str>Allegia 50 Clasteron</str>
 <str>Allegia 500 Clasteron</str>
 <str>Allegia 50B Clasteron</str>
 <str>Allegia 51 Clasteron</str>
 <str>Alpha 100</str>
 <str>Alpha 2</str>
 <str>Alpha 200</str>
 <str>Alpha 2A</str>
 <str>Alpha 2A-8000</str>
 <str>Alpha 2A-900</str>
 <str>Callisto Morphamax</str>
 <str>Callisto Morphamax 500</str>
 <str>Callisto Morphamax 5000</str>
 <str>Callisto Morphamax 600</str>
 <str>Callisto Morphamax 6000 SE2</str>
 <str>Callisto Morphamax 700</str>
 <str>Callisto Morphamax 7000</str>
 <str>Xiph Xlater 2000</str>
 <str>Xiph Xlater 300</str>
 <str>Xiph Xlater 40</str>
 <str>Xiph Xlater 5</str>
 <str>Xiph Xlater 50</str>
 <str>Xiph Xlater 500</str>
 <str>Xiph Xlater 5000</str>
 <str>Xiph Xlater 58</str>
</t>

желаемый, правильно "натурально отсортированный" результат получается :

<t>
   <str>10X Radonius</str>
   <str>20X Radonius</str>
   <str>20X Radonius Prime</str>
   <str>30X Radonius</str>
   <str>40X Radonius</str>
   <str>200X Radonius</str>
   <str>1000X Radonius Maximus</str>
   <str>Allegia 6R Clasteron</str>
   <str>Allegia 50 Clasteron</str>
   <str>Allegia 50B Clasteron</str>
   <str>Allegia 51 Clasteron</str>
   <str>Allegia 500 Clasteron</str>
   <str>Alpha 2</str>
   <str>Alpha 2A</str>
   <str>Alpha 2A-900</str>
   <str>Alpha 2A-8000</str>
   <str>Alpha 100</str>
   <str>Alpha 200</str>
   <str>Callisto Morphamax</str>
   <str>Callisto Morphamax 500</str>
   <str>Callisto Morphamax 600</str>
   <str>Callisto Morphamax 700</str>
   <str>Callisto Morphamax 5000</str>
   <str>Callisto Morphamax 6000 SE</str>
   <str>Callisto Morphamax 6000 SE2</str>
   <str>Callisto Morphamax 7000</str>
   <str>Xiph Xlater 5</str>
   <str>Xiph Xlater 40</str>
   <str>Xiph Xlater 50</str>
   <str>Xiph Xlater 58</str>
   <str>Xiph Xlater 300</str>
   <str>Xiph Xlater 500</str>
   <str>Xiph Xlater 2000</str>
   <str>Xiph Xlater 5000</str>
   <str>Xiph Xlater 10000</str>
</t>

Важное предположение : Это решение предполагает, что ни один номер не будет иметь более 40 цифр. Хотя это было бы верно в преобладающем количестве практических случаев, если возникает случай, когда этот предел недостаточен, это решение легко изменить, чтобы принять предельное значение в качестве внешнего / глобального параметра.

Наконец, производительность :

Обработка XML-документа, аналогичного описанному выше, но с 1700 str элементами занимает 0,659 сек. на моем 8-летнем одноядерном процессоре Pentium с частотой 3 ГГц и 2 ГБ ОЗУ.

Объяснение

  1. Это двухпроходное решение.

  2. При первом проходе все узлы копируются «как есть», за исключением того, что атрибут sortMe добавляется к каждому элементу str. Этот атрибут содержит строковое значение единственного дочернего элемента текстового узла str, в котором любое число дополняется слева пробелами до общей фиксированной длины 40.

  3. На втором этапе мы сортируем все элементы str в алфавитном порядке, используя один ключ сортировки - атрибут sortMe.

Теперь ответим на все 4 оригинальных вопроса :

Мой вопрос:

Является ли XSL лучшим вариантом сортировки для этой конкретной проблемы?
Если так, как я могу использовать XSL для естественной сортировки? (URL к ресурсам?)
Если да, обоим вопросы, какую библиотеку я должен использовать для лучшей совместимости и скорость
Если XSL не лучший выбор, то какой?

Ответы

  1. Любая реализация алгоритма оптимальной сортировки (независимо от языка) должна быть достаточной. В этом отношении XSLT - хороший выбор.

  2. Приведенный выше код обеспечивает полную и точную реализацию XSLT «естественной» сортировки.

  3. Библиотека не требуется - просто используйте приведенный выше код как есть. Если вам нужна помощь, как вызвать преобразование из вашего PL, обратитесь к соответствующей документации.

  4. Любой PL, включая XSLT, с реализацией оптимального алгоритма сортировки является подходящим выбором.

1 голос
/ 02 декабря 2011

Несколько ответов на вспомогательные вопросы:

(a) Sarissa не является XSLT-процессором, это слой-оболочка Javascript, который предоставляет общий API-интерфейс Javascript для XSLT-процессора, предоставляемого как часть браузера.

(b) xslt.js - это мертвый проект, который пытался внедрить процессор XSLT в Javascript.Забудь об этом, это история.

Более поздней разработкой в ​​этом направлении является Saxon-CE, который в настоящее время находится в альфа-версии (он написан на Java и кросс-компилирован в Javascript с использованием GWT).Когда закончите, это даст вам XSLT 2.0 в браузере.Серверная часть Saxon имеет параметры сортировки, которые дают вам «естественную сортировку» (<xsl:sort collation='http://saxon.sf.net/collation?alphanumeric=yes'/>), но она недоступна в текущей версии Saxon-CE.

(PS Я не встречал имя 'естественная сортировка "раньше. Спасибо.)

0 голосов
/ 02 декабря 2011

Функция сортировки вызывается больше раз, чем отсортировано в массиве элементов, на самом деле, намного больше.Для сортировки ваших 1700 элементов, функция сравнения, вероятно, вызывается от 10000 до 750,000 раз в зависимости от браузера ... Так как ваша функция сравнения сортировки медленная, вы получите большую выгоду, выполнив тяжелую работу один раз.за элемент и сохраняя результат, а затем сортируя по сохраненным результатам.

Бьюсь об заклад, главная проблема в том, что вы используете jquery в своей функции сортировки.Это должно быть дорого.Фактическое сравнение естественных сортов, вероятно, сравнительно быстро.Я не знаю вашу структуру XML, но если вы можете отказаться от jquery в функции сортировки, попробуйте скопировать ссылки на элементы в новый массив, который является линейным временем.Затем вы сортируете массив.Затем переберите теперь отсортированный массив и используйте ссылки на элементы, чтобы установить порядок в документе xml.

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