以下学习笔记来自Datawhale组队学习的推荐系统课程,项目地址: https://github.com/datawhalechina/fun-rec
本次任务为前后端的交互,目的是为了更加细致的了解整个系统的前后端交互细节,以及更全面的了解一个推荐系统所需的组成部分。
用户注册登录
为了对每个用户进行个性化推荐,需要每个使用该系统的人都先进行注册登入,使用雪花算法(一种ID生成算法)为每个用户生成唯一的用户id,根据用户的历史行为,实现对用户进行个性化推荐的效果。
注册部分
server.py的register 函数
def register():
"""用户注册"""
request_str = request.get_data()
request_dict = json.loads(request_str)
user = RegisterUser()
user.username = request_dict["username"]
user.passwd = request_dict["passwd"]
result = UserAction().user_is_exist(user, "register")
if result != 0:
return jsonify({"code": 500, "mgs": "this username is exists"})
user.userid = snowflake.client.get_guid()
user.age = request_dict["age"]
user.gender = request_dict["gender"]
user.city = request_dict["city"]
save_res = UserAction().save_user(user)
if not save_res:
return jsonify({"code": 500, "mgs": "register fail."})
return jsonify({"code": 200, "msg": "register success."})
在注册界面填写用户信息并点击注册按钮,前端会把json信息传给后端,后端会记录一些用户的一些基础属性,并将用户的注册信息写入msyql表当中。
登录部分:
server.py的login 函数
@app.route('/recsys/login', methods=["POST"])
def login():
"""用户登录
"""
request_str = request.get_data()
request_dict = json.loads(request_str)
user = RegisterUser()
user.username = request_dict["username"]
user.passwd = request_dict["passwd"]
try:
result = UserAction().user_is_exist(user, "login")
if result == 1:
return jsonify({"code": 200, "msg": "login success"})
elif result == 2:
return jsonify({"code": 500, "msg": "passwd is error"})
else:
return jsonify({"code": 500, "msg": "this username is not exist!"})
except Exception as e:
return jsonify({"code": 500, "mgs": "login fail."})
用户登陆部分,前端通过将输入的账号密码通过POST请求传给 /recsys/login,通过UserAction().user_is_exist()方法查询数据库中的用户名或者密码是否存在,其中1表示账号密码正确,2表示密码错误,0表示用户不存在。 如上图所示,点击登录后,前端调用login这个接口,将账号密码传给后端,后端判断该用户的确在用户数据库中,于是返回登陆成功的信息。
user_is_exist() 查询数据库中的用户名或者密码是否存在:
推荐页列表
该推荐系统项目通过瀑布流的方式将新闻内容进行展现,笔者上个暑假实习过一段时间,做的app也是通过瀑布流的方式将内容展示出来。通过瀑布流展示数据有很多好处,一次性将所有符合条件的数据取出并展示的花销太大了,通过每次取部分数据展示给用户,可满足大部分情况下用户在所给的数据中得到想要的信息,若当前数据不能满足需要,当窗口下滑超过所给数据时再一次调用接口即可。
@app.route('/recsys/rec_list', methods=["GET"])
def rec_list():
"""推荐页"""
user_name = request.args.get('user_id')
page_id = request.args.get('page_id')
user_id = UserAction().get_user_id_by_name(user_name)
if not user_id:
return False
if user_id is None or page_id is None:
return jsonify({"code": 2000, "msg": "user_id or page_id is none!"})
try:
rec_news_list = recsys_server.get_rec_list(user_id, page_id)
if len(rec_news_list) == 0:
return jsonify({"code": 500, "msg": "rec_list data is empty."})
return jsonify({"code": 200, "msg": "request rec_list success.", "data": rec_news_list, "user_id": user_id})
except Exception as e:
print(str(e))
return jsonify({"code": 500, "msg": "redis fail."})
该部分的主要逻辑是前端通过请求 “/recsys/rec_list” 接口,后端通过前端传递过来的用户姓名,从数据库中获取用户id,再根据用户id去推荐服务(recsys_server)中获取到推荐列表。
获取用户推荐列表
我们知道用户的推荐列表是通过推荐服务的 get_rec_list(user_id, page_id) 接口获取到的。其中需要两个参数:
- user_id:通过用户id,我们可以去redis中查找已经给用户构建好的新闻列表,将新闻信息返回给前端。
- page_id:通过page id定位到目前已经给用户推荐到列表的位置,然后在从该位置之后去新的新闻内容。
def get_rec_list(self, user_id, page_id):
"""给定页面的展示范围进行展示 user_id 后面做个性化推荐的时候需要用到"""
s = (int(page_id) - 1) * 10
e = s + 9
news_id_list = self.reclist_redis_db.zrange("rec_list", start=s, end=e)
news_info_list = []
news_expose_list = []
for news_id in news_id_list:
news_info_dict = self._get_news_simple(news_id)
news_info_list.append(news_info_dict)
news_expose_list.append(news_info_dict["news_id"])
self._save_user_exposure(user_id,news_expose_list)
return news_info_list
这里的逻辑,主要是先根据page id,计算从redis中推荐列表取的范围。在得到新闻id列表之后,通过_get_news_simple() 方法从mysql何redis中获取新闻列表所需的展现内容。
为了提高用户体验,这里考虑将已经在推荐列表中给用户曝光过的新闻,当天内不会再通过热门页对用户进行曝光。因此这里需要利用_save_user_exposure()方法来将已经曝光过的新闻存储到redis中,这样在热门推荐中,针对用户的曝光会对热门推荐的内容进行过滤。 在登录后前端请求了两次rec_list接口,当我们鼠标滚轮下滑时,前端又请求了一次rec_list接口,并成功显示出数据。
热门推荐页
热门推荐页部分,前端通过请求’/recsys/hot_list’接口,通过传递用户姓名和当前页号来获取热门新闻列表。主要的逻辑和获取推荐页相同,区别在于热门新闻信息主要是通过推荐服务(recsys_server)中的get_hot_list()方法来获取到热门新闻推荐列表。
@app.route('/recsys/hot_list', methods=["GET"])
def hot_list():
"""热门页面"""
if request.method == "GET":
user_name = request.args.get('user_id')
page_id = request.args.get('page_id')
if user_name is None or page_id is None:
return jsonify({"code": 2000, "msg": "user_name or page_id is none!"})
user_id = UserAction().get_user_id_by_name(user_name)
if not user_id:
return False
try:
rec_news_list = recsys_server.get_hot_list(user_id)
if len(rec_news_list) == 0:
return jsonify({"code": 200, "msg": "request redis data fail."})
return jsonify({"code": 200, "msg": "request hot_list success.", "data": rec_news_list, "user_id": user_id})
except Exception as e:
print(str(e))
return jsonify({"code": 2000, "msg": "request hot_list fail."})
新闻详情页
该部分主要包含一些新闻的详细信息,其中还有两个按钮,用于收集用户的显性反馈,用户可以根据自己对该文章的喜好程度进行喜欢和收藏的反馈内容。
@app.route('/recsys/news_detail', methods=["GET"])
def news_detail():
"""一篇文章的详细信息"""
user_name = request.args.get('user_name')
news_id = request.args.get('news_id')
user_id = UserAction().get_user_id_by_name(user_name)
if news_id is None or user_name is None:
return jsonify({"code": 2000, "msg": "news_id is none or user_name is none!"})
try:
news_detail = recsys_server.get_news_detail(news_id)
if UserAction().get_likes_counts_by_user(user_id,news_id) > 0:
news_detail["likes"] = True
else:
news_detail["likes"] = False
if UserAction().get_coll_counts_by_user(user_id,news_id) > 0:
news_detail["collections"] = True
else:
news_detail["collections"] = False
return jsonify({"code": 0, "msg": "request news_detail success.", "data": news_detail})
except Exception as e:
print(str(e))
return jsonify({"code": 2000, "msg": "error"})
上面就是详情页的后端逻辑,通过用户名字从mysql中获取用户id信息。防止用户id或者 page id出现空值的情况,需要进行判断。紧接着通过recsys_server服务的get_news_detail()方法,根据新闻的id进行获取内容。
如果用户对该新闻之前点击过喜欢或收藏,再次点击该新闻应该在喜欢或收藏按钮应该是点亮状态,因此还需要根据mysql中再次查询用户与该新闻是否存在记录,并将结果返回给前端,将其进行点亮展示。这里采用两个字段likes和collections,通过True,False来判断用户对该文章之前是否点击过喜欢或收藏。
用户的行为
在该系统中,用户在看新闻时主要会留下三种用户行为:一是阅读,即用户在点击一篇新闻的详细页时,用户产生的行为;二是喜欢,在新闻详情页下面会存在喜欢按钮,用户可以通过点击按钮触发系统记录该行为;三是收藏,和喜欢行为同理,需要通过用户主动的方式来触发。
因此在用户点进一篇新闻的详情页时候,前端会发送一个请求,并给后端传递一个json格式数据:
{
"user_name":"wang",
"news_id":"0a745412-db48-4e37-bf13-9a5b56028f7e",
"action_time":1638532127190,
"action_type":"read"
}
通过前端的传递的数据,后端对应的接口可以通过传递的参数对用户行为进行记录:
@app.route('/recsys/action', methods=["POST"])
def actions():
"""用户的行为:阅读,点赞,收藏"""
request_str = request.get_data()
request_dict = json.loads(request_str)
username = request_dict.get('user_name')
newsid = request_dict.get('news_id')
actiontype = request_dict.get("action_type")
actiontime = request_dict.get("action_time")
userid = UserAction().get_user_id_by_name(username)
if not userid:
return jsonify({"code": 2000, "msg": "user not register"})
action_type_list = actiontype.split(":")
if len(action_type_list) == 2:
_action_type = action_type_list[0]
if action_type_list[1] == "false":
if _action_type=="likes":
UserAction().del_likes_by_user(userid,newsid)
elif _action_type=="collections":
UserAction().del_coll_by_user(userid,newsid)
else:
if _action_type=="likes":
userlikes = UserLikes()
userlikes.new(userid,username,newsid)
UserAction().save_one_action(userlikes)
elif _action_type=="collections":
usercollections = UserCollections()
usercollections.new(userid,username,newsid)
UserAction().save_one_action(usercollections)
try:
logitem = LogItem()
logitem.new(userid,newsid,action_type_list[0])
LogController().save_one_log(logitem)
recsys_server.update_news_dynamic_info(news_id=newsid,action_type=action_type_list)
return jsonify({"code": 200, "msg": "action success"})
except Exception as e:
print(str(e))
return jsonify({"code": 2000, "msg": "action error"})
用户行为记录:
在前端传递过来的数据中存在一个字段 “action_type”:“like:ture” 或 “action_type”:“like:false”(收藏行为类似),对于action_type参数,其值会是一个组合字符串,冒号前面表示用户的具体行为,冒号后面表示用户当前的行为是点击喜欢还是取消喜欢。
通过true和false我们不仅可以知道当前用户是点击还是取消,其实还可以知道在数据库中是否存在该用户对该新闻的行为记录。原因是当传递来的是false时,表明like的状态是从true变为false,因此数据库中肯定会存在该记录,如果是true,表明like的状态是从false变为true,表明此时数据库中不存在该用户对该新闻的行为记录。通过这样的方式,我们可以比较简单的对数据库进行操作,记录用户的行为。
用户行为落日志:
在企业中,任何系统都会有日志的存在,其中最主要的作用是,日志相当于一个监控器,可以随时监测系统是否出现故障,通过日志可以及时定位系统中可能存在的问题。但是我们说的日志还有所区别,我们这里所说的日志主要是记录的一些线上信息,通过日志的方式进行记录,类似于我们这个系统,用户线上存在的行为,对于我们来说是十分具有意义的,我们需要通过分析这样的用户行为来更好的了解用户兴趣,从而进行更加个性化的推荐。
该新闻推荐系统中,这么做的原因有以下几点:
- 通过这样的方式让大家体会到日志的意义,我们可以直接通过日志获取一些线上有意义的用户数据。
- 通过日志数据,可以帮助我们更新用户画像中的一些动态特征。
- 在后面构建模型时,我们也能获取到用户的一些点击率,收藏率的建模,为后面的工作提供数据基础。
上诉代码中,我们通过 LogController() 的 save_one_log() 方法对数据进行了存储到了mysql中。
新闻动态数据更新
由于我们在展现时会显示该新闻的阅读人数、喜欢人数和收藏人数,因此用户的行为实际上会改变新闻这三个属性。因此我们需要更新redis中新闻的这些动态的数据。
主要是通过推荐服务里面的 update_news_dynamic_info()方法进行更新。
def update_news_dynamic_info(self, news_id,action_type):
"""更新新闻展示的详细信息"""
news_dynamic_info_str = self.dynamic_news_info_redis_db.get("dynamic_news_detail:" + news_id)
news_dynamic_info_str = news_dynamic_info_str.replace("'", '"' )
news_dynamic_info_dict = json.loads(news_dynamic_info_str)
if len(action_type) == 2:
if action_type[1] == "true":
news_dynamic_info_dict[action_type[0]] +=1
elif action_type[1] == "false":
news_dynamic_info_dict[action_type[0]] -=1
else:
news_dynamic_info_dict["read_num"] +=1
news_dynamic_info_str = json.dumps(news_dynamic_info_dict)
news_dynamic_info_str = news_dynamic_info_str.replace('"', "'" )
res = self.dynamic_news_info_redis_db.set("dynamic_news_detail:" + news_id, news_dynamic_info_str)
return res
上述代码主要是新闻动态特征更新的部分,主要是获取redis中的信息,根据前端传递过来的行为来更新对用新闻属性的值。更改完之后,从新将新的结果从新存储到redis中。 点击喜欢,就会调用action接口 如上图,再次请求该页面,会发现likes字段变为了true。
|