chore(comment): 精简查找评论相关的注释 (#765)

去掉复述代码和解释推导过程的注释,只留代码本身说明不了的信息:DOM 结构、选择器为什么这么选、两种按钮文案。纯注释改动,无逻辑变更。
This commit is contained in:
zy
2026-07-27 23:52:01 +08:00
committed by GitHub
parent aa93e0add0
commit f4518665bb
3 changed files with 24 additions and 64 deletions
+7 -18
View File
@@ -191,10 +191,7 @@ func (f *CommentFeedAction) ReplyToComment(ctx context.Context, feedID, xsecToke
}
// findCommentElement 滚动查找指定评论:优先按 commentID 命中,否则按 userID 匹配。
// 每轮先在已渲染的评论里查一次,再展开视野里的楼中楼,然后判断是否到底,最后滚动加载更多
//
// 展开这一步是楼中楼回复的前提:二级评论默认折叠在「展开 N 条回复」后面,不点开
// 就压根不在 DOM 里,此时哪怕 commentID 完全正确也只会得到「未找到评论」。
// 二级评论折叠在「展开 N 条回复」后面,未展开时不在 DOM 里,因此查找过程中要展开
func findCommentElement(ctx context.Context, page *rod.Page, commentID, userID string) (*rod.Element, error) {
logrus.Infof("开始查找评论 - commentID: %s, userID: %s", commentID, userID)
@@ -206,26 +203,21 @@ func findCommentElement(ctx context.Context, page *rod.Page, commentID, userID s
expandRounds := 0
deadline := time.Now().Add(maxSearchDuration)
// maxSearchScrolls 数的是「下滚」的次数,也就是这次查找能往下走多远
// 展开楼中楼不计入:它是在当前位置就地把内容摊开,不消耗向下的行程;
// 让它占用预算的话,楼中楼多的热帖反而扫不到几条一级评论。
// 展开那边由 deadline 兜住总时长。
// attempt 只数下滚,展开不计入,由 deadline 收口
for attempt := 0; attempt < maxSearchScrolls; {
// 0. 墙钟兜底。评论区没有固定规模,热帖翻到底可以很久,
// 到点就停,别让单次请求无限拖下去。
if time.Now().After(deadline) {
logrus.Warnf("查找超过 %s,停止", maxSearchDuration)
return nil, fmt.Errorf("评论区过大,%s 内未找到目标评论 (commentID: %s, userID: %s)",
maxSearchDuration, commentID, userID)
}
// 1. 先查已渲染的评论——目标可能一开始就在页面上
// 1. 先查已渲染的评论
if el := lookupComment(page, commentID, userID); el != nil {
logrus.Infof("✓ 找到目标评论(下滚 %d 次)", attempt)
return el, nil
}
// 2. 展开视野里的楼中楼,展开后立刻再查一次
// 2. 展开视野里的楼中楼再查一次
if expandRounds < maxExpandRounds {
if expanded := expandNearbyReplies(ctx, page); expanded > 0 {
expandRounds++
@@ -236,7 +228,6 @@ func findCommentElement(ctx context.Context, page *rod.Page, commentID, userID s
return el, nil
}
// 眼前还有没展开的,先展开完再往下滚,避免滚过去就漏了
continue
}
}
@@ -249,7 +240,7 @@ func findCommentElement(ctx context.Context, page *rod.Page, commentID, userID s
break
}
// 3. 评论数停滞说明加载不动了
// 4. 评论数停滞说明加载不动了
currentCount := getCommentCount(page)
if currentCount != lastCommentCount {
lastCommentCount = currentCount
@@ -262,7 +253,7 @@ func findCommentElement(ctx context.Context, page *rod.Page, commentID, userID s
}
}
// 4. 滚到最后一条评论再继续下滚,触发懒加载
// 5. 滚到最后一条评论再继续下滚,触发懒加载
if currentCount > 0 {
if elements, err := page.Timeout(2 * time.Second).Elements(".comment-item"); err == nil && len(elements) > 0 {
if err := elements[len(elements)-1].ScrollIntoView(); err != nil {
@@ -291,9 +282,7 @@ func lookupComment(page *rod.Page, commentID, userID string) *rod.Element {
return nil
}
// 只认 .comment-item:一级和二级评论都带这个 class,粒度正好是「一条评论」
// 不能带上 .parent-comment——它把整个楼层(含楼中楼)都包在里面,按 userID 找时
// 会先命中外层楼层并返回它,结果回复按钮取到的是楼主那条,等于回错了人。
// 一级和二级评论都带 .comment-item.parent-comment 会把整个楼层一起匹配上
elements, err := page.Timeout(2 * time.Second).Elements(".comment-item")
if err != nil {
return nil
+12 -38
View File
@@ -28,25 +28,10 @@ const (
buttonClickInterval = 3 // 每隔多少次尝试点击一次按钮
finalSprintPushCount = 15
// 查找条评论时最多下滚多少轮。normal 档一轮滚 0.7 屏,另加一次「滚到最后
// 一条评论」的跳转,25 轮覆盖几十屏,远超 get_feed_detail 默认加载的评论数——
// 调用方拿到的 comment_id 基本都落在这个范围内,翻不到多半是参数不对。
//
// 这个上限只管查找。批量拉评论走 get_feed_detail 的 defaultMaxAttempts
// 两者不共用。
maxSearchScrolls = 25
// 查找评论时,最多连续多少轮只展开楼中楼而不下滚。
// 防止一个几百条回复的大楼层把查找预算吃光,导致后面的评论根本没机会加载。
maxExpandRounds = 5
// 查找一条评论的墙钟上限。
//
// 评论区没有固定规模,热帖翻到底可以很久,只靠轮数和停滞检测兜不住
// (实测一条热帖跑满 12 分钟仍未收敛)。用时间兜底而不是评论条数:
// 条数换算不出耗时——展开一个 50 条回复的楼层,条数只涨 50,耗时却是几十秒。
// 实测正常命中在 18~25 秒,留一倍多余量。
maxSearchDuration = 90 * time.Second
// 以下三个只用于查找条评论,与批量加载的 defaultMaxAttempts 不共用
maxSearchScrolls = 25 // 最多下滚轮数
maxExpandRounds = 5 // 最多连续展开而不下滚的轮数
maxSearchDuration = 90 * time.Second // 单次查找的墙钟上限
)
// ========== 数据结构 ==========
@@ -414,12 +399,8 @@ func clickShowMoreButtonsSmart(ctx context.Context, page *rod.Page, maxRepliesTh
return clicked, skipped
}
// expandNearbyReplies 展开视口附近的「展开 N 条回复」,把楼中楼灌进 DOM返回本轮点开的个数。
//
// 与 clickShowMoreButtonsSmart 的差别有两点,都是为「边滚边找某条评论」这个场景准备的:
// - 只点视口附近的。clickElementWithHumanBehavior 会先 ScrollIntoView,若不加过滤,
// 顶部残留的按钮每轮都会把页面拽回去,和向下滚动来回打架。
// - 不按回复数跳过。那个阈值是给批量抓取省时间用的,这里漏掉一个大楼层就等于漏掉目标。
// expandNearbyReplies 展开视口附近的「展开 N 条回复」,返回本轮点开的个数。
// 限定在视口附近,避免 ScrollIntoView 把页面拽回顶部、与向下滚动互相抵消。
func expandNearbyReplies(ctx context.Context, page *rod.Page) int {
elements, err := page.Elements(".show-more")
if err != nil || len(elements) == 0 {
@@ -455,10 +436,7 @@ func expandNearbyReplies(ctx context.Context, page *rod.Page) int {
return clicked
}
// isSafeExpandButton 判断这个 .show-more 是不是真的展开回复按钮。
//
// 这两处的按钮是按 class 扫出来批量点的,不是调用方指定的元素,因此文案和
// 尺寸都要核一遍再点,避免同 class 的其它元素被误点。
// isSafeExpandButton 判断 .show-more 是不是展开回复按钮。
func isSafeExpandButton(el *rod.Element, text string) bool {
if !isExpandRepliesButton(text) {
logrus.Debugf("跳过展开按钮:文案不匹配 %q", text)
@@ -473,18 +451,14 @@ func isSafeExpandButton(el *rod.Element, text string) bool {
return true
}
// expandRepliesTextRegex 匹配展开楼中楼按钮的两种文案
// 大楼层点开一次后文案会从「展开 49 条回复」变成不带数字的「展开更多回复」,
// 只认数字那一种会在大楼层上半途停住。
// 两种文案:「展开 N 条回复」,以及点开一次后不带数字的「展开更多回复」
var expandRepliesTextRegex = regexp.MustCompile(`^展开\s*(\d+\s*条|更多)回复$`)
func isExpandRepliesButton(text string) bool {
return expandRepliesTextRegex.MatchString(strings.TrimSpace(text))
}
// hasReadableSize 判断元素尺寸是否达到正常按钮的量级。
// 展开按钮实测约 279x32;1x1 之类的元素同样有布局盒子、过得了可见性检查,
// 靠尺寸把它们排除掉。
// hasReadableSize 判断元素尺寸是否达到按钮的量级。
func hasReadableSize(el *rod.Element) bool {
const minWidth, minHeight = 24, 10
@@ -497,15 +471,15 @@ func hasReadableSize(el *rod.Element) bool {
return q[4]-q[0] >= minWidth && q[5]-q[1] >= minHeight
}
// isNearViewport 判断元素是否落在视口上下各一屏的范围内。
// 放宽到一屏是为了留重叠:滚动后刚划出去的按钮下一轮还能被捡来。
// isNearViewport 判断元素是否落在视口上下各一屏的范围内。上下各留一屏是为了重叠,
// 滚动后刚划出去的元素下一轮还能被捡来。
func isNearViewport(page *rod.Page, el *rod.Element) bool {
shape, err := el.Shape()
if err != nil || len(shape.Quads) == 0 {
return false
}
// CDP 给的 quads 是相对视口的 CSS 像素,取左上角的 y 即可。
// quads 是相对视口的 CSS 像素
top := shape.Quads[0][1]
height := float64(page.MustEval(`() => window.innerHeight`).Int())
+5 -8
View File
@@ -6,10 +6,7 @@ import (
"github.com/stretchr/testify/assert"
)
// TestIsExpandRepliesButton 只有读得出「展开…回复」的按钮才允许自动点
//
// 两种文案都必须认:大楼层点开一次后,「展开 49 条回复」会变成不带数字的
// 「展开更多回复」,只匹配数字那一种会在大楼层上半途停住。
// TestIsExpandRepliesButton 两种文案都要认:带数字的,以及点开一次后不带数字的
func TestIsExpandRepliesButton(t *testing.T) {
accepted := []string{
"展开 49 条回复",
@@ -18,7 +15,7 @@ func TestIsExpandRepliesButton(t *testing.T) {
"展开更多回复",
}
for _, text := range accepted {
assert.True(t, isExpandRepliesButton(text), "应认得: %q", text)
assert.True(t, isExpandRepliesButton(text), "应匹配: %q", text)
}
rejected := []string{
@@ -26,12 +23,12 @@ func TestIsExpandRepliesButton(t *testing.T) {
"收起",
"展开",
"查看全部评论",
"展开 2 条回复并关注", // 多余后缀,不是这个按钮
"点击领取优惠券", // 同 class 的其它元素
"展开 2 条回复并关注",
"点击领取优惠券",
"展开 abc 条回复",
}
for _, text := range rejected {
assert.False(t, isExpandRepliesButton(text), "不该认: %q", text)
assert.False(t, isExpandRepliesButton(text), "不应匹配: %q", text)
}
}