开启 Django 博客的 RSS 功能

2020-01-099917 阅读9 评论

博客提供 RSS 订阅应该是标配,这样读者就可以通过一些聚合阅读工具订阅你的博客,时时查看是否有文章更新,而不必每次都跳转到博客上来查看。现在我们就来为博客添加 RSS 订阅功能。

RSS 简介

RSS(Really Simple Syndication)是一种描述和同步网站内容的格式,它采用 XML 作为内容传递的格式。简单来说就是网站可以把内容包装成符合 RSS 标准的 XML 格式文档。一旦网站内容符合一个统一的规范,那么人们就可以开发一种读取这种规范化的 XML 文档的工具来聚合各大网站的内容。例如一个读者可能关注了很多的博客网站,如果这些博客网站都支持 RSS 订阅的话,他就只需要一个聚合阅读器订阅这些博客,就可以在聚合器工具里看到全部博客的更新内容,而不必再分别访问各个博客去看有没有内容更新了。更多 RSS 的介绍,可参考 rss (简易信息聚合)

使用 Django Feed 类

根据以上对 RSS 的介绍,我们可以发现关键的地方就是根据网站的内容生成规范化的 XML 文档,幸运的是,Django 已经内置了一些生成这个文档的方法,下面就使用这些方法来创建 RSS 订阅文档。

首先我们在 blog 应用的根目录下(models.py 所在目录)新建一个 feeds.py 文件以存放和 RSS 功能相关的代码。让后在 feeds.py 中写入如下代码:

blog/feeds.py

from django.contrib.syndication.views import Feed

from .models import Post


class AllPostsRssFeed(Feed):
    # 显示在聚合阅读器上的标题
    title = "HelloDjango-blog-tutorial"

    # 通过聚合阅读器跳转到网站的地址
    link = "/"

    # 显示在聚合阅读器上的描述信息
    description = "HelloDjango-blog-tutorial 全部文章"

    # 需要显示的内容条目
    def items(self):
        return Post.objects.all()

    # 聚合器中显示的内容条目的标题
    def item_title(self, item):
        return "[%s] %s" % (item.category, item.title)

    # 聚合器中显示的内容条目的描述
    def item_description(self, item):
        return item.body_html

代码应该不难理解,主要就是指定要生成的 XML 文档内容。各个属性和方法的含义已在代码中注释,你只需把相关的内容替换成符合你博客的描述即可。

需要注意一点的是这个方法:

    # 聚合器中显示的内容条目的描述
    def item_description(self, item):
        return item.body_html

item 是文章 (Post 模型的实例),聚合内容的描述我们返回了 body_html 属性的值。模型中原本使用 body 属性存储博客文章的内容,但是这些内容是以 Markdown 格式的,并非所有的聚合内容阅读器都支持 Markdown 格式的解析,因此我们返回的是已经解析后的 HTML 格式内容。

Post 模型原来并没有 body_html 这个属性的,那它是哪里来的呢?

回顾一下 PostDetailView 这个视图,我们在获取博客文章 post 的详细内容的视图中,使用 markdown 这个 Python 库在解析 post.body 这个属性的值。这个解析的过程其实在很多地方都可能用到,不应该写死在 PostDetailView 这个视图里,所以我们先将 PostDetailView 视图的 get_object 方法解析 Markdown 内容的过程封装为一个函数,暂且将其放在 blog/models.py 中。

blog/models.py

def generate_rich_content(value):
    md = markdown.Markdown(
        extensions=[
            "markdown.extensions.extra",
            "markdown.extensions.codehilite",
            # 记得在顶部引入 TocExtension 和 slugify
            TocExtension(slugify=slugify),
        ]
    )
    content = md.convert(value)
    m = re.search(r'<div class="toc">\s*<ul>(.*)</ul>\s*</div>', md.toc, re.S)
    toc = m.group(1) if m is not None else ""
    return {"content": content, "toc": toc}

解析过程和原来一样,只是将解析得到的 HTML 内容(content)和目录(toc)放在一个字典里返回。

然后,我们来给 Post 模型定义几个属性,以便方便地获取解析后的内容和目录:

blog/models.py

from django.utils.functional import cached_property

class Post(models.Model):
    # 省略其它代码

    @property
    def toc(self):
        return self.rich_content.get("toc", "")

    @property
    def body_html(self):
        return self.rich_content.get("content", "")

    @cached_property
    def rich_content(self):
        return generate_rich_content(self.body)

首先看到 rich_content 这个方法,它返回的是 generate_rich_content 函数调用后的结果,即将 body 属性的值经 Markdown 解析后的内容。但要注意的是我们使用了 django 提供的 cached_property 装饰器,这个装饰器和 Python 内置的 property 装饰器功能一样,可以将方法转为属性,这样就能够以属性访问的方式获取方法返回的值,不过 cached_property 进一步提供缓存功能,它将被装饰方法调用返回的值缓存起来,下次访问时将直接读取缓存内容,而不需重复执行方法获取返回结果。例如对博客文章内容的 Markdown 解析是比较耗时的,而解析的结果可能被多次访问,因此将其缓存起来能起到优化作用。

为了更方便地获取文章的 HTML 格式的内容和目录,我们进一步将 generate_rich_content 返回的值放到 tocbody_html 两个属性中,这里两个属性都从 rich_content 中取值,cached_property 的作用就发挥出来了。

添加 URL

