About

创建一个 R 脚本,用于从 GitHub Issue #1 中提取回复,整理为表格,并对内容中的链接生成网页截图。

准备工作

首先,我们需要安装并加载一些必要的R包,包括 ghwebshot2dplyrstringrtibblehttrjsonlite。如果你尚未安装这些包,可以运行以下代码来安装它们:

# 安装必要的包(如果尚未安装)
if (!require("gh", quietly = TRUE)) install.packages("gh")
if (!require("webshot2", quietly = TRUE)) install.packages("webshot2")
if (!require("dplyr", quietly = TRUE)) install.packages("dplyr")
if (!require("stringr", quietly = TRUE)) install.packages("stringr")
if (!require("tibble", quietly = TRUE)) install.packages("tibble")
if (!require("httr", quietly = TRUE)) install.packages("httr")
if (!require("jsonlite", quietly = TRUE)) install.packages("jsonlite")

# 加载包
library(gh)
library(webshot2)
library(dplyr)
library(stringr)
library(tibble)
library(httr)
library(jsonlite)

# 创建webshot文件夹(如果不存在)
if (!dir.exists("www/webshot")) {
  dir.create("www/webshot", recursive = TRUE)
}

这些包的作用如下:

  • gh:用于访问 GitHub API
  • webshot2:用于生成网页截图
  • dplyr:用于数据处理
  • stringr:用于字符串处理
  • tibble:用于创建数据框
  • httr:用于发起 HTTP 请求
  • jsonlite:用于处理 JSON 数据

获取评论

接下来,我们将使用 GitHub API 获取指定仓库的 Issue #1 的所有评论。

首先,将以下代码中的 repo_ownerrepo_nameissue_number 替换为要爬取的仓库和 Issue 编号:

# 指定仓库和Issue编号
# 请替换为你需要爬取的仓库
repo_owner <- "D2RS-2025spring"
repo_name <- "myHomePages"
issue_number <- 1

# 使用GitHub API获取issue评论
issue_comments <- gh::gh(
  "GET /repos/{owner}/{repo}/issues/{issue_number}/comments",
  owner = repo_owner,
  repo = repo_name,
  issue_number = issue_number,
  .limit = Inf
)

在这里使用 .limit = Inf 来获取所有评论,如果评论数量较多,可能需要等待一段时间。

打印原始 Issue 内容

在获取 ISSUE comments 的方法中,并不会包含第一条的评论(即 Issue 的标题和内容),所以我们需要单独获取 Issue 的标题和内容,打印原始 Issue 的标题和内容,以便了解 Issue 的背景信息。

# 获取原始issue内容
issue_data <- gh::gh(
  "GET /repos/{owner}/{repo}/issues/{issue_number}",
  owner = repo_owner,
  repo = repo_name,
  issue_number = issue_number
)

cli::cat_rule("原始Issue内容")
cat(issue_data$title, "\n")
cat(issue_data$body, "\n")

创建辅助函数

get() 函数用于从评论内容中提取匹配的内容。如果没有匹配,会打印警告;如果有多个匹配,会保留第一个;如果只有一个匹配,直接返回。

get_id() 这里使用的正则表达式为 (?<!\\d)\\d{13}(?!\\d),可以匹配 13 位数字,前后不是数字的情况。其中 (?<!\\d) 表示前面不是数字,\\d{13} 表示匹配 13 位数字,(?!\\d) 表示后面不是数字。

get_link() 这里使用的正则表达式为 https?://[^\\s()<>\"\\[\\]]+,可以匹配直接 URL。其中 https?:// 表示匹配 http://https://[^\\s()<>\"\\[\\]]+ 表示匹配除空格、括号、尖括号、引号和方括号之外的字符。

create_webshot() 用于生成网页截图,如果文件已经存在且大小大于 1 kb,则跳过。如果文件不存在或大小小于 1 kb,则尝试生成网页截图。如果生成成功,则打印成功信息;如果生成失败,则打印失败信息。

get = function(content, pattern){
    matches <- str_extract_all(content, pattern)[[1]]
    matches <- unique(matches)
    match <- NULL
    if (length(matches) < 1) {
        # 如果没有匹配,打印警告
        warning(glue::glue("{content} 中未检测到: {pattern}"))
    } else if (length(matches) > 1) {
        # 如果有多个匹配,只取第一个
        match <- matches[1]
        message(glue::glue("{content} 中检测到多个匹配: `{pattern}`。保留第一个。"))
    } else {
        # 如果只有一个匹配,直接取出
        match = matches[1]
    }
    return(match)
}

get_id = function(content){
    get(content, pattern = "(?<!\\d)\\d{13}(?!\\d)")
}

get_link = function(content){
    get(content, pattern = "https?://[^\\s()<>\"\\[\\]]+")
}

