RSS Can:使用 Golang Rod 解析浏览器中动态渲染的内容:(四)

news2024/11/26 20:45:40

第四篇文章,来聊聊 Golang 生态中如何“遥控”浏览器,更简单、可靠的使用基于 CDP (Chrome DevTools Protocol)协议的浏览器作为容器,获取诸如微博、B 站 这类动态渲染内容信息,将它们转换为 RSS 订阅源。

写在前面

前三篇文章中,我们从零到一实现了一个能够将网站信息转换为 RSS 订阅源的小工具雏形。

不过截止上一篇文章《RSS Can:将网站信息流转换为 RSS 订阅源(三)》,工具还只能处理传统的由服务器生成的内容。现如今,越来越多的网站的内容是由浏览器动态生成的,为了支持更广泛的信息获取,我们就需要借助 go-rod/Rod 这类可以通过 CDP(Chrome DevTools Protocol) 协议“遥控”浏览器(包括无头浏览器)的能力啦。

RSS Can(RSS 罐头)的相关代码已经开源在soulteary/RSS-Can。

项目中的代码,将会伴随文章更新而更新,如果你觉得项目有趣,欢迎“一键三连”。当然,如果你觉得这个事情有价值,也有趣,也欢迎加入项目,一起折腾。

如果你接触过 “CDP” 相关的项目,你或许会好奇,我为什么会选择 “Rod” 这个项目作为组件之一。

聊聊 CDP 相关的项目

提起能够调用浏览器进行自动化操作的 CDP 项目,最出名的三个项目都是 JavaScript 生态中的,分别是:puppeteer/puppeteer(81k stars)、microsoft/playwright(45k stars)、cypress-io/cypress(42k stars)。之前的文章里,也有提起过它们:《Playwright 简明入门教程:录制自动化测试用例,结合 Docker 使用》、《使用 Docker 和 Node 搭建公式渲染服务(后篇)》、《使用 Node.js 生成方便传播的图片》。

但是,在“高效解析动态渲染的网页信息”的场景下,这几个软件就不是那么合适了:

  1. 性能不够好,不论是针对 CDP 消息的大量编解码消耗,还是本身 Node 相比较 Golang 在拼执行时的稍逊一筹(即使生态非常好,语言灵活性也非常高)。
  2. 更偏重测试的定位,所以软件的复杂度会高很多。
  3. Golang 的类型安全,在编译产物的时候可以被保障。

类似的项目还有 Java 生态大名鼎鼎的SeleniumHQ/selenium(25k stars)等,相比较 Node 生态的三巨头,selenium 对于 CDP 的完整支持其实并没有想象中那么好,即使它非常老牌。

简单聊完 JavaScript 和 Java 生态的 CDP 工具后,将实现收回 Golang 生态,选择其实真的不多。相比较目前只有 3k Stars 的 go-rod/rod,拥有 8k Stars 的chromedp/chromedp可能是多数人的选择。虽然 chromedp 项目的示例更完备,但是代码书写的友好度其实没有 rod 好,其次组件灵活组合的能力 rod 也更好一些,最后,关于项目质量(可靠性)我也有一些疑问,这一点和 rod 文档里提出的有一部分是一致的:Crash 后好像进程没有清理掉。

如果我想直接使用 Golang 调用 Chrome ,恰好 chromedp 有现成的例子,我可能会直接用 chromedp。但如果我想做一个稳定的服务,我会选择更小巧、灵活、简单的 rod。

CSR (客户端)方式渲染的网页

之前的三篇文章中,我们使用的例子是静态生成内容的网站,在这里发挥不出 Rod 的神奇作用,所以我们将需要转换信息为 RSS 订阅源网站地址换成 B 站。

使用前端程序动态渲染的网页内容

虽然我们还是可以和第一篇文章《使用 Golang 实现更好的 RSS Hub 服务(一)》中一样,使用相同的方式获取存放了有效信息的 HTML 标签的路径。但是,查看网页源文件,可以看到信息流内的东西并不存在于网页的“源代码”里。这是因为上图中的内容列表中的内容,是在网页加载所有前端程序(js、wasm)之后,在请求服务端生成的。

想要解决这个问题,一般有两种方案:

  1. 解析逻辑,或者跟踪调试工具中展示的网络请求,直接获取接口中的信息。
  2. 用本文提到的 CDP 相关工具,模拟正常访问,然后从浏览器环境中解析获取我们所需要的信息。

