第三篇内容里,我们来聊聊把结构化数据转换为可以订阅的 RSS 订阅数据源。

写在前面

通过前两篇文章《RSS Can:使用 Golang 实现更好的 RSS Hub 服务(一)》和《RSS Can:借助 V8 让 Golang 应用具备动态化能力(二)》,我们已经能够将网站上的资讯信息,通过动态配置的方式整理成结构化的数据。

本篇文章,我们来简单聊聊,如何将这些结构化的数据变成可订阅的 RSS 订阅源,让网站的数据能够和我们的 RSS 阅读器“连通”起来。

RSS 格式标准

在聊代码实现之前,不论是作为开发者、还是作为 RSS 产品用户,了解下 RSS 格式标准还是非常有必要的。

互联网上关于 “RSS” 的格式标准比较出名的有三种流派,分别是:Atom、 RSS、JSON Feed,第三种出现于 RSS 式微,应用和呼声都不大,因此主要网络应用支持的格式都在集中在前两者:RSS 和 Atom。

TLDR,简单来说,如果你是内容提供方,你希望你的内容能够被更多的人用各种各样的 RSS 客户端访问,选择一定被支持的 RSS 2.0 将保持非常好的兼容性。如果你是读者,考虑到持续追踪文章的更新,以及更好的阅读体验,当网站同时提供多种 RSS 订阅格式时,不妨优先选择 Atom 格式的 RSS 订阅源

