如何使用 Flask 做一个评论系统


Posted in Python onNovember 27, 2020

因为我博客使用的Disqus代理服务下线,博客的评论系统可能有一阵子没有工作了。惭愧的是我竟然最近才发现,我的工作环境一直是没有GFW存在的,发现是因为有个朋友为了留言给我不惜通过赞赏1元钱的方式。赞赏功能也是我最近才上的功能,但我怎么是这么一个无良的博主呢,我认为一个好的评论交流环境还是非常有必要的。但是自建评论还是换用其他墙内友好的评论系统,我还是纠结了一阵的,大致上我有这么几个要求:

  1. 主要服务墙内,Disqus虽香但墙内用不了啊
  2. 颜值,要能匹配当前博客的主色调,或者能方便地自定义皮肤
  3. 评论要支持markdown语法
  4. 评论数据要有地方可管理、归档、导入导出等
  5. 外部用户使用评论的门槛要低
  6. 用户收到回复时能通过他「常用的」联系方式收到通知

评论系统大致有这么几个选择方向:一是使用类似Disqus这样的三方平台,这样数据托管不用操心,但服务随时有挂掉的风险,而且外观上也不够自由;二是使用Github Issue作为后端的评论系统,比如Gitment,utterances 好处是你不必担心Github挂掉,而且不用收钱。但不方便后续打包迁移,而且我一直反对过度利用Github;那么剩下的选择就是自己撸一个了,简单的构思评估以后我列出以下列功能大纲:

  • 评论数据模型
  • 评论展示
  • 评论管理
  • 导入disqus评论
  • 新评论通知
  • 第三方登录
  • 评论导出(低优先)

类比Workpress提供的评论功能,用户只需要填用户姓名和电子邮件这两个信息就够了,前者用来显示作者名,后者用来接收通知,个人网站用来推广自己,但不是必填的。我在这个基础上,希望增加第三方登录的功能,这样用户就不用填写这些信息,点一个按钮就好了。关于第三方登录的开发实现,我会留到下一篇文章中。

评论数据模型

首先是评论数据模型的设计,我的理念是够用就好,不用太多太复杂的东西,毕竟我的文章平均0.2条评论。所以,点赞什么的就不要了,评论删除直接删数据就好了,也不需要什么状态。

如何使用 Flask 做一个评论系统

其中分别有一个外键指向作者用户以及文章记录,User里面会记录这个用户的Email, 名称,头像信息。另外会有一个parent_id指向评论回复的对象(也是一条评论),这里有一个指向自身的外键,使用Flask-SQLAlchemy写起来是这样的:

class Comment(db.Model):
  id = db.Column(db.Integer, primary_key=True)
  post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
  author_id = db.Column(db.Integer, db.ForeignKey("user.id"))
  floor = db.Column(db.Integer)
  content = db.Column(db.Text())
  html = db.Column(db.Text())
  create_at = db.Column(db.DateTime(), default=datetime.utcnow)
  parent_id = db.Column(db.Integer, db.ForeignKey("comment.id"))
  replies = db.relationship(
    "Comment", backref=db.backref("parent", remote_side=[id]), lazy="dynamic"
  )

  __table_args__ = (db.UniqueConstraint("post_id", "floor", name="_post_floor"),)

floor表明评论是「第几楼」,注意这里有个限制,每篇文章楼层不能重复。

评论展示

接下来看看如何展示评论。每条评论都可能有若干回复,回复评论又有回复,所以这是一个树形的结构,最极端的,如果把所有树形都嵌套显示出来,就会像网易新闻评论盖楼那样。另一个极端,是把所有评论都展平,按回复时间排序显示,这样又会失去回复的上下文信息。还是那句话,够用就好,我选择了一条折中的方式:两层树形展示。直接评论的是第一层节点,然后回复这些评论的,和回复这些回复的,都展平成一层节点,算作这条评论的子节点。外层评论和子节点都按时间排序显示,但只有外层评论具有楼层属性。且子节点应该展示回复的是哪位作者,这样就大大减小了上下文混淆的可能(虽然我觉得我这评论的量,全显示成一层也不会怎样)。

