和Rob一样,我早就想放弃使用了十多年的 Disqus。我知道Maëlle Salmon在 2019 年(以及Nan Xiao在 2020 年)的作品,但当时犹豫了。与 Maëlle 不同,删除 Disqus 对我来说在情感上和技术上都很难,因为我在 Disqus 中有超过 10K 条评论,其中我有很多过去的回忆!
我在 Disqus 上遇到的问题
今年 9 月,我看到了Rob 关于从 Disqus 迁移到 Giscus 的帖子。似乎 Mitch O’Hara-Wild 已经完全自动化了提取 Disqus 评论并将它们重新发布到 Github 讨论的工作,这样我们就可以使用 Giscus,这对我来说听起来很棒,但同样,我网站上的 Disqus 评论数量很大。我知道 Github 有一些速率限制,我不知道发布 10K 条评论需要多长时间。另外,我还在犹豫另外两件事:
-
我不喜欢所有访客评论都必须使用我自己的 Github 帐户发布的事实,尽管每个 Github 评论中都有一个标题行,上面写着“此评论最初由 […] 在 […] 上发布”。如果读者不仔细看,他们可能会觉得我在自己的网站上自言自语了 17 年:) 更重要的是,如果有人回复他们的评论,将不再通知原作者。他们也没有一种简单的方法来查看他们的评论历史记录(Disqus 允许您在一个页面上查看所有过去的评论)。
-
我也感到有点难过,评论的原始时间戳无法保留,只能写在标题注释中。读者可能会看到 Github 上说是一周前发布的评论,但实际上是十年前发布的。
这两个问题都没有解决方案。您不能在 Github 上代表他人发表评论,也不能修改评论的时间戳。然而,我确实想出了一个补救方法 #1:我注册了一个 Github 帐户@giscus-bot
来发表客人评论,并使用我的个人帐户发表我自己的评论。然后整个评论线程看起来像这样(一个真实的例子):
@giscus-bot 3 days ago Guest *John Doe* @ 2012-03-02 08:38:03 originally posted: Hi Yihui! --------------------------------------------------------- @yihui 2 days ago Hi John! > Originally posted on 2012-03-02 20:28:48
为了进一步补救#1,我在一些朋友的评论中添加了@username
,我知道他们的 Github 用户名和 Disqus 名称。这意味着当有人将来回复他们的评论时,他们会收到通知。然而,这也意味着在迁移到 Github 讨论的过程中,这些朋友可能会收到大量的 Github 通知,具体取决于他们之前在我的网站上留下的评论数量。我告诉他们中的一些人,我们的友谊越亲密,这次你们从我这里收到的“垃圾邮件”就越多。后来,在我没有提前通知的人中,有两个告诉我他们对突然涌入的数十封电子邮件通知感到惊讶,另一个告诉我他有几千封……对不起!我希望我们的友谊经得起这次压力测试。
我也借此机会以编程方式清理了一些 Disqus 评论。例如,中国读者喜欢用~~
来表达可爱,但这恰好符合三振出局在 Github 的 Markdown 中。我用全角~~
代替了它们。另一个例子是 Disqus 缩短了长的裸链接,我把它们扩展回来。
话语问题
我也有一些话语评论。迁移它们要容易得多,因为 Github 允许我们将 Github Issues(话语所基于的)转换为 Github Discussions。然而,我的网站上还有另一个问题:我同时启用了 Utterances 和 Disqus,并从两个系统获得了对相同帖子的评论。我不得不合并这些评论。 Rob 的脚本仅用于创建新的 Github 讨论,因此我修改了它以检查是否存在讨论(从 Github 问题迁移)并在可能的情况下将 Disqus 评论发布到现有讨论中。
要发布到现有讨论,您需要知道其 ID。以下是我如何获得所有现有讨论的数据框(您需要先设置 Github 令牌,例如,在环境变量GITHUB_PAT
):
get_discussions = function(owner, repo) { has_next = TRUE next_cursor = NULL info = NULL while (has_next) { next_cursor = if (is.null(next_cursor)) '' else { paste0(', after: "', next_cursor, '"') } query = gh::gh_gql(sprintf('query FindRepo { repository(owner: "%s", name: "%s") { discussions(first: 100%s) { pageInfo { hasNextPage endCursor } edges { node { title body id } } } } }', owner, repo, next_cursor )) res = query$data$repository$discussions has_next = res$pageInfo$hasNextPage next_cursor = res$pageInfo$endCursor info = c(info, lapply(res$edges, function(x) unlist(x$node))) } info = do.call(rbind, info) as.data.frame(info) } # fetch all discussions from the repo yihui/yihui.org discussions = get_discussions('yihui', 'yihui.org')
在 Rob 的脚本中,我添加了一个检查以查看是否存在讨论以仅有条件地创建新讨论。
处理 Github GraphQL 中的字符转义
向 Github 讨论区发表评论的核心技术是GraphQL 。我以前并不熟悉它,但它看起来很容易学习(至少对于一些简单的任务,如查询或更新讨论)。以下是查询速率限制的示例:
gh::gh_gql('query { viewer { login } rateLimit { limit cost remaining resetAt } }')
刚开始有几次报错,后来发现有些特殊字符需要适当转义。为了避免疯狂的反斜杠(即,考虑我在gsub('"', '\\\\"', x)
中真正需要多少反斜杠,这会造成很大伤害),我发现使用jsonlite更容易,例如,
str_json = function(x) { jsonlite::toJSON(x, auto_unbox = TRUE) } str_json('A title containing "double quotes"')
然后将结果传递给gh::gh_gql()
。它保证是有效的 GraphQL 语法。例如,如果您想更新讨论的标题:
gh::gh_gql(sprintf('mutation { updateDiscussion(input: {discussionId: "%s", title: %s}) { discussion { id } } }', id, str_json(title)))
Giscus的严格匹配
话语让我很困扰的一件事是模糊匹配。我去年年初发送了一个 pull request ,但它似乎被忽略了。该问题可能导致评论被加载到错误的页面下。 Giscus 作为 Utterances 的继承者,提供了一个非常聪明的选项来解决这个问题: data-strict="1"
(干得好, Sage \@laymonage !)。
Github 不提供搜索讨论的严格匹配,但 Giscus 巧妙的方法使之成为可能。简而言之,当使用严格方法时,Giscus 搜索搜索词的哈希而不是直接搜索词。这很好地解决了我的问题:我更喜欢使用页面URL的pathname
作为搜索词,但如果直接搜索pathname
会很模糊。搜索pathname
的 SHA-1 散列会给出更准确的结果,这几乎可以保证网页和 Github 讨论之间的一对一映射。没有更多的模糊。
要启用data-strict
,我必须在创建它们时将 URL pathname
的 SHA-1 哈希附加到 Github 讨论中。哈希可以通过digest::digest()
计算:
sha1 = function(x) { digest::digest(x, 'sha1', serialize = FALSE) } sprintf('<!-- sha1: %s -->', sha1(pathname))
一定不要避免 HTML 转义,因为那样会转义<!-- -->
注释。如果您在 Rob 的 R 脚本中使用whisker::whisker.render()
,请使用}
而不是 。
其他小东西
我有很多用中文写的评论。为了将它们重新发布到 Github,我添加了中文标题注释,并测试了我使用的(常见)汉字:
has_chinese = function(x) { length(grep('[\u4E00-\u9FFF]', x)) > 0 }
要使用不同的 Github 帐户发布(例如,在我的例子中是giscus-bot
),您可以使用gh::gh_gql()
的.token
参数,例如,
gh::gh_gql(..., .token = 'ghp_xxxxxx')
当通过 Pandoc 将 Disqus 的 HTML 注释转换为 Markdown 时,我强烈建议使用带有选项--wrap=none
的gfm
输出格式。
rmarkdown::pandoc_convert( input = msg_html, from = "html", output = msg_md <- tempfile(fileext = ".md"), to = "gfm", options = '--wrap=none' )
在 Rob 的脚本中, to = "markdown"
不是一个很好的选择(例如,它会导致很多不必要的转义),而gfm
对于 Github 来说是一个更自然的选择。选项--wrap=none
也很关键,因为 Pandoc 默认会换行。不幸的是,Github 将 Markdown 中的换行符视为硬换行符(即<br/>
)。
for
循环
Mitch 使用for
循环将讨论一一发布。 for
循环似乎在 R 社区中声名狼藉(例如, for
循环丑陋且缓慢),但我认为有时这只是谣言或误解。我会把它留到以后的另一篇文章中。在这里我觉得for
循环是不可或缺的,其实也是极其有价值的。为什么?因为你永远不知道你会在循环中遇到什么样的错误(GraphQL 语法错误、网络问题等等)。如果确实遇到意外错误,恢复for
循环非常简单:您只需从当前步骤索引开始,而不是从头开始(通常是1
)。
一个快速而愚蠢的例子来说明这一点:
# take the square root of each element of a list elements = list(1, 2, 3, '4', 5, 6) roots = numeric(length(elements)) for (i in seq_along(elements)) { roots[i] = sqrt(elements[[i]]) }
假设您不知道数据中有一个字符值。当你运行循环时,你会遇到一个错误。未恐慌。现在您解决问题并检查i
的值。在你知道i
当前是4
之后,你从 4 而不是 1 重新开始循环:
elements[[i]] = 4 for (i in 4:length(elements)) { roots[i] = sqrt(elements[[i]]) }
这意味着您不需要为i = 1, 2, 3
重复计算。当计算成本相对较高时,这可以节省大量时间。我不记得在评论的迁移过程中我做了多少次。
while()
循环与browser()
我不能为此称赞米奇:
while (!is.null((out <- gh::gh_gql(query))$errors)) { if (out$errors[[1]]$message == "was submitted too quickly") { Sys.sleep(60) } else { # Unknown error, debug interactively browser() } }
它还让我有机会检查错误并在修复意外问题后恢复循环。
概括
我很高兴终于可以和 Disqus 说再见了。它为我服务了十多年,对此我心存感激,但我再也无法忍受它的重量和跟踪。
特别感谢 Rob、Maëlle 和 Mitch 提供的 R 代码,它们为我节省了无数时间!很抱歉我没有在这篇文章中分享我的完整脚本。虽然我相信 Rob 会授予我修改和发布代码的权限,但我的代码太乱了,也可能令人困惑。老实说,它不再是一个脚本了——我的 RStudio 编辑器中有几个Untitled-N*
脚本。你不会想读它们的。对于大多数人来说,我认为 Rob 的原始脚本就足够了。我的情况太复杂了。
知道我可以通过 Github GraphQL 以编程方式操纵(当然不是为了作恶)评论是我将评论转移到 Github 的强烈动机。我已经利用它来批量修改讨论标题(使它们更清晰,而不是只有pathname
)。
最后,我特别感谢这 17 年来在我的网站上发表评论的人(如果有兴趣,您可以在 Github 上查看所有评论)。我注意到社交媒体流行后评论数量明显下降,但我已经交到了足够多的老朋友。从现在开始,请随时使用您的 Github 帐户登录 Giscus,以便在底部发表评论。下一个17年见!