在 Xpath 中同时转义双引号和单引号

Posted

技术标签:

【中文标题】在 Xpath 中同时转义双引号和单引号【英文标题】:Simultaneously escape double and single quotes in Xpath 【发布时间】:2020-04-09 09:31:34 【问题描述】:

类似于How to deal with single quote in xpath,我想转义单引号。不同的是,我不能排除双引号也可能出现在目标字符串中的可能性。

目标:

使用 Xpath(在 R 中)同时转义双引号和单引号。目标元素应用作变量,而不是像现有答案之一那样进行硬编码。 (应该是一个变量,因为我事先不知道内容,它可能有单引号、双引号或两者都有)。

作品:

library(rvest)
library(magrittr)
html <- "<div>1</div><div>Father's son</div>"
target <- "Father's son"
html %>% xml2::read_html() %>% html_nodes(xpath = paste0("//*[contains(text(), \"", target,"\")]"))
xml_nodeset (1)
[1] <div>Father's son</div>

不起作用:

html <- "<div>1</div><div>Fat\"her's son</div>"
target <- "Fat\"her's son"
html %>% xml2::read_html() %>% html_nodes(xpath = paste0("//*[contains(text(), \"", target,"\")]"))
xml_nodeset (0)
Warning message:
In xpath_search(x$node, x$doc, xpath = xpath, nsMap = ns, num_results = Inf) :
  Invalid expression [1207]

更新

非常欢迎我尝试“翻译为 R”的非 R 答案。

【问题讨论】:

我的意思是*问题。 【参考方案1】:

这里的关键是意识到,使用 xml2,您可以使用 html 转义字符写回已解析的 html。这个函数可以解决问题。它比实际需要的要长,因为我包含了 cmets 和一些类型检查/转换逻辑。

contains_text <- function(node_set, find_this)

  # Ensure we have a nodeset
  if(all(class(node_set) == c("xml_document", "xml_node")))
    node_set %<>% xml_children()

  if(class(node_set) != "xml_nodeset")
    stop("contains_text requires an xml_nodeset or xml_document.")

  # Get all leaf nodes
  node_set %<>% xml_nodes(xpath = "//*[not(*)]")

  # HTML escape the target string
  find_this %<>% gsub("\"", "&quot;", .)

  # Extract, HTML escape and replace the nodes
  lapply(node_set, function(node) xml_text(node) %<>% gsub("\"", "&quot;", .))

  # Now we can define the xpath and extract our target nodes
  xpath <- paste0("//*[contains(text(), \"", find_this, "\")]")
  new_nodes <- html_nodes(node_set, xpath = xpath)

  # Since the underlying xml_document is passed by pointer internally,
  # we should unescape any text to leave it unaltered
  xml_text(node_set) %<>% gsub("&quot;", "\"", .)
  return(new_nodes)

现在:

library(rvest)
library(xml2)

html %>% xml2::read_html() %>% contains_text(target)
#> xml_nodeset (1)
#> [1] <div>Fat"her's son</div>
html %>% xml2::read_html() %>% contains_text(target) %>% xml_text()
#> [1] "Fat\"her's son"

附录

这是一种替代方法,它是@Alejandro 建议的方法的实现,但允许任意目标。它具有保持 xml 文档不变的优点,并且比上述方法快一点,但涉及到 xml 库应该防止的那种字符串解析。它的工作原理是获取目标,在每个 "' 之后将其拆分,然后将每个片段括在与其包含的相反类型的引号中,然后用逗号将它们全部粘贴在一起并将它们插入到 XPath @987654325 @函数。

library(stringr)

