redis搭建论坛

主要功能

用户账号
创建帖子、回复帖子、为帖子投票
为帖子分类,查看属于特定分类的帖子
将多个分类(category)归类至某个标签(tab),查看属于特定标签的帖子
每日热议帖子排行榜
基于算法的帖子推荐

创建用户账号

用户名(未使用)
密码
电子邮箱(未使用)

需要解决的问题:

  • 实现一个检查指定的邮箱和名字是否已经被使用的程序。

  • 实现一个储存邮箱地址、密码和名字等用户信息的程序,并为每个用户分配一个唯一的用户 ID

user类

from id_generator import IdGenerator

def make_user_key(uid):
    return 'bbs::user::' + str(uid)

def make_email_to_uid_key():
    return 'bbs::email_to_uid'

class User:

    """
    用户相关操作。
    """

    def __init__(self, client):
        self.client = client

    def create(self, name, email, password):
        """
        创建一个新的用户,创建前需要确保输入的用户名和邮箱未被使用。
        """
        # 储存用户信息
        new_id = IdGenerator(client, "bbs::user_id").gen()
        key = make_user_key(new_id)
        self.client.hmset(key, {
                                'id': new_id,
                                'name': name,
                                'email': email,
                                'password': password
                                })
        # 关联用户 email 与 ID
        self.client.hset(make_email_to_uid_key(), email, new_id)
        # 返回用户 ID 作为函数的值
        return new_id

    def get_by_id(self, uid):
        """
        根据给定的用户 id ,获取用户的详细信息。
        """
        key = make_user_key(uid)
        return self.client.hgetall(key)

    def try_login(self, email, password):
        """
        尝试使用指定的邮箱和密码进行登录,成功时返回用户信息,失败时返回 None 。
        """
        # 根据输入的邮箱,获取用户 ID
        uid = self.client.hget(make_email_to_uid_key(), email)
        # 如果找不到与邮箱对应的用户 ID ,那么说明这个邮箱未注册过账号
        if uid is None:
            return None

        # 根据用户 ID ,获取用户信息,并进行密码对比
        # 邮箱不用对比,因为用户信息就是根据邮箱来查找的
        user_info = self.get_by_id(uid)
        if user_info['password'] == password:
            return user_info

IdGenerator

class IdGenerator:

    """
    使用字符串键实现的自增唯一 ID 生成器。
    """

    def __init__(self, client, key):
        """
        设置储存 ID 生成器当前值的键。
        """
        self.client = client
        self.key = key

    def init(self, n):
        """
        初始化生成器的值,需要在系统正式运行之前调用,用于保留少于等于 n 的 ID 号码。
        """
        # 如果键 key 已经有值,那么说明 gen() 已经执行过
        # 为了防止产生重复的 ID ,程序不能执行这个 init() 操作
        if self.client.get(self.key) is not None:
            raise Exception
        self.client.set(self.key, n)

    def gen(self):
        """
        生成一个新的唯一 ID 。
        """
        new_id = self.client.incr(self.key)
        return int(new_id)

UniqueSet

    def __init__(self, client, key):    
        self.client = client
        self.key = key

    def add(self, element):
        self.client.sadd(self.key, element)

    def is_include(self, element):
        return self.client.sismember(self.key, element)

创建帖子

帖子内容

帖子的构成(用hash存储)
分类
标题
用户的投票数量
作者
发布时间
点击量
内容
TAG

例如

 HMSET bbs::topic::131853 id 131853
	 time 1410165226
	 click_counter 0
	 title “WordPress 4.0 \“Benny\” 正式版发布……”
	 author_id 48771
	 content “WordPress 4.0 简体中文版现已开放下载,……”
	 category “WordPress”
	 tags JSON([“WordPress”, “简体”, “benny”])

帖子相关

import json

from time import time
from id_generator import IdGenerator

ID_GENERATOR_KEY = 'bbs::topic_ids'

def make_topic_key(topic_id):
    return 'bbs::topic::' + str(topic_id)

