diff --git "a/Day41-55/45.\346\212\245\350\241\250\345\222\214\346\227\245\345\277\227.md" "b/Day41-55/45.\346\212\245\350\241\250\345\222\214\346\227\245\345\277\227.md" new file mode 100644 index 0000000000000000000000000000000000000000..71128f6b2ecaa3b616259a5764b489ab4e425c97 --- /dev/null +++ "b/Day41-55/45.\346\212\245\350\241\250\345\222\214\346\227\245\345\277\227.md" @@ -0,0 +1,132 @@ +## 报表 + +### 导出Excel报表 + +报表就是用表格、图表等格式来动态显示数据,所以有人用这样的公式来描述报表: + +``` +报表 = 多样的格式 + 动态的数据 +``` + +有很多的三方库支持在Python程序中写Excel文件,包括[xlwt]()、[xlwings]()、[openpyxl]()、[xlswriter]()、[pandas]()等,其中的xlwt虽然只支持写xls格式的Excel文件,但在性能方面的表现还是不错的。下面我们就以xlwt为例,来演示如何在Django项目中导出Excel报表,例如导出一个包含所有老师信息的Excel表格。 + +```Python +def export_teachers_excel(request): + # 创建工作簿 + wb = xlwt.Workbook() + # 添加工作表 + sheet = wb.add_sheet('老师信息表') + # 查询所有老师的信息 + queryset = Teacher.objects.all() + # 向Excel表单中写入表头 + colnames = ('姓名', '介绍', '好评数', '差评数', '学科') + for index, name in enumerate(colnames): + sheet.write(0, index, name) + # 向单元格中写入老师的数据 + props = ('name', 'detail', 'good_count', 'bad_count', 'subject') + for row, teacher in enumerate(queryset): + for col, prop in enumerate(props): + value = getattr(teacher, prop, '') + if isinstance(value, Subject): + value = value.name + sheet.write(row + 1, col, value) + # 保存Excel + buffer = BytesIO() + wb.save(buffer) + # 将二进制数据写入响应的消息体中并设置MIME类型 + resp = HttpResponse(buffer.getvalue(), content_type='application/vnd.ms-excel') + # 中文文件名需要处理成百分号编码 + filename = quote('老师.xls') + # 通过响应头告知浏览器下载该文件以及对应的文件名 + resp['content-disposition'] = f'attachment; filename*=utf-8''{filename}' + return resp +``` + +映射URL。 + +```Python +urlpatterns = [ + # 此处省略上面的代码 + path('excel/', views.export_teachers_excel), + # 此处省略下面的代码 +] +``` + +### 生成前端统计图表 + +如果项目中需要生成前端统计图表,可以使用百度的[ECharts]()。具体的做法是后端通过提供数据接口返回统计图表所需的数据,前端使用ECharts来渲染出柱状图、折线图、饼图、散点图等图表。例如我们要生成一个统计所有老师好评数和差评数的报表,可以按照下面的方式来做。 + +```Python +def get_teachers_data(request): + queryset = Teacher.objects.all() + names = [teacher.name for teacher in queryset] + good_counts = [teacher.good_count for teacher in queryset] + bad_counts = [teacher.bad_count for teacher in queryset] + # 返回JSON格式的数据 + return JsonResponse({'names': names, 'good': good_counts, 'bad': bad_counts}) +``` + +映射URL。 + +```Python +urlpatterns = [ + path('teachers_data/', views.export_teachers_excel), +] +``` + +使用ECharts生成柱状图。 + +```HTML + + + + + 老师评价统计 + + +
+

+ 返回首页 +