safe_xpath <- function(target)

  target                                 %<>%
  str_replace_all("\"", "&quot;&break;") %>%
  str_replace_all("'", "&apo;&break;")   %>%
  str_split("&break;")                   %>%
  unlist()

  safe_pieces    <- grep("(&quot;)|(&apo;)", target, invert = TRUE)
  contain_quotes <- grep("&quot;", target)
  contain_apo    <- grep("&apo;", target)

  if(length(safe_pieces) > 0) 
      target[safe_pieces] <- paste0("\"", target[safe_pieces], "\"")

  if(length(contain_quotes) > 0)
  
    target[contain_quotes] <- paste0("'", target[contain_quotes], "'")
    target[contain_quotes] <- gsub("&quot;", "\"", target[contain_quotes])
  

  if(length(contain_apo) > 0)
  
    target[contain_apo] <- paste0("\"", target[contain_apo], "\"")
    target[contain_apo] <- gsub("&apo;", "'", target[contain_apo])
  

  fragment <- paste0(target, collapse = ",")
  return(paste0("//*[contains(text(),concat(", fragment, "))]"))

现在我们可以像这样生成一个有效的 xpath:

safe_xpath(target)
#> [1] "//*[contains(text(),concat('Fat\"',\"her'\",\"s son\"))]"

这样

html %>% xml2::read_html() %>% html_nodes(xpath = safe_xpath(target))
#> xml_nodeset (1)
#> [1] <div>Fat"her's son</div>

【讨论】:

哇,这真是太好了。学到了很多,谢谢!我已经接受了答案,所以赏金将被自动分配,同时答案可以收集更多当之无愧的赞成票,...如果它符合您的兴趣。否则我也可以直接分配赏金。再次感谢! 此方法涉及更改基础文档,而不是编写正确的 XPath 表达式。 @Alejandro 我知道你的意思,但请记住,xml 在此函数结束之前返回到其初始状态,因此有关实现的这一事实对用户是隐藏的。我们不在多线程环境中,这种实现可能会出现问题。我还编写了一个函数(类似于您建议的方法),它可以分段构建 xpath,但在我看来它不太优雅。如果您打算这样做,将 html 解析为单个字符串几乎一样容易。如果ThankGuys 感兴趣,我可以将其包含在我的答案中。 如果不需要太多努力,我肯定会感兴趣。但也公平地说,我的规范没有对基础文档的(临时/持久)更改做出任何限制,因此问题得到了充分回答。事实上,我发现在基础文档中使用 xml2 进行临时更改的想法是一个非常聪明的想法。但我会牢记亚历杭德罗的暗示! 现在,答案的第二部分确实涵盖了将字符串注入嵌入式语言的常用方法:使用宿主语言清理字符串。【参考方案2】:

我将cat 函数添加到html_nodes() 函数调用中的目标。似乎可以处理这两种情况。 cat() 也有打印转义文本的副作用。

library(rvest)
library(magrittr)

html <- "<div>1</div><div>Father's son</div>"
target <- "Father's son"
html %>% xml2::read_html() %>% html_nodes(xpath = paste0("//*[contains(text(), \"",cat(target),"\")]"))
#> Father's son
#> xml_nodeset (4)
#> [1] <html><body>\n<div>1</div>\n<div>Father's son</div>\n</body></html>
#> [2] <body>\n<div>1</div>\n<div>Father's son</div>\n</body>
#> [3] <div>1</div>\n
#> [4] <div>Father's son</div>

html <- "<div>1</div><div>Father said \"Hello!\"</div>"
target <- 'Father said "Hello!"'
html %>% xml2::read_html() %>% html_nodes(xpath = paste0("//*[contains(text(), \"",cat(target),"\")]"))
#> Father said "Hello!"
#> xml_nodeset (4)
#> [1] <html><body>\n<div>1</div>\n<div>Father said "Hello!"</div>\n</body> ...
#> [2] <body>\n<div>1</div>\n<div>Father said "Hello!"</div>\n</body>
#> [3] <div>1</div>\n
#> [4] <div>Father said "Hello!"</div>

【讨论】:

感谢您的回答。似乎创建了 4 个节点的输出而不是一个。所以所有节点都被选中。我认为 cat 中的 xpath-part 只是被省略了,至少它看起来像如果你将它保存到一个变量。 再次感谢您的帮助。我接受了艾伦的回答,因为它非常详细,涵盖了很多场景。我希望没问题。【参考方案3】:

使用quote() 进行xpath 查询

library(XML)

字符串内只有单引号

target1 <- "Father's son"
doc1 <- XML::newHTMLDoc()
newXMLNode("div", 1, parent = getNodeSet(doc1, "//body"), doc = doc1)
newXMLNode("div", target1, parent = getNodeSet(doc1, "//body"), doc = doc1)
xpath_query1 <- paste0('//*[ contains(text(), ', '"', target1, '"', ')]')
getNodeSet(doc1, xpath_query1)

字符串中的单引号和双引号

target2 <- "Fat\"her's son"
doc2 <- XML::newHTMLDoc()
newXMLNode("div", 1, parent = getNodeSet(doc2, "//body"), doc = doc2)
newXMLNode("div", target2, parent = getNodeSet(doc2, "//body"), doc = doc2)
xpath_query2 <- quote('//body/*[contains(.,concat(\'Fat"\',"her\'s son"))]')
getNodeSet(doc2, xpath_query2)

输出:

getNodeSet(doc1, xpath_query1)
# [[1]]
# <div>Father's son</div> 
# 
# attr(,"class")
# [1] "XMLNodeSet"

getNodeSet(doc2, xpath_query2)
# [[1]]
# <div>Fat"her's son</div> 
# 
# attr(,"class")
# [1] "XMLNodeSet"

【讨论】:

谢谢你已经有很大的帮助了。也许我没有足够好地指定它。我需要动态插入target。所以一些事情:xpath_query2 &lt;- quote(paste0('//body/*[contains(.,concat(', target,'))]')) - (这个示例代码 obv。失败) - 但这样的事情可能吗? 是的,有可能。请注意 xpath 查询中的想法 - 2:单引号在双引号内,双引号在单引号内。然后使用 xpath 函数将它们连接起来。您可以通过识别字符串中的单引号和双引号来动态创建 xpath 查询并适当地处理它。你只需编写一个通用函数来实现这个想法。希望这会有所帮助。 据我所知,xpath 查询的问题是它不喜欢转义双引号。当您尝试在 xpath 查询中转义双引号时,您总是会遇到错误。 再次感谢您的帮助。正如我写给其他人一样:我接受了艾伦的回答,因为它非常详细并且涵盖了很多场景。我希望没问题。【参考方案4】:

因为您使用字符串操作来构建您的 XPath 表达式,所以您有责任确保该表达式是有效的 XPath。这个表达式:

//*[contains(.,concat('Fat"',"her's son"))]

选择:

<div>Fat"her's son</div>

在here中测试

使用 XPath 字符串变量会是更好的方法,但看起来 R 没有用于此的 API,即使使用 libxml。

【讨论】:

再次感谢您的帮助。我接受了艾伦的回答,因为它非常详细,涵盖了很多场景。我希望没问题。 @ThanksGuys 没问题。但这个答案本质上是错误的。您应该编写一个语法正确的 XPath 表达式,就像您需要一个语法正确的 R 程序一样。为此,您需要一个辅助 R 函数,该函数使不带引号的字符串保持不变,或者如果字符串包含单引号或包含双引号,则使用逆函数,或者当字符串同时具有单引号和双引号时,将该函数递归地应用于由引号字符标记的部分.

以上是关于在 Xpath 中同时转义双引号和单引号的主要内容,如果未能解决你的问题,请参考以下文章

SQL双引号和单引号同时存在的时候该如何转义呢? 比如这一句

Ansible 转义双引号和单引号

在mysql中插入时保留双引号和单引号以及转义字符

Javascript - 如何在 Kendo 模板上转义双引号和单引号

javascript中双引号和单引号之间的歧义

Perl双引号和单引号的区别