当然,本文中我们将借助开源软件库一并将前两篇文章中整理好的数据,一并输出为三种格式。(反正没什么成本

Atom 格式相比较 RSS 2.0 的主要优势

如果你不想针对 “RSS” 进行细致的开发,我们只了解使用即可,这个小节的内容可以跳过。

  1. 能够标记字段中的 HTML 内容是否经过转义或编码,方便开发者在渲染时使用数据。
  2. 不再需要将内容的“正文”和“摘要”都混在 description 字段中,提供了新的 summary 字段,可以区分“摘要”和“正文”,同时允许在正文中添加非文本内容。
  3. “RSS” 存在几个变体版本,Atom 更为稳定和一致。
  4. 提供了符合 XML 标准的命名空间、能够使用 XML 内置的标签来支持相对地址的描述、能够使用 XML 内置标签告诉订阅者内容语言、支持 XML Schema,这些 RSS 2.0 都不具备。
  5. 每一个信息条目具备唯一 ID,订阅者能够追踪具体的内容的更新。
  6. 有统一明确的时间表示规范,方便程序进行处理。
  7. 在 IANA 注册了 application/atom+xml 的 MIME 媒体类型,将其变成了标准规范,RSS 使用的 application/rss+xml 还没有纳入标准。

使用 Go 转换数据为 RSS Feed 格式

Go 生态中支持生成 RSS Feed 的软件包有很多,我选择的是有十年维护历史的 gorilla/feeds。虽然在这个月的 9 号,维护团队宣布开源组织内的仓库都将进入“休眠状态”(存档),不再进行维护。

但是,对于我们的需求来说,RSS 是一个“古老、稳定”的协议,gorilla/feeds 已经经过了长时间的验证,所以选择使用它还是比较合适的。加之,对于这类不活跃维护或者停止维护的项目,还可以通过 Go 的特殊的包管理方式,来帮助我们管理代码,做代码维护变更,这块我们后续的文章中会提到。

Gorilla Feeds 的一般使用

我们先来了解如何使用 Gorilla Feeds 来生成 RSS Feed 格式的订阅源,先引入软件包:

import ("time""github.com/gorilla/feeds"
)

这里之所以同时引入了 time ,是因为我不想麻烦的手动造数据。因为不同的 RSS 格式,对于时间的要求并不相同,所以关于时间的处理,后续展开一篇内容来聊,或许更为合适。

我们先以之前发布过的文章为例,编写一段 Mock 数据,等会用来测试 RSS 订阅源的生成:

now := time.Now()
feed := &feeds.Feed{Title:       "苏洋博客",Link:        &feeds.Link{Href: "https://soulteary.com/"},Description: "醉里不知天在水,满船清梦压星河。",Author:      &feeds.Author{Name: "soulteary", Email: "soulteary@gmail.com"},Created:     now,
}feed.Items = []*feeds.Item{{Title:       "RSS Can:借助 V8 让 Golang 应用具备动态化能力(二)",Link:        &feeds.Link{Href: "https://soulteary.com/2022/12/13/rsscan-make-golang-applications-with-v8-part-2.html"},Description: "继续聊聊之前做过的一个小东西的踩坑历程,如果你也想高效获取信息,或许这个系列的内容会对你有用。",Author:      &feeds.Author{Name: "soulteary", Email: "soulteary@qq.com"},Created:     now,},{Title:       "RSS Can:使用 Golang 实现更好的 RSS Hub 服务(一)",Link:        &feeds.Link{Href: "https://soulteary.com/2022/12/12/rsscan-better-rsshub-service-build-with-golang-part-1.html"},Description: "聊聊之前做过的一个小东西的踩坑历程,如果你也想高效获取信息,或许这个系列的内容会对你有用。这个事情涉及的东西比较多,所以我考虑拆成一个系列来聊,每篇的内容不要太长,整理负担和阅读负担都轻一些。本篇是系列第一篇内容。",Author:      &feeds.Author{Name: "soulteary", Email: "soulteary@gmail.com"},Created:     now,},{Title:       "在搭载 M1 及 M2 芯片 MacBook设备上玩 Stable Diffusion 模型",Link:        &feeds.Link{Href: "https://soulteary.com/2022/12/10/play-the-stable-diffusion-model-on-macbook-devices-with-m1-and-m2-chips.html"},Description: "本篇文章,我们聊了如何使用搭载了 Apple Silicon 芯片(M1 和 M2 CPU)的 MacBook 设备上运行 Stable Diffusion 模型。",Created:     now,},{Title:       "使用 Docker 来快速上手中文 Stable Diffusion 模型:太乙",Link:        &feeds.Link{Href: "https://soulteary.com/2022/12/09/use-docker-to-quickly-get-started-with-the-chinese-stable-diffusion-model-taiyi.html"},Description: "本篇文章,我们聊聊如何使用 Docker 快速运行中文 Stable Diffusion 模型:太乙。 ",Created:     now,},
}

接着,编写简单的调用语句,数据就可以“转换”成我们需要的结果啦:

atom, err := feed.ToAtom()
if err != nil {log.Fatal(err)
}rss, err := feed.ToRss()
if err != nil {log.Fatal(err)
}json, err := feed.ToJSON()
if err != nil {log.Fatal(err)
}fmt.Println(atom, "\n", rss, "\n", json)

将上面的代码放到可以被调用的函数中进行测试(比如 main),程序执行后,我们将看到类似下面的结果:

<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>苏洋博客</title><id>https://soulteary.com/</id><updated>2022-12-14T12:29:55+08:00</updated><subtitle>醉里不知天在水,满船清梦压星河。</subtitle><link href="https://soulteary.com/"></link><author><name>soulteary</name><email>soulteary@gmail.com</email></author><entry><title>RSS Can:借助 V8 让 Golang 应用具备动态化能力(二)</title><updated>2022-12-14T12:29:55+08:00</updated><id>tag:soulteary.com,2022-12-14:/2022/12/13/rsscan-make-golang-applications-with-v8-part-2.html</id><link href="https://soulteary.com/2022/12/13/rsscan-make-golang-applications-with-v8-part-2.html" rel="alternate"></link><summary type="html">继续聊聊之前做过的一个小东西的踩坑历程,如果你也想高效获取信息,或许这个系列的内容会对你有用。</summary><author><name>soulteary</name><email>soulteary@qq.com</email></author></entry>
...
...
</feed> <?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>苏洋博客</title><link>https://soulteary.com/</link><description>醉里不知天在水,满船清梦压星河。</description><managingEditor>soulteary@gmail.com (soulteary)</managingEditor><pubDate>Wed, 14 Dec 2022 12:29:55 +0800</pubDate><item><title>RSS Can:借助 V8 让 Golang 应用具备动态化能力(二)</title><link>https://soulteary.com/2022/12/13/rsscan-make-golang-applications-with-v8-part-2.html</link><description>继续聊聊之前做过的一个小东西的踩坑历程,如果你也想高效获取信息,或许这个系列的内容会对你有用。</description><author>soulteary</author><pubDate>Wed, 14 Dec 2022 12:29:55 +0800</pubDate></item><item><title>RSS Can:使用 Golang 实现更好的 RSS Hub 服务(一)</title><link>https://soulteary.com/2022/12/12/rsscan-better-rsshub-service-build-with-golang-part-1.html</link><description>聊聊之前做过的一个小东西的踩坑历程,如果你也想高效获取信息,或许这个系列的内容会对你有用。这个事情涉及的东西比较多,所以我考虑拆成一个系列来聊,每篇的内容不要太长,整理负担和阅读负担都轻一些。本篇是系列第一篇内容。</description><author>soulteary</author><pubDate>Wed, 14 Dec 2022 12:29:55 +0800</pubDate></item>
...
...</channel>
</rss>{"version": "https://jsonfeed.org/version/1","title": "苏洋博客","home_page_url": "https://soulteary.com/","description": "醉里不知天在水,满船清梦压星河。","author": {"name": "soulteary"},"items": [{"id": "","url": "https://soulteary.com/2022/12/13/rsscan-make-golang-applications-with-v8-part-2.html","title": "RSS Can:借助 V8 让 Golang 应用具备动态化能力(二)","summary": "继续聊聊之前做过的一个小东西的踩坑历程,如果你也想高效获取信息,或许这个系列的内容会对你有用。","date_published": "2022-12-14T12:29:55.50867+08:00","author": {"name": "soulteary"}},
...
...]
}

上面输出的日志结果中,就包含了前文中提到的三种格式,能够覆盖绝大多数的 RSS 客户端的订阅使用。

连接来自网站的信息

在之前的文章中,我们将前文中通过动态配置解析目标网站,并将网站中信息转换为了 Go 中的数据结构。在了解了 Gorilla Feeds 是如何输出 RSS 格式之后,我们只需要将两者“连接”到一起,就能够得到 RSS 格式的资讯订阅源啦。

首先,针对前文中提到的“根据配置解析网站信息”的函数做一些调整:

func getWebsiteDataWithConfig(config define.JavaScriptConfig) (result define.BodyParsed) {doc := network.GetRemoteDocument("https://36kr.com/", "utf-8")if doc.Body == "" {return result}return parser.ParsePageByGoQuery(doc, func(document *goquery.Document) []define.InfoItem {var items []define.InfoItemdocument.Find(config.ListContainer).Each(func(i int, s *goquery.Selection) {var item define.InfoItemtitle := 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)item.Title = titleitem.Author = authoritem.Date = timeitem.Category = categoryitem.Description = descriptionitem.Link = linkitems = append(items, item)})return items})
}

