飞道的博客

数据挖掘课程设计实战——基于Django开发的音乐推荐系统网站

168人阅读  评论(0)

当当当当~上一次发博客好像是在去年11月了。歇了这么久了,就趁这个风和日丽的下午,选在数字图像处理课上跟大家分享一下一下上学期最后的数据挖掘课程设计吧!(反正数字图像处理课我是听不懂~)

项目要求用Django开发前端可视化,前端可视化模块是我的组长负责的,所以本博客主要专注于推吉安算法模块的实现、代码讲解。如有问题欢迎与我交流。

1. 项目介绍

音乐推荐系统

必须实现的需求:用户注册/登录/退出,存储音乐数据,存储用户和歌手数据,计算音乐模型,根据基于内容的推荐算法和基于物品和用户的协同过滤算法等计算用户可能感兴趣的音乐,展示用户可能喜欢的音乐,具有选择用户,选择标签(可选歌曲、歌手等),为你推荐,歌单与歌手推荐,歌单详情与歌单详情页推荐,排行榜,我的足迹,后台管理等功能。  

2. 项目演示

网站首页

登陆页面

所有歌曲页面

“猜你喜欢”页面

“我的信息”页面

“浏览记录”页面

 

3. 模块分解

要开发搭建一个具有推荐功能的音乐网站,工程量着实不小。本次课程设计我们是分组每组三人进行的,三人之间的分工大概是这样的:

4. 数据库中的数据

我们选取了数据集中听歌次数最多的前500名用户,处理得到了这些用户听过的歌,共有25928首

数据处理过后的干净数据是长介个样子滴

【1】歌曲表(⭐歌曲热榜、全部歌曲模块所使用的表)

song_id title release artist_name year play_count
SOBONKR12A58A7A7E0 You're The One If There Was A Way Dwight Yoakam 1990 726885
SOAUWYT12A81C206F1 Undo Vespertine Live Bj?rk 2001 648239
SOSXLTC12AF72A7F54 Revelry Only By The Night Kings Of Leon 2008 527893
SOFRQTD12A81C233C0 Sehr kosmisch Musik von Harmonia Harmonia 1999 425463
SOEGIYH12A6D4FC0E3 Horn Concerto No. 4 in E flat K495: II. Romance (Andante cantabile) Mozart - Eine kleine Nachtmusik Barry Tuckwell/Academy of St Martin-in-the-Fields/Sir Neville Marriner 1999 389880
SOAXGDH12A8C13F8A1 Dog Days Are Over (Radio Edit) Now That's What I Call Music! 75 Florence + The Machine 1999 356533
.... .... .... .... .... ....

【2】歌手表(⭐歌曲热榜所使用的表)

artist_name
Linkin Park
Blackmore's Night
Vanessa Carlton
Tarkan
Stelios Kazantzides
Akon
Dierks Bentley
Ry Cooder
......


【3】用户表(⭐登录、注册模块所使用的表)

user_id username password sex age
093cb74eb3c517c5179ae24caf0ebec51b24d2a2 1 123456 17
d13609d62db6df876d3cc388225478618bb7b912 2 123456 21
4cbca37009400bb5676ba54c2a4cc24ff0531cb7 3 123456 24
d5d1fc74f29ef42eefc2acf4c8c59da2994a0a16 4 123456 19
81193c20d49bfbca4a72eaa1119f6bb2fc4d9e48 5 123456 17
d19e8c59d207bf3e3c7ca0248a1fa684c00d006b 6 123456 33
bedad07238f60df86cb77b2d4ef1441c8f0b3a3b 7 123456 31
9c6ebf1d5ba38bcb577149b22e19448f655f6252 8 123456 25
1fc2a7f42424249718cc544a0a1036a69d5bc7b8 9 123456 15
fc05f377863a77d7784b02de2cc06cdecb85968b 10 123456 27
.... .... .... .... ...

【4】年份表(⭐歌曲热榜所使用的表)

year
2007
2006
2002
1999
2004
2003
.....

【5】用户-歌曲表(⭐推荐算法模块所使用的表)

