通过 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 ed
。 ed
好像不支持 --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
这提出了两个请求:
- 获取名称并将其传递给变量
$header
- 获取 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>
它相对较慢,因为这实际上只需要一个请求就可以完成,但我不知道怎么做。
我想从 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 ed
。 ed
好像不支持 --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
这提出了两个请求:
- 获取名称并将其传递给变量
$header
- 获取 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>
它相对较慢,因为这实际上只需要一个请求就可以完成,但我不知道怎么做。