第一种方法看似高效,但是会因为各种原因出现“程序失效”,比如网站改版、网站升级 WAF 限制直接请求接口、网站使用非 Restful API 传输数据等等。试着想象下,当我们订阅了一千条甚至以上的 RSS 信息源之后,如果采用直接“刚”接口的方式,对于程序的维护负担还是比较大的。

相比较第一种方案,基于 CDP 的玩法,只需要消耗稍微多一些的硬件资源(毕竟要跑一个浏览器,哪怕是 headless 的)就能够根据界面特征得到我们想要的信息。

Rod 的基础使用

在了解了 CSR、CDP、Rod 的概况后,我们来开始今天的“旅途”,先来看看怎么简单上手这个工具。

启动 Chrome 的远程调试模式

虽然 Rod 会自动判断是否有合适“操作”浏览器,当缺少可运行浏览器时,会自动下载能够作为容器使用的浏览器。不过,除了调试开发模式或者极其简单的需求中,我个人的习惯是使用“外部浏览器”,开发环境和实际运行一致,实际使用改下远程运行容器(浏览器)地址,就能在各种环境下丝滑的提供服务啦。

如果你的操作系统里本身就安装了 Chrome,那么可以使用 --remote-debugging-port=9222 --headless 参数启动一个可以被 Rod 使用的 Headless 浏览器容器环境。以 macOS 为例,完整命令如下:(其他系统需要调整路径)

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --headless

当命令执行完毕,我们将看到下面的日志,提示我们可以开始玩了。

DevTools listening on ws://127.0.0.1:9222/devtools/browser/719824da-a03b-4d8f-bdad-08766d312261

如果你需要浏览器访问的地址需要代理服务器或者堡垒机中转,那么你还可以在配置中添加 --proxy-server 参数:

/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --headless --proxy-server=10.11.12.90:8001

编写基础的“浏览器”自动化命令

假设你已经参考上面启动了一个本地的、支持远程操控的 Chrome 实例,只需要下面不到二十行代码,就能够模拟浏览器打开网页的操作啦:

package main

import (
	"fmt"

	"github.com/go-rod/rod"
	"github.com/go-rod/rod/lib/launcher"
)

func main() {
	container := launcher.MustResolveURL("127.0.0.1:9222")
	browser := rod.New().ControlURL(container).MustConnect().MustPage("https://www.bilibili.com/read/home")

	fmt.Println(
		browser.MustEval("() => document.title"),
	)
}

上面的代码中,我们模拟用户打开了 B 站专栏页面,并从浏览器中执行了 JavaScript 代码 () => document.title,获取到了页面的标题。

当我们执行完代码,将得到下面的结果:“B站UP主专栏-个人空间-自媒体-哔哩哔哩官网”。

完善浏览器自动化程序

我们像第二篇文章《RSS Can:借助 V8 让 Golang 应用具备动态化能力(二)》里一样,简单调整上面的代码,添加一段 JavaScript 代码,尝试在页面中打印出信息流中的文章标题。

func main() {
	container := launcher.MustResolveURL("127.0.0.1:9222")
	browser := rod.New().ControlURL(container).MustConnect().MustPage("https://www.bilibili.com/read/home")

	result := client.MustEval(`() => Array.from(document.querySelectorAll('.article-list .article-item .article-title')).map(article=>article.innerText).join("\n")`)

	fmt.Println(result)
}

不出意外,代码执行完毕,我们将得到“空”结果。主要的原因在于“我们的代码执行的太快了”,比页面中渲染出我们想要的信息的时间点早了。页面脚本下载需要时间、请求服务器获取接口数据同样需要时间。

为了解决这个问题,我们可以这样调整代码:

func main() {
	container := launcher.MustResolveURL("127.0.0.1:9222")
	browser := rod.New().ControlURL(container).MustConnect().MustPage("https://www.bilibili.com/read/home")

	client := browser.
		MustWaitLoad().
		MustElement(".article-item")

	result := client.MustEval(`() => Array.from(document.querySelectorAll('.article-list .article-item .article-title')).map(article=>article.innerText).join("\n")`)

	fmt.Println(result)
}

在上面的程序中,我们添加了一个“元素检查”的功能,确保程序能够在合适的时机中再去执行必须的代码。再次执行程序,等待程序执行完毕,我们就能得到类似下面的日志结果啦:

