如何使用 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对离散变量的one-hot编码方法
Jul 11 Python
python 实现对数据集的归一化的方法(0-1之间)
Jul 17 Python
Python Pywavelet 小波阈值实例
Jan 09 Python
Python反爬虫技术之防止IP地址被封杀的讲解
Jan 09 Python
python树莓派红外反射传感器
Jan 21 Python
在pytorch中为Module和Tensor指定GPU的例子
Aug 19 Python
Python打包工具PyInstaller的安装与pycharm配置支持PyInstaller详细方法
Feb 27 Python
Python matplotlib绘制图形实例(包括点,曲线,注释和箭头)
Apr 17 Python
JupyterNotebook 输出窗口的显示效果调整实现
Sep 22 Python
python对批量WAV音频进行等长分割的方法实现
Sep 25 Python
python状态机transitions库详解
Jun 02 Python
基于python定位棋子位置及识别棋子颜色
Jul 26 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 $_SERVER当前完整url的写法
2009/11/12 PHP
PHP使用PHPexcel导入导出数据的方法
2015/11/14 PHP
Zend Framework实现多文件上传功能实例
2016/03/21 PHP
JavaScript 全面解析各种浏览器网页中的JS 执行顺序
2009/02/17 Javascript
Javascript 面向对象编程(一) 封装
2011/08/28 Javascript
常用的JavaScript模板引擎介绍
2015/02/28 Javascript
为什么JS中eval处理JSON数据要加括号
2015/04/13 Javascript
JavaScript中reduce()方法的使用详解
2015/06/09 Javascript
javascript中的作用域和闭包详解
2016/01/13 Javascript
利用jQuery实现打字机字幕效果实例代码
2016/09/02 Javascript
jquery操作checkbox火狐下第二次无法勾选的解决方法
2016/10/10 Javascript
详解create-react-app 2.0版本如何启用装饰器语法
2018/10/23 Javascript
解决基于 keep-alive 的后台多级路由缓存问题
2020/12/23 Javascript
windows10系统中安装python3.x+scrapy教程
2016/11/08 Python
Python使用SQLite和Excel操作进行数据分析
2018/01/20 Python
用Python徒手撸一个股票回测框架搭建【推荐】
2019/08/05 Python
浅析Python数字类型和字符串类型的内置方法
2019/12/22 Python
Python中remove漏删和索引越界问题的解决
2020/03/18 Python
Django 用户认证Auth组件的使用
2020/11/30 Python
selenium框架中driver.close()和driver.quit()关闭浏览器
2020/12/08 Python
Python 获取异常(Exception)信息的几种方法
2020/12/29 Python
如何使用css3实现一个类在线直播的队列动画的示例代码
2020/06/17 HTML / CSS
Net-A-Porter美国官网:全球时尚奢侈品名站
2017/02/11 全球购物
Helly Hansen工作服美国官方网上商店:为最恶劣的环境
2019/09/04 全球购物
海蓝之谜英国官网:La Mer英国
2020/01/15 全球购物
电子装配专业毕业生求职信
2014/04/23 职场文书
汽车运用工程专业求职信
2014/06/18 职场文书
幼儿园大班区域活动总结
2014/07/09 职场文书
经典毕业生求职信
2014/07/12 职场文书
学生逃课检讨书1000字
2014/10/20 职场文书
2014年优秀党员材料
2014/12/18 职场文书
入党函调证明材料
2014/12/24 职场文书
2015年信访维稳工作总结
2015/04/07 职场文书
大学军训心得体会800字
2016/01/11 职场文书
2016年优秀少先队辅导员事迹材料
2016/02/26 职场文书
Python保存并浏览用户的历史记录
2022/04/29 Python