背景

最近在弄GitHub主页美化的时候,搞了一些感觉比较好玩有趣的东西,有兴趣的朋友可以看看

这里贴个我的主页地址:https://github.com/JS-banana,有兴趣的可以看看~

当时在编辑个人信息介绍的时候,产生了一个想法:可以在我的GitHub主页同步我的博客更新状态吗?

当我更新博客的时候,我的GitHub主页会自动把我博客最新更新的内容同步过去,很棒啊有没有~

这是当时产生的一个想法,后来就研究了一下。最开始是想用nodejs写个爬虫搞一搞的,也没啥问题,不过这样搞会有很多缺陷,我自己也只能搞个半成品,也不具有一定的复用性,就排除了~

后来看到了Pythonfeedparser库,感觉非常合适有没有啊。(feedparserpython中最常用的RSS程序库,使用它我们可轻松地实现从任何 RSSAtom 订阅源得到标题、链接和文章的条目。)

也看了下效果,感觉很不错,这样我们只要做两件事即可:

  • 实现 Atom 订阅源(供feedparser库使用)
  • 实现 README.md 文件的动态更新(获取到订阅信息后更新主页)

RSS、Atom 订阅源

RSS订阅我们应该不陌生,我们在浏览很多大佬博客的时候、知名网站和服务时会发现他们都提供有RSS/Atom订阅,那么什么是RSS?什么是Atom呢?

什么是 RSS?

  • Really Simple Syndication(真正简易联合)
  • 使您有能力聚合(syndicate)网站的内容
  • 定义了非常简单的方法来共享和查看标题和内容
  • 文件可被自动更新
  • 允许为不同的网站进行视图的个性化
  • 使用 XML 编写

为什么使用 RSS?

RSS 被设计用来展示选定的数据。

如果没有 RSS,用户就不得不每日都来您的网站检查新的内容。对许多用户来说这样太费时了。通过 RSS feedRSS 通常被称为 News feedRSS feed),用户们可以使用 RSS 聚合器来更快地检查您的网站更新(RSS 聚合器是用来聚集并分类 RSS feed 的网站或软件)。

RSS的未来发展(Atom的诞生)

因为RSS 2.0的版权问题,该协议前途未卜

由于RSS前途未卜,而且RSS标准发展存在诸多问题或不足,于是ATOM横空出世,可以先简单的理解为RSS的替代品

FEED 是什么

FEED其实就是RSS(或ATOM)和订阅用户之间的“中间商”,起到帮忙批发传递信息的作用。所以,FEED的常见格式就是RSSATOM,网络上说的FEED订阅,更确切的说法应该仍然是RSSATOM订阅。

什么是订阅

订阅跟普通大家订阅报刊类似,不过几乎所有网站的RSS/ATOM订阅都是免费的,也有一些“非主流”一族要收费订阅的,当然FEED订阅只是网络上的信息传递,一般不涉及实体资料传递,所以大家遇到喜欢的网站,并且也喜欢使用在线或离线阅读,尽可订阅,而且可以随时退订。

总结

RSSAtom 具有相似的基于 XML 的格式。它们的基本结构是相同的,只是在节点的表达式上有一点区别。我们只要了解ATOM是对RSS2.0的改进就可以了。

生成自己网站的Atom订阅源

Atom订阅源 基本结构

了解 atom.xml 的基本格式和语法,看个最简单的demo

<!-- 头信息 -->
<?xml version="1.0" encoding="utf-8"?>

<!-- 主体 -->
<feed xmlns="http://www.w3.org/2005/Atom">
  <!-- 基本信息 -->
  <title>小帅の技术博客</title>
  <link href="https://ssscode.com/atom.xml" rel="self"/>
  <link href="https://ssscode.com/"/>
  <updated>2021-08-28 16:25:56</updated>
  <id>https://ssscode.com/</id>
  <author>
    <name>JS-banana</name>
    <email>[email protected]</email>
  </author>

  <!-- 内容区 -->
  <entry>
    <title>Webpack + React + TypeScript 构建一个标准化应用</title>
    <link href="https://ssscode.com/pages/c3ea73/" />
    <id>https://ssscode.com/pages/c3ea73/</id>
    <published>2021-08-28 16:25:56</published>
    <update>2021-08-28 16:25:56</update>
    <content type="html"></content>
    <summary type="html"></summary>
    <category term="webpack" scheme="https://ssscode.com/categories/?category=JavaScript"/>
  </entry>

  <entry>
    ...
  </entry>

    ...

</feed>

基本信息那一块完全可以自己自定义配置好,然后,再去头去尾之后,可以发现我们只要关心 <entry> ... </entry> 标签内容即可,也就是每条博客文章的基本信息~