上面的函数正常运行的情况下,就可以得到一个包含了结构化数据的数组。

接下来,写一个简单的函数,调用 Gorilla Feeds 生成我们需要的 RSS 订阅源:

func generateFeeds(data define.BodyParsed) {now := time.Now()rssFeed := &feeds.Feed{Title:   "36Kr",Link:    &feeds.Link{Href: "https://36kr.com/"},Created: now,}for _, data := range data.Body {feedItem := feeds.Item{Title:       data.Title,Author:      &feeds.Author{Name: data.Author},Description: data.Description,Link:        &feeds.Link{Href: data.Link},// 时间处理这块比较麻烦,后续文章再展开Created: now,}rssFeed.Items = append(rssFeed.Items, &feedItem)}atom, err := rssFeed.ToAtom()if err != nil {log.Fatal(err)}rss, err := rssFeed.ToRss()if err != nil {log.Fatal(err)}json, err := rssFeed.ToJSON()if err != nil {log.Fatal(err)}fmt.Println(atom, "\n", rss, "\n", json)
}

最后,调整程序的调用函数,以便于我们进行测试,将 RSS 生成结果打印到终端日志里:

func main() {jsApp, _ := os.ReadFile("./config/config.js")inject := string(jsApp)jsConfig, err := javascript.RunCode(inject, "JSON.stringify(getConfig());")if err != nil {fmt.Println(err)return}config, err := parser.ParseConfigFromJSON(jsConfig)if err != nil {fmt.Println(err)return}data := getWebsiteDataWithConfig(config)generateFeeds(data)
}

使用 go run main.go 执行程序,我们将得到符合预期的结果:

<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title>36Kr</title><id>https://36kr.com/</id><updated>2022-12-14T13:41:37+08:00</updated><link href="https://36kr.com/"></link><entry><title>iOS 16.2来了,这7个新功能值得关注</title><updated>2022-12-14T13:41:37+08:00</updated><id>tag:,2022-12-14:/p/2043412066405640</id><link href="/p/2043412066405640" rel="alternate"></link><summary type="html">Apple 画的饼终于来了。</summary><author><name>少数派</name></author><entry><title>如何更好地思考:人只能获得自己认知内的成就</title><updated>2022-12-14T13:41:37+08:00</updated><id>tag:,2022-12-14:/p/2018320727015942</id><link href="/p/2018320727015942" rel="alternate"></link><summary type="html">5个原则,让你成为一个更好的思考者。</summary><author><name>神译局</name></author></entry>
...

搞定了 RSS 客户端可以使用的数据格式,我们来解决“RSS 可订阅”的最后一步,启动一个简单的 Web 服务,将上面的数据变成可访问的接口地址。

使用 Gin 搞定 RSS Web 服务

Gin 是一个优秀的 HTTP Web 框架,它不见得是 Go 生态所有框架中最快的框架,但要论社区活跃度和易用性,妥妥名列前茅。

使用 Gin 启动一个简单的 Web 服务

Gin 对 Golang 的 net/http 能力进行了封装,提供了简单的调用方式,让我们能够启动一个 Web 服务,比如下面这段不到 20 行的代码:

package mainimport ("net/http""github.com/gin-gonic/gin"
)func main() {r := gin.Default()r.GET("/ping", func(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"message": "pong",})})r.Run()
}