class Topic:
    
    """
    帖子相关操作。
    """

    def __init__(self, client):
        self.client = client

    def create(self, title, author_id, content, category, *tags):
        """
        创建一个新帖子。
        """
        topic_id = IdGenerator(client, ID_GENERATOR_KEY).gen()
        topic_hash = make_topic_key(topic_id)
        info = {
                'id': topic_id,
                'time': time(),
                'click_counter': 0,
                'title': title,
                'author_id': author_id,
                'content': content,
                'category': category,
                'tags': json.dumps(tags)
                }
        self.client.hmset(topic_hash, info)
        return topic_id

    def get_by_id(self, topic_id):
        """
        根据给定的帖子 id ,查找并返回帖子的详细信息。
        """
        topic_hash = make_topic_key(topic_id)
        return self.client.hgetall(topic_hash)

查看帖子

按照分类(category)来展示

为了储存同一分类的帖子,并按照帖子最后一次被回复的 时间有序地排列帖子,程序会 为每个分类创建一个键名为 bbs::category:: 的有序集合,其中有序集合的元素 为帖子的 ID ,而元素的分值则是帖子最后一次被回复的 UNIX 时间戳。 因为同一分类的帖子会被有序地储存起来,所以程序只要 对有序集合调用 ZREVRANGE 命令,就可以按照回复时间从新到旧的顺序,取出属于同一分类的多个帖子。
ZADD bbs::category::WordPress 1410165226 131853
我们可以将这些针对分类的操作抽象为 Category 类。

from time import time as current_time

def make_category_key(name):
    return 'bbs::category::' + name

class Category:

    """
    创建一个分类列表来记录所有属于某个分类的文章。
    """

    def __init__(self, client, name):
        self.client = client
        self.category_zset = make_category_key(name)

    def include_topic(self, topic_id):
        """
        将指定的文章添加到当前分类里面。
        """
        # 文章按照添加的时间来排序。
        self.client.zadd(self.category_zset, topic_id, current_time())

    def is_included(self, topic_id):
        """
        检查指定的文章是否为当前分类的文章。
        """
        # 当 topic_id 作为元素存在于 ZSET 时,它的分值不为 None
        return self.client.zscore(self.category_zset, topic_id) is not None

    def count_topic(self):
        """
        返回分类目前包含的文章数量。
        """
        return self.client.zcard(self.category_zset)

    def paging(self, n, count):
        """
        按时间从新到旧的顺序,以 count 篇文章为一页,返回当前分类第 n 页上的文章。
        """
        start_index = (n-1)*count
        end_index = n*count-1
        return self.client.zrevrange(self.category_zset, start_index, end_index)

按标签(tag)来展示

标签
实现标签功能需要两个步骤:

  • 记录标签和分类之间的关系。 举个例子,程序要记住,“Linux”、“node.js”这些分类都属于“技术”标签。
  • 记录标签属下的帖子,当用户点击某个标签时,展示该标签属下的各个帖子。
    举个例子,帖子 132312 属于“程序员”分类,而该分类又属于“技术”标签,所以程序应该在用户点击“技术”页面时,展示出帖子 132312 。
    标签和分类之间的关系用集合set存储
from category import Category

from time import time as current_time

def make_tab_member_key(name):
    return 'bbs::tab::' + name + '::member'

def make_tab_list_key(name):
    return 'bbs::tab::' + name + 'topic_list'