+ + + + +``` + +运行效果如下图所示。 + +![](./res/echarts_bar_graph.png) diff --git "a/Day41-55/46. \346\227\245\345\277\227\345\222\214\350\260\203\350\257\225\345\267\245\345\205\267\346\240\217.md" "b/Day41-55/46. \346\227\245\345\277\227\345\222\214\350\260\203\350\257\225\345\267\245\345\205\267\346\240\217.md" new file mode 100644 index 0000000000000000000000000000000000000000..182f6876dfbfd4329977f7560f2e6232cafcceb5 --- /dev/null +++ "b/Day41-55/46. \346\227\245\345\277\227\345\222\214\350\260\203\350\257\225\345\267\245\345\205\267\346\240\217.md" @@ -0,0 +1,217 @@ +## 日志和调试工具栏 + +### 配置日志 + +项目开发阶段,显示足够的调试信息以辅助开发人员调试代码还是非常必要的;项目上线以后,将系统运行时出现的警告、错误等信息记录下来以备相关人员了解系统运行状况并维护代码也是很有必要的。与此同时,采集日志数据也是为网站做数字化运营奠定一个基础,通过对系统运行日志的分析,我们可以监测网站的流量以及流量分布,同时还可以挖掘出用户的使用习惯和行为模式。 + +接下来,我们先看看如何通过Django的配置文件来配置日志。Django的日志配置基本可以参照官方文档再结合项目实际需求来进行,这些内容基本上可以从官方文档上复制下来,然后进行局部的调整即可,下面给出一些参考配置。 + +```Python +LOGGING = { + 'version': 1, + # 是否禁用已经存在的日志器 + 'disable_existing_loggers': False, + # 日志格式化器 + 'formatters': { + 'simple': { + 'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S', + }, + 'verbose': { + 'format': '%(asctime)s %(levelname)s [%(process)d-%(threadName)s] ' + '%(module)s.%(funcName)s line %(lineno)d: %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S', + } + }, + # 日志过滤器 + 'filters': { + # 只有在Django配置文件中DEBUG值为True时才起作用 + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + # 日志处理器 + 'handlers': { + # 输出到控制台 + 'console': { + 'class': 'logging.StreamHandler', + 'level': 'DEBUG', + 'filters': ['require_debug_true'], + 'formatter': 'simple', + }, + # 输出到文件(每周切割一次) + 'file1': { + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': 'access.log', + 'when': 'W0', + 'backupCount': 12, + 'formatter': 'simple', + 'level': 'INFO', + }, + # 输出到文件(每天切割一次) + 'file2': { + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': 'error.log', + 'when': 'D', + 'backupCount': 31, + 'formatter': 'verbose', + 'level': 'WARNING', + }, + }, + # 日志器记录器 + 'loggers': { + 'django': { + # 需要使用的日志处理器 + 'handlers': ['console', 'file1', 'file2'], + # 是否向上传播日志信息 + 'propagate': True, + # 日志级别(不一定是最终的日志级别) + 'level': 'DEBUG', + }, + } +} +``` + +大家可能已经注意到了,上面日志配置中的`formatters`是**日志格式化器**,它代表了如何格式化输出日志,其中格式占位符分别表示: + +1. `%(name)s` - 记录器的名称 +2. `%(levelno)s` - 数字形式的日志记录级别 +3. `%(levelname)s` - 日志记录级别的文本名称 +4. `%(filename)s` - 执行日志记录调用的源文件的文件名称 +5. `%(pathname)s` - 执行日志记录调用的源文件的路径名称 +6. `%(funcName)s` - 执行日志记录调用的函数名称 +7. `%(module)s` - 执行日志记录调用的模块名称 +8. `%(lineno)s` - 执行日志记录调用的行号 +9. `%(created)s` - 执行日志记录的时间 +10. `%(asctime)s` - 日期和时间 +11. `%(msecs)s` - 毫秒部分 +12. `%(thread)d` - 线程ID(整数) +13. `%(threadName)s` - 线程名称 +14. `%(process)d` - 进程ID (整数) + +日志配置中的handlers用来指定**日志处理器**,简单的说就是指定将日志输出到控制台还是文件又或者是网络上的服务器,可用的处理器包括: + +1. `logging.StreamHandler(stream=None)` - 可以向类似与`sys.stdout`或者`sys.stderr`的任何文件对象输出信息 +2. `logging.FileHandler(filename, mode='a', encoding=None, delay=False)` - 将日志消息写入文件 +3. `logging.handlers.DatagramHandler(host, port)` - 使用UDP协议,将日志信息发送到指定主机和端口的网络主机上 +4. `logging.handlers.HTTPHandler(host, url)` - 使用HTTP的GET或POST方法将日志消息上传到一台HTTP 服务器 +5. `logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False)` - 将日志消息写入文件,如果文件的大小超出`maxBytes`指定的值,那么将重新生成一个文件来记录日志 +6. `logging.handlers.SocketHandler(host, port)` - 使用TCP协议,将日志信息发送到指定主机和端口的网络主机上 +7. `logging.handlers.SMTPHandler(mailhost, fromaddr, toaddrs, subject, credentials=None, secure=None, timeout=1.0)` - 将日志输出到指定的邮件地址 +8. `logging.MemoryHandler(capacity, flushLevel=ERROR, target=None, flushOnClose=True)` - 将日志输出到内存指定的缓冲区中 + +上面每个日志处理器都指定了一个名为`level`的属性,它代表了日志的级别,不同的日志级别反映出日志中记录信息的严重性。Python中定义了六个级别的日志,按照从低到高的顺序依次是:NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL。 + +最后配置的**日志记录器**是用来真正输出日志的,Django框架提供了如下所示的内置记录器: + +1. `django` - 在Django层次结构中的所有消息记录器 +2. `django.request` - 与请求处理相关的日志消息。5xx响应被视为错误消息;4xx响应被视为为警告消息 +3. `django.server` - 与通过runserver调用的服务器所接收的请求相关的日志消息。5xx响应被视为错误消息;4xx响应被记录为警告消息;其他一切都被记录为INFO +4. `django.template` - 与模板渲染相关的日志消息 +5. `django.db.backends` - 有与数据库交互产生的日志消息,如果希望显示ORM框架执行的SQL语句,就可以使用该日志记录器。 + +日志记录器中配置的日志级别有可能不是最终的日志级别,因为还要参考日志处理器中配置的日志级别,取二者中级别较高者作为最终的日志级别。 + +### 配置Django-Debug-Toolbar + +如果想调试你的Django项目,你一定不能不过名为Django-Debug-Toolbar的神器,它是项目开发阶段辅助调试和优化的必备工具,只要配置了它,就可以很方便的查看到如下表所示的项目运行信息,这些信息对调试项目和优化Web应用性能都是至关重要的。 + +| 项目 | 说明 | +| ----------- | --------------------------------- | +| Versions | Django的版本 | +| Time | 显示视图耗费的时间 | +| Settings | 配置文件中设置的值 | +| Headers | HTTP请求头和响应头的信息 | +| Request | 和请求相关的各种变量及其信息 | +| StaticFiles | 静态文件加载情况 | +| Templates | 模板的相关信息 | +| Cache | 缓存的使用情况 | +| Signals | Django内置的信号信息 | +| Logging | 被记录的日志信息 | +| SQL | 向数据库发送的SQL语句及其执行时间 | + +1. 安装Django-Debug-Toolbar。 + + ```Shell + pip install django-debug-toolbar + ``` + +2. 配置 - 修改settings.py。 + + ```Python + INSTALLED_APPS = [ + 'debug_toolbar', + ] + + MIDDLEWARE = [ + 'debug_toolbar.middleware.DebugToolbarMiddleware', + ] + + DEBUG_TOOLBAR_CONFIG = { + # 引入jQuery库 + 'JQUERY_URL': 'https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js', + # 工具栏是否折叠 + 'SHOW_COLLAPSED': True, + # 是否显示工具栏 + 'SHOW_TOOLBAR_CALLBACK': lambda x: True, + } + ``` + +3. 配置 - 修改urls.py。 + + ```Python + if settings.DEBUG: + + import debug_toolbar + + urlpatterns.insert(0, path('__debug__/', include(debug_toolbar.urls))) + ``` + +4. 在配置好Django-Debug-Toolbar之后,页面右侧会看到一个调试工具栏,如下图所示,上面包括了如前所述的各种调试信息,包括执行时间、项目设置、请求头、SQL、静态资源、模板、缓存、信号等,查看起来非常的方便。 + + ![](res/django-debug-toolbar.png) + +#### 优化ORM代码 + +在配置了日志或Django-Debug-Toolbar之后,我们可以查看一下之前将老师数据导出成Excel报表的视图函数执行情况,这里我们关注的是ORM框架生成的SQL查询到底是什么样子的,相信这里的结果会让你感到有一些意外。执行`Teacher.objects.all()`之后我们可以注意到,在控制台看到的或者通过Django-Debug-Toolbar输出的SQL是下面这样的: + +```SQL +SELECT `tb_teacher`.`no`, `tb_teacher`.`name`, `tb_teacher`.`detail`, `tb_teacher`.`photo`, `tb_teacher`.`good_count`, `tb_teacher`.`bad_count`, `tb_teacher`.`sno` FROM `tb_teacher`; args=() +SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,) +SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,) +SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,) +SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,) +SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,) +SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,) +``` + +这里的问题通常被称为“1+N查询”(或“N+1查询”),原本获取老师的数据只需要一条SQL,但是由于老师关联了学科,当我们查询到N条老师的数据时,Django的ORM框架又向数据库发出了N条SQL去查询老师所属学科的信息。每条SQL执行都会有较大的开销而且会给数据库服务器带来压力,如果能够在一条SQL中完成老师和学科的查询肯定是更好的做法,这一点也很容易做到,相信大家已经想到怎么做了。是的,我们可以使用连接查询,但是在使用Django的ORM框架时如何做到这一点呢?对于多对一关联(如投票应用中的老师和学科),我们可以使用`QuerySet`的用`select_related()`方法来加载关联对象;而对于多对多关联(如电商网站中的订单和商品),我们可以使用`prefetch_related()`方法来加载关联对象。 + +在导出老师Excel报表的视图函数中,我们可以按照下面的方式优化代码。 + +```Python +queryset = Teacher.objects.all().select_related('subject') +``` + +事实上,用ECharts生成前端报表的视图函数中,查询老师好评和差评数据的操作也能够优化,因为在这个例子中,我们只需要获取老师的姓名、好评数和差评数这三项数据,但是在默认的情况生成的SQL会查询老师表的所有字段。可以用`QuerySet`的`only()`方法来指定需要查询的属性,也可以用`QuerySet`的`defer()`方法来指定暂时不需要查询的属性,这样生成的SQL会通过投影操作来指定需要查询的列,从而改善查询性能,代码如下所示: + +```Python +queryset = Teacher.objects.all().only('name', 'good_count', 'bad_count') +``` + +当然,如果要统计出每个学科的老师好评和差评的平均数,利用Django的ORM框架也能够做到,代码如下所示: + +```Python +queryset = Teacher.objects.values('subject').annotate( + good=Avg('good_count'), bad=Avg('bad_count')) +``` + +这里获得的`QuerySet`中的元素是字典对象,每个字典中有三组键值对,分别是代表学科编号的`subject`、代表好评数的`good`和代表差评数的`bad`。如果想要获得学科的名称而不是编号,可以按照如下所示的方式调整代码: + +```Python +queryset = Teacher.objects.values('subject__name').annotate( + good=Avg('good_count'), bad=Avg('bad_count')) +``` + +可见,Django的ORM框架允许我们用面向对象的方式完成关系数据库中的分组和聚合查询。 + diff --git "a/Day41-55/46.\346\212\245\350\241\250\345\222\214\346\227\245\345\277\227.md" "b/Day41-55/46.\346\212\245\350\241\250\345\222\214\346\227\245\345\277\227.md" deleted file mode 100644 index dffdcfc1f050cc8dafa24089ea758e70039e3bc0..0000000000000000000000000000000000000000 --- "a/Day41-55/46.\346\212\245\350\241\250\345\222\214\346\227\245\345\277\227.md" +++ /dev/null @@ -1,351 +0,0 @@ -## 报表和日志 - -### 导出Excel报表 - -报表就是用表格、图表等格式来动态显示数据,所以有人用这样的公式来描述报表: - -``` -报表 = 多样的格式 + 动态的数据 -``` - -有很多的三方库支持在Python程序中写Excel文件,包括[xlwt]()、[xlwings]()、[openpyxl]()、[xlswriter]()、[pandas]()等,其中的xlwt虽然只支持写xls格式的Excel文件,但在性能方面的表现还是不错的。下面我们就以xlwt为例,来演示如何在Django项目中导出Excel报表,例如导出一个包含所有老师信息的Excel表格。 - -```Python -def export_teachers_excel(request): - # 创建工作簿 - wb = xlwt.Workbook() - # 添加工作表 - sheet = wb.add_sheet('老师信息表') - # 查询所有老师的信息(注意:这个地方稍后需要优化) - queryset = Teacher.objects.all() - # 向Excel表单中写入表头 - colnames = ('姓名', '介绍', '好评数', '差评数', '学科') - for index, name in enumerate(colnames): - sheet.write(0, index, name) - # 向单元格中写入老师的数据 - props = ('name', 'detail', 'good_count', 'bad_count', 'subject') - for row, teacher in enumerate(queryset): - for col, prop in enumerate(props): - value = getattr(teacher, prop, '') - if isinstance(value, Subject): - value = value.name - sheet.write(row + 1, col, value) - # 保存Excel - buffer = BytesIO() - wb.save(buffer) - # 将二进制数据写入响应的消息体中并设置MIME类型 - resp = HttpResponse(buffer.getvalue(), content_type='application/vnd.ms-excel') - # 中文文件名需要处理成百分号编码 - filename = quote('老师.xls') - # 通过响应头告知浏览器下载该文件以及对应的文件名 - resp['content-disposition'] = f'attachment; filename="{filename}"' - return resp -``` - -映射URL。 - -```Python -urlpatterns = [ - # 此处省略上面的代码 - path('excel/', views.export_teachers_excel), - # 此处省略下面的代码 -] -``` - -### 生成前端统计图表 - -如果项目中需要生成前端统计图表,可以使用百度的[ECharts]()。具体的做法是后端通过提供数据接口返回统计图表所需的数据,前端使用ECharts来渲染出柱状图、折线图、饼图、散点图等图表。例如我们要生成一个统计所有老师好评数和差评数的报表,可以按照下面的方式来做。 - -```Python -def get_teachers_data(request): - # 查询所有老师的信息(注意:这个地方稍后也需要优化) - queryset = Teacher.objects.all() - # 用生成式将老师的名字放在一个列表中 - names = [teacher.name for teacher in queryset] - # 用生成式将老师的好评数放在一个列表中 - good = [teacher.good_count for teacher in queryset] - # 用生成式将老师的差评数放在一个列表中 - bad = [teacher.bad_count for teacher in queryset] - # 返回JSON格式的数据 - return JsonResponse({'names': names, 'good': good, 'bad': bad}) -``` - -映射URL。 - -```Python -urlpatterns = [ - # 此处省略上面的代码 - path('teachers_data/', views.export_teachers_excel), - # 此处省略下面的代码 -] -``` - -使用ECharts生成柱状图。 - -```HTML - - - - - 老师评价统计 - - -
-

- 返回首页 -

- - - - -``` - -运行效果如下图所示。 - -![](./res/echarts_bar_graph.png) - -### 配置日志 - -项目开发阶段,显示足够的调试信息以辅助开发人员调试代码还是非常必要的;项目上线以后,将系统运行时出现的警告、错误等信息记录下来以备相关人员了解系统运行状况并维护代码也是很有必要的。要做好这两件事件,我们需要为Django项目配置日志。 - -Django的日志配置基本可以参照官方文档再结合项目实际需求来进行,这些内容基本上可以从官方文档上复制下来,然后进行局部的调整即可,下面给出一些参考配置。 - -```Python -LOGGING = { - 'version': 1, - # 是否禁用已经存在的日志器 - 'disable_existing_loggers': False, - # 日志格式化器 - 'formatters': { - 'simple': { - 'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s', - 'datefmt': '%Y-%m-%d %H:%M:%S', - }, - 'verbose': { - 'format': '%(asctime)s %(levelname)s [%(process)d-%(threadName)s] ' - '%(module)s.%(funcName)s line %(lineno)d: %(message)s', - 'datefmt': '%Y-%m-%d %H:%M:%S', - } - }, - # 日志过滤器 - 'filters': { - # 只有在Django配置文件中DEBUG值为True时才起作用 - 'require_debug_true': { - '()': 'django.utils.log.RequireDebugTrue', - }, - }, - # 日志处理器 - 'handlers': { - # 输出到控制台 - 'console': { - 'class': 'logging.StreamHandler', - 'level': 'DEBUG', - 'filters': ['require_debug_true'], - 'formatter': 'simple', - }, - # 输出到文件(每周切割一次) - 'file1': { - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'access.log', - 'when': 'W0', - 'backupCount': 12, - 'formatter': 'simple', - 'level': 'INFO', - }, - # 输出到文件(每天切割一次) - 'file2': { - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'error.log', - 'when': 'D', - 'backupCount': 31, - 'formatter': 'verbose', - 'level': 'WARNING', - }, - }, - # 日志器记录器 - 'loggers': { - 'django': { - # 需要使用的日志处理器 - 'handlers': ['console', 'file1', 'file2'], - # 是否向上传播日志信息 - 'propagate': True, - # 日志级别(不一定是最终的日志级别) - 'level': 'DEBUG', - }, - } -} -``` - -大家可能已经注意到了,上面日志配置中的formatters是**日志格式化器**,它代表了如何格式化输出日志,其中格式占位符分别表示: - -1. %(name)s - 记录器的名称 -2. %(levelno)s - 数字形式的日志记录级别 -3. %(levelname)s - 日志记录级别的文本名称 -4. %(filename)s - 执行日志记录调用的源文件的文件名称 -5. %(pathname)s - 执行日志记录调用的源文件的路径名称 -6. %(funcName)s - 执行日志记录调用的函数名称 -7. %(module)s - 执行日志记录调用的模块名称 -8. %(lineno)s - 执行日志记录调用的行号 -9. %(created)s - 执行日志记录的时间 -10. %(asctime)s - 日期和时间 -11. %(msecs)s - 毫秒部分 -12. %(thread)d - 线程ID(整数) -13. %(threadName)s - 线程名称 -14. %(process)d - 进程ID (整数) - -日志配置中的handlers用来指定**日志处理器**,简单的说就是指定将日志输出到控制台还是文件又或者是网络上的服务器,可用的处理器包括: - -1. logging.StreamHandler(stream=None) - 可以向类似与sys.stdout或者sys.stderr的任何文件对象输出信息 -2. logging.FileHandler(filename, mode='a', encoding=None, delay=False) - 将日志消息写入文件 -3. logging.handlers.DatagramHandler(host, port) - 使用UDP协议,将日志信息发送到指定主机和端口的网络主机上 -4. logging.handlers.HTTPHandler(host, url) - 使用HTTP的GET或POST方法将日志消息上传到一台HTTP 服务器 -5. logging.handlers.RotatingFileHandler(filename, mode='a', maxBytes=0, backupCount=0, encoding=None, delay=False) - 将日志消息写入文件,如果文件的大小超出maxBytes指定的值,那么将重新生成一个文件来记录日志 -6. logging.handlers.SocketHandler(host, port) - 使用TCP协议,将日志信息发送到指定主机和端口的网络主机上 -7. logging.handlers.SMTPHandler(mailhost, fromaddr, toaddrs, subject, credentials=None, secure=None, timeout=1.0) - 将日志输出到指定的邮件地址 -8. logging.MemoryHandler(capacity, flushLevel=ERROR, target=None, flushOnClose=True) - 将日志输出到内存指定的缓冲区中 - -上面每个日志处理器都指定了一个名为“level”的属性,它代表了日志的级别,不同的日志级别反映出日志中记录信息的严重性。Python中定义了六个级别的日志,按照从低到高的顺序依次是:NOTSET、DEBUG、INFO、WARNING、ERROR、CRITICAL。 - -最后配置的**日志记录器**是用来真正输出日志的,Django框架提供了如下所示的内置记录器: - -1. django - 在Django层次结构中的所有消息记录器 -2. django.request - 与请求处理相关的日志消息。5xx响应被视为错误消息;4xx响应被视为为警告消息 -3. django.server - 与通过runserver调用的服务器所接收的请求相关的日志消息。5xx响应被视为错误消息;4xx响应被记录为警告消息;其他一切都被记录为INFO -4. django.template - 与模板渲染相关的日志消息 -5. django.db.backends - 有与数据库交互产生的日志消息,如果希望显示ORM框架执行的SQL语句,就可以使用该日志记录器。 - -日志记录器中配置的日志级别有可能不是最终的日志级别,因为还要参考日志处理器中配置的日志级别,取二者中级别较高者作为最终的日志级别。 - -### 配置Django-Debug-Toolbar - -Django-Debug-Toolbar是项目开发阶段辅助调试和优化的神器,只要配置了它,就可以很方便的查看到如下表所示的项目运行信息,这些信息对调试项目和优化Web应用性能都是至关重要的。 - -| 项目 | 说明 | -| ----------- | --------------------------------- | -| Versions | Django的版本 | -| Time | 显示视图耗费的时间 | -| Settings | 配置文件中设置的值 | -| Headers | HTTP请求头和响应头的信息 | -| Request | 和请求相关的各种变量及其信息 | -| StaticFiles | 静态文件加载情况 | -| Templates | 模板的相关信息 | -| Cache | 缓存的使用情况 | -| Signals | Django内置的信号信息 | -| Logging | 被记录的日志信息 | -| SQL | 向数据库发送的SQL语句及其执行时间 | - -1. 安装Django-Debug-Toolbar。 - - ```Shell - pip install django-debug-toolbar - ``` - -2. 配置 - 修改settings.py。 - - ```Python - INSTALLED_APPS = [ - 'debug_toolbar', - ] - - MIDDLEWARE = [ - 'debug_toolbar.middleware.DebugToolbarMiddleware', - ] - - DEBUG_TOOLBAR_CONFIG = { - # 引入jQuery库 - 'JQUERY_URL': 'https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js', - # 工具栏是否折叠 - 'SHOW_COLLAPSED': True, - # 是否显示工具栏 - 'SHOW_TOOLBAR_CALLBACK': lambda x: True, - } - ``` - -3. 配置 - 修改urls.py。 - - ```Python - if settings.DEBUG: - - import debug_toolbar - - urlpatterns.insert(0, path('__debug__/', include(debug_toolbar.urls))) - ``` - -4. 使用 - 如下图所示,在配置好Django-Debug-Toolbar之后,页面右侧会看到一个调试工具栏,上面包括了如前所述的各种调试信息,包括执行时间、项目设置、请求头、SQL、静态资源、模板、缓存、信号等,查看起来非常的方便。 - -### 优化ORM代码 - -在配置了日志或Django-Debug-Toolbar之后,我们可以查看一下之前将老师数据导出成Excel报表的视图函数执行情况,这里我们关注的是ORM框架生成的SQL查询到底是什么样子的,相信这里的结果会让你感到有一些意外。执行`Teacher.objects.all()`之后我们可以注意到,在控制台看到的或者通过Django-Debug-Toolbar输出的SQL是下面这样的: - -```SQL -SELECT `tb_teacher`.`no`, `tb_teacher`.`name`, `tb_teacher`.`detail`, `tb_teacher`.`photo`, `tb_teacher`.`good_count`, `tb_teacher`.`bad_count`, `tb_teacher`.`sno` FROM `tb_teacher`; args=() -SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,) -SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,) -SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,) -SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 101; args=(101,) -SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,) -SELECT `tb_subject`.`no`, `tb_subject`.`name`, `tb_subject`.`intro`, `tb_subject`.`create_date`, `tb_subject`.`is_hot` FROM `tb_subject` WHERE `tb_subject`.`no` = 103; args=(103,) -``` - -这里的问题通常被称为“1+N查询”(或“N+1查询”),原本获取老师的数据只需要一条SQL,但是由于老师关联了学科,当我们查询到N条老师的数据时,Django的ORM框架又向数据库发出了N条SQL去查询老师所属学科的信息。每条SQL执行都会有较大的开销而且会给数据库服务器带来压力,如果能够在一条SQL中完成老师和学科的查询肯定是更好的做法,这一点也很容易做到,相信大家已经想到怎么做了。是的,我们可以使用连接查询,但是在使用Django的ORM框架时如何做到这一点呢?对于多对一关联(如投票应用中的老师和学科),我们可以使用`QuerySet`的用`select_related()`方法来加载关联对象;而对于多对多关联(如电商网站中的订单和商品),我们可以使用`prefetch_related()`方法来加载关联对象。 - -在导出老师Excel报表的视图函数中,我们可以按照下面的方式优化代码。 - -```Python -queryset = Teacher.objects.all().select_related('subject') -``` - -事实上,用ECharts生成前端报表的视图函数中,查询老师好评和差评数据的操作也能够优化,因为在这个例子中,我们只需要获取老师的姓名、好评数和差评数这三项数据,但是在默认的情况生成的SQL会查询老师表的所有字段。可以用`QuerySet`的`only()`方法来指定需要查询的属性,也可以用`QuerySet`的`defer()`方法来指定暂时不需要查询的属性,这样生成的SQL会通过投影操作来指定需要查询的列,从而改善查询性能,代码如下所示: - -```Python -queryset = Teacher.objects.all().only('name', 'good_count', 'bad_count') -``` - -当然,如果要统计出每个学科的老师好评和差评的平均数,利用Django的ORM框架也能够做到,代码如下所示: - -```Python -queryset = Teacher.objects.values('subject').annotate( - good=Avg('good_count'), bad=Avg('bad_count')) -``` - -这里获得的`QuerySet`中的元素是字典对象,每个字典中有三组键值对,分别是代表学科编号的`subject`、代表好评数的`good`和代表差评数的`bad`。如果想要获得学科的名称而不是编号,可以按照如下所示的方式调整代码: - -```Python -queryset = Teacher.objects.values('subject__name').annotate( - good=Avg('good_count'), bad=Avg('bad_count')) -``` - -可见,Django的ORM框架允许我们用面向对象的方式完成关系数据库中的分组和聚合查询。 \ No newline at end of file diff --git "a/Day41-55/47.\344\270\255\351\227\264\344\273\266\347\232\204\345\272\224\347\224\250.md" "b/Day41-55/47.\344\270\255\351\227\264\344\273\266\347\232\204\345\272\224\347\224\250.md" index 4eecd40c96e7b15361f2e52cae03a5735d7c159f..1eef01d5624fb210322127fd1e4db697417f09e7 100644 --- "a/Day41-55/47.\344\270\255\351\227\264\344\273\266\347\232\204\345\272\224\347\224\250.md" +++ "b/Day41-55/47.\344\270\255\351\227\264\344\273\266\347\232\204\345\272\224\347\224\250.md" @@ -1,55 +1,6 @@ ## 中间件的应用 -### 实现登录验证 - -我们继续来完善投票应用。在上一个章节中,我们在用户登录成功后通过session保留了用户信息,接下来我们可以应用做一些调整,要求在为老师投票时必须要先登录,登录过的用户可以投票,否则就将用户引导到登录页面,为此我们可以这样修改视图函数。 - -```Python -def praise_or_criticize(request: HttpRequest): - """投票""" - if 'username' in request.session: - try: - tno = int(request.GET.get('tno', '0')) - teacher = Teacher.objects.get(no=tno) - if request.path.startswith('/praise'): - teacher.good_count += 1 - else: - teacher.bad_count += 1 - teacher.save() - data = {'code': 200, 'message': '操作成功'} - except (ValueError, Teacher.DoesNotExist): - data = {'code': 404, 'message': '操作失败'} - else: - data = {'code': 401, 'message': '请先登录'} - return JsonResponse(data) -``` - -前端页面在收到`{'code': 401, 'message': '请先登录'}`后,可以将用户引导到登录页面,修改后的teacher.html页面的JavaScript代码部门如下所示。 - -```HTML - -``` - -> 注意:为了在登录成功之后能够回到刚才投票的页面,我们在跳转登录时设置了一个`backurl`参数,把当前浏览器中的URL作为返回的页面地址。 - -这样我们已经实现了用户必须登录才能投票的限制,但是一个新的问题来了。如果我们的应用中有很多功能都需要用户先登录才能执行,例如将前面导出Excel报表和查看统计图表的功能都加以登录限制,那么我们是不是需要在每个视图函数中添加代码来检查session中是否包含了登录用户的信息呢?答案是否定的,如果这样做了,我们的视图函数中必然会充斥着大量的重复代码。编程大师*Martin Fowler*曾经说过:**代码有很多种坏味道,重复是最坏的一种**。在Django项目中,我们可以把验证用户是否登录这样的重复性代码放到中间件中。 +之前我们已经实现了用户必须登录才能投票的限制,但是一个新的问题来了。如果我们的应用中有很多功能都需要用户先登录才能执行,例如将前面导出Excel报表和查看统计图表的功能都做了必须登录才能访问的限制,那么我们是不是需要在每个视图函数中添加代码来检查session中是否包含`userid`的代码呢?答案是否定的,如果这样做了,我们的视图函数中必然会充斥着大量的重复代码。编程大师*Martin Fowler*曾经说过:**代码有很多种坏味道,重复是最坏的一种**。在Python程序中,我们可以通过装饰器来为函数提供额外的能力;在Django项目中,我们可以把类似于验证用户是否登录这样的重复性代码放到**中间件**中。 ### Django中间件概述 @@ -144,7 +95,3 @@ MIDDLEWARE = [ 注意上面这个中间件列表中元素的顺序,当收到来自用户的请求时,中间件按照从上到下的顺序依次执行,这行完这些中间件以后,请求才会最终到达视图函数。当然,在这个过程中,用户的请求可以被拦截,就像上面我们自定义的中间件那样,如果用户在没有登录的情况下访问了受保护的资源,中间件会将请求直接重定向到登录页,后面的中间件和视图函数将不再执行。在响应用户请求的过程中,上面的中间件会按照从下到上的顺序依次执行,这样的话我们还可以对响应做进一步的处理。 中间件执行的顺序是非常重要的,对于有依赖关系的中间件必须保证被依赖的中间件要置于依赖它的中间件的前面,就好比我们刚才自定义的中间件要放到`SessionMiddleware`的后面,因为我们要依赖这个中间件为请求绑定的`session`对象才能判定用户是否登录。 - -### 小结 - -至此,除了对用户投票数量加以限制的功能外,这个投票应用就算基本完成了,整个项目的完整代码请参考,其中用户注册时使用的手机验证码功能请大家使用自己注册的短信平台替代它。如果需要投票应用完整的视频讲解,可以在首页扫码打赏后留言联系作者获取视频下载地址,谢谢大家的理解和支持。 \ No newline at end of file diff --git "a/Day41-55/48.\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\345\205\245\351\227\250.md" "b/Day41-55/48.\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\345\205\245\351\227\250.md" index 2e0879e64840413ec09f72997b0e740793a99af9..ab3798c209201c2131637dd1f7ed34a300fc8797 100644 --- "a/Day41-55/48.\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\345\205\245\351\227\250.md" +++ "b/Day41-55/48.\345\211\215\345\220\216\347\253\257\345\210\206\347\246\273\345\274\200\345\217\221\345\205\245\351\227\250.md" @@ -161,4 +161,4 @@ class SubjectMapper(ModelMapper): 前后端分离的开发需要将前端页面作为静态资源进行部署,项目实际上线的时候,我们会对整个Web应用进行动静分离,静态资源通过Nginx或Apache服务器进行部署,生成动态内容的Python程序部署在uWSGI或者Gunicorn服务器上,对动态内容的请求由Nginx或Apache路由到uWSGI或Gunicorn服务器上。 -在开发阶段,我们通常会使用Django自带的测试服务器,如果要尝试前后端分离,可以先将静态页面放在之前创建的放静态资源的目录下,具体的做法可以参考[项目完整代码]()。 \ No newline at end of file +在开发阶段,我们通常会使用Django自带的测试服务器,如果要尝试前后端分离,可以先将静态页面放在之前创建的放静态资源的目录下,具体的做法可以参考[项目完整代码]()。 \ No newline at end of file diff --git "a/Day41-55/52.\346\226\207\344\273\266\344\270\212\344\274\240.md" "b/Day41-55/52.\346\226\207\344\273\266\344\270\212\344\274\240.md" new file mode 100644 index 0000000000000000000000000000000000000000..719c9ffb161a2724f225d02d53ca4c706169101f --- /dev/null +++ "b/Day41-55/52.\346\226\207\344\273\266\344\270\212\344\274\240.md" @@ -0,0 +1,4 @@ +## 文件上传 + + + diff --git "a/Day41-55/52.\346\226\207\344\273\266\344\270\212\344\274\240\345\222\214\345\257\214\346\226\207\346\234\254\347\274\226\350\276\221.md" "b/Day41-55/52.\346\226\207\344\273\266\344\270\212\344\274\240\345\222\214\345\257\214\346\226\207\346\234\254\347\274\226\350\276\221.md" deleted file mode 100644 index 33d5f3f0dc92fe9c9435bcf2386da9c0c31ebc64..0000000000000000000000000000000000000000 --- "a/Day41-55/52.\346\226\207\344\273\266\344\270\212\344\274\240\345\222\214\345\257\214\346\226\207\346\234\254\347\274\226\350\276\221.md" +++ /dev/null @@ -1,4 +0,0 @@ -## 文件上传和富文本编辑 - - - diff --git "a/Day41-55/54.\345\274\202\346\255\245\344\273\273\345\212\241\345\222\214\345\256\232\346\227\266\344\273\273\345\212\241.md" "b/Day41-55/53.\345\274\202\346\255\245\344\273\273\345\212\241\345\222\214\345\256\232\346\227\266\344\273\273\345\212\241.md" similarity index 100% rename from "Day41-55/54.\345\274\202\346\255\245\344\273\273\345\212\241\345\222\214\345\256\232\346\227\266\344\273\273\345\212\241.md" rename to "Day41-55/53.\345\274\202\346\255\245\344\273\273\345\212\241\345\222\214\345\256\232\346\227\266\344\273\273\345\212\241.md" diff --git "a/Day41-55/53.\347\237\255\344\277\241\345\222\214\351\202\256\344\273\266.md" "b/Day41-55/53.\347\237\255\344\277\241\345\222\214\351\202\256\344\273\266.md" deleted file mode 100644 index f6f3431cf50424d037871e513f3fd91419064978..0000000000000000000000000000000000000000 --- "a/Day41-55/53.\347\237\255\344\277\241\345\222\214\351\202\256\344\273\266.md" +++ /dev/null @@ -1,4 +0,0 @@ -## 短信和邮件 - - - diff --git "a/Day41-55/55.\345\215\225\345\205\203\346\265\213\350\257\225\345\222\214\351\241\271\347\233\256\344\270\212\347\272\277.md" "b/Day41-55/54.\345\215\225\345\205\203\346\265\213\350\257\225.md" similarity index 100% rename from "Day41-55/55.\345\215\225\345\205\203\346\265\213\350\257\225\345\222\214\351\241\271\347\233\256\344\270\212\347\272\277.md" rename to "Day41-55/54.\345\215\225\345\205\203\346\265\213\350\257\225.md" diff --git "a/Day41-55/55.\351\241\271\347\233\256\344\270\212\347\272\277.md" "b/Day41-55/55.\351\241\271\347\233\256\344\270\212\347\272\277.md" new file mode 100644 index 0000000000000000000000000000000000000000..a1632494d73135ba85643804964bad2d714ce0ce --- /dev/null +++ "b/Day41-55/55.\351\241\271\347\233\256\344\270\212\347\272\277.md" @@ -0,0 +1,3 @@ +## 项目上线 + + diff --git a/README.md b/README.md index df3469f2d439bd5f00783868e459c7b7353ef0a4..919310a888fb38711a304f71c5305ce6e2f9f738 100644 --- a/README.md +++ b/README.md @@ -302,7 +302,9 @@ - 在项目中使用celery实现任务异步化 - 在项目中使用celery实现定时任务 -#### Day55 - [单元测试和项目上线](./Day41-55/55.单元测试和项目上线.md) +#### Day54 - [单元测试](./Day41-55/54.单元测试.md) + +#### Day55 - [项目上线](./Day41-55/55.项目上线.md) - Python中的单元测试 - Django框架对单元测试的支持