• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

RSS Can借助 V8 让 Golang 应用具备动态化能力二

武飞扬头像
soulteary
帮助1

继续聊聊之前做过的一个小东西的踩坑历程,如果你也想高效获取信息,或许这个系列的内容会对你有用。

写在前面

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

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

为什么需要动态化

Golang 是传统的“编译型选手”,本身的动态化能力很弱,姑且不讨论 Golang 应用是否应该做动态化的哲学问题,单就效率角度来看 ,存在太多问题了。

比如,当我们遇到目标网站改版、想要快速调整规则完善获取信息的时候,重复编译 Golang 程序,即使构建速度再快,也是一件反效率的事情,前后牵扯的七七八八的事情一箩筐。

但其实,我们的程序主体并没有修改调整,需要调整的内容只有一些细微的规则,所以,将这块经常变化的内容抽象出来单独维护,是一件有必要的事情;考虑到部署涉及到额外的测试、补停机切换等需要不少基础技术设施,我们没有必要为一个需求建立一座城堡。所以,将程序部分动态化或许是最简单的省事策略之一

为什么选择 JavaScript 作为动态化的 DSL

为什么考虑使用 JS 作为程序动态化的 DSL ,而不是使用 JSON、TOML、YAML 等传统的“静态”配置文件格式呢?

学新通

首先,动态语言相比“静态配置”对于程序要 “fancy” 不少。除了描述配置,还具备了和“程序实际运行的上下文交互”的能力,甚至在一些场景里,可以用 JavaScript 中现成的功能处理数据,而非一定要在 Golang 里做程序实现。

最后,我挺喜欢 JavaScript 这门年轻但是充满活力的和表现力的语言的。在 Golang 生态里,虽然各种语言的运行时实现都有,但是不论是 V8 实现,还是 Quick JS 实现,都深得我心。

考虑到后面要我们展开的 CSR 部分的内容,项目这里就先选择使用 “V8” 实现,暂时不使用 Quick JS 啦。

我们先来聊聊如何在 Go 里调用 JavaScript 代码。

如何在 Go 里调用 JavaScript

想要在 Go 里调用 JavaScript 代码,在引入上文提到的 “v8” 之后,最简单的方式莫过于下面这样的简单代码示例:

// 创建一个用于运行代码的“容器”(虚拟机)
ctx := v8.NewContext()
// 全局执行代码
ctx.RunScript("const add = (a, b) => a   b", "math.js")
// 继续执行新代码,可以访问之前的代码
ctx.RunScript("const result = add(3, 4)", "main.js")
// 在 Go 里访问刚刚代码的执行结果
val, _ := ctx.RunScript("result", "value.js")
// 在 Go 里打印日子好,将结果打印出来
fmt.Printf("addition result: %s", val)

当然,如果我们想让 JavaScript 代码在 Go 里和 Go 的函数进行更多的交互,还需要做一些函数的调用绑定。当我们将代码放进项目里,执行 go run main.go,将得到下面符合预期的结果:

addition result: 7

不过,真的想在 Go 里放心的运行 JavaScript ,我们还需要对执行的方法做一些额外的处理,避免出现“意外”。

更安全的 JavaScript 沙箱环境

在引入 V8 之后,其实除了运行我们的动态配置、灵活的“小动态函数”之外,还能够运行来自三方的代码。不论是运行哪一种代码,都有可能出现等效下面的逻辑:

while (true) {
    // Loop forever
}

我们当然不希望程序整体,因为这类原因 “hang” 死,甚至影响同机器运行的其他服务。所以,对于调用 JavaScript 的 Golang 方法,需要做出一些改进:

const JS_EXECUTE_TIMEOUT = 200 * time.Millisecond
const JS_EXECUTE_THORTTLING = 2 * time.Second

func safeJsSandbox(ctx *v8.Context, unsafe string, fileName string) (*v8.Value, error) {
	vals := make(chan *v8.Value, 1)
	errs := make(chan error, 1)

	start := time.Now()
	go func() {
		val, err := ctx.RunScript(unsafe, fileName)
		if err != nil {
			errs <- err
			return
		}
		vals <- val
	}()

	duration := time.Since(start)
	select {
	case val := <-vals:
		fmt.Fprintf(os.Stderr, "cost time: %v\n", duration)
		return val, nil
	case err := <-errs:
		return nil, err
	case <-time.After(JS_EXECUTE_THORTTLING):
		vm := ctx.Isolate()
		vm.TerminateExecution()
		err := <-errs
		fmt.Fprintf(os.Stderr, "execution timeout: %v\n", duration)
		time.Sleep(JS_EXECUTE_TIMEOUT)
		return nil, err
	}
}
学新通

上面的程序将会保证我们想要执行的代码按照预期执行,当程序出现需要运行特别久的情况时(例子中是200毫秒),会自动停止运行代码,并休息 2s 避免潜在的重复调用造成系统负载飙升。

func main()
	ctx := v8.NewContext()

	safeJsSandbox(ctx, `
	while (true) {
	    // Loop forever
	}`, "loop.js")
}

当我们再次执行程序,会得到程序自动终止了运行时间过长的动态代码的日志提醒:

execution timeout: 12.206µs

使用 JavaScript 定义简单的动态配置

function getConfig(){
    return {
        ListContainer: "#app .main-right .kr-home-main .kr-home-flow .kr-home-flow-list .kr-flow-article-item",
        Title: ".article-item-title",
        Author: ".kr-flow-bar-author",
        Category: ".kr-flow-bar-motif a",
        DateTime: ".kr-flow-bar-time",
        Description: ".article-item-description",
        Link: ".article-item-title",
    }
}