# 使用 Tab 来记录所有分类属下文章的另一种做法是,直接对各个分类的文章列表进行并集计算:
# 这种实现可以避免每个 Tab 单独维护一个文章列表,缺点是并集计算比较耗时,
# 所以为了高效起见,这里还是使用单独维护一个文章列表的做法。
class Tab:

    """
    一个标签可以包含任意多个分类,并按照发布时间先后,记录被包含分类属下的文章。
    """

    def __init__(self, client, name):
        self.client = client
        # 储存标签属下分类的集合
        self.tab_member_set = make_tab_member_key(name)
        # 储存标属下分配的文章的有序集合
        self.tab_topic_list = make_tab_list_key(name)

    def add_member(self, category_name):
        """
        将指定分类添加到当前标签中。
        """
        self.client.sadd(self.tab_member_set, category_name)

    def is_member(self, category_name):
        """
        检查指定分类是否属于当前标签。
        """
        return self.client.sismember(self.tab_member_set, category_name)

    def get_all_member(self):
        """
        返回标签包含的所有分类。
        """
        return self.client.smembers(self.tab_member_set)

    def count_member(self):
        """
        返回标签包含的分类数量。
        """
        return self.client.scard(self.tab_member_set)

    def include_topic(self, category_name, topic_id):
        """
        将属于 category 分类的文章添加到当前标签的时间线里面。
        """
        # 先检查文章是否属于 category 分类
        # 然后检查 category 分类是否属于当前标签
        # 只有两个条件都通过时,才将文章添加到当前时间线
        category = Category(self.client, category_name)
        if self.is_member(category_name) and category.is_included(topic_id):
            self.client.zadd(self.tab_topic_list, topic_id, current_time())
            

    def paging(self, n, count):
        """
        按时间从新到旧的顺序,以 count 篇文章为一页,返回当前标签第 n 页上的文章。
        """
        start_index = (n-1)*count
        end_index = n*count-1
        return self.client.zrevrange(self.tab_topic_list, start_index, end_index)

    def count_topic(self):
        """
        返回标签目前包含的文章数量。
        """
        return self.client.zcard(self.tab_topic_list)

回复帖子

用户可以对帖子进行评论,主要实现两点:

  1. 对于每条评论,程序都会分配一个唯一的 评论 ID ,并使用散列键来储存评论的发布者、发布时间和内容。实现为comment.py
  2. 对于每条帖子,程序都会使用一个列表来 储存该微博获得的所有评论的 ID .实现为comment_list.py

comment

# encoding: utf-8

from time import time
from id_generator import IdGenerator

ID_GENERATOR_KEY = 'bbs::comment_ids'

def make_comment_key(comment_id):
    return 'bbs::comment::' + str(comment_id)

class Comment:

    """
    帖子评论相关操作。
    """

    def __init__(self, client):
        self.client = client

    def create(self, author_id, content):
        """
        创建一个新的评论。
        """
        comment_id = IdGenerator(client, ID_GENERATOR_KEY).gen()
        comment_hash = make_comment_key(comment_id)
        info = {
                'id': comment_id,
                'author_id': author_id,
                'content': content,
                'time': time()
               } 
        self.client.hmset(comment_hash, info)
        return comment_id

    def get_by_id(self, comment_id):
        """
        根据评论 id ,查找并返回评论的详细信息。
        """
        comment_hash = make_comment_key(comment_id)
        return self.client.hgetall(comment_hash)

comment_list

# encoding: utf-8

def make_comment_list_key(topic_id):
    return 'bbs::topic::' + str(topic_id) + '::comments'

class CommentList:

    """
    创建一个列表来记录某一帖子下的所有评论(的 ID)。
    """

    def __init__(self, client, topic_id):
        """
        设置被评论的帖子的 ID 。
        """
        self.client = client
        self.topic_id = topic_id
        self.comment_list = make_comment_list_key(topic_id)

    def push(self, comment_id):
        """
        将评论推入到评论列表里面。
        """
        self.client.lpush(self.comment_list, comment_id)

    def count(self):
        """
        返回帖子目前已有的评论数量。
        """
        return self.client.llen(self.comment_list)

    def paging(self, number, count):
        """
        以分页形式返回帖子的评论。
        """
        start_index = (number-1)*count
        end_index = number*count-1
        return self.client.lrange(self.comment_list, start_index, end_index)


为帖子投票

创建一个支持票的集合,里面存放对该帖子支持的用户id项
创建一个反对票的集合,里面存放对该帖子反对的用户id项
# encoding: utf-8

