Строчная часть строки с регулярным выражением XPath

В узле строка может содержать одну или несколько подстрок, заключенных в одинарные или двойные кавычки. Например

<node>Some text "and Some" More</node>

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

some text "and Some" more

Я пробовал две вещи:

  1. с replace: replace('Some text "and Some" More', '"([^"]*)"', '*') это заменит текст в двойных кавычках на *. Но как я могу сделать это в нижнем регистре? Это не дает желаемого результата: replace('Some text "and Some" More', '"([^"]*)"', lower-case('$1'))
  2. с tokenize: for $t in tokenize('Some text "and Some" More', '"') return $t. Поскольку мой узел не будет начинаться с ", я знаю, что нечетные записи будут подстроками, заключенными в кавычки. Но я не знаю, как выбирать и переводить в нижний регистр только нечетные записи. Я пробовал с position(), но возвращает 1 на каждой итерации.

Спасибо, что изучили это. Очень признателен.


person Martin Dimitrov    schedule 13.03.2013    source источник
comment
Мартин, существует единственное решение XPath, которое работает даже в самом общем случае - когда нет гарантированного порядка или количества полей в кавычках и без кавычек. Другите решения бледнеят :)   -  person Dimitre Novatchev    schedule 14.03.2013


Ответы (3)


Вот одно выражение XPath 2.0, которое обрабатывает нужным образом любое сочетание строк в кавычках и без кавычек — в любом порядке:

  string-join(
  (for $str in tokenize(replace(., "(.*?)("".*?"")([^""]*)", "|$1|$2|$3|", "x"),"\|")
     return
      if(not(contains($str, """")))
        then lower-case($str)
        else $str
  ),
  "")

Для комплексного теста я оцениваю приведенное выше выражение в следующем XML-документе:

<node>Some "Text""and Some" More "Text" XXX "Even More"</node>

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

some "Text""and Some" more "Text" xxx "Even More"

Подтверждение XSLT 2.0:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/">
  <xsl:sequence select=
  'string-join(
  (for $str in tokenize(replace(., "(.*?)("".*?"")([^""]*)", "|$1|$2|$3|", "x"),"\|")
     return
      if(not(contains($str, """")))
        then lower-case($str)
        else $str
  ),
  "")
  '/>
 </xsl:template>
</xsl:stylesheet>

Когда это преобразование применяется к указанному выше XML-документу, вычисляется выражение XPath, и результат этой оценки копируется в выходные данные:

some "Text""and Some" more "Text" xxx "Even More"

Наконец, решение XSLT 2.0 – гораздо проще в написании и понимании:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:template match="/*">
  <xsl:analyze-string select="." regex='".*?"'>
   <xsl:non-matching-substring>
     <xsl:sequence select="lower-case(.)"/>
   </xsl:non-matching-substring>
   <xsl:matching-substring><xsl:sequence select="."/></xsl:matching-substring>
  </xsl:analyze-string>
 </xsl:template>
</xsl:stylesheet>
person Dimitre Novatchev    schedule 14.03.2013
comment
Решение XSLT действительно красивое. Спасибо. Одна раздражающая деталь: он добавляет дополнительный пробел вокруг совпадающей подстроки. Интересно, почему. - person Martin Dimitrov; 14.03.2013
comment
@MartinDimitrov, просто замените инструкции xsl:sequence на xsl:value-of. Я думал, что вам не нужен XSLT-слот. Кажется, ты не против. Я отмечу ваш вопрос как xslt-2.0 - person Dimitre Novatchev; 14.03.2013
comment
Это прекрасно работает. Решение XSLT намного чище и приятнее, поэтому я предпочитаю его. В чем разница между xsl:value-of и xsl:sequence? - person Martin Dimitrov; 14.03.2013
comment
@MartinDimitrov, когда сериализуется последовательность, созданная xsl:sequence, в качестве разделителя между элементами используется пробел. С другой стороны, xsl:value-of создает (в данном случае) один текстовый узел. Все такие последовательные текстовые узлы затем объединяются в один текстовый узел (как того требует модель данных XPath), и в этом процессе объединения не используются никакие разделители. - person Dimitre Novatchev; 14.03.2013
comment
Спасибо за объяснение. - person Martin Dimitrov; 14.03.2013

Фух.

В случае, если вам нравится трудный путь:

concat(translate(substring-before(//node/text(), '"'),'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') ,substring(substring-after(//node/text(), '"'), 1, string-length(substring-after(//node/text(), '"')) - string-length(substring-after(substring-after(//node/text(), '"'), '"')) -1) , translate(substring-after(substring-after(//node/text(), '"'), '"'), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'))

Просто замените //node/text() любым XPath, который приведет вас к нужному тексту. Я просто сделал это для удовольствия, это не самое «чистое» (HA!) Решение.

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

person JWiley    schedule 13.03.2013
comment
Благодарю. Это выглядит очень забавно :) Учитывает ли это возможность иметь более 1 подстроки, заключенной в кавычки? - person Martin Dimitrov; 14.03.2013
comment
нет, только одна подстрока. Я не пытаюсь сделать больше, чем просто XPath ... хотя, думаю, это может быть полезно. - person JWiley; 14.03.2013
comment
НО вы не упомянули о возможности нескольких подстрок в своем вопросе. может содержать подстроку a - person JWiley; 14.03.2013

В XQuery вы можете использовать

string-join(
  for $x at $i  in tokenize('Some text "and Some" More', '"') return
    if ($i mod 2 = 1) then lower-case($x)
    else $x
  , '"')

но xpath, только имеет калеку без at.

В XPath 3 вы можете использовать ! простой оператор карты (который похож на for, за исключением того, что он устанавливает . и position()):

string-join(
  tokenize('Some text "and Some" More', '"') !
    if (position() mod 2 = 1) then lower-case(.)
    else .
  , '"')

И, наконец, в XPath 2 вы можете выполнить итерацию по индексу и получить подстроку для каждого индекса:

string-join(
  for $i in 1 to count(tokenize('Some text "and Some" More', '"')) return
    if ($i mod 2 = 1) then lower-case(tokenize('Some text "and Some" More', '"')[$i])
    else tokenize('Some text "and Some" More', '"')[$i]
  , '"')
person BeniBela    schedule 13.03.2013
comment
Отлично. Большое тебе спасибо. - person Martin Dimitrov; 14.03.2013