song_id user_id listen_count title play_count release artist_name year fractional_listen_count
SOABHYV12A6D4F6D0F 093cb74eb3c517c5179ae24caf0ebec51b24d2a2 11 Shadow Of The Day (Album Version) 13132 Minutes To Midnight Linkin Park 2007 0.000837648
SOABHYV12A6D4F6D0F d13609d62db6df876d3cc388225478618bb7b912 1 Shadow Of The Day (Album Version) 3657 Minutes To Midnight Linkin Park 2007 0.000273448
SOABHYV12A6D4F6D0F 4cbca37009400bb5676ba54c2a4cc24ff0531cb7 33 Shadow Of The Day (Album Version) 3470 Minutes To Midnight Linkin Park 2007 0.009510086
... ... ... ... ... ... ... ... ...

【6】在Django框架的models中建好表,把他们分别导进去就可以啦!

5. 我所负责的模块

我负责完成的是推荐算法的实现模块,实现了以下几种推荐算法:

UserCF(基于用户的协同过滤)

ItemCF(基于物品的协同过滤)

SVD分解

冷启动(对新注册的用户进行音乐的推荐)

以下是我负责的推荐算法模块的代码


  
  1. # Thanks to Siraj Raval for this module
  2. '''两种推荐算法:基于物品相似度和对于新用户的冷启动'''
  3. import pandas as pd
  4. import numpy as np
  5. from scipy.sparse import coo_matrix
  6. import math as mt
  7. from scipy.sparse.linalg import * #used for matrix multiplication
  8. from scipy.sparse.linalg import svds
  9. from scipy.sparse import csc_matrix
  10. #Class for Popularity based Recommender System model
  11. #计算统计返回歌曲表中每首歌的被听次数,用于新用户(解决冷启动)
  12. class popularity_recommender_py():
  13. def __init__(self):
  14. self.train_data = None
  15. self.user_id = None
  16. self.item_id = None
  17. self.popularity_recommendations = None
  18. def create(self, train_data, user_id, item_id):
  19. self.train_data = train_data
  20. self.user_id = user_id
  21. self.item_id = item_id #进行分类的指标:可以选择 歌曲名 歌手 年份 等等
  22. train_data_grouped = train_data.groupby([self.item_id]).agg({self.user_id: 'count'}).reset_index() #计算按分类指标的播放次数
  23. train_data_grouped.rename(columns = {self.user_id: 'score'},inplace= True)
  24. train_data_sort = train_data_grouped.sort_values([ 'score', self.item_id], ascending = [ 0, 1]) #对计算结果进行排序
  25. train_data_sort[ 'Rank'] = train_data_sort[ 'score'].rank(ascending= 0, method= 'first')
  26. self.popularity_recommendations = train_data_sort.head( 10) #返回前十名推荐歌曲作为推荐结果
  27. def recommend(self, user_id):
  28. user_recommendations = self.popularity_recommendations
  29. user_recommendations[ 'user_id'] = user_id
  30. cols = user_recommendations.columns.tolist()
  31. cols = cols[ -1:] + cols[: -1]
  32. user_recommendations = user_recommendations[cols]
  33. return user_recommendations
  34. #基于用户的歌曲相似度进行推荐(基于用户的协同过滤算法)返回的是推荐歌曲的dataFrame
  35. class user_similarity_recommender_py():
  36. def __init__(self):
  37. self.train_data = None
  38. self.user_id = None
  39. self.user_title = None #user_id
  40. self.item_title = None
  41. self.ans_user_set = {}
  42. def create(self,train_data,user_title,item_title, user_id): #构造对象
  43. self.user_title = user_title
  44. self.train_data = train_data
  45. self.item_title = item_title
  46. self.user_id = user_id
  47. def get_all_user_train_data(self): #获取training data中的所有用户id
  48. all_users = list(self.train_data[self.user_title].unique())
  49. return all_users
  50. def get_user_items(self, user): # 获取user用户听过的所有歌的列表
  51. user_data = self.train_data[self.train_data[self.user_title] == user] # user这个用户都听了哪几首歌
  52. user_items = list(user_data[self.item_title].unique())
  53. return user_items
  54. def get_similarity_users(self): #获取前十个相似的用户
  55. all_users = self.get_all_user_train_data() #得到所有用户
  56. song_user = set(self.get_user_items(self.user_id)) #得到待推荐用户听过的歌
  57. lis = []
  58. for i in all_users: #对于数据集中的每一个用户
  59. l = []
  60. song_i = set(self.get_user_items(i))
  61. songs_intersection = song_i.intersection(song_user) #计算这两名用户听过歌曲的交集
  62. if len(songs_intersection) != 0:
  63. songs_union = song_i.union(song_user) #计算这两名用户听过歌曲的并集
  64. score = float(len(songs_intersection))/float(len(songs_union)) #计算这两名用户的相似度
  65. l.append(i)
  66. l.append(score)
  67. lis.append(l)
  68. lis.sort(key= lambda x : x[ 1], reverse= True) #按相似度对用户进行排序
  69. ans = []
  70. return lis
  71. for i in range( 0, min( 5,len(lis))):
  72. ans.append(lis[i][ 0]) #得到前五名用户的user_id
  73. return ans
  74. def get_ans(self):
  75. ini_songs = self.get_user_items(self.user_id)
  76. users = self.get_similarity_users() #得到与被推荐用户最相似的前五名用户的id
  77. columns = [ 'similary_users', 'songs']
  78. ans = []
  79. for i in users: #对于每一个推荐结果用户
  80. recommend_user = i[ 0]
  81. songs = self.get_user_items(recommend_user) #得到推荐结果用户听过的所有歌
  82. for recommend_song in songs:
  83. if recommend_song not in ini_songs and recommend_song not in ans: #如果这首歌没被推荐过,待推荐用户也没有听过
  84. ans.append(recommend_song)
  85. if len(ans) == 10: #限制返回的歌曲数量
  86. return ans
  87. #Class for Item similarity based Recommender System model
  88. #基于歌曲的受众相似度进行推荐(基于物品的协同过滤算法)返回的是推荐歌曲的dataFrame
  89. class item_similarity_recommender_py():
  90. def __init__(self):
  91. self.train_data = None
  92. self.unique_song_title = None
  93. self.user_id = None
  94. self.item_id = None
  95. self.cooccurence_matrix = None
  96. self.songs_dict = None
  97. self.rev_songs_dict = None
  98. self.item_similarity_recommendations = None
  99. self.dic = {}
  100. def create(self, train_data, user_id, item_id, unique_song_title):
  101. self.train_data = train_data
  102. self.user_id = user_id
  103. self.item_id = item_id
  104. self.unique_song_title = unique_song_title
  105. self.get_my_dic()
  106. def get_user_items(self, user):
  107. user_data = self.train_data[self.train_data[self.user_id] == user] #user这个用户都听了哪几首歌
  108. user_items = list(user_data[self.item_id].unique())
  109. return user_items
  110. def get_item_users(self, item):
  111. item_data = self.train_data[self.train_data[self.item_id] == item]
  112. item_users = set(item_data[self.user_id].unique())
  113. return item_users
  114. def get_all_items_train_data(self):
  115. all_items = list(self.train_data[self.item_id].unique())
  116. # print("all_items:",all_items)
  117. return all_items
  118. def get_my_dic(self):
  119. user_id = self.train_data[ 'user_id']
  120. song_title = self.train_data[ 'title']
  121. for i in song_title:
  122. self.dic[i] = []
  123. lis = []
  124. j = 0
  125. for i in song_title:
  126. self.dic[i].append(user_id[j])
  127. j += 1
  128. def construct_cooccurence_matrix(self, user_songs, all_songs): #构造相似度矩阵
  129. cooccurence_matrix = np.matrix(np.zeros(shape=(len(user_songs), len(all_songs))), float)
  130. for i in range( 0, len(all_songs)): #遍历所有的歌
  131. users_i = set(self.dic[all_songs[i]]) #取出听过这一首歌的用户
  132. for j in range( 0, len(user_songs)): #遍历待推荐用户听过的所有歌
  133. users_j = set(self.dic[user_songs[j]]) #取出听过这一首歌的用户
  134. users_intersection = users_i.intersection(users_j)
  135. if len(users_intersection) != 0:
  136. users_union = users_i.union(users_j)
  137. cooccurence_matrix[j, i] = float(len(users_intersection)) / float(len(users_union)) #用Jacccard相似系数作为衡量相似度
  138. else:
  139. cooccurence_matrix[j, i] = 0
  140. return cooccurence_matrix
  141. def generate_top_recommendations(self, user, cooccurence_matrix, all_songs, user_songs):
  142. user_sim_scores = cooccurence_matrix.sum(axis= 0)/float(cooccurence_matrix.shape[ 0])
  143. #每首歌总得分为这首歌与user_songs中所有歌相似度的平均值
  144. user_sim_scores = np.array(user_sim_scores)[ 0].tolist() #得到每首歌的推荐下标
  145. sort_index = sorted(((e,i) for i,e in enumerate(list(user_sim_scores))), reverse= True) #将歌曲按总相似度进行排序
  146. columns = [ 'user_id', 'song', 'score', 'rank']
  147. df = pd.DataFrame(columns=columns) #构造一个DataFrame类型暂时存储推荐结果
  148. rank = 1 #限制返回的歌曲数量
  149. for i in range( 0,len(sort_index)): #遍历推荐列表
  150. if ~np.isnan(sort_index[i][ 0]) and all_songs[sort_index[i][ 1]] not in user_songs and rank <= 10:
  151. #如果这首歌待推荐用户没有听过,并且推荐下标在10以内
  152. df.loc[len(df)]=[user,all_songs[sort_index[i][ 1]],sort_index[i][ 0],rank] #将这首歌的信息存入df
  153. rank = rank+ 1
  154. ans = list(df[ 'song']) #返回推荐的歌曲名列表
  155. return ans
  156. def recommend(self, user):
  157. user_songs = self.get_user_items(user) #得到待推荐用户听过的所有的歌
  158. all_songs = self.unique_song_title #得到数据集中所有的歌名
  159. cooccurence_matrix = self.construct_cooccurence_matrix(user_songs, all_songs) #计算得到相似度矩阵
  160. recommendations = self.generate_top_recommendations(user, cooccurence_matrix,all_songs, user_songs) #得到推荐的歌曲名列表
  161. return recommendations
  162. #SVD分解预测用户的评分 返回值类型还需处理
  163. class SVD():
  164. def __init__(self):
  165. self.User_id = None
  166. self.uTest = None
  167. self.train_data = None
  168. self.user_id = None
  169. self.mt_candidate = None
  170. self.small_set = None
  171. self.K = None
  172. self.MAX_UID = None
  173. self.MAX_PID = None
  174. self.urm = None
  175. def create(self,train_data, user_id):
  176. self.train_data = train_data
  177. self.User_id = user_id
  178. # self.user_id = user_id
  179. def process_data(self):
  180. self.small_set = self.train_data
  181. user_codes = self.small_set.user_id.drop_duplicates().reset_index() # 得到所有的用户,去除重复值
  182. song_codes = self.small_set.song_id.drop_duplicates().reset_index() # 得到所有的歌id,去除重复值
  183. user_codes.rename(columns={ 'index': 'user_index'}, inplace= True)
  184. song_codes.rename(columns={ 'index': 'song_index'}, inplace= True)
  185. song_codes[ 'so_index_value'] = list(song_codes.index)
  186. user_codes[ 'us_index_value'] = list(user_codes.index)
  187. self.small_set = pd.merge(self.small_set, song_codes, how= 'left')
  188. self.small_set = pd.merge(self.small_set, user_codes, how= 'left')
  189. us_ids = user_codes[ 'user_id']
  190. us_index_values = user_codes[ 'us_index_value']
  191. for i in range(len(us_ids)):
  192. if us_ids[i] == self.User_id:
  193. self.uTest = us_index_values[i]
  194. break
  195. if self.uTest != None:
  196. break
  197. self.mat_candidate = self.small_set[[ 'us_index_value', 'so_index_value', 'fractional_play_count']]
  198. def get_data_sparse(self):
  199. data_array = self.mat_candidate.fractional_play_count.values
  200. row_array = self.mat_candidate.us_index_value.values
  201. col_array = self.mat_candidate.so_index_value.values
  202. data_sparse = coo_matrix((data_array, (row_array, col_array)), dtype=float)
  203. self.K = 500
  204. self.urm = data_sparse
  205. self.MAX_PID = self.urm.shape[ 1]
  206. self.MAX_UID = self.urm.shape[ 0]
  207. def compute_svd(self, urm, K): #进行矩阵分解,返回U、S、Vt三个矩阵
  208. U, s, Vt = svds(urm, K)
  209. dim = (len(s), len(s))
  210. S = np.zeros(dim, dtype=np.float32)
  211. for i in range( 0, len(s)):
  212. S[i, i] = mt.sqrt(s[i])
  213. U = csc_matrix(U, dtype=np.float32) #用来进行稀疏矩阵的压缩
  214. S = csc_matrix(S, dtype=np.float32)
  215. Vt = csc_matrix(Vt, dtype=np.float32)
  216. return U, S, Vt
  217. def compute_estimated_matrix(self, urm, U, S, Vt, K, test):
  218. rightTerm = S * Vt # 对于每一个用户 U * S * Vt 得到的是他的坐标
  219. max_recommendation = 250
  220. estimatedRatings = np.zeros(shape=(self.MAX_UID, self.MAX_PID), dtype=np.float16)
  221. recomendRatings = np.zeros(shape=(self.MAX_UID, max_recommendation), dtype=np.float16)
  222. userTest = self.uTest
  223. prod = U[userTest, :] * rightTerm
  224. estimatedRatings[userTest, :] = prod.todense() #得到用户U的预测评分列表
  225. recomendRatings[userTest, :] = (-estimatedRatings[userTest, :]).argsort()[:max_recommendation] #将评分表进行排序
  226. #将计算结果按评分排序后返回
  227. return recomendRatings
  228. def get_ans(self):
  229. self.process_data()
  230. self.get_data_sparse()
  231. U, S, Vt = self.compute_svd(self.urm, self.K)
  232. uTest_recommended_items = self.compute_estimated_matrix(self.urm, U, S, Vt , self.K, True)
  233. user = self.uTest
  234. ans_lis = []
  235. for i in uTest_recommended_items[user, 0: 10]: #得到前十个评分最高的歌曲信息
  236. song_details = self.small_set[self.small_set.so_index_value == i].drop_duplicates( 'so_index_value')[
  237. [ 'title', 'artist_name', 'year', 'release']]
  238. ans_lis.append(list(song_details[ 'title'])[ 0]) #将歌曲名添加至答案列表中
  239. return ans_lis