from redis import WatchError

def make_vote_up_key(topic_id):
    return 'bbs::topic::' + str(topic_id) + '::vote_up'

def make_vote_down_key(topic_id):
    return 'bbs::topic::' + str(topic_id) + '::vote_down'

class TopicVote:

    """
    对帖子进行投票,可以投支持票也可以投反对票。
    """

    def __init__(self, client, topic_id):
        """
        设置要被投票的帖子。
        """
        self.client = client
        self.vote_up_set = make_vote_up_key(topic_id)
        self.vote_down_set = make_vote_down_key(topic_id)

    def is_voted(self, user_id):
        """
        检查用户是否已经对这个帖子投过票。
        """
        return self.client.sismember(self.vote_up_set, user_id) or \
               self.client.sismember(self.vote_down_set, user_id)

    def vote_up(self, user_id):
        """
        用户对这个帖子投支持票。
        """
        if not self.is_voted(user_id):
            self.client.sadd(self.vote_up_set, user_id)

    def vote_down(self, user_id):
        """
        用户对这个帖子投反对票。
        """
        if not self.is_voted(user_id):
            self.client.sadd(self.vote_down_set, user_id)

    def undo(self, user_id):
        """
        撤销用户的投票。
        """
        self.client.srem(self.vote_up_set, user_id)
        self.client.srem(self.vote_down_set, user_id)

    def vote_up_count(self):
        """
        返回帖子目前获得的支持票数量。
        """
        return self.client.scard(self.vote_up_set)

    def vote_down_count(self):
        """
        返回帖子目前获得的反对票数量。
        """
        return self.client.scard(self.vote_down_set)

每日热议帖子排行

  • 每日热议帖子排行榜展示了每天回复数量最多的帖子, 这个排行榜每天更新一次。
  • 为了实现这个排行榜,程序需要使用一个 键名为 bbs::reply_day_rank 的有序集合,其中有序集合的元素 为帖子的 ID ,而元素的分值则是帖子在当前被回复的数量。
  • 每当 ID 为 N 的帖子新增一条回复时,程序就执行以下命令来增加帖子在排行榜中的回复数量
  • 程序会为 bbs:reply_day_rank 设置生存时间,让它在一天之后自动过期,并自动创建新榜单。
# encoding: utf-8

from datetime import datetime, timedelta

def today_remaind_seconds():
    """
    返回当天剩余的秒数。
    """
    now = datetime.now()
    tomorrow = now + timedelta(days=1) - timedelta(hours=now.hour, minutes=now.minute,seconds=now.second)
    return (tomorrow-now).seconds

DAY_RANK_KEY = 'bbs::reply_day_rank'

class ReplyDayRank:

    """
    按照帖子获得的回复数量,对帖子进行排行。
    排行榜每天更新一次。
    """

    def __init__(self, client):
        self.key = DAY_RANK_KEY
        self.client = client

    def incr_reply_count(self, topic_id):
        """
        对指定帖子在当天的回复数量增一,每次帖子有新回复出现时调用。
        """
        # 为计数器增一
        self.client.zincrby(self.key, topic_id, 1)
        # 如果键未设置生存时间或过期时间,那么将它的生存时间设置为当天剩下的秒数
        if self.client.ttl(self.key) is None:
            self.client.expire(self.key, today_remaind_seconds())

    def get_top(self, n, show_score=False):
        """
        返回当天回复数量排在前 n 位的帖子。
        """
        return self.client.zrevrange(self.key, 0, n-1, withscores=show_score)

帖子推荐系统

实现推荐系统的方法:
1. 选择一个推荐算法,用于计算帖子的评分。
2. 计算各个帖子的评分,并以有序的方式展示它们。
为了实现帖子系统,我们选择了 Reddit 使用的推荐算法( reddit_algorithm.py),这个算法进行评分时考虑的是帖子的发布时间、获得的支持票数量以及反 对票数量,这个算法用于计算评分的函数为:hot(ups, downs, date)
帖子推荐算法
推荐标签包含的帖子仍然使用有序集合来 储存,其中有序集合的元素 为帖子的 ID,而元素的分值则是算法为帖子计算出的评分。 当用户创建一个帖子的时候,或者有用户对帖子进行投票之后,我们就调用 how 函数来更新帖子的评分。
reddit_algorithm.py