接下来就是指定 URL 模式,让人们访问这个 URL 后就可以看到 Feed 生成的内容。通常 RSS 的 URL 配置直接写在项目的 urls.py 文件里,即打开 blogproject/urls.py 文件,添加如下代码:

blogproject/urls.py

from blog.feeds import AllPostsRssFeed

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),
    path('', include('comments.urls')),

    # 记得在顶部引入 AllPostsRssFeed
    path('all/rss/', AllPostsRssFeed(), name='rss'),
]

修改模板

简单修改一下 templates/base.html 模板,把 RSS 的 URL 添加到模板中:

<div class="rss">
    <a href="{% url 'rss' %}"><span class="ion-social-rss-outline"></span> RSS 订阅</a>
</div>

此外,由于将 Markdown 解析的内容放在了 body_html 属性,将之前博客文章详情模板里获取文章内容的代码也做相应调整:

templates/blog/detail.html

<div class="entry-content clearfix">
    {{ post.body_html|safe }}
</div>

运行开发服务器,在侧边栏点击 RSS 订阅的链接,就跳转到 /all/rss/,你会看到这对乱码一样的东西,这就是生成的 RSS 标准文档,当然这个文档不是给你读的,而是给 RSS 聚合阅读器工具读的。

RSS 测试插件

可以在本地测试一下订阅效果,我使用的 Chrome 浏览器,安装了一个 RSS Feed Reader 的应用,如果你也使用的 Chrome 浏览器,可以从应用商店添加它,然后就可以在本地测试订阅效果了。如果是其它浏览器,自行查找相关 RSS 阅读插件即可。我本地测试效果如下:

rss订阅

可以看到订阅成功了,订阅界面显示的信息就是我们在 AllPostsRssFeed 类中指定的相关信息。大功告成,现在任何人都可以订阅我们的博客了!

-- EOF --

9 评论
登录后回复
Fan()
2020-10-22 21:27:41

看了下Feed有一个__call__方法, 怪不得可以写成这样

path('all/rss/', AllPostsRssFeed(), name='rss'),

涨姿势了

回复
Ifr4me
2020-07-13 16:48:34

随便打开任意一篇文章,显示有以下问题。

Environment:

Request Method: GET
Request URL: http://127.0.0.1:8000/posts/6/

Django Version: 3.0.8
Python Version: 3.8.3
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'pure_pagination',
 'blog.apps.BlogConfig',
 'comments.apps.CommentsConfig']


Traceback (most recent call last):
  File "C:\Users\a\.virtualenvs\blog-tutorial-B8yKSGLt\lib\site-packages\django\core\handlers\exception.py", line 34, in inner
    response = get_response(request)
  File "C:\Users\a\.virtualenvs\blog-tutorial-B8yKSGLt\lib\site-packages\django\core\handlers\base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "C:\Users\a\.virtualenvs\blog-tutorial-B8yKSGLt\lib\site-packages\django\core\handlers\base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "C:\Users\a\.virtualenvs\blog-tutorial-B8yKSGLt\lib\site-packages\django\views\generic\base.py", line 71, in view
    return self.dispatch(request, *args, **kwargs)
  File "C:\Users\a\.virtualenvs\blog-tutorial-B8yKSGLt\lib\site-packages\django\views\generic\base.py", line 97, in dispatch
    return handler(request, *args, **kwargs)
  File "E:\project\blog-tutorial\blog\views.py", line 88, in get
    response = super(PostDetailView, self).get(request, *args, **kwargs)
  File "C:\Users\a\.virtualenvs\blog-tutorial-B8yKSGLt\lib\site-packages\django\views\generic\detail.py", line 106, in get
    self.object = self.get_object()
  File "E:\project\blog-tutorial\blog\views.py", line 106, in get_object
    post.toc = m.group(1) if m is not None else ''

Exception Type: AttributeError at /posts/6/
Exception Value: can't set attribute
    def get_object(self, queryset=None):
        # 覆写 get_object 方法的目的是因为需要对 post 的 body 值进行渲染
        post = super().get_object(queryset=None)
        md = markdown.Markdown(extensions=[
            'markdown.extensions.extra',
            'markdown.extensions.codehilite',
            # 'markdown.extensions.toc',
            TocExtension(slugify=slugify),
        ])
        post.body = md.convert(post.body)
        m = re.search(r'<div class="toc">\s*<ul>(.*)</ul>\s*</div>', md.toc, re.S) 
        post.toc = m.group(1) if m is not None else ''      # line106 显示这行有问题。
        return post

我这里试着注释toc的 @property 好了。请问下博主,这个应该怎么解决?
#@property
def toc(self):
return self.rich_content.get("toc", "")

回复
追梦人物 Ifr4me
2020-07-19 20:25:22

Post 模型中已经有了 toc 属性获取文章目录,原来视图中的处理方式就可以删掉了,两种是对同一个功能的实现。

回复
CJ-nan 追梦人物
2020-08-11 16:16:51

所以应该怎么改?

回复
Fan() CJ-nan
2020-10-22 21:26:13

把PostDetailView中我们自己重写的get_object全部注释掉

回复
startlearner CJ-nan
2021-02-05 18:25:27

注释掉报错那一行就好了

回复
lllwqqq
2020-06-18 11:15:53

哈哈,确实,生搬硬套是不行的,看了github上面的代码也不是最新款,博主用心良苦啊

回复
追梦人物 lllwqqq
2020-06-26 17:03:19

什么情况?代码无法用了吗?

回复
kk545141
2020-06-17 15:06:24

此页面又给小白留坑了,哈哈

回复