上面的代码在运行之后,会启动一个 Web 服务,默认提供服务的地址是 http://localhost:8080 。当我们在浏览器中访问 /ping,服务器将响应并返回 pong

制作 RSS 订阅数据接口

上文提到过,因为生成不同格式的 RSS 并没有什么成本,所以我们可以将其全部都支持起来,应对各种 RSS 客户端的请求。

实际提供服务的时候,我们需要根据客户端请求的 RSS 格式类型,来输出不同的数据。所以,需要先调整下上文中我们用来生成 RSS Feed 的函数,让它支持根据请求参数中的类型来生成内容:

func generateFeeds(data define.BodyParsed, rssType string) string {now := time.Now()rssFeed := &feeds.Feed{Title:   "36Kr",Link:    &feeds.Link{Href: "https://36kr.com/"},Created: now,}for _, data := range data.Body {feedItem := feeds.Item{Title:       data.Title,Author:      &feeds.Author{Name: data.Author},Description: data.Description,Link:        &feeds.Link{Href: data.Link},// 时间处理这块比较麻烦,后续文章再展开Created: now,}rssFeed.Items = append(rssFeed.Items, &feedItem)}var rss stringvar err errorswitch rssType {case "RSS":rss, err = rssFeed.ToRss()case "ATOM":rss, err = rssFeed.ToAtom()case "JSON":rss, err = rssFeed.ToJSON()default:rss = ""}if err != nil {fmt.Println(err)return ""}return rss
}

完成了生成函数的调整之后,我们来完成一个简单的功能实现,支持根据不同的 API 请求路径,调用上面的函数输出不同格式的 RSS 订阅源:

route := gin.Default()
route.GET("/:type/", func(c *gin.Context) {var rssType RSSTypeif err := c.ShouldBindUri(&rssType); err != nil {c.JSON(http.StatusNotFound, gin.H{"msg": err})return}var response stringvar mimetype stringswitch strings.ToUpper(rssType.Type) {case "RSS":mimetype = "application/rss+xml"response = generateFeeds(data, "RSS")case "ATOM":mimetype = "application/atom+xml"response = generateFeeds(data, "ATOM")case "JSON":mimetype = "application/feed+json"response = generateFeeds(data, "JSON")}c.Data(http.StatusOK, mimetype, []byte(response))
})route.Run(":8080")

启动服务,我们访问 http://localhost:8080/rsshttp://localhost:8080/atomhttp://localhost:8080/json 中的任意一个地址,就能在浏览器中看到 RSS 订阅源的数据啦。

有不少 RSS 订阅工具支持根据网页中的标签,对 RSS 订阅源进行自动探测,比如 Reeder。

为了方便我们在 Reeder 中进行测试,我们可以将上面的 RSS 订阅源地址都写到一个 HTML 页面中,然后“绑定”到这个 Web 服务的 / 根目录:

const hello = `<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>RSS Feed Discovery.</title><link rel="alternate" type="application/rss+xml" title="RSS 2.0 Feed" href="http://localhost:8080/rss"><link rel="alternate" type="application/atom+xml" title="RSS Atom Feed" href="http://localhost:8080/atom"><link rel="alternate" type="application/rss+json" title="RSS JSON Feed" href="http://localhost:8080/json">
</head>
<body>RSS Feed Discovery.
</body>
</html>`route.GET("/", func(c *gin.Context) {c.Data(http.StatusOK, "text/html", []byte(hello))
})

重新运行程序,当我们在 Reeder 等 RSS 订阅工具中输入 http://127.0.0.1:8080 的时候,Reeder 会告知我们发现了三个订阅源。因为三个订阅源的数据是一样的,所以这里随便选择哪一个都行(推荐 Atom)。

点击“订阅”按钮,来自网站的信息就出现在了 Reeder 的信息列表中啦。

至此,我们就初步解决了第一篇文章中提到的,某些不能被 RSS 订阅工具订阅的信息源的订阅问题。至于前两篇文章中提到的“关键词筛选”,“NLP 内容摘要聚合”,我们将在后续的文章中继续展开。

其他:一个隐蔽的内存泄漏隐患

在上篇文章里,为了安全的运行可能出现“死循环”的外部 JavaScript 代码,我们使用了下面的代码来解决问题:

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_TIMEOUT):vm := ctx.Isolate()vm.TerminateExecution()err := <-errsfmt.Fprintf(os.Stderr, "execution timeout: %v\n", duration)time.Sleep(JS_EXECUTE_THORTTLING)return nil, err
}