高能慎入!凭借神怪美学意外出圈!随便画画都能引发百万观众围观?
《弈仙牌》简评:来一局紧张刺激的“仙侠自走牌”
炉石传说:盘点停服事件各大主播的看法!夜吹难掩泪光!老师可以老年人生活了!
《天地创造》传奇和中国民间汉化往事
动画承载的战争记忆,到底有多残酷?
从乐手视角浅析《孤独摇滚》中的音乐知识与乐队心得
《暗邪西部》评测:槽点与爽点并存的西部猎杀之旅
魔兽世界:暴雪官宣代理到期,明年1月停服,国服玩家何去何从?
注意看,这个男人用实力毁掉了暴雪公司
2022动画歌曲日历:你最喜欢哪一首(天)?
柯南终于做“人”了?为什么说最新剧场版口碑回归?
谁能告诉我,白菜到!底!是!什!么!

Rod 的进阶使用

上面的细节只是使用 Rod 这类 CDP 软件的小细节之一,关于 Rod 的详细使用,或许单独展开一篇内容更为合适。

实际使用的时候,我们还需要注意下面的细节:网页访问是否一直转圈儿没有加载完毕、网页证书是否过期导致无法访问、我们该怎么设置调试模式来观察程序执行过程,以及在前几篇文章中提到的,如何使用 JS SDK 来获取页面中的数据。

下面的程序简单封装了上面提到的一些问题:

package csr

import (
	"encoding/json"
	"fmt"
	"os"
	"time"

	"github.com/go-rod/rod"
	"github.com/go-rod/rod/lib/launcher"
	"github.com/go-rod/rod/lib/launcher/flags"
	"github.com/soulteary/RSS-Can/internal/define"
)

func ParsePageByGoRod(config define.JavaScriptConfig, container string, proxyAddr string) (result define.BodyParsed) {
	var browser *rod.Browser
	var page *rod.Page

	if define.CSR_DEBUG {
		l := launcher.New().Headless(false).Devtools(true)
		if proxyAddr != "" {
			l.Set(flags.ProxyServer, proxyAddr)
		}
		browser = rod.New().ControlURL(l.MustLaunch()).Trace(true).SlowMotion(2 * time.Second)
		launcher.Open(browser.ServeMonitor(""))
	} else {
		browser = rod.New().ControlURL(launcher.MustResolveURL(container))
	}
	browser = browser.MustConnect()

	if define.CSR_IGNORE_CERT_ERRORS {
		browser = browser.MustIgnoreCertErrors(true)
	}

	if define.CSR_INCOGNITO {
		page = browser.MustIncognito().MustPage()
	} else {
		page = browser.MustPage()
	}

	page.MustEvalOnNewDocument(`window.alert = () => {};window.prompt = () => {}`)

	page.
		Timeout(5 * time.Second).
		MustNavigate(config.URL).
		MustWaitLoad().
		MustElement(config.ListContainer).
		CancelTimeout()

	jsCSR, _ := os.ReadFile("./internal/jssdk/jquery.min.js")
	jsSDK, _ := os.ReadFile("./internal/jssdk/sdk.js")
	jsApp := fmt.Sprintf("%s\n%s\n", jsCSR, jsSDK)

	jsRule, err := os.ReadFile(config.File)
	if err != nil {
		fmt.Println(err)
		return result
	}
	inject := page.MustEval(fmt.Sprintf(`
()=> (function(window){
%s
var potted = new POTTED();
%s;
potted.GetData();
return potted.value;
})(window)`, string(jsApp), string(jsRule)))

	now := time.Now()
	var items []define.InfoItem
	json.Unmarshal([]byte(fmt.Sprint(inject)), &items)
	if err != nil {
		fmt.Println(err)
		return result
	}
	code := define.ERROR_CODE_NULL
	status := define.ERROR_STATUS_NULL
	return define.MixupBodyParsed(code, status, now, items)
}

上面这个函数的调用,类似下面这样:

const container = "127.0.0.1:9222"
const proxy = ""

csr.ParsePageByGoRod(config, container, proxy)

当我们执行程序之后,程序将根据我们的实际配置,判断是否是调试环境,打开一个浏览器窗口,或者启动一个无头浏览器进程,在网页加载完毕之后,注入方便处理 DOM 结构的 jQuery 和 JS SDK,然后根据我们定义的 JS 配置获取页面中的数据,生成可以订阅的 RSS 数据。

得到可订阅的 RSS 数据

使用 Docker 取代本地浏览器运行容器

使用 Docker 容器来运行浏览器容器,对于实际的生产环境来说非常实用。如果你只是想了解无头浏览器的使用,可以忽略本小节的内容。

我们可以通过下面的命令,来启动一个包含“浏览器”的 Docker 容器:

docker run -p 9222:9222 ghcr.io/go-rod/rod chrome --headless --no-sandbox --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0

当容器启动完毕之后,我们只需要将上文中的连接 CDP 容器 container 的地址换成容器的实际地址和端口即可,比如:10.11.12.90:9222 即可。

和上文中在本地启动浏览器一样,如果需要设置代理服务器或者堡垒机进行中转,可以添加 --proxy-server= 参数,类似这样:

docker run -p 9222:9222 ghcr.io/go-rod/rod chrome --headless --no-sandbox --remote-debugging-port=9222 --remote-debugging-address=0.0.0.0 --proxy-server=10.11.12.90:8001

除了使用 rod 项目提供的 Docker 镜像之外,我们还可以使用在 Docker Hub 上拥有 100M 惊人使用量的 browserless/chrome 项目提供的容器:

docker run -p 9222:3000 browserless/chrome

在后续的文章中,我们会继续展开这部分细节,关于如何部署和使用高可用的无头浏览器集群。如果你比较着急,可以先浏览 browserless 官方文档 进行实践 😄

最后

虽然现在的 RSS Can 的核心功能都就绪了,但是为了更加好用,我们还需要折腾一番。

接下来的文章里,我们继续“填坑”。

–EOF


我们有一个小小的折腾群,里面聚集了一些喜欢折腾的小伙伴。

在不发广告的情况下,我们在里面会一起聊聊软硬件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术沙龙的资料。

喜欢折腾的小伙伴,欢迎阅读下面的内容,扫码添加好友。

  • 关于“交友”的一些建议和看法
  • 添加好友,请备注实名和公司或学校、注明来源和目的,否则不会通过审核。
  • 关于折腾群入群的那些事

本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

本文作者: 苏洋

创建时间: 2022年12月15日
统计字数: 8772字
阅读时间: 18分钟阅读
本文链接: https://soulteary.com/2022/12/15/rsscan-use-golang-rod-to-parse-the-content-dynamically-rendered-in-the-browser-part-4.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/92384.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C语言进阶】不会处理字符串?一万三千五百字包会保姆级教程

目录 😘前言😘: 一、字符串处理函数介绍🤯: 1.strlen 函数🥎: 2.strcpy 函数⚾: 3.strcat 函数🏀: 4.strcmp 函数🏈: 5.strncpy 函数…

WSL_03 WSL2 从C盘迁移到D盘

文章目录1 动机1 查看虚拟机状态,并关闭要迁移的虚拟机2 迁移WSL22.1 出现的问题:已存在具有提供的名称的分发(已解决)3 设置启动时的默认用户,没有设置默认为root参考1 动机 WSL2默认安装在C盘中,win R运行中使用%localappdata…

python科学计算 之 Numpy库的使用详解

目录 一:Numpy简介 二:ndarray的创建 三:ndarray的属性 四:切片 索引 五:数组的轴(二维数组) 六:二维数组 切片索引 七:高级索引 八:Numpy广播 九:ufunc函数 算…

使用pypy来提升你的python项目性能

一、PyPy介绍 PyPy是用Python实现的Python解释器的动态编译器,是Armin Rigo开发的产品,能够提升我们python项目的运行速度。PyPy 是利用即时编译的 Python 的替代实现。背后的原理是 PyPy 开始时就像一个解释器,直接从源文件运行我们的 Pyth…

Revit二次开发小技巧(十五)构件的最小矩形外轮廓

前言:我们会经常遇到需要计算一个构件的最小外轮廓,一般直接取BoundingBox只有最大和最小值坐标,也是基于x-y坐标系下的。往往不是最小的矩形,所以分享下面的算法来计算最小的外轮廓,条件为法向量是指向Z轴的&#xff…

mqtt服务器搭建与qt下的mqtt客户端实现

一、mqtt介绍 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一个基于客户端-服务器的消息发布/订阅传输协议。MQTT协议是轻量、简单、开放和易于实现的,这些特点使它适用范围非常广泛。在很多情…

前端基础(九)_CSS的三大特征

CSS的三大特征 1、层叠性 1.样式冲突&#xff0c;遵循就近原则 2.样式不冲突&#xff0c;不会层叠&#xff0c;会叠加 1.1.样式冲突&#xff0c;遵循就近原则例子&#xff1a; <!DOCTYPE html> <html lang"en"><head><meta charset"UT…

[附源码]Nodejs计算机毕业设计基于的服装商城系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

Java优雅的记录日志:log4j实战篇