评论框编辑器使用的是simple-mde,使用起来非常简单:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css" rel="external nofollow" >
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
...
<textarea name="content"></textarea>
...
<script>
var simplemde = new SimpleMDE();
</script>

完事!后续可能会考虑加上emoji选择器。markdown保存,后端渲染html,前端取出展示。最后结果非常漂亮令我满意,大家可以在本篇文章下面看到效果。

评论管理

对应的,在管理员页面也加上一个评论管理页面,以及开启内置评论的开关。因为最初设计的是评论一经发出,只能删除,不能修改,所以这种页面对我这样的CRUD程序员来说不在话下。

现在就到了激动人心的时刻了,把Disqus的评论数据迁移过来!我到Disqus页面上去看,发现Disqus支持导出评论数据为特定的结构,是一个xml,只要是结构化的数据,那就问题不大了。主要分为两个部分,前半部分是thread的列表,表示有哪些文章开启了Disqus的评论,包含文章的url等信息(取决于你如何开启的Disqus),后半部分是评论列表,每条评论有评论内容、作者信息、回复的上级评论ID,还好数据模型设计得好,这些都在射程范围内。于是写了一个函数解析,导入这些数据,注意有些已删除的或者垃圾评论直接过滤掉即可,函数放在这里了。

如何使用 Flask 做一个评论系统

上传文件,导入,成功,Disqus的评论就完美迁移过来了!

评论通知

评论通知需要拿到用户的联系方式,所以表单中电子邮件是必填的,接入第三方登录时,我也要考虑哪些服务是可以获得联系方式的,目前决定是用Github,Google两种方式,至于新浪微博,虽然国人常用,但好像没有谁会在微博上留联系方式,所以排除,微信倒是很好,但微信的第三方登录好像很麻烦的样子,暂不考虑。所以最后就是邮件通知。那就简单了,用Flask的扩展Flask-Mail全都搞定,但在使用中我遇到两个坑:

如果在后台任务中做发送邮件的操作,注意获取g对象需要应用上下文,获取请求信息需要请求上下文,而光用Flask提供的copy_current_request_context只复制请求上下文,而会创建新的应用上下文,我写了两个函数,一个是添加应用和请求上下文到一个函数,另一个是将函数转换成后台任务:

def with_app_context(f):
  ctx = _app_ctx_stack.top
  req_ctx = _request_ctx_stack.top.copy()

  def wrapper(*args, **kwargs):
    with ctx:
      with req_ctx:
        return f(*args, **kwargs)
  return update_wrapper(wrapper, f)


def background_task(f):
  def wrapper(*args, **kwargs):
    future = gevent.spawn(with_app_context(f), *args, **kwargs)

    def callback(result):
      exc = result.exception
      current_app.log_exception((type(exc), exc, exc.__traceback__))

    future.link_exception(with_app_context(callback))
    return future

  return update_wrapper(wrapper, f)

这里后台任务用了gevent,如果用线程方式,则改成

from concurrent.futures import ThreadPoolExecutor

def background_task(f):
  def wrapper(*args, **kwargs):
    with ThreadPoolExecutor() as pool:
      future = pool.submit(with_app_context(f), *args, **kwargs)

    def callback(result):
      exc = result.exception()
      if exc is not None:
        current_app.log_exception((type(exc), exc, exc.__traceback__))

    future.add_done_callback(with_app_context(callback))
    return future

  return update_wrapper(wrapper, f)

腾讯云的主机默认禁掉了25端口,害我找了半天原因,只要自己在控制台解禁一下即可立刻生效。

参考链接

源码仓库: https://github.com/frostming/Flog
Implementing User Comments with SQLAlchemy - miguelgrinberg.com