今天折腾群里的同学 @Etran 提醒,这里存在一处隐秘的内存泄漏问题,time.After() 可能晚于我们接收到 vals 数据执行,导致计时器没有被正确释放。

那么,要如何解决这个问题呢?修正代码很简单:

duration := time.Since(start)
timeout := time.NewTimer(define.JS_EXECUTE_TIMEOUT)select {case val := <-vals:if !timeout.Stop() {<-timeout.C}fmt.Fprintf(os.Stderr, "cost time: %v\n", duration)return val, nil
case err := <-errs:return nil, err
case <-timeout.C:timeout.Stop()vm := ctx.Isolate()vm.TerminateExecution()err := <-errsfmt.Fprintf(os.Stderr, "execution timeout: %v\n", duration)time.Sleep(define.JS_EXECUTE_THORTTLING)return nil, err
}

最后

写在这篇文章的时候,我再次回顾了 RSS 的发展史,以及核心灵魂人物 David Winter 的从业历史,尝试用我的视角来概要的描绘 RSS 历史长河里的精彩瞬间。

在文章即将发布的时候,我改变了想法,关于 RSS 的故事,或许应该在本系列文章结束的时候再发布更为合适。

–EOF


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

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

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

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

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

本文作者: 苏洋

创建时间: 2022年12月14日
统计字数: 11361字
阅读时间: 23分钟阅读
本文链接: https://soulteary.com/2022/12/14/rsscan-convert-website-information-stream-to-rss-feed-part-3.html

