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

6
如何在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
我指的是这个问题。 - Tlatwork
4个回答

7
这里的关键是意识到使用xml2,您可以使用html转义字符将内容写回解析后的html中。这个函数将完成这个技巧。它比必要的长,因为我包括了注释和一些类型检查/转换逻辑。
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 concatenate函数中。

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
1
@Alejandro 我知道你的意思,但请记住,在此函数结束之前,xml会被返回到其初始状态,因此这种实现方式对用户来说是隐藏的。我们不处于多线程环境中,这种实现方式也不会有问题。我还编写了一个函数(类似于您建议的方法),它逐步构建xpath,但在我看来,它不够优雅。如果您要这样做,将html解析为单个字符字符串几乎同样容易。如果ThanksGuys感兴趣,我可以在我的答案中包含它。 - Allan Cameron
如果不需要太多的努力,我肯定会感兴趣。但是公平地说,我的规格对底层文档的(临时/持久)更改没有做出任何限制,所以问题已经完全回答了。事实上,我认为使用xml2在底层文档中进行临时更改的想法非常聪明。但我会记住Alejandro的提示! - Tlatwork
现在,答案的第二部分涵盖了将字符串注入到嵌入式语言中常见的方法:使用宿主语言对字符串进行清理处理。 - Alejandro

6

由于您正在使用字符串操作来构建XPath表达式,因此您有责任确保该表达式是有效的XPath。这个表达式:

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

选择:

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

这里进行测试

更好的方法是使用XPath字符串变量,但看起来R没有API可以做到这一点,即使使用libxml。


1
@ThanksGuys 没问题。但是实质上那个答案是错误的。你应该像编写一个语法正确的 R 程序一样编写一个语法正确的 XPath 表达式。为此,您需要一个辅助的 R 函数,它可以保留一个没有引号的字符串或者使用反向引号,如果字符串包含单引号或双引号,则递归地将该函数应用于由引号字符标记化的部分。 - Alejandro

4

使用 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 <- quote(paste0('//body/*[contains(.,concat(', target,'))]')) - (这个代码示例显然失败了) - 但类似这样的东西是否可能呢? - Tlatwork
是的,这是可能的。请注意xpath查询中的想法-2:单引号在双引号内,双引号在单引号内。然后使用xpath函数将它们连接起来。您可以通过识别字符串中的单引号和双引号并适当处理它来动态创建xpath查询。您只需编写一个实现此想法的通用函数即可。希望这可以帮助到您。 - Sathish
1
据我所知,XPath查询的问题在于它不喜欢转义双引号。当您尝试在XPath查询中转义双引号时,总是会出现错误。 - Sathish

0

我在html_nodes()函数调用内部添加了cat函数到目标中。看起来可以处理两种情况。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部分被省略了,至少如果将其保存到变量中,它看起来是这样的。 - Tlatwork

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接