如何使用 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逐行读取文件内容的三种方法
Jan 20 Python
python中while循环语句用法简单实例
May 07 Python
python连接MySQL数据库实例分析
May 12 Python
Python 单元测试(unittest)的使用小结
Nov 14 Python
PyQt5 QTable插入图片并动态更新的实例
Jun 18 Python
对Python函数设计规范详解
Jul 19 Python
Python Subprocess模块原理及实例
Aug 26 Python
wxPython:python首选的GUI库实例分享
Oct 05 Python
Python对接支付宝支付自实现功能
Oct 10 Python
Python 实现OpenCV格式和PIL.Image格式互转
Jan 09 Python
用Python 爬取猫眼电影数据分析《无名之辈》
Jul 24 Python
如何Tkinter模块编写Python图形界面
Oct 14 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 extract 将数组拆分成多个变量的函数
2010/06/30 PHP
php array_map()数组函数使用说明
2011/07/12 PHP
PHP版网站缓存加快打开速度的方法分享
2012/06/03 PHP
PHP根据传入参数合并多个JS和CSS文件的简单实现
2014/06/13 PHP
PHP中Restful api 错误提示返回值实现思路
2016/04/12 PHP
PHP实现一个轻量级容器的方法
2019/01/28 PHP
laravel框架模型中非静态方法也能静态调用的原理分析
2019/11/23 PHP
用Javascript实现UTF8编码转换成gb2312编码
2006/12/22 Javascript
JavaScript 脚本将当地时间转换成其它时区
2009/03/19 Javascript
jQuery+CSS 实现的超Sexy下拉菜单
2010/01/17 Javascript
javascript 仿QQ滑动菜单效果代码
2010/09/03 Javascript
js 可拖动列表实现代码
2011/12/13 Javascript
js实现全屏漂浮广告移入光标停止移动
2013/12/02 Javascript
jquery动态添加删除一行数据示例
2014/06/12 Javascript
第五篇Bootstrap 排版
2016/06/21 Javascript
Javascript的动态增加类的实现方法
2016/10/20 Javascript
JavaScript实现简单的星星评分效果
2017/05/18 Javascript
vue中for循环更改数据的实例代码(数据变化但页面数据未变)
2017/09/15 Javascript
angular基于ng-alain定义自己的select组件示例
2018/02/23 Javascript
Vue自定义全局Toast和Loading的实例详解
2019/04/18 Javascript
使用vue-cli3新建一个项目并写好基本配置(推荐)
2019/04/24 Javascript
[37:45]2014 DOTA2国际邀请赛中国区预选赛5.21 DT VS Orenda
2014/05/22 DOTA
[49:40]2018DOTA2亚洲邀请赛小组赛 A组加赛 TNC vs Newbee
2018/04/03 DOTA
Python中__name__的使用实例
2015/04/14 Python
详解python 发送邮件实例代码
2016/12/22 Python
python基础_文件操作实现全文或单行替换的方法
2017/09/04 Python
python绘制立方体的方法
2018/07/02 Python
TensorFlow——Checkpoint为模型添加检查点的实例
2020/01/21 Python
python 读txt文件,按‘,’分割每行数据操作
2020/07/05 Python
使用Python将xmind脑图转成excel用例的实现代码(一)
2020/10/12 Python
详解python算法常用技巧与内置库
2020/10/17 Python
小学清明节活动方案
2014/03/08 职场文书
寻找最美乡村教师观后感
2015/06/18 职场文书
教师廉政准则心得体会
2016/01/20 职场文书
忠诚教育学习心得体会
2016/01/23 职场文书
选对餐饮营销策略,营业额才会上涨
2019/08/27 职场文书