以上就是如何使用 Flask 做一个评论系统的详细内容,更多关于flask 做评论系统的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
Python Socket实现简单TCP Server/client功能示例
Aug 05 Python
python3如何将docx转换成pdf文件
Mar 23 Python
python 常用的基础函数
Jul 10 Python
简单瞅瞅Python vars()内置函数的实现
Sep 27 Python
python GUI库图形界面开发之PyQt5信号与槽多窗口数据传递详细使用方法与实例
Mar 08 Python
通过Python实现一个简单的html页面
May 16 Python
Python-openCV开运算实例
Jul 05 Python
Python利用pip安装tar.gz格式的离线资源包
Sep 14 Python
简单了解Python字典copy与赋值的区别
Sep 16 Python
Python进行特征提取的示例代码
Oct 15 Python
python爬虫调度器用法及实例代码
Nov 30 Python
matplotlib bar()实现百分比堆积柱状图
Feb 24 Python
python+openCV对视频进行截取的实现
Nov 27 #Python
Python环境配置实现pip加速过程解析
Nov 27 #Python
python实现学生信息管理系统(精简版)
Nov 27 #Python
基于Python采集爬取微信公众号历史数据
Nov 27 #Python
Django中日期时间型字段进行年月日时分秒分组统计
Nov 27 #Python
Python基于execjs运行js过程解析
Nov 27 #Python
celery在python爬虫中定时操作实例讲解
Nov 27 #Python
You might like
用PHP+MySQL搭建聊天室功能实例代码
2012/08/20 PHP
编写php应用程序实现摘要式身份验证的方法详解
2013/06/08 PHP
PHP获取文件夹大小函数用法实例
2015/07/01 PHP
使用PHP实现生成HTML静态页面
2015/11/18 PHP
Smarty环境配置与使用入门教程
2016/05/11 PHP
PHP微信支付实例解析
2016/07/22 PHP
使用 PHP Masked Package 屏蔽敏感数据的实现方法
2019/10/15 PHP
JS 学习笔记 防止发生命名冲突
2009/07/30 Javascript
javascript的函数、创建对象、封装、属性和方法、继承
2011/03/10 Javascript
jQuery表格行换色的三种实现方法
2011/06/27 Javascript
关于JavaScript中name的意义冲突示例介绍
2014/05/29 Javascript
Javascript学习指南
2014/12/01 Javascript
浅谈javascript 函数内部属性
2015/01/21 Javascript
使用nodejs中httpProxy代理时候出现404异常的解决方法
2016/08/15 NodeJs
xcode中获取js文件的路径方法(推荐)
2016/11/05 Javascript
javascript 实现动态侧边栏实例详解
2016/11/11 Javascript
Chrome调试折腾记之JS断点调试技巧
2017/09/11 Javascript
JavaScript中Object基础内部方法图
2018/02/05 Javascript
layui的table中显示图片方法
2018/08/17 Javascript
从零开始实现Vue简单的Toast插件
2018/12/03 Javascript
微信小程序 Storage更新详解
2019/07/16 Javascript
解决layui追加或者动态修改的表单元素“没效果”的问题
2019/09/18 Javascript
vue element-ui实现input输入框金额数字添加千分位
2019/12/29 Javascript
删除目录下相同文件的python代码(逐级优化)
2012/05/25 Python
python读写json文件的简单实现
2017/04/11 Python
使用Python &amp; Flask 实现RESTful Web API的实例
2017/09/19 Python
Python实现12306火车票抢票系统
2019/07/04 Python
python实现简单聊天室功能 可以私聊
2019/07/12 Python
Django Docker容器化部署之Django-Docker本地部署
2019/10/09 Python
pycharm设置默认的UTF-8编码模式的方法详解
2020/06/01 Python
测绘工程专业求职信
2014/07/15 职场文书
群众路线教育实践活动对照检查材料思想汇报(副处级领导)
2014/10/04 职场文书
2014年人力资源工作总结
2014/11/19 职场文书
Spring实现内置监听器
2021/07/09 Java/Android
python之json文件转xml文件案例讲解
2021/08/07 Python
SQLServer常见数学函数梳理总结
2022/08/05 MySQL