通过 Bash 使用 XMLStarlet 将元素 [s] (<td>) 插入 HTML Table 的每一行

Insert element[s] (<td>) into each row of HTML Table using XMLStarlet via Bash

我想从 link 的列表中提取每个 html table。我使用的代码如下:

wget -O - "https://example.com/section-1/table-name/financial-data/" | xmllint --html --xpath '//*[@id="financial-data"]/div/table/tbody' - 2>/dev/null >> /Applications/parser/output.txt

这工作得很好,但是,考虑到这不是唯一的 table 我想提取它会让我难以识别 financial-data 属于哪个table。在这种情况下,它只会解析附加到该输出文件的一个 table,其中 SDTOUT 如下所示:

<tbody>

                    <tr class="text-right">
                      <td class="text-left">Sep 08, 2017</td>
                      <td>4605.16</td>     
                      <td>4661.00</td>
                      <td>4075.18</td>
                      <td>4228.75</td>
                      <td>2,700,890,000</td>
                      <td>76,220,200,000</td>
                    </tr>

                    <tr class="text-right">
                      <td class="text-left">Sep 07, 2017</td>
                      <td>4589.14</td>     
                      <td>4655.04</td>
                      <td>4491.33</td>
                      <td>4599.88</td>
                      <td>1,844,620,000</td>
                      <td>75,945,000,000</td>
                    </tr>
...
</tbody>

但我正在寻找这个:

<tbody>

                    <tr class="text-right">
                      <td>TABLE-NAME</td>
                      <td class="text-left">Sep 08, 2017</td>
                      <td>4605.16</td>     
                      <td>4661.00</td>
                      <td>4075.18</td>
                      <td>4228.75</td>
                      <td>2,700,890,000</td>
                      <td>76,220,200,000</td>
                    </tr>

                    <tr class="text-right">
                      <td>TABLE-NAME</td>
                      <td class="text-left">Sep 07, 2017</td>
                      <td>4589.14</td>     
                      <td>4655.04</td>
                      <td>4491.33</td>
                      <td>4599.88</td>
                      <td>1,844,620,000</td>
                      <td>75,945,000,000</td>
                    </tr>
...
</tbody>

其中 TABLE-NAME 是特定资产的名称。可以使用 XPath /html/body/div[3]/div/div[1]/div[3]/div[1]/h1/text() 提取名称,它出现在与 table 相同的 URL 中,或者从 link 本身 /table-name/.

我搞不懂语法。

NB:我故意省略了 wget 命令中的 -q 标志,因为我想查看脚本执行时终端中发生的情况。

谢谢!


更新

根据@DanielHaley 的说法,这可以通过 XMLStarlet 完成,但是,当我通读 documentation 时,我找不到如何使用它的示例。

正确的语法是什么?我是否必须先通过 xmllint --html --xpath 解析 HTML table 然后再应用 xmlstarlet

这是我目前发现的:

-i or --insert <xpath> -t (--type) elem|text|attr -n <name> -v (--value) <value>
-a or --append <xpath> -t (--type) elem|text|attr -n <name> -v (--value) <value>

新更新

根据这个link,我遇到了像这样轻松添加子节点的脚本:

wget -O - "https://example.com/section-1/table-name/financial-data/" |
xmllint --html --xpath '//*[@id="financial-data"]/div/table/tbody' - 2>/dev/null |
xmlstarlet ed --subnode "/tbody/tr" --type elem -n td -v "Hello World" >> /Applications/parser/output.txt

将以下内容写入 STDOUT:

<tbody>

                    <tr class="text-right">
                      <td class="text-left">Sep 08, 2017</td>
                      <td>4605.16</td>     
                      <td>4661.00</td>
                      <td>4075.18</td>
                      <td>4228.75</td>
                      <td>2,700,890,000</td>
                      <td>76,220,200,000</td>
                      <td>Hello World</td>
                    </tr>

                    <tr class="text-right">
                      <td class="text-left">Sep 07, 2017</td>
                      <td>4589.14</td>     
                      <td>4655.04</td>
                      <td>4491.33</td>
                      <td>4599.88</td>
                      <td>1,844,620,000</td>
                      <td>75,945,000,000</td>
                      <td>Hello World</td>
                    </tr>
...
</tbody>

到目前为止一切顺利,但是,这会使用选项 -v 重现一些声明为 文本字符串 的默认文本,即在这种情况下 "Hello World".我希望用资产的实际名称替换此 文本字符串 。如前所述,TABLE-NAME 位于 table 所在的同一页面中,并且可以通过其他 XPath 访问,因此我尝试了以下操作代码:

wget -O - "https://example.com/section-1/table-name/financial-data/" |
header=$(xmllint --html --xpath '/html/body/div[3]/div/div[1]/div[3]/div[1]/h1' -) |
xmllint --html --xpath '//*[@id="financial-data"]/div/table/tbody' - 2>/dev/null |
xmlstarlet ed --subnode "/tbody/tr" --type elem -n td -v "$header" >> /Applications/parser/output.txt

在这里你可以清楚地看到我尝试声明一个变量$header,其中应包含资产的名称。这不起作用并使我的输出文件为空,可能是因为声明错误或管道语法不正确。