6.我学到了什么

1. 使用python的数据科学处理模块pandas进行数据预处理

当我们进行大数据项目的开发时,首要的第一步就是从网站上获取数据,这时候就有很多种方式了:使用网络爬虫,下载使用国内外的公开数据集等等。

拿到的数据集往往十分庞大,并且数据集中的数据往往存在数据冗余,数据缺失,数据错误等问题,我们想要进行模型的训练,当然要先对获取的海量数据进行数据预处理。

我们这次项目中拿到的数据文件类型是csv文件,对csv文件进行处理,可以使用python的强大数据科学分析库pandas来进行。首先我们import pandas as pd


  
  1. import pandas as pd
  2. #(1)读取csv文件中的数据:(值得注意的是:encoding应该与使用记事本打开的编码保持一致)
  3. data = pd.read_csv(文件名, encoding= "utf-8") #读取出来后是一个DataFrame类型
  4. #(2)合并两个具有相同列的DataFrame
  5. data_merge = pd.merge(data1, data2, on=共同的列名)
  6. #(3)取出DataFrame中的某一列,并去除重复值(取出的是一个列表类型)
  7. col = data_merge[列名].unique()
  8. #(4)打印DataFrame的前几行
  9. print(data.head( 10))

然后再配合excel中的“删除重复值”、“排序”等功能,就可以进行数据预处理,得到我们想要的数据啦

