You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用R与chromote提取JS渲染后的纯HTML(tinytable场景)

提取tinytable生成的JS动态样式后的纯HTML

我用tinytable包生成HTML表格,这类表格通过JavaScript动态为单元格应用CSS样式,目标是提取所有样式应用完成后的纯HTML。

示例代码

library(tinytable)
tt(mtcars[1:4, 1:4]) |> 
    style_tt(j = 1:2, background = "teal", color = "white") |>
    save_tt("example.html", overwrite = TRUE)

生成的example.html通过JS应用颜色样式,我需要将其转换为包含样式的纯HTML。

已尝试的方案(存在超时问题)

我曾尝试将HTML保存到临时文件,用servr搭建服务,再通过chromote无头浏览器提取,但一直遇到超时问题,代码如下:

library(servr)
library(chromote)
library(tinytable)

serve_and_strip <- function(filename) {
    fn <- file.path(tempdir(), "index.html")
    file.copy(filename, fn, overwrite = TRUE)
    srv <- servr::httd(tempdir())
    url <- file.path(srv$url, "index.html")
    b <- ChromoteSession$new()
    b$Page$navigate(url)
    tab <- b$Runtime$evaluate("document.querySelector('table').outerHTML")$result$value
    sty <- b$Runtime$evaluate("document.querySelector('style').outerHTML")$result$value
    out <- list(tab, sty)
    b$close()
    servr::daemon_stop(srv$daemon)
    return(out)
}

serve_and_strip("example.html")

补充说明

直接爬取原始HTML时,第一个单元格为<td>21.0</td>,但在浏览器中渲染后会变为<td class="tinytable_css_n9oxlmixvkthzx38wcrd">21.0</td>——这是因为JS为单元格添加了类信息,我需要获取JS执行后的HTML和CSS代码,因此最初选择无头浏览器方案。

转换前的原始HTML代码

<!DOCTYPE html> 
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>tinytable_xg1s2bqenh3yuyr9x2mg</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
.table td.tinytable_css_u43qs0b5ucz8ik5jm341, .table th.tinytable_css_u43qs0b5ucz8ik5jm341 {    border-bottom: solid 0.1em #d3d8dc; }
.table td.tinytable_css_4rjvz3zmw0n1t4i0jlbe, .table th.tinytable_css_4rjvz3zmw0n1t4i0jlbe {    color: white; background-color: teal; }
    </style>
    <script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
    <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
    <script>
    MathJax = {
      tex: {
        inlineMath: [['$', '$'], ['\\(', '\\)']]
      },
      svg: {
        fontCache: 'global'
      }
    };
    </script>
  </head>

  <body>
    <div class="container">
      <table class="table table-borderless" id="tinytable_xg1s2bqenh3yuyr9x2mg" style="width: auto; margin-left: auto; margin-right: auto;" data-quarto-disable-processing='true'>
        <thead>
        
              <tr>
                <th scope="col">mpg</th>
                <th scope="col">cyl</th>
                <th scope="col">disp</th>
                <th scope="col">hp</th>
              </tr>
        </thead>
        
        <tbody>
                <tr>
                  <td>21.0</td>
                  <td>6</td>
                  <td>160</td>
                  <td>110</td>
                </tr>
                <tr>
                  <td>21.0</td>
                  <td>6</td>
                  <td>160</td>
                  <td>110</td>
                </tr>
                <tr>
                  <td>22.8</td>
                  <td>4</td>
                  <td>108</td>
                  <td> 93</td>
                </tr>
                <tr>
                  <td>21.4</td>
                  <td>6</td>
                  <td>258</td>
                  <td>110</td>
                </tr>
        </tbody>
      </table>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
    <script>
      function styleCell_tinytable_pii6zht3qrjzjy0n9jwp(i, j, css_id) {
        var table = document.getElementById("tinytable_xg1s2bqenh3yuyr9x2mg");
        table.rows[i].cells[j].classList.add(css_id);
      }
      function insertSpanRow(i, colspan, content) {
        var table = document.getElementById('tinytable_xg1s2bqenh3yuyr9x2mg');
        var newRow = table.insertRow(i);
        var newCell = newRow.insertCell(0);
        newCell.setAttribute("colspan", colspan);
        // newCell.innerText = content;
        // this may be unsafe, but innerText does not interpret <br>
        newCell.innerHTML = content;
      }
      function spanCell_tinytable_pii6zht3qrjzjy0n9jwp(i, j, rowspan, colspan) {
        var table = document.getElementById("tinytable_xg1s2bqenh3yuyr9x2mg");
        const targetRow = table.rows[i];
        const targetCell = targetRow.cells[j];
        for (let r = 0; r < rowspan; r++) {
          // Only start deleting cells to the right for the first row (r == 0)
          if (r === 0) {
            // Delete cells to the right of the target cell in the first row
            for (let c = colspan - 1; c > 0; c--) {
              if (table.rows[i + r].cells[j + c]) {
                table.rows[i + r].deleteCell(j + c);
              }
            }
          }
          // For rows below the first, delete starting from the target column
          if (r > 0) {
            for (let c = colspan - 1; c >= 0; c--) {
              if (table.rows[i + r] && table.rows[i + r].cells[j]) {
                table.rows[i + r].deleteCell(j);
              }
            }
          }
        }
        // Set rowspan and colspan of the target cell
        targetCell.rowSpan = rowspan;
        targetCell.colSpan = colspan;
      }

window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(0, 0, 'tinytable_css_u43qs0b5ucz8ik5jm341') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(0, 1, 'tinytable_css_u43qs0b5ucz8ik5jm341') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(0, 2, 'tinytable_css_u43qs0b5ucz8ik5jm341') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(0, 3, 'tinytable_css_u43qs0b5ucz8ik5jm341') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(0, 0, 'tinytable_css_4rjvz3zmw0n1t4i0jlbe') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(0, 1, 'tinytable_css_4rjvz3zmw0n1t4i0jlbe') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(1, 0, 'tinytable_css_4rjvz3zmw0n1t4i0jlbe') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(1, 1, 'tinytable_css_4rjvz3zmw0n1t4i0jlbe') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(2, 0, 'tinytable_css_4rjvz3zmw0n1t4i0jlbe') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(2, 1, 'tinytable_css_4rjvz3zmw0n1t4i0jlbe') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(3, 0, 'tinytable_css_4rjvz3zmw0n1t4i0jlbe') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(3, 1, 'tinytable_css_4rjvz3zmw0n1t4i0jlbe') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(4, 0, 'tinytable_css_4rjvz3zmw0n1t4i0jlbe') })
window.addEventListener('load', function () { styleCell_tinytable_pii6zht3qrjzjy0n9jwp(4, 1, 'tinytable_css_4rjvz3zmw0n1t4i0jlbe') })
    </script>

  </body>