写在前面 项目开发中&#xff0c;记录错误日志有以下好处&#xff1a; 方便调试 便于发现系统运行过程中的错误 存储业务数据&#xff0c;便于后期分析 在java中&#xff0c;记录日志有很多种方式&#xff1a; 自己实现&#xff1a;自己写类&#xff0c;将日志数据&#xf…

HTTP Range:范围请求

文章目录HTTP 范围请求HTTP 范围请求 Range 头是在 HTTP/1.1 协议中新增的一个请求头。包含 Range 头的请求通常称为范围请求&#xff0c;因为 Range 头允许服务器只发送部分响应到客户端&#xff0c;它是下载工具&#xff08;例如迅雷&#xff09;实现多线程下载的核心所在&a…

Python -- 列表

目录 1.列表的基本使用 1.1 列表的格式 1.2 使用下标获取列表元素 2.列表的增删改查 2.1 添加元素 2.2 修改元素 2.3 查找元素 2.4 删除元素 2.5 排序&#xff08;sort、reverse&#xff09; 3.列表遍历 3.1 使用while循环 3.2 使用for循环 4.列表的嵌套 1.列表的基本…

面向切面编程 AOP

AOPAOP的概念AOP &#xff08;底层原理&#xff09;AOP 底层使用动态代理AOP &#xff08; JDK 动态代理&#xff09;首先我们来看一下 Spring 的百度百科   Spring 框架是一个开放源代码的 J2EE 应用程序框架&#xff0c;由 Rod Johnson 发起&#xff0c;是针对 Bean 的生命…

cpp项目中遇到的一些错误

1.解决由于找不到xxx.dll&#xff0c;无法继续执行代码的问题 解决由于找不到xxx.dll&#xff0c;无法继续执行代码的问题_happylife_mini的博客-CSDN博客_由于找不到emp.dll无法继续执行代码在用vs写项目&#xff0c;或者你下载github上的C代码的时候&#xff0c;是不是经常遇…

【Redis技术探索】「底层架构原理」探索分析服务系统的网络架构和线程模型

Redis网络基础架构 网络编程离不开Socket&#xff0c;网络I/O模型最常用的无非是同步阻塞、同步非阻塞、异步阻塞、异步非阻塞&#xff0c;高性能网络服务器最常见的线程模型也就是基于EventLoop模式的单线程模型。 Redis基础组建结构 Redis网络层基础组件主要包括四个部分&a…

acm是什么?你准备好去打了吗?(未完结)

1.引言2.acm究竟是什么&#xff1f;3.acm的时间安排4.acm该如何准备1.引言 作为一个零基础的小白&#xff0c;acm这条路走的并不顺畅&#xff0c;接触的信息很少&#xff0c;以至于在这条道路上走了不少弯路&#xff0c;浪费了大量的时间&#xff0c;现在也快要退役的阶段&…

Linux基础-软件包管理器RPM与yum

该文章主要为完成实训任务&#xff0c;详细实现过程及结果见【参考文章】 参考文章&#xff1a;https://howard2005.blog.csdn.net/article/details/127131286?spm1001.2014.3001.5502 文章目录一、使用RPM软件包管理器1. RPM安装软件包2. RPM更新与升级软件包3. RPM查询软件包…

Qt-Web混合开发-Qt读写Json数据(5)

Qt-Web混合开发-Qt使用内置json库读写json示例&#x1f34f; 文章目录Qt-Web混合开发-Qt使用内置json库读写json示例&#x1f34f;1、概述&#x1f353;2、实现效果&#x1f345;3、实现功能&#x1f95d;4、关键代码&#x1f33d;5、源代码&#x1f346;更多精彩内容&#x1f…

面试怎么回答MySQL索引问题,看这里

前言 小A在宿舍里跟哥们开五黑打排位中&#xff0c;突然收到女神小美的消息&#xff1a;“小A&#xff0c;我今天面试碰到索引问题了&#xff0c;我没回答好”。小A顾不上游戏抓紧回复到&#xff1a;“到你宿舍某某咖啡店吧&#xff0c;我帮你一起看下”。 小A抓紧时间换了衣…

物联公司网页设计制作 简单静态HTML网页作品 静态企业网页作业成品 学生网站模板

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

Linux系统部署

Linux系统部署 下载vmware centos7 xshell6 xftp6新建虚拟机&#xff0c;注意设置网络连接&#xff0c;设置登录名&#xff1a;root,密码&#xff1a;root,等待登录&#xff0c;输入用户名和密码&#xff08;注意密码输入不显示&#xff09;登录成功&#xff0c;执行命令Ifc…