2. 使用python对文件夹中的海量图片进行批量命名

当文件夹中有成百上千张不规则命名的图片,我们如何将这些图片修改成统一的文件名格式呢?

贴一个大神博客链接:python批量修改文件夹中的图片名称

十分好用!相当牛逼!

3. django框架知识

Django是一个开放源代码的Web应用框架,由Python写成。采用了MTV的框架模式,即模型M,视图V和模版T。它最初是被开发来用于管理劳伦斯出版集团旗下的一些以新闻内容为主的网站的,即是CMS(内容管理系统)软件。并于2005年7月在BSD许可证下发布。这套框架是以比利时的吉普赛爵士吉他手Django Reinhardt来命名的。2019年12月2日,Django 3. 0发布

常用的Django命令:

django-admin startproject 程序名 #建立一个新的Django项目

django-admin startapp app名 #建立一个新的Django app

python manage.py runserver #运行建立的Django项目

python manage.py makemigrations #进行模型数据库文件的迁移, 记录我们对models.py的所有改动,并且将这个改动迁移到migrations这个文件下生成一个文件

python manage.py migrate #这条命令的主要作用就是把这些改动作用到数据库也就是执行migrations里面新改动的迁移文件更新数据库,比如创建数据表,或者增加字段属性

4. 推荐算法知识