from datetime import datetime, timedelta
from math import log

epoch = datetime(1970, 1, 1)

def epoch_seconds(date):
    """Returns the number of seconds from the epoch to date."""
    td = date - epoch
    return td.days * 86400 + td.seconds + (float(td.microseconds) / 1000000)

def score(ups, downs):
    return ups - downs

def hot(ups, downs, date):
    """The hot formula. Should match the equivalent function in postgres."""
    s = score(ups, downs)
    order = log(max(abs(s), 1), 10)
    sign = 1 if s > 0 else -1 if s < 0 else 0
    seconds = epoch_seconds(date) - 1134028003
    return round(order + sign * seconds / 45000, 7)

topic_click_counter

def make_topic_key(topic_id):
    return 'bbs::topic::' + str(topic_id)

class TopicClickCounter:

    """
    帖子的点击计数器。
    """

    def __init__(self, client, topic_id):
        """
        设置要记录点击数量的帖子。
        """
        self.client = client
        self.topic_id = topic_id
        self.topic_hash = make_topic_key(topic_id)

    def incr(self, increment=1):
        """
        对计数器执行自增操作。
        """
        return self.client.hincrby(self.topic_hash, 'click_counter', increment)

    def get(self):
        """
        获取帖子当前的点击数量。
        """
        return self.client.hget(self.topic_hash, 'click_counter')

topic_recommand

# encoding: utf-8

import reddit_algorithm

from datetime import datetime

def make_recommand_list_key():
    return 'bbs::recommand::list'

class TopicRecommand:

    """
    根据帖子获得的投票数量以及帖子的发布时间,对帖子进行评分,
    并根据评分对帖子进行排序。
    """

    def __init__(self, client):
        self.client = client
        self.recommand_list = make_recommand_list_key()

    def update_rank(self, topic_id, up_vote, down_vote, post_time):
        """
        根据文章的投票数和发布时间,更新文章在推荐列表中的排位。
        """
        # 因为算法只接受 datetime 格式的发布时间,而我们记录的文章发布时间是 UNIX 时间戳
        # 所以这里需要将 UNIX 时间戳转换为 Python 的 datetime 时间
        post_time_in_datetime_format = datetime.fromtimestamp(post_time)
        # 使用推荐算法计算出帖子的分值
        score = reddit_algorithm.hot(up_vote, down_vote, post_time_in_datetime_format)
        # 为帖子设置新的分值,并更新帖子在推荐列表中的排位
        self.client.zadd(self.recommand_list, topic_id, score)

    def paging(self, n, count):
        """
        按时间从新到旧的顺序,以 count 篇文章为一页,返回推荐列表第 n 页上的文章。
        """
        start_index = (n-1)*count
        end_index = n*count-1
        return self.client.zrevrange(self.recommand_list, start_index, end_index)

总结

功能实现方式
用户账号使用 UniqueSet 保证用户名为邮箱地址的唯一性,然后使用 User 类创建账号。
创建帖子为每个帖子分配唯一 ID ,并使用散列来储存帖子信息。
根据分类展示帖子将同一个分类的帖子放到一个有序集合里面。
根据标签展示帖子使用集合储存标签属下的所有分类,并将属于这些分类的帖子放到标签对应的有序集合里面。
回复帖子使用散列储存回复内容,为每个帖子使用一个列表来储存该帖子的所有回复。
为帖子投票重用 UpDownVote 类来对帖子进行投票。
每日热议帖子排行重用 DayRank 类来对帖子进行每日热议排行
实现帖子推荐使用 Reddit 的帖子推荐算法来计算帖子的评分,并按评分将帖子储存到有序集合里面
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值