如何使用 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 文件操作实现代码
Oct 07 Python
Python脚本实现下载合并SAE日志
Feb 10 Python
python每隔N秒运行指定函数的方法
Mar 16 Python
python输出当前目录下index.html文件路径的方法
Apr 28 Python
python获取局域网占带宽最大3个ip的方法
Jul 09 Python
详解Python实现按任意键继续/退出的功能
Aug 19 Python
python框架django基础指南
Sep 08 Python
Python实现的拟合二元一次函数功能示例【基于scipy模块】
May 15 Python
Python解决走迷宫问题算法示例
Jul 27 Python
Python安装selenium包详细过程
Jul 23 Python
解决pandas展示数据输出时列名不能对齐的问题
Nov 18 Python
Python web如何在IIS发布应用过程解析
May 27 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数据浏览器
2007/03/11 PHP
php rsa 加密,解密,签名,验签详解
2016/12/06 PHP
PHP编程实现微信企业向用户付款的方法示例
2017/07/26 PHP
laravel 验证错误信息到 blade模板的方法
2019/09/29 PHP
JS array 数组详解
2009/03/22 Javascript
JS函数验证总结(方便js客户端输入验证)
2010/10/29 Javascript
JS鼠标滑过图片时切换图片实现思路
2013/09/12 Javascript
jquery与prototype框架的详细对比
2013/11/21 Javascript
CSS javascript 结合实现悬浮固定菜单效果
2015/08/23 Javascript
如何使用jquery修改css中带有!important的样式属性
2016/04/28 Javascript
微信小程序 支付功能开发错误总结
2017/02/21 Javascript
一次围绕setTimeout的前端面试经验分享
2017/06/15 Javascript
在一个页面实现两个zTree联动的方法
2017/12/20 Javascript
关于js对textarea换行符的处理方法浅析
2018/08/03 Javascript
浅谈JavaScript 代码整洁之道
2018/10/23 Javascript
JavaScript禁用右键单击优缺点分析
2019/01/20 Javascript
vue结合el-upload实现腾讯云视频上传功能
2020/07/01 Javascript
JavaScript日期库date-fn.js使用方法解析
2020/09/09 Javascript
python计算N天之后日期的方法
2015/03/31 Python
python实现的希尔排序算法实例
2015/07/01 Python
Python-Tkinter Text输入内容在界面显示的实例
2019/07/12 Python
python实现文字版扫雷
2020/04/24 Python
Python更改pip镜像源的方法示例
2020/12/01 Python
基于Html5实现的react拖拽排序组件示例
2018/08/13 HTML / CSS
移动端Html5页面生成图片解决方案
2018/08/07 HTML / CSS
amazeui树节点自动展开折叠面板并选中第一个树节点的实现
2020/08/24 HTML / CSS
Super-Pharm波兰:药房和香水在一个地方
2020/08/18 全球购物
写自荐信的七个技巧
2013/10/15 职场文书
高中语文教学反思
2014/01/16 职场文书
中学运动会广播稿
2014/01/19 职场文书
2015年医德医风工作总结
2015/04/02 职场文书
清明扫墓感想
2015/08/11 职场文书
优秀毕业生主要事迹材料
2015/11/04 职场文书
十二月早安励志心语大全
2019/12/03 职场文书
在Centos 8.0中安装Redis服务器的教程详解
2022/03/21 Redis
源码安装apache脚本部署过程详解
2022/09/23 Servers