因此,我们只要按照这个规范、格式、语法,完全可以自己生成atom.xml,nice😎~

不想自己写的可以试试这个 feed

编写 atom.xml 文件生成函数

因为我的博客是以vuepress搭建的(webpack + vue2.x),这里就以nodejs为例

读取所有markdwon文件就不细说了,我们拿到所有的列表数据,进行一下简单的处理,这里只填写一些我们需要的数据即可,如果想阅读订阅源使用,也可以自己丰富信息内容~

const DATA_FORMAT = 'YYYY-MM-DD HH:mm:ss';

// posts 是所有的博客文章信息
// xml 中的 & 符号需要替换为 &amp; 否则会有语法错误
function toXml(posts) {
  const feed = `<?xml version="1.0" encoding="utf-8"?>
  <feed xmlns="http://www.w3.org/2005/Atom">
    <title>小帅の技术博客</title>
    <link href="https://ssscode.com/atom.xml" rel="self"/>
    <link href="https://ssscode.com/"/>
    <updated>${dayjs().format(DATA_FORMAT)}</updated>
    <id>https://ssscode.com/</id>
    <author>
      <name>JS-banana</name>
      <email>[email protected]</email>
    </author>
    ${posts
      .map(item => {
        return `
        <entry>
          <title>${item.title.replace(/(&)/g, '&amp;')}</title>
          <link href="https://ssscode.com${item.permalink}" />
          <id>https://ssscode.com${item.permalink}</id>
          <published>${item.date.slice(0, 10)}</published>
          <update>${item.date}</update>
        </entry>`;
      })
      .join('\n')}
  </feed>`;

  fs.writeFile(path.resolve(process.cwd(), './atom.xml'), feed, function(err) {
    if (err) return console.log(err);
    console.log('文件写入成功!');
  });
}

node执行该文件,应该会在同级目录下生成一个 atom.xml 文件,可以看到

ok,atom订阅源搞定~

feedparser的简单用法

python feedparser,网上似乎也有node版本的,这里就先不关心了

把刚才的demo内容片段复制到atom.xml文件,简单测试下用法,看下返回值格式,为了更清晰的看结构,我把python执行的结果处理了一下

atom.xml源文件

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>小帅の技术博客</title>
  <link href="https://ssscode.com/atom.xml" rel="self"/>
  <link href="https://ssscode.com/"/>
  <updated>2021-08-28 16:25:56</updated>
  <id>https://ssscode.com/</id>
  <author>
    <name>JS-banana</name>
    <email>[email protected]</email>
  </author>
  <entry>
    <title>Webpack + React + TypeScript 构建一个标准化应用</title>
    <link href="https://ssscode.com/pages/c3ea73/" />
    <id>https://ssscode.com/pages/c3ea73/</id>
    <published>2021-08-28 16:25:56</published>
    <update>2021-08-28 16:25:56</update>
  </entry>
</feed>

main.py脚本

import feedparser

blog_feed_url = "./atom.xml"

feeds = feedparser.parse(blog_feed_url)

print (feeds)

输出结果大致结构如下

{
  bozo: 1,
  // entries
  entries: [
    {
      title: "Webpack + React + TypeScript 构建一个标准化应用",
      title_detail: {
        type: "text/dplain",
        language: None,
        base: "",
        value: "Webpack + React + TypeScript 构建一个标准化应用",
      },
      links: [{ href: "https://ssscode.com/pages/c3ea73/", rel: "alternate", type: "text/html" }],
      link: "https://ssscode.com/pages2/c3ea73/",
      id: "https://ssscode.com/pages/c3ea73/",
      guidislink: False,
      published: "2021-08-28 16:25:56",
      publoished_parsed: time.struct_time(), // 一个日期处理函数,参数比较多,我删掉了,只看代码结构
      update: "2021-08-28 16:25:56",
    },
  ],
  // feed
  feed: {
    title: "小帅の技术博客",
    title_detail: { type: "text/plain", language: None, base: "", value: "小帅の技术博客" },
    links: [
      { href: "https://ssscode.com/atom.xml", rel: "self", type: "application/atom+xml" },
      { href: "https://ssscode.com/", rel: "alternate", type: "text/html" },
    ],
    link: "https://ssscode.com/",
    updated: "2021-08-28 16:25:56",
    updated_parsed: time.struct_time(),
    id: "https://ssscode.com/",
    guidislink: False,
    authors: [{ name: "JS-banana", email: "[email protected]" }],
    author_detail: { name: "JS-banana", email: "[email protected]" },
    author: "JS-banana ([email protected])",
  },
  headers: {},
  encoding: "utf-8",
  version: "atom10",
  bozo_exception: SAXParseException("XML or text declaration not at start of entity"),
  namespaces: { "": "http://www.w3.org/2005/Atom" },
}

