如何使用 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脚本处理空格的方法
Aug 08 Python
pandas 将索引值相加的方法
Nov 15 Python
使用Python制作表情包实现换脸功能
Jul 19 Python
django之静态文件 django 2.0 在网页中显示图片的例子
Jul 28 Python
修改Pandas的行或列的名字(重命名)
Dec 18 Python
python 实现图像快速替换某种颜色
Jun 04 Python
keras实现基于孪生网络的图片相似度计算方式
Jun 11 Python
Python开发入门——迭代的基本使用
Sep 03 Python
python中使用.py配置文件的方法详解
Nov 23 Python
python Pexpect模块的使用
Dec 25 Python
FP-growth算法发现频繁项集——构建FP树
Jun 24 Python
Python中非常使用的6种基本变量的操作与技巧
Mar 22 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如何抛出异常处理错误
2011/03/02 PHP
如何解决CI框架的Disallowed Key Characters错误提示
2013/07/05 PHP
Laravel中扩展Memcached缓存驱动实现使用阿里云OCS缓存
2015/02/10 PHP
利用php_imagick实现复古效果的方法
2016/10/18 PHP
php设计模式之中介者模式分析【星际争霸游戏案例】
2020/03/23 PHP
JavaScript 无符号右移运算符
2009/04/17 Javascript
百度移动版的url编码解码示例
2014/04/29 Javascript
JQuery中上下文选择器实现方法
2015/05/18 Javascript
JavaScript编程的单例设计模讲解
2015/11/10 Javascript
JS在一定时间内跳转页面及各种刷新页面的实现方法
2016/05/26 Javascript
JavaScript进阶练习及简单实例分析
2016/06/03 Javascript
Vue.js每天必学之计算属性computed与$watch
2016/09/05 Javascript
微信js-sdk界面操作接口用法示例
2016/10/12 Javascript
详解前后端分离之VueJS前端
2017/05/24 Javascript
vue实现图片加载完成前的loading组件方法
2018/02/05 Javascript
详解Vue中的scoped及穿透方法
2019/04/18 Javascript
Node.js实现一个HTTP服务器的方法示例
2019/05/13 Javascript
vue+elementUI实现表单和图片上传及验证功能示例
2019/05/14 Javascript
layer ui插件显示tips时,修改字体颜色的实现方法
2019/09/11 Javascript
使用python实现rsa算法代码
2016/02/17 Python
Python脚本实现虾米网签到功能
2016/04/12 Python
Python中实例化class的执行顺序示例详解
2018/10/14 Python
tensorflow之tf.record实现存浮点数数组
2020/02/17 Python
IE10 Error.stack 让脚本调试更加方便快捷
2013/04/22 HTML / CSS
HTML5事件方法全部汇总
2016/05/12 HTML / CSS
Bed Bath & Beyond加拿大官网:购买床上用品、浴巾、厨房电器等
2019/10/04 全球购物
Magee 1866官网:Donegal粗花呢外套和大衣专家
2019/11/01 全球购物
实习老师个人总结的自我评价
2013/09/28 职场文书
校园安全标语
2014/06/07 职场文书
2014年国庆节活动总结
2014/08/26 职场文书
终止劳动合同协议书
2014/10/05 职场文书
违反交通安全法检讨书
2014/10/24 职场文书
2015年宣传部工作总结范文
2015/03/31 职场文书
单独二胎证明
2015/06/24 职场文书
分享五个Node.js开发的优秀实践 
2022/04/07 NodeJs
JDK8中String的intern()方法实例详细解读
2022/09/23 Java/Android