项目的结构:
项目的结构一定要熟悉
- HMSol 是和项目同名的文件夹,它里面有setting和总的路由
- common 、 homework 、lesson 、 myadmin 、 student 、 teacher 使我们创建的app,看到上面有一个小点了不,表示他是Python Package,不是一般的文件夹
- static 文件夹存放一些资源,比如我们用到的框架的代码,图片之类的
- media 存放用户上传的文件,比如课程资料,作业等等
- templates 存放模板的文件夹
重点:
记得MVT不?在每个app里面都能体现
- M:模型层,models.py,存放我们定义的数据库
- V:视图层,views.py,存放我们定义的具体实现功能的函数
- T:模板层,就是位于 templates 文件夹里面的html文件
- 路由:urls.py,就是沟通V和T的桥梁
Ajax的部分
Ajax是什么?标准的Ajax是什么样的
后端如何获取前端传来的数据?
如果前端的ajax是 post 请求:
// 提交作业$.ajax({ url: '/homework/send-homework', // 路由 type: 'POST', // 类型 contentType: "application/json", async: false, data: JSON.stringify({ 要传到后端的数据,键值对,例如 'id': '001' }), success: function (data) { 成功之后要做的事 }, error: function (data) { 失败之后要做的事 }});
后端接收:
# 获取前端传的参数 data的形式{'id': '001'}data = json.loads(request.body)my_id = data['id']
如果前端的ajax是 get 请求:
$.ajax({ url: '/get-user-info', type: 'get', contentType: "application/json", async: false, data: JSON.stringify({ 要传到后端的数据,键值对,例如 'id': '001' }), success: function (user_data) { 成功后要做的事 }, error: function (ret) { 失败后做的事 }});
后端接收:
# 获取前端传的参数 data的形式{'id': '001'}my_id = request.GET.get('id', '')
从session中获取数据
id = request.session.get('id')
定时发送邮件功能:
定时发送邮件的功能位于 homework 的 views.py 中
使用的模块是 apscheduler (我读作ap司改就)
准备的部分:(了解即可)
安装好 django-apscheduler 后,在 setting.py 中添加:
INSTALLED_APPS = ( # ... "django_apscheduler",)
然后通过迁移命令(还记得吗,数据库迁移的两条命令:python manage.py makemigrations 和 python manage.py migrate)
就会在数据库中生成两张表:(你可以点进去看看是什么子)
- django_apscheduler_djangojob 表保存注册的任务以及下次执行的时间
- django_apscheduler_djangojobexecution 保存每次任务执行的时间和结果和任务状态
具体用到的部分:(重要)
# 定时任务相关的包from apscheduler.schedulers.background import BackgroundSchedulerfrom django_apscheduler.jobstores import DjangoJobStore, register_job'''开启定时任务,实现自动发送邮件提醒'''# 1.实例化调度器scheduler = BackgroundScheduler()# 2.调度器使用DjangoJobStore()scheduler.add_jobstore(DjangoJobStore(), "default")try: # 3.设置定时任务 # 另一种方式为每天固定时间执行任务,对应代码为 @register_job(scheduler, "cron", id='test2', hour=0) # @register_job(scheduler, "interval", id='test1', minutes=1) def my_job(): # 发送邮件提醒 send_email() # 4.注册定时任务(0.4.0版本之后不需要注册) # register_events(scheduler) # 5.开启定时任务 scheduler.start()except Exception as e: print(e) # 有错误就停止定时器 # scheduler.shutdown()# 获取应当被提醒的学生的邮箱列表def send_email(): today = datetime.date.today() print('今天的日期是:', today) # 替换用字典 var = { 'ENDDATE': "AND d.end_date = '" + str(today + datetime.timedelta(days=1)) + " 00:00:00'", } sql_raw = ''' SELECt a.id, a.name, b.email, c.name AS lesson_name, d.name AS homework_name, d.end_date FROM student a LEFT JOIN user b ON a.id = b.id LEFT JOIN lesson c ON a.class_id = c.classes LEFT JOIN homework d ON c.id = d.lesson_id WHERe 1=1 [ENDDATE] ''' sql = sql_fmt(sql_raw, var) print(sql) res = do_sql(sql) print(res) for r in res: if r is not None: send_email1(r) print('给', r['name'], '发送邮件提醒成功!')
用到了我们写在 common 下的 API的 send_email.py 里的函数:
发送邮件的功能是由django提供的(你看那个 django.core.mail ),因此我们只需要准备好需要的东西(数据),按照格式填进去就好了。
发送邮件需要QQ邮箱的SMTP服务,毕竟是Django以你的身份(用你的QQ邮箱)给别人发邮件对不对,所以就需要这个密码。
from django.core.mail import get_connection, send_maildef send_email1(info_dict): print('发送邮件提醒') password = 'XXXXXXXXXX' conn = get_connection(host='smtp.qq.com', username='XXXXXXXXX@qq.com', password=password) student_name = info_dict['name'] email = info_dict['email'] lesson_name = info_dict['lesson_name'] homework_name = info_dict['homework_name'] end_date = str(info_dict['end_date']) msg = student_name + ',您好!n 您参与的课程[' + lesson_name + ']的作业[' + homework_name + ']即将在 ' + end_date + ' 截止n请尽快提交作业n From: 在线作业管理系统' send_mail(subject='作业截止提醒', message=msg, from_email='XXXXXXXXX@qq.com', recipient_list=[email], connection=conn)
补充:
重点在这个装饰器 @register_job()
- 注册后的任务会根据 id 以及相关的配置进行定时任务
- 设置触发器:'date’为单次任务,比如指定5月12日执行;'interval’为间隔,比如每隔一分钟执行一次;'cron’为定时执行,比如每天的8点半
代码查重功能:
代码查重的功能也位于 homework 的 views.py 中
大致思路:(重点)
- 前端我们有两个上传文件的按钮还有两个框,选择完文件后,会浏览本地文件,将文件名放入框框里
- 当文件框内的字发生了改变且两个框里都有文件的时候,就会触发上传,将两个文件传到我们的media文件夹下的code_comparison文件夹中
- 然后完成上传后,继续发ajax,将两个文件名传到后端,那么后端拿到文件名后,就可以打开文件,然后进行代码查重的处理(这个你看下面)
- 完成处理之后呢,将结果返回,结果显示到页面上(结果显示的部分先是隐藏的,有结果后再显示,html里有个 hide() 和 show() 你看看)。
代码查重的思路:(重点)
- 有文件名了就有路径了,有路径了就可以读取文件,就是下面的 Code() 的部分
- 他通过这个 PythonLexer (我读作莱克色) 来分析每个字段,区分他是关键字呀还是变量呀还是标点呀还是换行呀
- 每个字段就是对应一个block,那么整个代码就能换成一个二维的block数组,每个block通过下面的方法计算相似度
- 根据设定的阈值来判断块是否重复的,比如阈值是60分,这个块的相似度是80分,那他是不是就是重复了
- 总的代码的重复度就是 超过相似度阈值的block数除以总block数 (例如:宿舍4个人,其中有3个人超过了60分,那么总体的及格率是不是就是 3 / 4?就是这个道理)
代码查重的代码:(了解,知道函数是干什么的就可以了)
是不是比较简单?清除文件的没有用上就先不管,然后热力图没实现的也不管,就下面这俩:
# 展示 代码查重页面def show_code_compare(request): return render(request, 'code_compare.html')# 代码查重def compare_code(request): print('进行代码查重') data = json.loads(request.body) print(data) f1_name = data['f1_name'] f2_name = data['f2_name'] Similarity_threshold = data['Similarity_threshold'] kgrams = data['kgrams'] window_size = data['window_size'] print(kgrams, window_size, Similarity_threshold) # 代码查重 winnowing_rate = code_compare(f1_name, f2_name, kgrams, window_size, Similarity_threshold) return JsonResponse({'code': 0, 'winnowing_rate': winnowing_rate, 'message': '查重成功'})
code_compare() 函数呢是我们写在common.API.code_hander里面的:
从 code_compare() 开始看奥,有些用不到的我在底下删掉了,比如热力图这种的,这样子看应该会清晰一点
import osfrom pygments.lexers import PythonLexerfrom pygments import tokenfrom re import searchimport iofrom hashlib import sha1from difflib import SequenceMatcherfrom operator import itemgetterimport plotly.graph_objects as gofrom plotly.subplots import make_subplotsfrom HMSol.settings import MEDIA_ROOTcategories = { 'Call': ['A', 'rgb(80, 138, 44)'], 'Builtin': ['B', 'rgb(212, 212, 102)'], 'Comparison': ['C', 'rgb(176, 176, 176)'], 'FunctionDef': ['D', 'rgb(4, 163, 199)'], 'Function': ['F', 'rgb(199, 199, 72)'], 'Indent': ['I', 'rgb(237, 237, 237)'], 'Keyword': ['K', 'rgb(161, 53, 219)'], 'Linefeed': ['L', 'rgb(255, 255, 255)'], 'Namespace': ['M', 'rgb(232, 232, 209)'], 'Number': ['N', 'rgb(192, 237, 145)'], 'Operator': ['O', 'rgb(212, 212, 212)'], 'Punctuation': ['P', 'rgb(214, 216, 216)'], 'Pseudo': ['Q', 'rgb(14, 3, 163)'], 'String': ['S', 'rgb(194, 126, 0)'], 'Variable': ['V', 'rgb(184, 184, 176)'], 'WordOp': ['W', 'rgb(8, 170, 207)'], 'NamespaceKw': ['X', 'rgb(161, 53, 219)']}# 定义要使用的哈希函数的种类def hash_fun(text): hs = sha1(text.encode("utf-8")) hs = hs.hexdigest()[-4:] hs = int(hs, 16) return hs# 将字符串转化为kgramsdef kgrams(text, n): text = list(text) return zip(*[text[i:] for i in range(n)])# 获取每个grams的哈希值def do_hashing(kgrams): hashlist = [] for i, kg in enumerate(list(kgrams)): ngram_text = "".join(kg) hashvalue = hash_fun(ngram_text) hashlist.append((hashvalue, i)) return hashlistdef computeCode(f1, f2): # 若文件为空,则返回0, 0 if None in [f1, f2]: return 0, 0 c1 = Code(f1, f1.name) c2 = Code(f2, f2.name) # c1.calculate_similarity(c2) # Calculate similarity return c1, c2# 给token分类def get_category(t): category = '' # 用单个大写字母代表类别 if t[0] == token.Keyword.Namespace: category = categories['NamespaceKw'][0] elif t[0] == token.Name.Namespace: category = categories['Namespace'][0] elif t[0] == token.Name.Function: category = categories['Function'][0] elif t[0] == token.Name: category = categories['Variable'][0] elif t[0] == token.Name.Builtin.Pseudo: category = categories['Pseudo'][0] elif t[0] == token.Name.Builtin: category = categories['Builtin'][0] elif t[0] in token.Literal.Number: category = categories['Number'][0] elif t[0] in token.Literal.String and t[1] not in [''', '"'] and t[0] not in token.Literal.String.Doc: category = categories['String'][0] elif t[0] == token.Keyword and t[1] == 'def': category = categories['FunctionDef'][0] elif t[0] == token.Keyword: category = categories['Keyword'][0] elif t[0] == token.Text and (search(r's{2,}S', t[1]) is not None): category = categories['Indent'][0] elif t[0] == token.Operator.Word: category = categories['WordOp'][0] elif t[0] == token.Operator and (t[1] == '==' or t[1] == '!='): category = categories['Comparison'][0] elif t[0] == token.Punctuation: category = categories['Punctuation'][0] elif t[0] == token.Operator: category = categories['Operator'][0] # elif t[0] == token.Text and t[1] == 'n': # category = categories['Linefeed'][0] elif t[0] == token.Whitespace and t[1] == 'n': # 换行 category = categories['Linefeed'][0] else: category = None # 若为注释或其他未分配的token,则忽略 return category# 为定义的类别创建cmap, 用于在plot中显示颜色def get_cmap(): clist = [] prev_c = [0, "rgb(255, 255, 255)"] for key in categories: clist.append(prev_c) clist.append([ord(categories[key][0]), prev_c[1]]) prev_c = [ord(categories[key][0]), categories[key][1]] # Normalize keys of colour representation # Needed for plotly max_ = max(clist, key=itemgetter(0))[0] min_ = min(clist, key=itemgetter(0))[0] converted = [] for element in clist: converted.append([(element[0] - min_) / (max_ - min_), element[1]]) # Return cmap for categories return converted# 展示使用winnowing算法的相似度def show_winnowing(c1, c2): # 设置参数 # k_size 'KGrams大小', 2, 15, 5 # win_size '滑动窗口大小', 2, 15, 4 k_size = 5 win_size = 4 # 'Winnowing-相似度: **{:.0f}%**'.format(c1.winnowing_similarity(c2, k_size, win_size) * 100)) print('Winnowing-相似度: **{:.0f}%**'.format(c1.winnowing_similarity(c2, k_size, win_size) * 100)) # [论文](https://theory.stanford.edu/~aiken/publications/papers/sigmod03.pdf)以获得更多信息"# 返回交集的长度def intersection(lst1, lst2): temp = set(lst2) lst3 = [value for value in lst1 if value in temp] return len(lst3)# 将滑动窗口应用于哈希列表def sl_window(hashes, n): return zip(*[hashes[i:] for i in range(n)])# 获取滑动窗口的最小值def get_min(windows): result = [] prev_min = () for w in windows: # 找到最小散列并取最右边的出现 min_h = min(w, key=lambda x: (x[0], -x[1])) # 仅在与前一个最小值不同时才使用散列 if min_h != prev_min: result.append(min_h) prev_min = min_h return result# winnowing算法def winnowing(text, size_k, window_size): hashes = (do_hashing(kgrams(text, size_k))) return set(get_min(sl_window(hashes, window_size)))# 使用winnowing算法和jaccard距离得到相似度def winnowing_similarity(text_a, text_b, size_k=5, window_size=4): # Get fingerprints using winnowing w1 = winnowing(text_a, size_k, window_size) w2 = winnowing(text_b, size_k, window_size) # print('w1:', w1) # Do use list instead of set to also consider number of occurece of copied content hash_list_a = [x[0] for x in w1] hash_list_b = [x[0] for x in w2] # 交集 intersect = intersection(hash_list_a, hash_list_b) + intersection(hash_list_b, hash_list_a) # 全集 union = len(hash_list_a) + len(hash_list_b) # print('intersect:', intersect) # print('union:', union) return intersect / union# 为一个block创建tokensclass Block(object): def __init__(self, tokens, similarity=0, compared=False): self._similarity = similarity # self._compared = compared self._tokens = tokens @property def similarity(self): return self._similarity @similarity.setter def similarity(self, s): self._similarity = s @property def tokens(self): return self._tokens # 运算符重载 def __len__(self): return len(self.tokens) def __str__(self): return ''.join(str(t[0]) for t in self.tokens) # 对象方法: # 使用token比较两个block def compare(self, other): if isinstance(other, Block): # 使用DIFFLIB计算相似度 return SequenceMatcher(None, str(self), str(other)).ratio() # 使用原始字符串比较两个block def compare_str(self, other): if isinstance(other, Block): return SequenceMatcher(None, self.clnstr(), other.clnstr()).ratio() def clnstr(self): return ''.join(str(t[3].lower()) for t in self.tokens) def max_row(self): return max(self.tokens, key=itemgetter(1))[1] def max_col(self): return max(self.tokens, key=itemgetter(2))[2]# 将源代码用色块tokens表示, 便于实现相似性比较class Code: # 初始化 def __init__(self, text, name="", similarity_threshold=0.9): self._blocks = [] self._max_row = 0 self._max_col = 0 self._name = name self._similarity_threshold = similarity_threshold self._lvs_blocksize = 8 self.__tokenizeFromText(text) # 分析代码 def __tokenizeFromText(self, text): lexer = PythonLexer() # 使用lexer做词法分析, 判断属于哪种编程语言 tokens = lexer.get_tokens(text) tokens = list(tokens) # 转化为list result = [] prev_c = '' # 前一个的类别 row = 0 col = 0 # 使用category简化tokens for token in tokens: c = get_category(token) if c is not None: # 换行检测,更新坐标但不加入result if c == 'L': row = row + 1 col = 0 # 检测到新的block, prev_c为n且不为缩进 elif prev_c == 'L' and c != 'I' and result: self.blocks.append(Block(result)) result = [] # 不为空行 if c != 'L': # 区分函数调用和变量 if prev_c == 'V' and token[1] == '(': result[-1] = 'A', result[-1][1], result[-1][2], result[-1][3] result.append((c, row, col, token[1])) col += 1 if col > self._max_col: self._max_col = col prev_c = c self._max_row = row # 依照代码的行数更新最大行数 # 结果不为空则追加最后一个block if result: self.blocks.append(Block(result)) @property def blocks(self): return self._blocks @blocks.setter def blocks(self, b): self._blocks = b # 重置所有block的相似度 def resetSimilarity(self): for block in self.blocks: block.similarity = 0 # 使用string compare和annotate方法查找能匹配的block def __pre_process(self, other): other_blocks = other.blocks for block_a in self.blocks: for block_b in other_blocks: if block_a.similarity == 1: break if block_a.clnstr() == block_b.clnstr(): block_a.similarity = 1.0 block_b.similarity = 1.0 # 处理代码的相似度 def __process_similarity(self, other): for block_a in self.blocks: if block_a.similarity == 0: best_score = 0 # 记录最佳匹配分数 for block_b in other.blocks: if len(block_a) > self._lvs_blocksize: score = block_a.compare(block_b) else: score = block_a.compare_str(block_b) # 找到最大的匹配分数 if score >= best_score: best_score = score # 也为代码b设置最佳匹配数 if block_b.similarity < best_score: block_b.similarity = best_score block_a.similarity = best_score # 计算每个块的相似度分数(使用levensthein计算方法) def calculate_similarity(self, other): # 将所有block的相似度置为空 self.resetSimilarity() other.resetSimilarity() self.__pre_process(other) self.__process_similarity(other) other.__process_similarity(self) # 返回计算的相似度,为超过相似度阈值的block数除以总block数 def getSimScore(self): total_len = 0 len_plagiat = 0 for block in self.blocks: total_len += len(block) if block.similarity >= self._similarity_threshold: len_plagiat += len(block) * block.similarity return len_plagiat / total_len # 计算每个块的相似度分数(使用使用winnowing方法) def winnowing_similarity(self, other, size_k=5, window_size=4): score = winnowing_similarity(str(self), str(other), size_k, window_size) return score# 打印结果 也是返回一个列表def printResult(c1, c2, threshold): threshold /= 100 # 除以100 比如我们设置的是60 实际用到的是0.6 # 这就是下面用到的 判断中等、高的门槛 plagrism_threshold_high = 90 plagrism_threshold_medium = 60 # 两个代码的相似度阈值都设为我们传入的这个threshold c1._similarity_threshold = threshold c2._similarity_threshold = threshold ans = [] if (c1.getSimScore() * 100) >= plagrism_threshold_high: ans.append(''{}' 的相似度为 {:.0f}% 相似度高, 可以认为是抄袭'.format(c1._name, c1.getSimScore() * 100)) elif (c1.getSimScore() * 100) >= plagrism_threshold_medium: ans.append(''{}' 的相似度为 {:.0f}% 相似度中等, 可以认为不是抄袭'.format(c1._name, c1.getSimScore() * 100)) else: ans.append(''{}' 的相似度为 {:.0f}% 相似度低, 可以认为不是抄袭'.format(c1._name, c1.getSimScore() * 100)) if (c2.getSimScore() * 100) >= plagrism_threshold_high: ans.append(''{}' 的相似度为 {:.0f}% 相似度高, 可以认为是抄袭'.format(c2._name, c2.getSimScore() * 100)) elif (c2.getSimScore() * 100) >= plagrism_threshold_medium: ans.append(''{}' 的相似度为 {:.0f}% 相似度中等, 可以认为不是抄袭'.format(c2._name, c2.getSimScore() * 100)) else: ans.append(''{}' 的相似度为 {:.0f}% 相似度低, 可以认为不是抄袭'.format(c2._name, c2.getSimScore() * 100)) return ans# 代码查重 传入两个文件的名称 还有滑块的三个参数def code_compare(file_1_name, file_2_name, k_size, win_size, Similarity_threshold): # 文件所在路径 file_1_url = MEDIA_ROOT + '/code_comparison/' + file_1_name file_2_url = MEDIA_ROOT + '/code_comparison/' + file_2_name # 读取文件部分 file_1 = open(file_1_url, 'br') # 使用二进制 file_2 = open(file_2_url, 'br') io_file_1 = io.BytesIO(file_1.read()) # 使用BytesIO读取 io_file_2 = io.BytesIO(file_2.read()) file1 = io_file_1.read().decode(errors='忽略') file2 = io_file_2.read().decode(errors='忽略') c1 = Code(file1, file_1_name) # Code()是上面写的类 你可以点过去看 c2 = Code(file2, file_2_name) # 计算相似度 c1.calculate_similarity(c2) # 若文件不为空,则将结果加到这个ans列表里,返回 if 0 not in [c1, c2]: ans = [] w = 'Winnowing-相似度: {:.0f}%'.format(c1.winnowing_similarity(c2, k_size, win_size) * 100) ans.append(w) res = printResult(c1, c2, Similarity_threshold) for r in res: ans.append(r) return ans
有些函数你就看注释就好了 知道他是干什么的,比如这个
就记他是处理代码的相似度的
# 处理代码的相似度def __process_similarity(self, other): for block_a in self.blocks: if block_a.similarity == 0: best_score = 0 # 记录最佳匹配分数 for block_b in other.blocks: if len(block_a) > self._lvs_blocksize: score = block_a.compare(block_b) else: score = block_a.compare_str(block_b) # 找到最大的匹配分数 if score >= best_score: best_score = score # 也为代码b设置最佳匹配数 if block_b.similarity < best_score: block_b.similarity = best_score block_a.similarity = best_score
批量导入部分:
位于 myadmin 模块
用到 xlrd 模块,我读作xl read
思路:
- 用 xlrd 读取文件
- 遍历每个单元格,对需要进行字典转换的进行转换,一行读完,就作为字典存入列表
- 遍历列表,对于每一个字典,如果数据库中存在该用户信息就更新,不存在就新增
# 上传文件 批量生成用户@csrf_exempt # 取消当前View视图函数防御def upload_xlsx(request): # 读文件 file = request.FILES.get('file') wb = xlrd.open_workbook(filename=None, file_contents=file.read()) table = wb.sheets()[0] # 转换用字典 my_dict = { 'ID': 'id', '姓名': 'name', '性别': 'sex', '类型': 'type', '邮箱': 'email' } my_dict_2 = { '教师': 1, '学生': 2, '男': 1, '女': 0, } # 行数 列数 rows_count = table.nrows cols_count = table.ncols print('行数:', rows_count, '列数: ', cols_count) flag = 0 message = '' if rows_count > 1: list_data = [] # 从第二行开始 for i in range(1, rows_count): dict_row = {} for j in range(0, cols_count): cell = table.cell_value(i, j) # 浮点转成整型 if table.cell(i, j).ctype == 2 and cell % 1 == 0.0: cell = int(cell) # 字典转换 if cell in my_dict_2: dict_row[my_dict.get(table.cell_value(0, j))] = my_dict_2[cell] else: dict_row[my_dict.get(table.cell_value(0, j))] = cell list_data.append(dict_row) print(list_data) # 对数据库更新 {} success_count = 0 for tmp in list_data: this_id = tmp['id'] # 选择数据 doctor_data = User.objects.filter(id=this_id) if doctor_data: doctor_data.update(**tmp) else: User.objects.create( id=this_id, pwd=this_id, name=tmp['name'], sex=tmp['sex'], type=tmp['type'], email=tmp['email'], ) success_count = success_count + 1 message += '成功创建了' + str(success_count) + '个用户! 失败了' + str(rows_count - success_count - 1) + '条记录!' return JsonResponse({'ret': flag, 'msg': message})
关于CSRF:(了解)
CSRF(跨站请求伪造漏洞),是网站的一种漏洞
系统为了避免CSRF,所以在跨域的时候需要加上验证
就是那个在html里面经常看到的那一串:
// ajax 头部增加csrf_tokenlet token = "{{ csrf_token }}";$.ajaxSetup({ headers: { 'X-CSRFTOKEN': `${token}` }, // 这里是headers,不是data, CSRF});
在有发送ajax的情况下就需要加上这么一块,否则会报错