如何将相应的 XPath(引用资产名称)插入新创建的子节点 <td>?变量是我想到的第一件事;可以用别的方法吗?

您应该在将输出附加到 output.txt 之前尝试插入附加列。确保您需要的表名存储在变量中。你想做类似

的事情
tbl=testtbl
echo "<tbody>
                    <tr class="text-right">
                      <td class="text-left">Sep 08, 2017</td>
                      <td>4605.16</td>
                      <td>4661.00</td>
                      <td>4075.18</td>
                      <td>4228.75</td>
                      <td>2,700,890,000</td>
                      <td>76,220,200,000</td>
                    </tr>

                    <tr class="text-right">
                      <td class="text-left">Sep 07, 2017</td>
                      <td>4589.14</td>
                      <td>4655.04</td>
                      <td>4491.33</td>
                      <td>4599.88</td>
                      <td>1,844,620,000</td>
                      <td>75,945,000,000</td>
                    </tr>
" | sed 's#.*<tr.*#&\n      <td>'"${tbl}"'</td>#'

sed 命令中,正常的斜杠被替换为 '#',因此您不要对 </td>.
中的斜杠进行转义 当你有一个文件 alltables.txt 带有 apporox. 1160张桌子,你要这样循环:

while IFS= read -r tbl; do
   wget -O - "https://example.com/section-1/table-name/financial-data/" |
      xmllint --html --xpath '//*[@id="financial-data"]/div/table/tbody' - 2>/dev/null |
      sed 's#.*<tr.*#&\n      <td>'"${tbl}"'</td>#' >> /Applications/parser/output.txt
done < alltables.txt

您可能可以使用 xmlstarlet 中的 ed (edit) command 来完成此操作,但我对 xmlstarlet 的了解还不足以给您一个简单的答案。

此外,正如您所说,看起来您必须先通过 xmllint 传递 HTML 或使用 fo xmlstarlet command,然后再将其传递给 xmlstarlet eded 好像不支持 --html.

我要做的是将 xmlstarlet tr (transform) command 与 XSLT 样式表一起使用。

它非常冗长,但比尝试 parse HTML/XML with regex 安全得多。它也更容易扩展。

这是 XSLT。我添加了评论以帮助您了解正在发生的事情。

XSLT 1.0 (stylesheet.xsl)

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

  <!--Parameter to capture the table name. This is set on the command line.-->
  <xsl:param name="tablename"/>

  <!--Identity transform. Will basically output attributes/nodes without 
  change if not matched by a more specific template.-->
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>

  <!--Template matching the root element. I do this to narrow the scope of what's 
  being processed.-->
  <xsl:template match="/*">
    <!--Process tbody.-->
    <xsl:apply-templates select=".//*[@id='financial-data']/div/table/tbody"/>
  </xsl:template>

  <!--Match tr elements so we can add the new td with the table name.-->
  <xsl:template match="tr">
    <!--Output the tr element.-->
    <xsl:copy>
      <!--Process any attributes.-->
      <xsl:apply-templates select="@*"/>
      <!--Create new td element.-->
      <td><xsl:value-of select="$tablename"/></td>
      <!--Process any children of tr.-->
      <xsl:apply-templates/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>

命令行

wget -O - "https://example.com/section-1/table-name/financial-data/" | 
xml tr --html stylesheet.xsl -p tablename="/html/body/div[3]/div/div[1]/div[3]/div[1]/h1"

我能够通过在本地 html 文件上使用 cat 而不是 wget 在本地进行测试。如果您希望我将测试 file/result 添加到我的答案中,请告诉我。

此脚本有效但效率低下;它需要一些编辑:

name_query="html/body/div[3]/div/div[1]/div[3]/div[1]/h1/text()"

# Use xargs to TRIM result.
header=$(wget -O - "https://example.com/section-1/name-1/financial-data/" |
    xmllint --html --xpath "$name_query" - 2>/dev/null |
    xargs)

wget -O - "https://example.com/section-1/name-1/financial-data/" |
    xmllint --html --xpath '//*[@id="financial-data"]/div/table/tbody' - 2>/dev/null |
    xmlstarlet ed --subnode "/tbody/tr" --type elem -n td -v "$header" >> /Applications/parser/output.txt

这提出了两个请求:

  1. 获取名称并将其传递给变量$header
  2. 获取 table 并附加一个子节点 <td>$header</td>

因此,这会将以下内容写入我的 output.txt 文件:

<tbody>

                    <tr class="text-right">
                      <td class="text-left">Sep 08, 2017</td>
                      <td>4605.16</td>     
                      <td>4661.00</td>
                      <td>4075.18</td>
                      <td>4228.75</td>
                      <td>2,700,890,000</td>
                      <td>76,220,200,000</td>
                      <td>Name 1</td>
                    </tr>

                    <tr class="text-right">
                      <td class="text-left">Sep 07, 2017</td>
                      <td>4589.14</td>     
                      <td>4655.04</td>
                      <td>4491.33</td>
                      <td>4599.88</td>
                      <td>1,844,620,000</td>
                      <td>75,945,000,000</td>
                      <td>Name 1</td>
                    </tr>
...
</tbody>

它相对较慢,因为这实际上只需要一个请求就可以完成,但我不知道怎么做。