使用 Go 解析动态配置

如何在 Golang 中执行上面的 JavaScript 代码,并得到执行结果,其实也非常简单,我们可以借助上文中提到的能够“安全执行” JavaScript 代码的函数:

func main() {
	jsApp, _ := os.ReadFile("./config.js")
	inject := string(jsApp)
	ctx := v8.NewContext()
	safeJsSandbox(ctx, inject, "main.js")
	result, _ := ctx.RunScript("JSON.stringify(getConfig());", "config.js")
	jsonRaw := []byte(fmt.Sprintf("%s", result))
	fmt.Printf("addition result: %s", jsonRaw)
}

当我们执行完毕上面的代码后,将得到下面的结果:

cost time: 10.382µs
addition result: {"ListContainer":"#app .main-right .kr-home-main .kr-home-flow .kr-home-flow-list .kr-flow-article-item","Title":".article-item-title","Author":".kr-flow-bar-author","Category":".kr-flow-bar-motif a","DateTime":".kr-flow-bar-time","Description":".article-item-description","Link":".article-item-title"}

想要快速将上面的 “JSON” 格式的输出内容解析成 Go 的内存对象,我们可以使用 “JSON-to-Go” 来偷个懒,将上面的内容粘贴到网站的编辑器中,网页程序将自动转换出我们所需要的 Go Struct 定义。

学新通

简单调整得到的代码,不难写出下面的程序,来将上文中的 JSON 数据转换为程序需要的内存对象。

func main() {
...
	type Config struct {
		ListContainer string `json:"ListContainer"`
		Title         string `json:"Title"`
		Author        string `json:"Author"`
		Category      string `json:"Category"`
		DateTime      string `json:"DateTime"`
		Description   string `json:"Description"`
		Link          string `json:"Link"`
	}

	var jsonData Config
	err := json.Unmarshal(jsonRaw, &jsonData)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(jsonData)
}
学新通

调用动态配置获取网站数据

func getFeeds() {
	// Request the HTML page.
	doc, err := getRemoteDocument("https://36kr.com/")
	if err != nil {
		log.Fatal(err)
	}

	// Find the article items
	doc.Find("#app .main-right .kr-home-main .kr-home-flow .kr-home-flow-list .kr-flow-article-item").Each(func(i int, s *goquery.Selection) {
		title := strings.TrimSpace(s.Find(".article-item-title").Text())
		time := strings.TrimSpace(s.Find(".kr-flow-bar-time").Text())
		fmt.Printf("Aritcle %d: %s (%s)\n", i 1, title, time)
	})
}

我们将动态配置和上面的程序进行结合,可以将程序简单调整为类似下面这样:

...

type Config struct {
	ListContainer string `json:"ListContainer"`
	Title         string `json:"Title"`
	Author        string `json:"Author"`
	Category      string `json:"Category"`
	DateTime      string `json:"DateTime"`
	Description   string `json:"Description"`
	Link          string `json:"Link"`
}

func getFeeds(config Config) {
	// Request the HTML page.
	doc, err := getRemoteDocument("https://36kr.com/")
	if err != nil {
		log.Fatal(err)
	}

	// Find the article items
	doc.Find(config.ListContainer).Each(func(i int, s *goquery.Selection) {
		title := strings.TrimSpace(s.Find(config.Title).Text())
		author := strings.TrimSpace(s.Find(config.Author).Text())
		time := strings.TrimSpace(s.Find(config.DateTime).Text())
		category := strings.TrimSpace(s.Find(config.Category).Text())
		description := strings.TrimSpace(s.Find(config.Description).Text())

		href, _ := s.Find(config.Link).Attr("href")
		link := strings.TrimSpace(href)

		fmt.Printf("Aritcle #%d\n", i 1)
		fmt.Printf("%s (%s)\n", title, time)
		fmt.Printf("[%s] , [%s]\n", author, category)
		fmt.Printf("> %s %s\n", description, link)
		fmt.Println()
	})
}

func main() {
	jsApp, _ := os.ReadFile("./config/config.js")
	inject := string(jsApp)
	ctx := v8.NewContext()
	safeJsSandbox(ctx, inject, "main.js")
	result, _ := ctx.RunScript("JSON.stringify(getConfig());", "config.js")

	var config Config
	err := json.Unmarshal([]byte(fmt.Sprintf("%s", result)), &config)
	if err != nil {
		fmt.Println(err)
		return
	}
	getFeeds(config)
}
学新通

当我们再次运行程序,Go 程序就会跟着 JavaScript 代码中定义的配置,来尝试解析页面中的信息啦。

Aritcle #1
动画市场迎来《三体》,然后呢? (1小时前)
[娱乐独角兽] , [文娱直播新动向]
> 内容生产需要向上走。 /p/2041078218796039

Aritcle #2
...

最后

接下来的内容里,我们继续聊聊,如何将这些信息源转换为 RSS 阅读器可以使用的信息源,以及如何针对不同类型的网站进行信息整理。

当然,也会继续聊聊之前系列文章中提到的有趣的技术点。

–EOF


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

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

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


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

本文作者: 苏洋

创建时间: 2022年12月13日
统计字数: 7118字
阅读时间: 15分钟阅读
本文链接: https://soulteary.com/2022/12/13/rsscan-make-golang-applications-with-v8-part-2.html

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgfbfkb
系列文章
更多 icon
同类精品
更多 icon
继续加载