可以看到,拿到所有的entries即可,编写个函数,取一些我们需要的内容

def fetch_blog_entries():
    entries = feedparser.parse(blog_feed_url)["entries"]
    return [
        {
            "title": entry["title"],
            "url": entry["link"].split("#")[0],
            "published": entry["published"].split("T")[0],
        }
        for entry in entries
    ]

替换markdown文件指定区域内容

剩下最后一步就是:怎么把我们README.md主页文件中指定的区域内容替换掉,然后在推送到GitHub完成更新即可

### Hello, 我是小帅! 👋

...
...
其他信息

<!-- start -->
  这里显示博客信息
<!-- end -->

如上,除了指定的区域需要更新,其他地方是不需要变动的

这时就可以通过Python可以读取注释,然后使用正则处理替换,即可

我们在 README.md 中标记注释

<!-- blog starts -->
  ...
<!-- blog ends -->

代码:

def replace_chunk(content, marker, chunk, inline=False):
    r = re.compile(
        r"<!\-\- {} starts \-\->.*<!\-\- {} ends \-\->".format(marker, marker),
        re.DOTALL,
    )
    if not inline:
        chunk = "\n{}\n".format(chunk)
    chunk = "<!-- {} starts -->{}<!-- {} ends -->".format(marker, chunk, marker)
    return r.sub(chunk, content)

最后,再结合接口请求、文件读取等,完整代码如下

import feedparser
import json
import pathlib
import re
import os
import datetime

blog_feed_url = "https://ssscode.com/atom.xml"

root = pathlib.Path(__file__).parent.resolve()

def replace_chunk(content, marker, chunk, inline=False):
    r = re.compile(
        r"<!\-\- {} starts \-\->.*<!\-\- {} ends \-\->".format(marker, marker),
        re.DOTALL,
    )
    if not inline:
        chunk = "\n{}\n".format(chunk)
    chunk = "<!-- {} starts -->{}<!-- {} ends -->".format(marker, chunk, marker)
    return r.sub(chunk, content)

def fetch_blog_entries():
    entries = feedparser.parse(blog_feed_url)["entries"]
    return [
        {
            "title": entry["title"],
            "url": entry["link"].split("#")[0],
            "published": entry["published"].split("T")[0],
        }
        for entry in entries
    ]

if __name__ == "__main__":
    readme = root / "README.md"
    readme_contents = readme.open(encoding='UTF-8').read()

    entries = fetch_blog_entries()[:5]
    entries_md = "\n".join(
        ["* <a href='{url}' target='_blank'>{title}</a> - {published}".format(**entry) for entry in entries]
    )
    rewritten = replace_chunk(readme_contents, "blog", entries_md)

    readme.open("w", encoding='UTF-8').write(rewritten)

我对Python也不熟,不过跟着前人的脚步,模仿着使用也能达到预期效果,还行~

最近稍微接触了一些Python相关的脚本库,发现还挺有意思的,觉得还是很有必要学习学习,日常使用中还是很有帮助的,毕竟现在Python也是很火热的嘛,就算当工具用,感觉也很强力~

配置 GitHub Action 定时任务

实现功能的脚本已经搞定了,现在就是希望在我们完成博客更新后,脚本可以自动执行

这里我们直接使用 GitHub Action 的定时任务即可

项目里添加文件 .github/workflows/ci.yml

name: Build README

on:
  workflow_dispatch:
  schedule:
    - cron: "30 0 * * *" # 每天 0:30 时运行,北京时间需要 + 8

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repo # 获取代码分支
        uses: actions/checkout@v2

      - name: Set up Python # python 环境
        uses: actions/setup-python@v2
        with:
          python-version: 3.8

      - uses: actions/cache@v2 # 依赖缓存
        name: Configure pip caching
        with:
          path: ~/.cache/pip
          key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
          restore-keys: |
            ${{ runner.os }}-pip-

      - name: Install Python dependencies # 安装依赖
        run: |
          python -m pip install -r requirements.txt

      - name: Update README # 执行脚本
        run: |-
          python build_readme.py
          cat README.md

      - name: Commit and push if changed # Git 提交
        run: |-
          git diff
          git config --global user.email "[email protected]"
          git config --global user.name "JS-banana"
          git pull
          git add -A
          git commit -m "Updated README content" || exit 0
          git push

大功告成~

看下效果:


这样脚本每天都会跑一次,同步博客相关信息~

结语

之前只知道RSS订阅,完全不清楚还有这么些的细节,这次也算梳理搞清楚了一些,也尝试自己玩了一下,还是挺不错的~

感觉多会一门语言还是很棒的啊,有时会给你完全不一样的思路,或许就会有更加好的方案~

扶我起来,我还能学~笑~

参考

03-05 23:50