RSS Can:将网站信息流转换为 RSS 订阅源(三)相关推荐

  1. 如何订阅没有RSS输出的网站

    1.利用Google资讯 也许国内使用Google资讯的人并不多,Google 资讯是一个由计算机生成的资讯网站.它汇集了来自中国大陆超过1,000 个中文资讯来源的新闻资源,并将相似的报道组合在一起 ...

  2. 为网站加入Drupal星球制作RSS订阅源

    目前中文 Drupal 星球的版块还未成立,但大家的积极性挺高,不少站长都已经调整好自己的网站,生成了可供Drupal Planet 使用的RSS订阅源. 如果你也想让网站做好准备,可以不必再花上不少 ...

  3. 使用Feed43为网页生成RSS订阅源

    简介 在我们使用Rss时候发现很多的网站并不支持Rss服务,如果自己使用Rsshub,Huginn等搭建订阅源,不单单需要懂一些编程和服务器部署的知识,还需要买服务器.如果只是轻度的使用那么完全可以试 ...

  4. 顶级生物信息学 RSS 订阅源

    早在 2018 年的时候我在"生信草堂"的公众号上写过一篇关于 RSS 的文章<使用 RSS 打造你的科研资讯头条>,介绍了关于 RSS 的一些内容和如何使用 inor ...

  5. Feed43自定义 RSS 订阅源

    Google Reader 的关闭后,到处充斥着 RSS 将死的论调.如今看来并没有想象中的那么惨,许多人依旧喜欢沿用 RSS 作为自己获取信息的方式. 前段时间,少数派 Matrix 进行了一次关于 ...

  6. irreader RSS 订阅源阅读器工具软件 - 一款强大的网络内容阅读器

    irreader 是一款强大的网络内容阅读器,不仅支持订阅 RSS 文章,还能够订阅网站.播客等内容,将众多订阅源聚合于一处,告别纷杂的互联网信息. 方便的内置订阅源 软件界面采取源列表.文章列表和内 ...

  7. RSS阅读——在繁杂的社会接受纯粹的信息 RSS介绍与RSS订阅源分享

    在互联网信息大爆炸的背景下,人们需要一种全新的知识获取与整理方法.当我们面对繁杂的信息时,一种全新的技术诞生了 RSS(简易信息聚合技术)的诞生与发展 RSS技术的诞生 RSS技术是由Netscape ...

  8. rss学习,可订阅源总结,无脑傻瓜式操作

    你必读的 RSS 订阅源有哪些? - 知乎 (zhihu.com) 链接里的        win 上    阅读器feeddemon                        手机上也有,但是我 ...

  9. rss订阅源推荐个人收集

    新闻类: 百度rss新闻订阅:http://www.baidu.com/search/rss.html 网易rss订阅中心:http://www.163.com/rss 网易新闻·有态度专栏:http ...

最新文章

  1. 【怎样写代码】确保对象的唯一性 -- 单例模式(二):解决方案
  2. IT人怎能忘记这些开源?
  3. 阿里首次公开量子通信技术 为20年后做准备
  4. 基于“飞桨”的深度学习智能车
  5. Python安装MySQL模块
  6. 装机、做系统必备:秒懂MBR和GPT分区表____转载网络
  7. 当我学完Python我学了些什么
  8. 深度学习笔记 第四门课 卷积神经网络 第四周 特殊应用:人脸识别和神经风格转换...
  9. 扩展插件_Adobe扩展工具插件系列
  10. Centos7 Apache 2.4.18编译安装
  11. [转载]使用Vitamio打造自己的Android万能播放器(7)——在线播放(下载视频)...
  12. [原创]二维数组的动态分配及参数传递
  13. linux之systemctl命令
  14. 常见的电子商务模式理解
  15. 手机型号JSON数据
  16. JAVA md5加盐加密解密_md5加密,md5加盐加密和解密
  17. 2022-2027年中国洗发水行业市场全景评估及发展战略规划报告
  18. 计算机如何一次性删除音乐,win10怎么删除windows音乐文件夹?
  19. springboot Basic Auth 暴露API 访问认证
  20. Chrome常见黑客插件及用法

热门文章

  1. 001-REST-简介
  2. 阿里云第三方:_身份证二要素API接口
  3. 保险入门,我不推荐买保险
  4. WTO框架下经济结构调整和产业升级
  5. 北航计算机科学与技术课表,北航计算机科学与技术五年课程参考
  6. 停车还能360全方位影像_路虎(揽胜运动星脉极光发现)车主如何选购360全景安全辅助系统...
  7. python-opencv 读取摄像头并保存为.mp4视频 及 VideoCapture()的使用
  8. Python中如何将浮点型数据转换成整型
  9. github新手使用指南
  10. 2020.11.9--AE--文字的文本属性、文字动画效果、内置动画预设