create_webshot = function(link, verbose = TRUE) {
    # valid link
    if (is.null(link) || !str_detect(link, "^https?://")) {
        warning(glue::glue("Invalid link: {link}"))
        return(NULL)
    }

    # 创建一个安全的文件名
    safe_filename <- paste0("www/webshot/", 
                            str_replace_all(gsub("https?://", "", link), "[^a-zA-Z0-9]", "_"), 
                            ".png")
    
    # 如果文件已经存在且大小大于 1 kb,跳过
    if (file.exists(safe_filename) & file.size(safe_filename) > 1000) {
        if (verbose) message(sprintf("跳过截图: %s (文件已存在)\n", link))
    } else {
        # 尝试截图
        tryCatch({
            webshot2::webshot(url = link, 
                                file = safe_filename, 
                                delay = 5)
            if (verbose) message(sprintf("成功截图: %s\n", link))
        }, error = function(e) {
            if (verbose) warning(sprintf("截图失败: %s, 错误: %s\n", link, e$message))
        })
    }
    
    safe_filename = gsub("www/", "", safe_filename)
    return(safe_filename)
}

整理评论

利用 lapply 函数将评论整理为一个列表,然后使用 bind_rows 函数将列表转换为 tibble,方便后续处理。

# 创建一个列表来存储评论信息
comments_list = lapply(issue_comments, function(comment) {
    # 提取评论信息
    author = comment$user$login
    content = comment$body
    content_url = comment$html_url
    time = comment$created_at
    id = get_id(comment$body)
    link = get_link(comment$body)
    screenshot_path = create_webshot(link, verbose = FALSE)

    # 返回一个 tibble
    tibble(
        author = author,
        content = content,
        content_url = content_url,
        time = time,
        id = id,
        link = link,
        screenshot_path = screenshot_path
    )
})

# 将列表转换为 tibble
comments_df = bind_rows(comments_list)

comments_df 是一个数据框,包含了评论的作者、内容、评论链接和时间。

保存结果

最后,将整理好的评论数据保存为CSV文件,并输出处理结果。对于存在多个链接的评论(),将多个链接用分号分隔,合并为一个评论。

# 保存结果到CSV
write.csv(comments_df, "github_issue_comments.csv", row.names = FALSE, fileEncoding = "UTF-8")

# 输出结果
cat("处理完成! 共处理了", nrow(comments_df), "条评论\n")
cat("结果已保存到 github_issue_comments.csv\n")
print(comments_df)

小结

这个 R 脚本会完成以下任务:

  • 安装并加载必要的R包
  • 创建一个 webshot 文件夹用于保存网页截图
  • 从 GitHub API 获取 Issue #1 的内容和所有评论
  • 将评论整理成一个表格,包含 authorcontenttime 列等
  • 从评论内容中提取所有链接和学号
  • 使用 webshot2 包访问链接并生成网页截图
  • 将截图保存在 webshot 文件夹中
  • 将截图的文件路径添加到表格中
  • 将结果保存为CSV文件

附加

利用md5检测当前目录下面的 png 文件,删掉重复的。然后将 png 文件重命名,去掉文件名开始的 \d+_\d+_

可以使用 R 处理当前目录下的 PNG 文件,完成以下任务:

  1. 计算 MD5 哈希值并删除重复的 PNG 文件
  2. 重命名 PNG 文件,去掉文件名开头的 \d+_\d+_

以下是 R 代码:

library(digest)

# 计算文件 MD5 哈希值
get_md5 = function(file_path) {
  digest(file_path, algo = "md5", file = TRUE)
}

# 删除重复的 PNG 文件
remove_duplicates = function(directory) {
  files = list.files(directory, pattern = "\\.png$", full.names = TRUE, ignore.case = TRUE)
  seen_hashes = list()
  
  for (file in files) {
    file_hash = get_md5(file)
    
    if (file_hash %in% names(seen_hashes)) {
      message("删除重复文件: ", file)
      file.remove(file)
    } else {
      seen_hashes[[file_hash]] = file
    }
  }
}

# 重命名 PNG 文件,去掉开头的 \d+_\d+_
rename_files = function(directory) {
  files = list.files(directory, pattern = "\\.png$", full.names = TRUE, ignore.case = TRUE)
  
  for (file in files) {
    filename = basename(file)
    new_name = sub("^\\d+_\\d+_", "", filename)
    
    if (new_name != filename) {
      new_path = file.path(dirname(file), new_name)
      message("重命名: ", file, " -> ", new_path)
      file.rename(file, new_path)
    }
  }
}

# 运行
current_directory = "webshot"
remove_duplicates(current_directory)
rename_files(current_directory)

说明:

  • get_md5(file_path): 计算 PNG 文件的 MD5 哈希值。
  • remove_duplicates(directory): 通过 MD5 识别重复 PNG 文件并删除。
  • rename_files(directory): 通过正则表达式 ^\\d+_\\d+_ 处理文件名,去掉前缀。