(1)基于用户的协同过滤:UserCF 伪代码:


  
  1. itemCF:要给user i推荐歌曲
  2. 1. 找出i听的所有歌曲的集合song_set_i[song1,song2,song3,...]
  3. 2. 找出歌曲集中的所有歌曲song_set_all[song1,........................]
  4. 3. for song_i in song_set_all :
  5. ​ 找出听过歌曲song_i的用户的集合user_i[user1,user2,.....]
  6. for song_j in song_set_i :
  7. ​ 找出听过歌曲song_j的用户的集合user_j[user1,user2,.....]
  8. ​ 求出user_i 和 user_j 的交集 / 并集 的值,作为song_i和song_j的相似分,填在相似度矩阵coo中
  9. ​ song_i的得分:song_i 这一行的 所有 song_j 的相似分 的总和/song_set_i的大小
  10. 4. 将所有song的得分进行排序,返回前十名
  11. (根据歌曲的受众,进行相似歌曲的推荐。其主要通过分析用户的行为记录计算物品之间的相似度。物品A和物品B具有很大的相似度是因为喜欢歌曲A的用户大都也喜欢歌曲B。)

(2)基于物品的协同过滤:ItemCF 伪代码:


  
  1. userCF:要给user i 推荐歌曲
  2. 1. 找出user_i听的所有歌曲的集合song_set_i
  3. 2. 找出用户集中的所有用户user_set_all
  4. 3. for user in user_set_all :
  5. ​ 找出用户user听过的歌曲的集合song_set_user
  6. ​ 求出song_set_i和song_set_user的交集 / 并集 的值,作为user_i 和 user 的相似分
  7. 4. 将user_set_all中所有user的相似分进行排序,选出前十名user
  8. 5. 将前十名user听的歌曲返回,作为推荐的结果
  9. (根据用户的相似度,进行推荐。与待推荐用户听歌记录相似的用户喜欢听的歌,很大可能上也会是待推荐用户喜欢听的歌)

