使用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
相关产品推荐
相关产品推荐