</html>

替代方案

方案1:用webdriver包替代chromote,优化等待逻辑

webdriver包对浏览器控制的稳定性更好,且可以明确等待页面加载完成,避免超时:

library(webdriver)
library(tinytable)

extract_styled_html <- function(filename) {
  # 启动Chrome驱动
  driver <- run_chrome()
  on.exit(driver$quit())
  
  # 加载本地HTML文件
  driver$navigate(paste0("file://", normalizePath(filename)))
  
  # 等待页面完全加载(确保JS执行完毕)
  driver$wait_for("document.readyState === 'complete'")
  
  # 获取处理后的表格和样式
  table_html <- driver$execute_script("return document.querySelector('table').outerHTML")
  style_html <- driver$execute_script("return document.querySelector('style').outerHTML")
  
  return(list(table = table_html, style = style_html))
}

# 调用函数
result <- extract_styled_html("example.html")
cat(result$table, "\n", result$style)

方案2:直接解析JS逻辑修改HTML(无需浏览器,效率最高)

观察tinytable生成的JS代码,所有样式应用逻辑都是通过styleCell_xxx函数给指定行、列的单元格添加CSS类。我们可以直接解析这些调用参数,手动修改HTML,完全不需要启动浏览器:

library(xml2)
library(stringr)

process_tinytable_html <- function(filename) {
  # 读取HTML文件
  html <- read_html(filename)
  
  # 提取style标签内容
  style_content <- xml_text(xml_find_first(html, "//style"))
  
  # 提取所有styleCell调用语句
  script_text <- xml_text(xml_find_last(html, "//script"))
  style_calls <- str_extract_all(script_text, "styleCell_tinytable_[^\\(]+\\((\\d+), (\\d+), '([^']+)'\\)")[[1]]
  
  # 遍历每个调用,给对应单元格添加类
  for (call in style_calls) {
    # 解析行、列索引和CSS类名
    match_result <- str_match(call, "styleCell_tinytable_[^\\(]+\\((\\d+), (\\d+), '([^']+)'\\)")
    js_row_idx <- as.integer(match_result[2])
    js_col_idx <- as.integer(match_result[3])
    css_class <- match_result[4]
    
    # xml2的索引从1开始,JS从0开始,需要转换
    xml_row_idx <- js_row_idx + 1
    xml_col_idx <- js_col_idx + 1
    
    # 找到目标单元格(包括th和td)
    cell <- xml_find_first(html, sprintf("//table//tr[%d]/td[%d] | //table//tr[%d]/th[%d]", 
                                         xml_row_idx, xml_col_idx, xml_row_idx, xml_col_idx))
    
    if (!is.na(cell)) {
      # 合并现有类和新类
      current_classes <- xml_attr(cell, "class")
      new_classes <- if (is.na(current_classes)) css_class else paste(current_classes, css_class)
      xml_set_attr(cell, "class", new_classes)
    }
  }
  
  # 生成最终的表格HTML和样式HTML
  table_html <- as.character(xml_find_first(html, "//table"))
  style_html <- paste0("<style>\n", style_content, "\n</style>")
  
  return(list(table = table_html, style = style_html))
}

# 调用函数
result <- process_tinytable_html("example.html")
cat(result$table, "\n", result$style)

这个方案的优势是速度极快,没有浏览器启动和加载的开销,也不会出现超时问题,完全适配tinytable的样式生成逻辑。

内容的提问来源于stack exchange,提问作者Vincent

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 23:34:51