(3)SVD分解

SVD分解就是用来将一个大的矩阵以降低维数的方式进行有损地压缩。应用在推荐系统中,我们的思路是利用新用户的评分向量找出该用户的相似用户。SVD分解就是将矩阵分解为U,S,V三个矩阵。k就是在进行矩阵降维时,选择的S矩阵中的特征值个数。对于新用户的评分,乘以U矩阵和S矩阵,得到他的降维结果。再与其余用户计算相似度,将与他最相似的用户评过分他却没评过分的歌曲排序,返回前几首作为推荐结果。

(4)冷启动(解决新用户的推荐策略)

将曲库中听的最多的歌曲推荐给新用户。

7. 心得和感悟

本次数据分析与挖掘课程设计让我亲身体验了一次开发完整的实际项目的经历。在本次课程设计中我负责推荐算法模块的开发设计。刚开始的几天,我一度觉得基于物品的推荐算法十分晦涩难懂。但我很快找到了解决的方案:用编译器自带的调试功能一步一步进行调试,观察每一个变量的变化情况。经过成百次的调试,我终于懂了算法的底层实现原理,然后又根据自己的理解手动实现了基于用户的推荐算法。虽然实现了算法,我们却又遇到了新的问题:算法的计算效率过慢,万级别的数据往往需要一分钟甚至是好几分钟才能够计算出答案。经过不断的打断点测试,计算运行时间,我们终于找到了时间复杂度过高的根源:建立倒排表这一步花费了大量的时间。在经过思考后,我决定将数据库中的数据按物品编号进行排序,并预处理每个物品的倒排表,将程序的运行时间缩短在十秒以内。经过学习和改进,又实现了更快的推荐算法:SVD(奇异值分解),经过一起努力,最终将推荐算法联通到了数据库与前端页面。每种算法背后都有着简洁的思想,正确的原理,将它们理解并运用到代码中,是我这次课程设计最大的收获。在这次课程设计中我其实获得了很多,也明白了很多我自己的缺陷。作为计算机系的学生,动手能力一定要强,所以有很多东西不一定到了用的时候再去学,而是要提前了解,提前去学,这样才能够真正地提高自己。


转载:https://blog.csdn.net/weixin_43727229/article/details/112528722
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场