diff --git a/Day91-100/Django知识点概述.md b/Day91-100/Django知识点概述.md index cd2cf2d..951f2e2 100644 --- a/Day91-100/Django知识点概述.md +++ b/Day91-100/Django知识点概述.md @@ -2,84 +2,91 @@ ### Web应用 +问题1:描述一个Web应用的工作流程。 + ![](./res/web-application.png) -问题1:描述一个Web应用的工作流程。(如上图所示) +问题2:描述项目的物理架构。(上图中补充负载均衡(反向代理)服务器、数据库服务器、文件服务器、邮件服务器、缓存服务器、防火墙等,而且每个节点都有可能是多节点构成的集群,如下图所示) -问题2:描述项目的物理架构。(上图中补充反向代理服务器、负载均衡服务器、数据库服务器、文件服务器、缓存服务器、防火墙等,每个节点都有可能是多节点构成的集群) +![](./res/05.django_massive_cluster.png) 问题3:描述Django项目的工作流程。(如下图所示) -![](./res/django-flowchart.png) +![](./res/django_request_response_cycle.png) ### MVC架构模式 -![](./res/mvc.png) - 问题1:为什么要使用MVC架构模式?(模型和视图解耦合) -问题2:MVC架构中每个部分的作用?(如上图所示) +问题2:MVC架构中每个部分的作用?(如下图所示) + +![](./res/mvc.png) ### HTTP请求和响应 -#### HTTP请求 - -HTTP请求 = 请求行+请求头+空行+[消息体] +#### HTTP请求 = 请求行+请求头+空行+[消息体] ![](./res/http-request.png) -HTTP响应 = 响应行+响应头+空行+消息体 +#### HTTP响应 = 响应行+响应头+空行+消息体 ![](./res/http-response.png) 1. `HTTPRequest`对象的属性和方法: - - `method` - - `path` / `get_full_path()` - - `scheme` / `is_secure()` / `get_host()` / `get_port()` - - `META` / `COOKIES` - - `GET` / `POST` / `FILES` - - `get_signed_cookie()` - - `is_ajax()` - - `body` / `content_type` / `encoding` + + - `method` - 获取请求方法 + - `path` / `get_full_path()` - 获取请求路径/带查询字符串的路径 + - `scheme` / `is_secure()` / `get_host()` / `get_port()` - 获取请求的协议/主机/端口 + - `META` / `COOKIES` - 获取请求头/Cookie信息 + - `GET` / `POST` / `FILES` - 获取GET或POST请求参数/上传的文件 + - `get_signed_cookie()` - 获取带签名的Cookie + - `is_ajax()` - 是不是Ajax异步请求 + - `body` / `content_type` / `encoding` - 获取请求的消息体(bytes流)/MIME类型/编码 2. 中间件添加的属性: + - `session` / `user` / `site` 3. `HttpResponse`对象的属性和方法: - - `set_cookie()` / `set_signed_cookie()` / `delete_cookie()` - - `__setitem__` / `__getitem__` / `__delitem__` - - `charset` / `content` / `status_code` + + - `set_cookie()` / `set_signed_cookie()` / `delete_cookie()` - 添加/删除Cookie + - `__setitem__` / `__getitem__` / `__delitem__` - 添加/获取/删除响应头 + - `charset` / `content` / `status_code` - 响应的字符集/消息体(bytes流)/状态码 + - 1xx:请求已经收到,继续处理 + - 2xx(成功):请求已经成功收到、理解和接收。 + - 3xx(重定向):为完成请求要继续执行后续的操作。 + - 4xx(客户端错误):请求不正确或不能够被受理。 + - 5xx(服务器错误):服务器处理请求失败。 4. `JsonResponse`(`HttpResponse`的子类型)对象 ```Python - - class HouseJsonEncoder(JsonEncoder): - - def default(self, o): - # 定义如何将对象转成dict类型并返回这个字典 - pass - ``` - - ```Python - - >>> from django.http import JsonResponse + >>> from django.http import HttpResponse, JsonResponse + >>> >>> response = JsonResponse({'foo': 'bar'}) >>> response.content - + >>> >>> response = JsonResponse([1, 2, 3], safe=False) - >>> response = JsonResponse(house, encoder=HouseJsonEncoder) - - >>> response = HttpResponse('') + >>> response.content + >>> + >>> response = HttpResponse(b'...') >>> response['cotent-type'] = 'application/pdf'; >>> response['content-disposition'] = 'inline; filename="xyz.pdf"' >>> response['content-disposition'] = 'attachment; filename="xyz.pdf"' - >>> response.set_signed_cookie('', '', salt='') + >>> + >>> response.set_signed_cookie('foo', 'bar', salt='') >>> response.status_code = 200 ``` - + ### 数据模型(Model) -问题1:关系型数据库表的设计应该注意哪些问题?(范式理论) +问题1:关系型数据库表的设计应该注意哪些问题(范式理论和逆范式)?如何通过表来创建模型类(反向工程)?如何通过模型类来创建表(正向工程)? -问题2:关系型数据库中数据完整性指的是什么?(实体完整性/参照完整性/域完整性) +```Shell +python manage.py makemigrations +python manage.py migrate + +python manage.py inspectdb > /models.py +``` + +问题2:关系型数据库中数据完整性指的是什么?什么时候需要牺牲数据完整性?(实体完整性/参照完整性/域完整性) 问题3:ORM是什么以及解决了什么问题?(对象模型-关系模型双向转换) @@ -114,7 +121,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 - `related_name` ```Python - class Dept(models.Model): pass @@ -126,22 +132,31 @@ HTTP响应 = 响应行+响应头+空行+消息体 Dept.objects.get(no=10).emp_set.all() Emp.objects.filter(dept__no=10) ``` + + > 说明:`related_name`设置为`'+'`,可以防止一对多外键关联从“一”的一方查询“多”的一方。 + - 其他属性: + - `to_field` / `limit_choices_to` / `swappable` 3. `Model`的属性和方法 - - `objects`/ `pk` + - `objects` / `pk` + - `save()` / `delete()` - - `from_db()` / `get_XXX_display()` / `clean()` / `full_clean()` + + - `clean()` / `validate_unique()` / `full_clean()` 4. `QuerySet`的方法 - `get()` / `all()` / `values()` + > 说明:`values()`返回的`QuerySet`中不是模型对象而是字典 + - `count()` / `order_by()` / `exists()` / `reverse()` - `filter()` / `exclude()` + - `exact` / `iexact`:精确匹配/忽略大小写的精确匹配查询 - `contains` / `icontains` / `startswith / istartswith / endswith / iendswith`:基于`like`的模糊查询 @@ -165,7 +180,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 - `Avg` / `Count` / `Sum` / `Max` / `Min` ```Python - >>> from django.db.models import Avg >>> Emp.objects.aggregate(avg_sal=Avg('sal')) (0.001) SELECT AVG(`TbEmp`.`sal`) AS `avg_sal` FROM `TbEmp`; args=() @@ -173,7 +187,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 ``` ```Python - >>> Emp.objects.values('dept').annotate(total=Count('dept')) (0.001) SELECT `TbEmp`.`dno`, COUNT(`TbEmp`.`dno`) AS `total` FROM `TbEmp` GROUP BY `TbEmp`.`dno` ORDER BY NULL LIMIT 21; args=() 说明:调用`first()`方法相当于用`[0]`对`QuerySet`进行切片。 + - `only()` / `defer()` ```Python - >>> Emp.objects.filter(pk=7800).only('name', 'sal') (0.001) SELECT `TbEmp`.`empno`, `TbEmp`.`ename`, `TbEmp`.`sal` FROM `TbEmp` WHERE `TbEmp`.`empno` = 7800 LIMIT 21; args=(7800,) ]> @@ -196,7 +210,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 - `create()` / `update()` / `raw()` ```Python - >>> Emp.objects.filter(dept__no=20).update(sal=F('sal') + 100) (0.011) UPDATE `TbEmp` SET `sal` = (`TbEmp`.`sal` + 100) WHERE `TbEmp`.`dno` = 20; args=(100, 20) >>> @@ -207,7 +220,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 5. `Q`对象和`F`对象 ```Python - >>> from django.db.models import Q >>> Emp.objects.filter( ... Q(name__startswith='张'), @@ -217,7 +229,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 ``` ```Python - reporter = Reporters.objects.filter(name='Tintin') reporter.update(stories_filed=F('stories_filed') + 1) ``` @@ -225,9 +236,9 @@ HTTP响应 = 响应行+响应头+空行+消息体 6. 原生SQL查询 ```Python - from django.db import connection + with connection.cursor() as cursor: cursor.execute("UPDATE TbEmp SET sal=sal+10 WHERE dno=30") cursor.execute("SELECT ename, job FROM TbEmp WHERE dno=10") @@ -237,7 +248,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 7. 模型管理器 ```Python - class BookManager(models.Manager): def title_count(self, keyword): @@ -250,22 +260,30 @@ HTTP响应 = 响应行+响应头+空行+消息体 1. 用户的每个操作对应一个视图函数。 -2. 每个视图函数构成一个事务边界。 - - - 事务的概念。 +2. [每个视图函数可以构成一个事务边界](https://docs.djangoproject.com/en/2.1/ref/settings/)。 - 事务的ACID特性。 - - 事务隔离级别。 + - 原子性(Atomicity):事务中各项的操作要么全做要么全不做; + - 一致性(Consistentcy):事务前后系统的状态是一致的; + - 隔离性(Isolation):并发执行的事务无法看到彼此的中间状态; + - 持久性(Duration):事务完成后所做的改动都会被持久化。 - Read Uncommitted < Read Committed < Repeatable Read < Serializable + - 事务隔离级别 - 设置事务隔离级别是为了数据库底层依据事务隔离级别为数据加上适当的锁。如果需要保证数据的强一致性,那么关系型数据库仍然是唯一的也是最好的选择,因为关系型数据库可以通过锁机制来保护数据。事务隔离级别从低到高依次是:Read Uncommitted(读未提交)、Read Committed(读提交)、Repeatable Read(可重复读)、Serializable(串行化)。事务隔离级别越高,数据并发访问的问题越少,但是性能越差;事务隔离级别越低,数据并发访问的问题越多,但是性能越好。 + + - 数据并发访问会产生5种问题(请参考我的[《Java面试题全集(上)》](https://blog.csdn.net/jackfrued/article/details/44921941)第80题对该问题的讲解): + + - 第1类丢失更新(A事务撤销覆盖B事务更新的数据)和第2类丢失更新(A事务提交覆盖B事务更新的数据)。 + - 脏读(读脏数据):一个事务读取到其他尚未提交的事务的数据。 + - 不可重复读: 一个事务在读取它的查询结果时,被另一个事务更新了它的查询记录导致无法读到数据。 + - 幻读:一个事务在读取它的查询结果时,发现读到了被另一个事务提交的新数据。 ```SQL - + -- 设置全局默认的事务隔离级别 set global transaction isolation level repeatable read; + -- 设置当前会话的事务隔离级别 set session transaction isolation level read committed; - - + -- 查询当前会话的事务隔离级别 select @@tx_isolation; ``` @@ -274,35 +292,30 @@ HTTP响应 = 响应行+响应头+空行+消息体 - 给每个请求绑定事务环境(反模式)。 ```Python - ATOMIC_REQUESTS = True ``` - - 使用事务装饰器。 + - 使用事务装饰器(简单易用)。 ```Python - @transaction.non_atomic_requests @transaction.atomic ``` - - 使用上下文语法。 + - 使用上下文语法(事务控制的范围更加精准)。 ```Python - with transaction.atomic(): pass ``` - - 关闭自动提交。 + - 关闭自动提交使用手动提交。 ```Python - AUTOCOMMIT = False ``` ```Python - transaction.commit() transaction.rollback() ``` @@ -312,7 +325,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 1. 可以让部分URL只在调试模式下生效。 ```Python - from django.conf import settings urlpatterns = [ @@ -326,7 +338,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 2. 可以使用命名捕获组捕获路径参数。 ```Python - url(r'api/code/(?P1[3-9]\d{9})'), path('api/code/'), ``` @@ -342,7 +353,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 7. 可以用`reverse`函数实现URL的逆向解析(从名字解析出URL),在模板中也可以用`{% url %}`实现同样的操作。 ```Python - path('', views.index, name='index') return redirect(reverse('index')) @@ -357,7 +367,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 1. 模板的配置和渲染函数。 ```Python - TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -376,8 +385,7 @@ HTTP响应 = 响应行+响应头+空行+消息体 ``` ```Python - - resp = render(request, 'foo.html', {'foo': 'bar'}) + resp = render(request, 'index.html', {'foo': ...}) ``` 2. 模板遇到变量名的查找顺序。 @@ -411,7 +419,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 - 文件系统加载器 ```Python - TEMPLATES = [{ 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')], @@ -421,7 +428,6 @@ HTTP响应 = 响应行+响应头+空行+消息体 - 应用目录加载器 ```Python - TEMPLATES = [{ 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'APP_DIRS': True, @@ -438,7 +444,7 @@ HTTP响应 = 响应行+响应头+空行+消息体 #### 其他视图 -1. MIME类型。 +1. MIME(多用途Internet邮件扩展)类型 - 告知浏览器传输的数据类型。 | Content-Type | 说明 | | ---------------- | ------------------------------------------------------------ | @@ -454,55 +460,52 @@ HTTP响应 = 响应行+响应头+空行+消息体 | video/mp4 | [MP4](https://zh.wikipedia.org/wiki/MP4)视频文件 | | video/quicktime | [QuickTime](https://zh.wikipedia.org/wiki/QuickTime)视频文件 | -2. 如何处置生成的内容。 +2. 如何处置生成的内容(inline / attachment)。 ```Python - - response['content-type'] = 'application/pdf' - response['content-disposition'] = 'attachment; filename="xyz.pdf"' + >>> from urllib.parse import quote + >>> + >>> response['content-type'] = 'application/pdf' + >>> filename = quote('Python语言规范.pdf') + >>> filename + 'Python%E8%AF%AD%E8%A8%80%E8%A7%84%E8%8C%83.pdf' + >>> response['content-disposition'] = f'attachment; filename="{filename}"' ``` - > 提醒:URL以及请求和响应中的中文都应该处理成[百分号编码](https://zh.wikipedia.org/zh-hans/%E7%99%BE%E5%88%86%E5%8F%B7%E7%BC%96%E7%A0%81)。 + > 提醒:URL以及请求和响应头中的中文都应该处理成[百分号编码](https://zh.wikipedia.org/zh-hans/%E7%99%BE%E5%88%86%E5%8F%B7%E7%BC%96%E7%A0%81)。 -3. 生成CSV / Excel / PDF。 +3. 生成CSV / Excel / PDF / 统计报表。 - 向浏览器传输二进制数据。 ```Python - buffer = ByteIO() resp = HttpResponse(content_type='...') - resp['Content-Disposition'] = 'attachment;filename="..."' + resp['Content-Disposition'] = 'attachment; filename="..."' resp.write(buffer.getvalue()) ``` - 大文件的流式处理:`StreamingHttpResponse`。 ```Python - - class EchoBuffer(object): - - def write(self, value): - return value - - - def some_streaming_csv_view(request): - rows = (["Row {}".format(idx), str(idx)] for idx in range(65536)) - writer = csv.writer(EchoBuffer()) - resp = StreamingHttpResponse((writer.writerow(row) for row in rows), - content_type="text/csv") - resp['Content-Disposition'] = 'attachment; filename="data.csv"' + def download_file(request): + file_stream = open('...', 'rb') + # 如果文件的二进制数据较大则最好用迭代器进行处理避免过多的占用服务器内存 + file_iter = iter(lambda: file_stream.read(4096), b'') + resp = StreamingHttpResponse(file_iter) + # 中文文件名要处理成百分号编码 + filename = quote('...', 'utf-8') + resp['Content-Type'] = '...' + resp['Content-Disposition'] = f'attachment; filename="{filename}"' return resp ``` - - 生成PDF:需要安装`reportlab`。 - - - 生成Excel:需要安装`openpyxl`。 - -4. 统计图表。 + > 说明:如果需要生成PDF文件,可以需要安装`reportlab`;生成Excel可以使用`openpyxl`或`xlrd`。 - [ECharts](http://echarts.baidu.com/)或[Chart.js](https://www.chartjs.org/)。 - - 思路:后端只提供JSON格式的数据,前端JavaScript渲染生成图表。 + + - 思路:后端只提供JSON格式的数据,前端JavaScript渲染生成图表。 + ### 中间件 @@ -510,14 +513,11 @@ HTTP响应 = 响应行+响应头+空行+消息体 问题2:中间件有哪些不同的实现方式?(参考下面的代码) -问题4:描述Django内置的中间件及其执行顺序。 - -推荐阅读:[Django官方文档 - 中间件 - 中间件顺序](https://docs.djangoproject.com/zh-hans/2.0/ref/middleware/#middleware-ordering)。 +问题3:描述Django内置的中间件及其执行顺序。(推荐阅读:[Django官方文档 - 中间件 - 中间件顺序](https://docs.djangoproject.com/zh-hans/2.0/ref/middleware/#middleware-ordering)) #### 激活中间件 ```Python - MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -534,7 +534,6 @@ MIDDLEWARE = [ ```Python - def simple_middleware(get_response): def middleware(request): @@ -547,7 +546,6 @@ def simple_middleware(get_response): ``` ```Python - class MyMiddleware(object): def __init__(self, get_response): @@ -565,7 +563,6 @@ class MyMiddleware(object): ``` ```Python - class MyMiddleware(object): def __init__(self): @@ -590,20 +587,26 @@ class MyMiddleware(object): #### 内置中间件 -1. CommonMiddleware - - DISALLOWED_USER_AGENTS - - APPEND_SLASH - - USE_ETAG -2. SecurityMiddleware - - SECURE_HSTS_SECONDS - - SECURE_HSTS_INCLUDE_SUBDOMAINS - - SECURE_CONTENT_TYPE_NOSNIFF - - SECURE_BROWSER_XSS_FILTER - - SECURE_SSL_REDIRECT - - SECURE_REDIRECT_EXEMPT -3. SessionMiddleware -4. CsrfViewMiddleware - 防范跨栈身份伪造。 -5. XFrameOptionsMiddleware - 防范点击劫持攻击 +1. CommonMiddleware - 基础设置中间件 + - DISALLOWED_USER_AGENTS - 不被允许的用户代理(浏览器) + - APPEND_SLASH - 是否追加`/` + - USE_ETAG - 浏览器缓存相关 + +2. SecurityMiddleware - 安全相关中间件 + - SECURE_HSTS_SECONDS - 强制使用HTTPS的时间 + - SECURE_HSTS_INCLUDE_SUBDOMAINS - HTTPS是否覆盖子域名 + - SECURE_CONTENT_TYPE_NOSNIFF - 是否允许浏览器推断内容类型 + - SECURE_BROWSER_XSS_FILTER - 是否启用跨站脚本攻击过滤器 + - SECURE_SSL_REDIRECT - 是否重定向到HTTPS连接 + - SECURE_REDIRECT_EXEMPT - 免除重定向到HTTPS + +3. SessionMiddleware - 会话中间件 + +4. CsrfViewMiddleware - 防范跨站身份伪造中间件 + +5. XFrameOptionsMiddleware - 防范点击劫持攻击中间件 + + ![](./res/click-jacking.png) ![](./res/builtin-middlewares.png) @@ -627,10 +630,24 @@ class MyMiddleware(object): 问题1:使用Cookie能解决什么问题?(用户跟踪,解决HTTP协议无状态问题) 1. URL重写 + + ``` + http://www.abc.com/path/resource?foo=bar + ``` + 2. 隐藏域(隐式表单域) + + ```HTML +
+ + + +
+ ``` + 3. Cookie -问题2:Cookie和Session之间关系是什么?(Session的标识会通过Cookie记录) +问题2:Cookie和Session之间关系是什么?(Session的标识通过Cookie保存和传输) #### Session的配置 @@ -641,7 +658,6 @@ class MyMiddleware(object): - 基于数据库(默认方式) ```Python - INSTALLED_APPS = [ 'django.contrib.sessions', ] @@ -650,9 +666,8 @@ class MyMiddleware(object): - 基于缓存(推荐使用) ```Python - SESSION_ENGINE = 'django.contrib.sessions.backends.cache' - SESSION_CACHE_ALIAS = 'default' + SESSION_CACHE_ALIAS = 'session' ``` - 基于文件(基本不考虑) @@ -660,14 +675,12 @@ class MyMiddleware(object): - 基于Cookie(不靠谱) ```Python - SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' ``` 3. Cookie相关的配置。 ```Python - SESSION_COOKIE_NAME = 'djang_session_id' SESSION_COOKIE_AGE = 1209600 SESSION_EXPIRE_AT_BROWSER_CLOSE = False @@ -679,20 +692,19 @@ class MyMiddleware(object): - `session_key` / `session_data` / `expire_date` - `__getitem__` / `__setitem__` / `__delitem__` / `__contains__` - - `set_expiry()` / `get_expiry_age()` / `get_expiry_date()` - - `flush()` - - `set_test_cookie()` / `test_cookie_worked()` / `delete_test_cookie()` + - `set_expiry()` / `get_expiry_age()` / `get_expiry_date()` - 设置/获取会话超期时间 + - `flush()` - 销毁会话 + - `set_test_cookie()` / `test_cookie_worked()` / `delete_test_cookie()` - 测试浏览器是否支持Cookie(提示用户如果浏览器禁用Cookie可能会影响网站的使用) 5. session的序列化。 ```Python - - SESSION_SERIALIZER = django.contrib.sessions.serializers.JSONSerializer + SESSION_SERIALIZER = 'django.contrib.sessions.serializers.JSONSerializer' ``` - - JSONSerializer(默认)- 如果想将自定义的对象放到session中,会遇到“Object of type 'XXX' is not JSON serializable”的问题。 - - - PickleSerializer(1.6以前的默认,但是因为安全问题不推荐使用,但是只要不去反序列化用户构造的恶意的Payload就行了。关于这种方式的安全漏洞,请参考《[Python Pickle的任意代码执行漏洞实践和Payload构造》](http://www.polaris-lab.com/index.php/archives/178/)。) + - JSONSerializer(1.6及以后默认)- 如果想将自定义的对象放到session中,会遇到“Object of type 'XXX' is not JSON serializable”的问题。 + - PickleSerializer(1.6以前的默认)- 因为安全问题不推荐使用,但是只要不去反序列化用户构造的恶意的Payload其实也没有什么风险。关于这种方式的安全漏洞,可以参考《[Python Pickle的任意代码执行漏洞实践和Payload构造》](http://www.polaris-lab.com/index.php/archives/178/)一文或《软件架构-Python语言实现》上关于这个问题的讲解。 + - 说明:如果使用了django_redis整合Redis作为session的存储引擎,那么由于django_redis又封装了一个PickleSerializer来提供序列化,所以不会存在上述的问题,且Redis中保存的value是pickle序列化之后的结果。 ### 缓存 @@ -701,25 +713,66 @@ class MyMiddleware(object): ```Python - CACHES = { + # 默认缓存 'default': { 'BACKEND': 'django_redis.cache.RedisCache', 'LOCATION': [ - 'redis://120.77.222.217:6379/0', - 'redis://120.77.222.217:6380/0', - 'redis://120.77.222.217:6381/0', - # 'redis://120.77.222.217:6382/0', + 'redis://1.2.3.4:6379/0', ], - # 'KEY_PREFIX': 'fang', - # 'TIMEOUT': 120, + 'KEY_PREFIX': 'fang', 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', 'CONNECTION_POOL_KWARGS': { - 'max_connections': 100, + 'max_connections': 1000, + }, + 'PASSWORD': '1qaz2wsx', + } + }, + # 页面缓存 + 'page': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': [ + 'redis://1.2.3.4:6379/1', + ], + 'KEY_PREFIX': 'fang:page', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + 'CONNECTION_POOL_KWARGS': { + 'max_connections': 500, + }, + 'PASSWORD': '1qaz2wsx', + } + }, + # 会话缓存 + 'session': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': [ + 'redis://1.2.3.4:6379/2', + ], + 'KEY_PREFIX': 'fang:session', + 'TIMEOUT': 1209600, + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + 'CONNECTION_POOL_KWARGS': { + 'max_connections': 2000, + }, + 'PASSWORD': '1qaz2wsx', + } + }, + # 验证码缓存 + 'code': { + 'BACKEND': 'django_redis.cache.RedisCache', + 'LOCATION': [ + 'redis://1.2.3.4:6379/3', + ], + 'KEY_PREFIX': 'fang:code:tel', + 'OPTIONS': { + 'CLIENT_CLASS': 'django_redis.client.DefaultClient', + 'CONNECTION_POOL_KWARGS': { + 'max_connections': 500, }, 'PASSWORD': '1qaz2wsx', - # 'COMPRESSOR': 'django_redis.compressors.zlib.ZlibCompressor' } }, } @@ -727,7 +780,6 @@ CACHES = { #### 全站缓存 ```Python - MIDDLEWARE_CLASSES = [ 'django.middleware.cache.UpdateCacheMiddleware', ... @@ -743,17 +795,17 @@ CACHE_MIDDLEWARE_KEY_PREFIX = 'djang:cache' #### 视图层缓存 ```Python - from django.views.decorators.cache import cache_page +from django.views.decorators.vary import vary_on_cookie -@cache_page(60 * 15) +@cache_page(timeout=60 * 15, cache='page') +@vary_on_cookie def my_view(request): pass ``` ```Python - from django.views.decorators.cache import cache_page urlpatterns = [ @@ -770,35 +822,45 @@ urlpatterns = [ 2. 使用底层API访问缓存。 ```Python - >>> from django.core.cache import cache + >>> >>> cache.set('my_key', 'hello, world!', 30) >>> cache.get('my_key') >>> cache.clear() ``` ```Python - >>> from django.core.cache import caches - >>> cache1 = caches['myalias'] - >>> cache2 = caches['myalias'] + >>> cache1 = caches['page'] + >>> cache2 = caches['page'] >>> cache1 is cache2 True + >>> cache3 = caches['session'] + >>> cache2 is cache3 + False ``` + ```Python + >>> from django_redis import get_redis_connection + >>> + >>> redis_client = get_redis_connection() + >>> redis_client.hgetall() + ``` + + ### 日志 #### 日志级别 -NOTSET < DEBUG < INFO < WARNING < ERROR < FETAL +NOTSET < DEBUG < INFO < WARNING < ERROR < FATAL #### 日志配置 ```Python - LOGGING = { 'version': 1, 'disable_existing_loggers': False, + # 配置日志格式化器 'formatters': { 'simple': { 'format': '%(asctime)s %(module)s.%(funcName)s: %(message)s', @@ -810,20 +872,29 @@ LOGGING = { 'datefmt': '%Y-%m-%d %H:%M:%S', } }, + # 配置日志过滤器 + 'filters': { + 'require_debug_true': { + '()': 'django.utils.log.RequireDebugTrue', + }, + }, + # 配置日志处理器 'handlers': { 'console': { 'class': 'logging.StreamHandler', 'level': 'DEBUG', + 'filters': ['require_debug_true'], + 'formatter': 'simple', }, - 'inf': { + 'file1': { 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'info.log', + 'filename': 'fang.log', 'when': 'W0', 'backupCount': 12, 'formatter': 'simple', 'level': 'INFO', }, - 'err': { + 'file2': { 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': 'error.log', 'when': 'D', @@ -832,21 +903,13 @@ LOGGING = { 'level': 'WARNING', }, }, + # 配置日志器 'loggers': { 'django': { - 'handlers': ['console'], + 'handlers': ['console', 'file1', 'file2'], + 'propagate': True, 'level': 'DEBUG', }, - 'inform': { - 'handlers': ['inf'], - 'level': 'DEBUG', - 'propagate': True, - }, - 'error': { - 'handlers': ['err'], - 'level': 'DEBUG', - 'propagate': True, - } } } ``` @@ -858,7 +921,6 @@ LOGGING = { 1. Linux相关命令:head、tail、grep、awk、uniq、sort ```Shell - tail -10000 access.log | awk '{print $1}' | uniq -c | sort -r ``` @@ -868,9 +930,9 @@ LOGGING = { 4. [《集中式日志系统ELK》](https://www.ibm.com/developerworks/cn/opensource/os-cn-elk/index.html)。 - - ElasticSearch - - Logstash - - Kibana + - ElasticSearch:搜索引擎,实现全文检索。 + - Logstash:负责从指定节点收集日志。 + - Kibana:日志可视化工具。 ### RESTful @@ -889,83 +951,166 @@ LOGGING = { 推荐阅读:[《如何有效防止API的重放攻击》](https://help.aliyun.com/knowledge_detail/50041.html)。 -#### 修改配置文件 +#### 使用djangorestframework + +安装djangorestfrmework(为了描述方便,以下统一简称为drf)。 + +```Shell +pip install djangorestframework +``` + +配置drf。 ```Python - INSTALLED_APPS = [ 'rest_framework', ] + +REST_FRAMEWORK = { + # 配置默认页面大小 + 'PAGE_SIZE': 5, + # 配置默认的分页类 + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', +} ``` #### 编写序列化器 ```Python - from rest_framework import serializers -class ProvinceSerializer(serializers.ModelSerializer): +class HouseTypeSerializer(serializers.ModelSerializer): class Meta: - model = Province - fields = ('prov_id', 'prov_name') + model = HouseType + fields = '__all__' + + +class DistrictSerializer(serializers.ModelSerializer): + + class Meta: + model = District + fields = ('distid', 'name', 'intro') + + +class AgentSerializer(serializers.ModelSerializer): + # 如果属性需要通过代码来获取就定义为SerializerMethodField + # 获取estates属性的方法应该命名为get_estates + estates = serializers.SerializerMethodField() + + @staticmethod + def get_estates(agent): + ret_value = [] + # 对于多对一外键关联(ForeignKey)可以用select_related提前加载关联属性 + # 通过这种方式可以使用内连接或左外连接直接获取关联属性避免1+N查询问题 + items = agent.estates.all().select_related('district') + for item in items: + ret_value.append({"estateid": item.estateid, + "name": item.name, + "district": DistrictSerializer(item.district).data}) + return ret_value + + class Meta: + model = Agent + fields = ('agentid', 'name', 'tel', 'certificated', 'estates') ``` -#### 最怂的做法 +#### 方法1:使用装饰器 ```Python - -@csrf_exempt -def list_provinces(request): - if request.method == 'GET': - serializer = ProvinceSerializer(Province.objects.all(), many=True) - elif request.method = 'POST': - serializer = ProvinceSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return HttpResponse(json.dumps(serializer.data), - content_type="application/json; charset=utf-8") -``` - -#### 使用装饰器 - -```Python - -@api_view(['GET', 'POST']) -def list_provinces(request): - if request.method == 'GET': - serializer = ProvinceSerializer(Province.objects.all(), many=True) - return Response(serializer.data) - elif request.method == 'POST': - serializer = ProvinceSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +@api_view(['GET']) +@cache_page(timeout=None) +def provinces(request): + query_set = District.objects.filter(parent__isnull=True) + serializer = DistrictSerializer(query_set, many=True) + return JsonResponse(serializer.data, safe=False) @api_view(['GET']) -def list_cities_by_prov(request, prov_id): - serializer = CitySerializer(City.objects.filter(prov__prov_id=prov_id), many=True) - return Response(serializer.data) +@cache_page(timeout=120) +def districts(request, pid): + query_set = District.objects.filter(parent__distid=pid) + serializer = DistrictSerializer(query_set, many=True) + return JsonResponse(serializer.data, safe=False) ``` ```Python - urlpatterns = [ - path('provinces/', views.list_provinces), - path('provinces/', views.list_cities_by_prov), + path('districts/', views.provinces, name='districts'), + path('districts/', views.districts, name='district'), ] ``` +> 说明:上面使用了Django自带的视图装饰器(@cache_page)来实现对API接口返回数据的缓存。 + +#### 方法2:使用APIView及其子类 + +更好的复用代码,不要重“复发明轮子”。 + +```Python +from rest_framework.generics import ListAPIView +from rest_framework.response import Response +from rest_framework_extensions.cache.decorators import cache_response + + +def customize_cache_key(view_instance, view_method, request, args, kwargs): + """自定义缓存的key的函数""" + full_path = request.get_full_path() + return f'fangall:api:{full_path}' + + +class AgentDetailView(ListAPIView): + queryset = Agent.objects.all() + serializer_class = AgentDetailSerializer + pagination_class = None + + @cache_response(key_func=customize_cache_key) + def get(self, request, agentid, *args, **kwargs): + query_set = Agent.objects.filter(agentid=agentid)\ + .prefetch_related("estates").last() + serializer = AgentDetailSerializer(query_set) + return Response(serializer.data) +``` + +```Python +urlpatterns = [ + path('agents/', views.AgentDetailView.as_view(), name='agent'), +] +``` + +> 说明:上面使用了drf_extensions提供的@cache_response实现了对接口数据的缓存,并使用自定义的函数来生成缓存中的key。 + +#### 方法3:使用ViewSet及其子类 + +```Python +class HouseTypeViewSet(CacheResponseMixin, viewsets.ModelViewSet): + queryset = HouseType.objects.all() + serializer_class = HouseTypeSerializer + pagination_class = None +``` + +```Python +router = DefaultRouter() +router.register('housetypes', views.HouseTypeViewSet) + +urlpatterns += router.urls +``` + +> 说明:上面使用了drf_extensions提供的CacheResponseMixin混入类实现了对接口数据的缓存。 + +drf提供了基于Bootstrap定制的页面来显示接口返回的JSON数据,当然也可以使用[POSTMAN](https://www.getpostman.com/)这样的工具对API接口进行测试。 + +#### 补充说明 + +在这里顺便提一下跟前端相关的几个问题。 + 问题1:如何让浏览器能够发起DELETE/PUT/PATCH? ```HTML -
@@ -974,13 +1119,11 @@ urlpatterns = [ ``` ```Python - if request.method == 'POST' and '_method' in request.POST: request.method = request.POST['_method'].upper() ``` ```HTML - - + + ``` -#### 使用类视图 +#### 身份认证 -更好的复用代码,不要重“复发明轮子”。 +查看drf中APIView类的代码可以看出,drf默认的认证方案是 `DEFAULT_AUTHENTICATION_CLASSES`,如果修改authentication_classes就可以自行定制身份认证的方案。 ```Python +class APIView(View): -class CityView(APIView): + # The following policies may be set at either globally, or per-view. + renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + parser_classes = api_settings.DEFAULT_PARSER_CLASSES + authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES + throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES + permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS + metadata_class = api_settings.DEFAULT_METADATA_CLASS + versioning_class = api_settings.DEFAULT_VERSIONING_CLASS - def get(self, request, pk, format=None): + # 此处省略下面的代码 +``` + +```Python +DEFAULTS = { + # Base API policies + 'DEFAULT_RENDERER_CLASSES': ( + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + ), + 'DEFAULT_PARSER_CLASSES': ( + 'rest_framework.parsers.JSONParser', + 'rest_framework.parsers.FormParser', + 'rest_framework.parsers.MultiPartParser' + ), + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.BasicAuthentication' + ), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.AllowAny', + ), + 'DEFAULT_THROTTLE_CLASSES': (), + 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', + 'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata', + 'DEFAULT_VERSIONING_CLASS': None, + + # 此处省略下面的代码 +} +``` + +自定义认证类,继承`BaseAuthentication`并重写`authenticate(self, request)`方法,通过请求中的userid和token来确定用户身份。如果认证成功,该方法应返回一个二元组(用户和令牌的信息),否则产生异常。也可以重写 `authenticate_header(self, request)`方法来返回一个字符串,该字符串将用于`HTTP 401 Unauthorized`响应中的WWW-Authenticate响应头的值。如果未重写该方法,那么当未经身份验证的请求被拒绝访问时,身份验证方案将返回`HTTP 403 Forbidden`响应。 + +```Python +class Authentication(BaseAuthentication): + + def authenticate(self, request): try: - serializer = CitySerializer(City.objects.get(pk=pk)) - return Response(serializer.data) - except City.DoesNotExist: - raise Http404 + userid = request.GET['userid'] + token = request.GET['token'] + user = User.objects.filter(userid=userid, token=token).first() + if not user: + raise AuthenticationFailed('用户身份信息认证失败') + return user, user + except KeyError: + raise NotAuthenticated('请提供当前用户身份认证信息') - def put(self, request, pk, format=None): - try: - city = City.objects.get(pk=pk) - serializer = CitySerializer(city, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - except City.DoesNotExist: - raise Http404 - - def delete(self, request, pk, format=None): - try: - city = City.objects.get(pk=pk) - city.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - except City.DoesNotExist: - raise Http404 + def authenticate_header(self, request): + pass ``` +使用自定义的认证类。 + ```Python +class AgentDetailView(ListAPIView): + queryset = Agent.objects.all() + serializer_class = AgentDetailSerializer + authentication_classes = [Authentication, ] + pagination_class = None -urlpatterns = [ - path('cities/', views.CityView.as_view()), -] + @cache_response(key_func=customize_cache_key) + def get(self, request, agentid, *args, **kwargs): + query_set = Agent.objects.filter(agentid=agentid)\ + .prefetch_related("estates").last() + serializer = AgentDetailSerializer(query_set) + return Response(serializer.data) ``` -#### 使用ViewSet +#### 授予权限 + +权限检查总是在视图的最开始处运行,在任何其他代码被允许进行之前。最简单的权限是允许通过身份验证的用户访问,并拒绝未经身份验证的用户访问,这对应于dfr中的`IsAuthenticated`类,可以用它来取代默认的`AllowAny`类。权限策略可以在Django的drf配置中用`DEFAULT_PERMISSION_CLASSES`全局设置。 ```Python - -class DistrictViewSet(viewsets.ReadOnlyModelViewSet): - - queryset = District.objects.all() - serializer_class = DistrictSerializer +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.IsAuthenticated', + ) +} ``` +也可以在基于`APIView`类的视图上设置身份验证策略。 + ```Python +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView -class DistrictViewSet(viewsets.ModelViewSet): - - queryset = District.objects.all() - serializer_class = DistrictSerializer +class ExampleView(APIView): + permission_classes = (IsAuthenticated, ) + # 此处省略其他代码 ``` +或者在基于`@api_view`装饰器的视图函数上设置。 + ```Python +from rest_framework.decorators import api_view, permission_classes +from rest_framework.permissions import IsAuthenticated -router = routers.DefaultRouter() -router.register('districts', views.DistrictViewSet) - -urlpatterns += router.urls +@api_view(['GET']) +@permission_classes((IsAuthenticated, )) +def example_view(request, format=None): + # 此处省略其他代码 ``` -#### 认证用户身份 +自定义权限需要继承`BasePermission`并实现以下方法中的一个或两个,下面是BasePermission的代码。 -1. 利用Django自带的User。 +```Python +@six.add_metaclass(BasePermissionMetaclass) +class BasePermission(object): + """ + A base class from which all permission classes should inherit. + """ -2. 自行对用户及其身份验证的摘要进行处理。 + def has_permission(self, request, view): + """ + Return `True` if permission is granted, `False` otherwise. + """ + return True - ```Python - - sha1_proto = hashlib.sha1() - - - def check_user_sign(get_response): - - def middleware(request): - if request.path.startswith('/api'): - data = request.GET if request.method == 'GET' else request.data - try: - user_id = data['user_id'] - user_token = cache.get(f'fang:user:{user_id}') - user_sign = data['user_sign'] - hasher = sha1_proto.copy() - hasher.update(f'{user_id}:{user_token}'.encode('utf-8')) - if hasher.hexdigest() == user_sign: - return get_response(request) - except KeyError: - pass - return JsonResponse({'msg': '身份验证失败拒绝访问'}) - else: - return get_response(request) - - return middleware - ``` + def has_object_permission(self, request, view, obj): + """ + Return `True` if permission is granted, `False` otherwise. + """ + return True +``` -3. 请求的时效性问题。(请求中再加上一个随机的令牌) +如果请求被授予访问权限,则方法应该返回True,否则返False。下面的例子演示了阻止黑名单中的IP地址访问接口数据(这个在反爬虫的时候很有用哟)。 -### 其他问题 +```Python +from rest_framework import permissions -问题1:如何设计一套权限系统?(RBAC或ACL) -1. RBAC - 基于角色的访问控制(用户-角色-权限,都是多对多关系)。 +class BlacklistPermission(permissions.BasePermission): + """ + Global permission check for blacklisted IPs. + """ + + def has_permission(self, request, view): + ip_addr = request.META['REMOTE_ADDR'] + blacklisted = Blacklist.objects.filter(ip_addr=ip_addr).exists() + return not blacklisted +``` + +如果要实现更为完整的权限验证,可以考虑RBAC或ACL。 + +1. RBAC - 基于角色的访问控制(用户-角色-权限-资源,都是多对多关系)。 2. ACL - 访问控制列表(每个用户绑定自己的访问白名单)。 -问题2:如何实现异步任务和定时任务?(Celery) +#### 访问限流 -问题3:如何解决JavaScript跨域获取数据的问题?(django-cors-headers) +可以修改dfr配置的`DEFAULT_THROTTLE_CLASSES` 和 `DEFAULT_THROTTLE_RATES`两个值来设置全局默认限流策略。例如: -问题4:网站图片(水印、剪裁)和视频(截图、水印、转码)是如何处理的?(云存储、FFmpeg) +```Python +REST_FRAMEWORK = { + 'DEFAULT_THROTTLE_CLASSES': ( + 'rest_framework.throttling.AnonRateThrottle', + 'rest_framework.throttling.UserRateThrottle' + ), + 'DEFAULT_THROTTLE_RATES': { + 'anon': '100/day', + 'user': '1000/day' + } +} +``` -问题5:网站如何架设(静态资源)文件系统? +`DEFAULT_THROTTLE_RATES`中使用的频率描述可能包括`second`、`minute`、`hour`或`day`。 + +如果要为接口单独设置限流,可以在每个视图或视图集上设置限流策略,如下所示: + +```Python +from rest_framework.throttling import UserRateThrottle +from rest_framework.views import APIView + + +class ExampleView(APIView): + throttle_classes = (UserRateThrottle, ) + # 此处省略下面的代码 +``` + +或 + +```Python +@api_view(['GET']) +@throttle_classes([UserRateThrottle, ]) +def example_view(request, format=None): + # 此处省略下面的代码 +``` + +当然也可以通过继承`BaseThrottle`来自定义限流策略,需要重写`allow_request`和`wait`方法。 + +### 异步任务和计划任务 #### Celery的应用 @@ -1174,7 +1404,6 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis 1. 安装RabbitMQ。 ```Shell - docker pull rabbitmq docker run -d -p 5672:5672 --name myrabbit rabbitmq docker container exec -it myrabbit /bin/bash @@ -1183,18 +1412,16 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis 2. 创建用户、资源以及分配操作权限。 ```Shell - - rabbitmqctl add_user jackfrued 123456 - rabbitmqctl set_user_tags jackfrued administrator - rabbitmqctl add_vhost myvhost - rabbitmqctl set_permissions -p myvhost jackfrued ".*" ".*" ".*" + rabbitmqctl add_user luohao 123456 + rabbitmqctl set_user_tags luohao administrator + rabbitmqctl add_vhost vhost1 + rabbitmqctl set_permissions -p vhost1 luohao ".*" ".*" ".*" ``` 3. 创建Celery实例。 ```Python - - project_name = 'fang' + project_name = '...' project_settings = '%s.settings' % project_name # 注册环境变量 @@ -1202,8 +1429,7 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis app = celery.Celery( project_name, - backend='amqp://jackfrued:123456@120.77.222.217:5672/myvhost', - broker='amqp://jackfrued:123456@120.77.222.217:5672/myvhost' + broker='amqp://luohao:123456@120.77.222.217:5672/vhost1' ) # 从默认的配置文件读取配置信息 @@ -1213,53 +1439,82 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) ``` -4. 启动Celery创建woker。 +4. 启动Celery创建woker(消息的消费者)。 ```Shell - - celery -A fang worker -l info & + celery -A worker -l debug & ``` 5. 执行异步任务。 ```Python - @app.task def send_email(from, to, cc, subject, content): pass - async_result = send_email.delay('', [], [], '', '') - async_result.get() + send_email.delay('', [], [], '', '') ``` 6. 创建定时任务。 ```Python - - from celery.schedules import crontab - from celery.task import periodic_task - - - @periodic_task(run_every=crontab('*', '12,18')) - def print_dummy_info(): - print('你妈喊你回家吃饭啦') + # 配置定时任务(计划任务) + app.conf.update( + timezone=settings.TIME_ZONE, + enable_utc=True, + # 定时任务(计划任务)相当于是消息的生产者 + # 如果只有生产者没有消费者那么消息就会在消息队列中积压 + # 将来实际部署项目的时候生产者、消费者、消息队列可能都是不同节点 + beat_schedule={ + 'task1': { + 'task': 'common.tasks.show_msg', + 'schedule': crontab(), + 'args': ('刘强东,奶茶妹妹喊你回家喝奶啦', ) + }, + }, + ) ``` -7. 检查定时任务并交给worker执行。 + ```Python + @app.task + def show_msg(content): + print(content) + ``` + +7. 启动Celery创建执行定时任务的beat(消息的生产者)。 ```Shell - - celery -A fang beat -l info + celery -A beat -l info ``` 8. 检查消息队列状况。 ```Shell - - rabbitmqctl list_queues -p myvhost + rabbitmqctl list_queues -p vhost1 ``` +### 其他问题 + +问题1:如何解决JavaScript跨域获取数据的问题?(django-cors-headers) + +```Python +INSTALLED_APPS = [ + 'corsheaders', +] +MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', +] +# 配置跨域白名单 +# CORS_ORIGIN_WHITELIST = () +# 配置是否跨域读取Cookie信息 +# CORS_ALLOW_CREDENTIALS = True +``` + +问题2:网站图片(水印、剪裁)和视频(截图、水印、转码)是如何处理的?(云存储、FFmpeg) + +问题3:网站如何架设(静态资源)文件系统?(FastDFS、云存储、CDN) + ### 安全保护 问题1:什么是跨站脚本攻击,如何防范?(对提交的内容进行消毒) @@ -1272,51 +1527,48 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis #### Django提供的安全措施 - 签名数据的API +签名数据的API ```Python - - >>> from django.core.signing import Signer - >>> signer = Signer() - >>> value = signer.sign('hello, world!') - >>> value - 'hello, world!:BYMlgvWMTSPLxC-DqxByleiMVXU' - >>> signer.unsign(value) - 'hello, world!' - >>> - >>> signer = Signer(salt='1qaz2wsx') - >>> signer.sign('hello, world!') - 'hello, world!:9vEvG6EA05hjMDB5MtUr33nRA_M' - >>> - >>> from django.core.signing import TimestampSigner - >>> signer = TimestampSigner() - >>> value = signer.sign('hello, world!') - >>> value - 'hello, world!:1fpmcQ:STwj464IFE6eUB-_-hyUVF3d2So' - >>> signer.unsign(value, max_age=5) - Traceback (most recent call last): - File "", line 1, in - File "/Users/Hao/Desktop/fang.com/venv/lib/python3.6/site-packages/django/core/signing.py", line 198, in unsign - 'Signature age %s > %s seconds' % (age, max_age)) - django.core.signing.SignatureExpired: Signature age 21.020604848861694 > 5 seconds - >>> signer.unsign(value, max_age=120) - 'hello, world!' +>>> from django.core.signing import Signer +>>> signer = Signer() +>>> value = signer.sign('hello, world!') +>>> value +'hello, world!:BYMlgvWMTSPLxC-DqxByleiMVXU' +>>> signer.unsign(value) +'hello, world!' +>>> +>>> signer = Signer(salt='1qaz2wsx') +>>> signer.sign('hello, world!') +'hello, world!:9vEvG6EA05hjMDB5MtUr33nRA_M' +>>> +>>> from django.core.signing import TimestampSigner +>>> signer = TimestampSigner() +>>> value = signer.sign('hello, world!') +>>> value +'hello, world!:1fpmcQ:STwj464IFE6eUB-_-hyUVF3d2So' +>>> signer.unsign(value, max_age=5) +Traceback (most recent call last): + File "", line 1, in + File "/Users/Hao/Desktop/fang.com/venv/lib/python3.6/site-packages/django/core/signing.py", line 198, in unsign + 'Signature age %s > %s seconds' % (age, max_age)) + django.core.signing.SignatureExpired: Signature age 21.020604848861694 > 5 seconds +>>> signer.unsign(value, max_age=120) +'hello, world!' ``` -5. CSRF令牌和小工具 +CSRF令牌和小工具 - ```HTML - - {% csrf_token %} - ``` +```HTML +{% csrf_token %} +``` - ```Python - - @csrf_exempt - @csrf_protect - @require_csrf_token - @ensure_csrf_cookie - ``` +- @csrf_exempt:免除令牌 +- @csrf_protect:提供令牌保护 +- @require_csrf_token:提供令牌保护 +- @ensure_csrf_cookie:强制视图发送带令牌的cookie + +> 说明:可以在Chrome浏览器中安装EditThisCookie插件来方便的查看Cookie。 #### 用户敏感信息的保护 @@ -1324,26 +1576,28 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis 1. 哈希摘要(签名) ```Python - >>> import hashlib >>> - >>> md5_proto = hashlib.md5() - >>> md5_hasher = md5_proto.copy() + >>> md5_hasher = hashlib.md5() >>> md5_hasher.update('hello, world!'.encode()) >>> md5_hasher.hexdigest() '3adbbad1791fbae3ec908894c4963870' >>> - >>> sha1_proto = hashlib.sha1() - >>> sha1_hasher = sha1_proto.copy() + >>> sha1_hasher = hashlib.sha1() >>> sha1_hasher.update('hello, world!'.encode()) + >>> sha1_hasher.update('goodbye, world!'.encode()) >>> sha1_hasher.hexdigest() '1f09d30c707d53f3d16c530dd73d70a6ce7596a9' ``` -2. 加密和解密(对称加密和非对称加密) +2. 加密和解密(对称加密[AES]和非对称加密[RSA]) + + ```Shell + pip install rsa + pip install pycrypto + ``` ```Python - >>> pub_key, pri_key = rsa.newkeys(1024) >>> message = 'hello, world!' >>> crypto = rsa.encrypt(message.encode(), pub_key) @@ -1364,17 +1618,44 @@ Celery是一个本身不提供队列服务,官方推荐使用RabbitMQ或Redis ### 测试相关 -1. 测试为项目带来的好处有哪些?(更早的发现问题,更好的团队协作) -2. 什么是黑盒测试和白盒测试?(方法是黑盒子,不知道实现细节,通过设定输入和预期输出进行来断言方法是否实现了应有的功能) +测试是发现和标记缺陷的过程。所谓的缺陷是指实际结果和期望结果之间的任何差别。有的地方,测试也被认为是执行以找出错误为目的的程序的过程。 测试是为了让产品达到以下目标: + +1. 满足需求用户满意 +2. 改善产品的市场占有率 +3. 树立对产品的信任 +4. 减少开发和维护的成本 + +#### 功能测试 + +如果一个软件单元的行为方式与它的开发规范完全一样,那么该软件单元就通过了它的功能测试。 + - 白盒测试:开发人员自己实现,最基本的形式是单元测试,还有集成测试和系统测试。 + - 黑盒测试:由开发团队之外的人执行,对测试代码没有可见性,将被测系统视为黑盒子。通常由测试人员或QA工程师来执行,Web应用可以通过Selenium这样的测试框架自动化实施。 + +#### 性能测试 + +软件在高工作负载下对其响应性和健壮性展开的测试。 + +- 负载测试:在特定负载下执行的测试。 + + - 压力测试:突发条件或极限条件下的性能测试。 + +#### 安全性测试 + +系统的敏感数据都是经过认证和授权之后才能访问。 + +#### 其他测试 + +易用性测试 / 安装测试 / 可访问性测试 #### 单元测试 -目标:测试函数和对象的方法(程序中最基本的单元)。 +测试函数和对象的方法(程序中最小最基本的单元)。通过对实际输出和预期输出的比对以及各种的断言条件来判定被测单元是否满足设计需求。 -实施:通过对实际输出和预期输出的比对以及各种的断言条件来判定被测单元是否满足设计需求。 +- 测试用例 +- 测试固件 - 每次测试时都要使用的东西。 +- 测试套件(测试集)- 组合了多个测试用例而构成的集合。 ```Python - class UtilTest(TestCase): def setUp(self): @@ -1394,57 +1675,185 @@ class UtilTest(TestCase): self.assertEqual(value, to_md5_hex(key)) ``` -#### 测试模型 +`TestCase`的断言方法: + +- assertEqual / assertNotEqual +- assertTrue / assertFalse / assertIsNot +- assertRaise / assertRaiseRegexp +- assertAlmostEqual / assertNotAlmostEqual +- assertGreater / assertGreaterEqual / assertLess / assertLessEqual +- assertRegexpMatches / assertNotRegexpMatches +- assertListEqual / assertSetEqual / assertTupleEqual / assertDictEqual + +可以使用nose2或pytest来辅助执行单元测试,同时通过cov-core或pytest-cov可以对测试覆度进行评估。覆盖率由百分比表示。比如测试代码执行过了程序的每一行,那么覆盖率就是100%。这种时候,几乎不会出现新程序上线后突然无法运行的尴尬情况。覆盖率不关心代码内容究竟是什么,覆盖率是用来检查“测试代码不足、测试存在疏漏”的一个指标,“测试内容是否妥当”并不归它管。 + +```Shell +pip install nose2 pytest cov-core pytest-cov +``` + +可以使用Selenium来实现Web应用的自动化测试,它还可以用于屏幕抓取与浏览器行为模拟,通过爬虫抓取页面上的动态数据也可以使用它。Selenium其实包括三个部分: + +- Selenium IDE:嵌入到浏览器的插件,可以录制和回放脚本。 + + ![](./res/selenium_ide.png) + +- Selenium WebDriver:支持多种语言可以操控浏览器的API。 + +- Selenium Standalone Server:Selenium Grid、远程控制、分布式部署。 + +```Shell +pip install selenium +``` ```Python +from selenium import webdriver +import pytest +import contextlib -class ModelTest(TestCase): - def test_save_province(self): - pass +@pytest.fixture(scope='session') +def chrome(): + # 设置使用无头浏览器(不会打开浏览器窗口) + options = webdriver.ChromeOptions() + options.add_argument('--headless') + driver = webdriver.Chrome(options=options) + yield driver + driver.quit() - def test_del_province(self): - pass - def test_update_province(self): - pass - - def test_get_all_provinces(self): - pass - - def test_get_province_by_id(self): - pass +def test_baidu_index(chrome): + chrome.get('https://www.baidu.com') + assert chrome.title == '百度一下,你就知道' ``` -#### 测试视图 +```Shell +nose2 -v -C +pytest --cov +``` + +```Shell +Ran 7 tests in 0.002s + +OK +Name Stmts Miss Cover +---------------------------------------------- +example01.py 15 0 100% +example02.py 49 49 0% +example03.py 22 22 0% +example04.py 61 61 0% +example05.py 29 29 0% +example06.py 39 39 0% +example07.py 19 19 0% +example08.py 27 27 0% +example09.py 18 18 0% +example10.py 19 19 0% +example11.py 22 22 0% +example12.py 28 28 0% +example13.py 28 28 0% +test_ddt_example.py 18 0 100% +test_pytest_example.py 11 6 45% +test_unittest_example.py 22 0 100% +---------------------------------------------- +TOTAL 427 367 14% +``` + +在测试过程中需要孤立各种外部依赖(数据库、外部接口调用、时间依赖),具体又包括两个方面: + +1. 数据源:数据本地化 / 置于内存中 / 测试之后回滚 + +2. 资源虚拟化:存根/桩(stub)、仿制/模拟(mock)、伪造(fake) + + - stub:测试期间为提供响应的函数生成的替代品 + - mock:代替实际对象(以及该对象的API)的对象 + + - fake:没有达到生产级别的轻量级对象 + +#### 集成测试 + +集成多个函数或方法的输入输出的测试,测试时需要将多个测试对象组合在一起。 + + - 测试组件互操作性 / 需求变更测试 / 外部依赖和API / 调试硬件问题 / 在代码路径中发现异常 + + +#### 系统测试 + +对需求的测试,测试成品是否最终满足了所有需求,在客户验收项目时进行。 + +#### 数据驱动测试 + +使用外部数据源实现对输入值与期望值的参数化,避免在测试中使用硬编码的数据。 + +被测函数: ```Python +def add(x, y): + return x + y +``` -class RentViewTest(TestCase): +data.csv文件: - def test_index_view(self): +``` +3,1,2 +0,1,-1 +100,50,50 +100,1,99 +15,7,8 +``` + +测试代码: + +```Python +import csv + +from unittest import TestCase +from ddt import ddt, data, unpack + + +@ddt +class TestAdd(TestCase): + + def load_data_from_csv(filename): + data_items = [] + with open(filename, 'r', newline='') as fs: + reader = csv.reader(fs) + for row in reader: + data_items.append(list(map(int, row))) + return data_items + + + @data(*load_data_from_csv('data.csv')) + @unpack + def test_add(self, result, param1, param2): + self.assertEqual(result, add(param1, param2)) +``` + +#### Django中的测试 + +1. 测试Django视图 - Django中提供的`TestCase`扩展了`unittest`中的`TestCase`,绑定了一个名为`client`的属性,可以用来模拟浏览器发出的GET、POST、DELETE、PUT等请求。 + +```Python +class SomeViewTest(TestCase): + + def test_example_view(self): resp = self.client.get(reverse('index')) - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.context['num'], 5) + self.assertEqual(200, resp.status_code) + self.assertEqual(5, resp.context['num']) ``` -#### 运行测试 - -配置测试数据库 +2. 运行测试 - 配置测试数据库。 ```Python - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'HOST': 'localhost', 'PORT': 3306, - 'NAME': 'House', + 'NAME': 'DbName', 'USER': os.environ['DB_USER'], 'PASSWORD': os.environ['DB_PASS'], 'TEST': { - 'NAME': 'House_for_testing', + 'NAME': 'DbName_for_testing', 'CHARSET': 'utf8', }, } @@ -1452,17 +1861,15 @@ DATABASES = { ``` ```Shell - python manage.py test python manage.py test common python manage.py test common.tests.UtilsTest python manage.py test common.tests.UtilsTest.test_to_md5_hex ``` -#### 测试覆盖度 +3. 评估测试覆盖度 ```Shell - pip install coverage coverage run --source= --omit= manage.py test common coverage report @@ -1489,12 +1896,12 @@ TOTAL 267 176 34% #### 性能测试 -问题1:性能测试的指标有哪些?() +问题1:性能测试的指标有哪些? -1. ab +1. ab - Apache Benchmark / webbench / httpperf ```Shell - + yum -y install httpd ab -c 10 -n 1000 http://www.baidu.com/ ... Benchmarking www.baidu.com (be patient).....done @@ -1536,7 +1943,6 @@ TOTAL 267 176 34% 2. mysqlslap ```Shell - mysqlslap -a -c 100 -h 1.2.3.4 -u root -p mysqlslap -a -c 100 --number-of-queries=1000 --auto-generate-sql-load-type=read -h <负载均衡服务器IP地址> -u root -p mysqlslap -a --concurrency=50,100 --number-of-queries=1000 --debug-info --auto-generate-sql-load-type=mixed -h 1.2.3.4 -u root -p @@ -1545,7 +1951,6 @@ TOTAL 267 176 34% 3. sysbench ```Shell - sysbench --test=threads --num-threads=64 --thread-yields=100 --thread-locks=2 run sysbench --test=memory --num-threads=512 --memory-block-size=256M --memory-total-size=32G run ``` @@ -1554,7 +1959,50 @@ TOTAL 267 176 34% 请查看[《使用JMeter进行性能测试》](https://www.ibm.com/developerworks/cn/java/l-jmeter/index.html)。 -5. LoadRunner +5. LoadRunner / QTP + +### 项目调试 + +可以使用django-debug-toolbar来辅助项目调试。 + +1. 安装 + + ```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. 使用 - 在页面右侧可以看到一个调试工具栏,上面包括了执行时间、项目设置、请求头、SQL、静态资源、模板、缓存、信号等调试信息,查看起来非常的方便。 ### 部署相关 @@ -1579,7 +2027,6 @@ TOTAL 267 176 34% 2. 开启[模板缓存](https://docs.djangoproject.com/en/2.0/ref/templates/api/#django.template.loaders.cached.Loader)来加速模板的渲染。 ```Python - TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -1611,8 +2058,11 @@ TOTAL 267 176 34% 1. 用ID生成器代替自增主键(性能更好、适用于分布式环境)。 + - 自定义ID生成器 + + - UUID + ```Python - >>> my_uuid = uuid.uuid1() >>> my_uuid UUID('63f859d0-a03a-11e8-b0ad-60f81da8d840') @@ -1622,22 +2072,29 @@ TOTAL 267 176 34% 2. 避免不必要的外键列上的约束(除非必须保证参照完整性),更不要使用触发器之类的机制。 -3. 使用索引来优化查询性能(索引放在要用于查询的字段上)。 +3. 使用索引来优化查询性能(索引放在要用于查询的字段上)。InnoDB用的是BTREE索引,使用>、<、>=、<=、BETWEEN或者LIKE 'pattern'(pattern不以通配符开头)时都可以用到索引。因为建立索引需要额外的磁盘空间,而主键上是有默认的索引,所以主键要尽可能选择较短的数据类型来减少磁盘占用,提高索引的缓存效果。 ```SQL - + create index idx_goods_name on tb_goods (gname(10)); + ``` + + ```SQL + -- 无法使用索引 + select * from tb_goods where gname like '%iPhone%'; + -- 可以使用索引 select * from tb_goods where gname like 'iPhone%'; ``` ```Python - + # 无法使用索引 + Goods.objects.filter(name_icontains='iPhone') + # 可以使用索引 Goods.objects.filter(name__istartswith='iPhone'); ``` 4. 使用存储过程(存储在服务器端编译过的一组SQL语句)。 ```SQL - drop procedure if exists sp_avg_sal_by_dept; create procedure sp_avg_sal_by_dept(deptno integer, out avg_sal float) @@ -1651,7 +2108,6 @@ TOTAL 267 176 34% ``` ```Python - >>> from django.db import connection >>> cursor = connection.cursor() >>> cursor.callproc('sp_avg_sal_by_dept', (10, 0)) @@ -1660,19 +2116,70 @@ TOTAL 267 176 34% (2675.0,) ``` -5. 使用`explain`来分析查询性能 - 执行计划。 +5. 使用数据分区。通过分区可以存储更多的数据、优化查询更大的吞吐量、可以快速删除过期的数据。关于这个知识点可以看看MySQL的[官方文档](https://dev.mysql.com/doc/refman/5.7/en/partitioning-overview.html)。 + + - RANGE分区:基于连续区间范围,把数据分配到不同的分区。 + - LIST分区:基于枚举值的范围,把数据分配到不同的分区。 + - HASH分区 / KEY分区:基于分区个数,把数据分配到不同的分区。 + + ```SQL + CREATE TABLE tb_emp ( + eno INT NOT NULL, + ename VARCHAR(20) NOT NULL, + job VARCHAR(10) NOT NULL, + hiredate DATE NOT NULL, + dno INT NOT NULL + ) + PARTITION BY HASH(dno) + PARTITIONS 4; + ``` + + ```SQL + CREATE TABLE tb_emp ( + eno INT NOT NULL, + ename VARCHAR(20) NOT NULL, + job VARCHAR(10) NOT NULL, + hiredate DATE NOT NULL, + dno INT NOT NULL + ) + PARTITION BY RANGE( YEAR(hiredate) ) ( + PARTITION p0 VALUES LESS THAN (1960), + PARTITION p1 VALUES LESS THAN (1970), + PARTITION p2 VALUES LESS THAN (1980), + PARTITION p3 VALUES LESS THAN (1990), + PARTITION p4 VALUES LESS THAN MAXVALUE + ); + ``` + +6. 使用`explain`来分析查询性能 - 执行计划。 ```SQL - explain select * from ...; ``` - 请参考网易出品的《深入浅出MySQL》上面对应部分的讲解(已经有第二版)。 + `explain`结果解析: -6. 使用慢查询日志来发现性能低下的查询。 + - select_type:表示select操作的类型,常见的值有SIMPLE(简单查询,没有使用子查询或者表连接查询)、PRIMARY(主查询,外层的查询)、UNION(并集操作中的第二个或者后面的查询)、SUBQUERY(子查询中的第一个SELECT)等。 + - table:输出结果的表。 + - type:MySQL在表中找到所需行的方式,也称为访问类型,常见的值有: + - ALL:全表扫描(遍历全表找到匹配的行) + - index:索引全扫描(遍历整个索引) + - range:索引范围扫描 + - ref:非唯一索引扫描或唯一索引的前缀扫描 + - eq_ref:唯一索引扫描 + - const / system:表中最多有一行匹配 + - NULL:不用访问表或者索引 + - possible_keys:查询时可能用到的索引。 + - key:实际使用的索引。 + - key_len:使用到索引字段的长度。 + - rows:扫描行的数量。 + - Extra:额外的信息(执行情况的说明或描述)。 + + > 说明:关于MySQL更多的知识尤其是性能调优和运维方面的内容,推荐大家阅读网易出品的《深入浅出MySQL(第2版)》,网易出品必属精品。 + +7. 使用慢查询日志来发现性能低下的查询。 ```SQL - mysql> show variables like 'slow_query%'; +---------------------------+----------------------------------+ | Variable_name | Value | @@ -1690,14 +2197,11 @@ TOTAL 267 176 34% ``` ```SQL - mysql> set global slow_query_log='ON'; - mysql> set global slow_query_log_file='/usr/local/mysql/data/slow.log'; mysql> set global long_query_time=1; ``` ```INI - [mysqld] slow_query_log = ON slow_query_log_file = /usr/local/mysql/data/slow.log @@ -1706,4 +2210,4 @@ TOTAL 267 176 34% #### 其他 -请参考《Python项目性能调优》。 \ No newline at end of file +请参考《Python性能调优》。 \ No newline at end of file diff --git a/Day91-100/Docker简易上手指南.md b/Day91-100/Docker简易上手指南.md index effaf02..69bcf2c 100644 --- a/Day91-100/Docker简易上手指南.md +++ b/Day91-100/Docker简易上手指南.md @@ -1,10 +1,10 @@ -## Docker入门 +## Docker简易上手指南 ### Docker简介 软件开发中最为麻烦的事情可能就是配置环境了。由于用户使用的操作系统具有多样性,即便使用跨平台的开发语言(如Java和Python)都不能保证代码能够在各种平台下都可以正常的运转,而且可能在不同的环境下我们的软件需要依赖的其他软件包也是不一样的。 -那么问题来了,我们再安装软件的时候可不可以把软件运行的环境一并安装课?也就是说在安装软件的时候,我们是不是可以把原始环境一模一样地复制过来呢? +那么问题来了,我们再安装软件的时候可不可以把软件运行的环境一并安装?也就是说在安装软件的时候,我们是不是可以把原始环境一模一样地复制过来呢? 虚拟机(virtual machine)就是带环境安装的一种解决方案,它可以在一种操作系统里面运行另一种操作系统,比如在Windows系统里面运行Linux系统,在macOS上运行Windows,而应用程序对此毫无感知。使用过虚拟机的人都知道,虚拟机用起来跟真实系统一模一样,而对于虚拟机的宿主系统来说,虚拟机就是一个普通文件,不需要了就删掉,对宿主系统或者其他的程序并没有影响。但是虚拟机通常会占用较多的系统资源,启动和关闭也非常的缓慢,总之用户体验没有想象中的那么好。 @@ -87,7 +87,7 @@ docker stop docker stop ``` - 对于那些不会自动终止的容器,就可以用下面的方式来停止。 +对于那些不会自动终止的容器,就可以用下面的方式来停止。 ```Shell docker container kill @@ -107,10 +107,10 @@ service docker start ```JavaScript { - "registry-mirrors": [ - "http://hub-mirror.c.163.com", - "https://registry.docker-cn.com" - ] + "registry-mirrors": [ + "http://hub-mirror.c.163.com", + "https://registry.docker-cn.com" + ] } ``` @@ -195,3 +195,4 @@ select user, host, plugin, authentication_string from user where user='root'; 2 rows in set (0.00 sec) ``` +接下来就已经可以访问你的MySQL服务器啦,当然远程连接的时候不要忘了在防火墙上开启对应的端口。 \ No newline at end of file diff --git a/Day91-100/res/01.django_single_server.png b/Day91-100/res/01.django_single_server.png new file mode 100644 index 0000000..70d3bf3 Binary files /dev/null and b/Day91-100/res/01.django_single_server.png differ diff --git a/Day91-100/res/02.django_dedicated_db_server.png b/Day91-100/res/02.django_dedicated_db_server.png new file mode 100644 index 0000000..143eb6e Binary files /dev/null and b/Day91-100/res/02.django_dedicated_db_server.png differ diff --git a/Day91-100/res/03.django_dedicated_static_server.png b/Day91-100/res/03.django_dedicated_static_server.png new file mode 100644 index 0000000..f13a247 Binary files /dev/null and b/Day91-100/res/03.django_dedicated_static_server.png differ diff --git a/Day91-100/res/04.django_load_balance.png b/Day91-100/res/04.django_load_balance.png new file mode 100644 index 0000000..16e7515 Binary files /dev/null and b/Day91-100/res/04.django_load_balance.png differ diff --git a/Day91-100/res/05.django_massive_cluster.png b/Day91-100/res/05.django_massive_cluster.png new file mode 100644 index 0000000..9163fb2 Binary files /dev/null and b/Day91-100/res/05.django_massive_cluster.png differ diff --git a/Day91-100/res/Celery_RabitMQ.png b/Day91-100/res/Celery_RabitMQ.png new file mode 100644 index 0000000..4140130 Binary files /dev/null and b/Day91-100/res/Celery_RabitMQ.png differ diff --git a/Day91-100/res/Producer-Broker-Consumer-Arrangement.png b/Day91-100/res/Producer-Broker-Consumer-Arrangement.png new file mode 100644 index 0000000..b073a60 Binary files /dev/null and b/Day91-100/res/Producer-Broker-Consumer-Arrangement.png differ diff --git a/Day91-100/res/alipay_web_developer.png b/Day91-100/res/alipay_web_developer.png new file mode 100644 index 0000000..0716552 Binary files /dev/null and b/Day91-100/res/alipay_web_developer.png differ diff --git a/Day91-100/res/aliyun-certificate.png b/Day91-100/res/aliyun-certificate.png new file mode 100644 index 0000000..c19a30b Binary files /dev/null and b/Day91-100/res/aliyun-certificate.png differ diff --git a/Day91-100/res/aliyun-dnslist.png b/Day91-100/res/aliyun-dnslist.png new file mode 100644 index 0000000..e6eda2f Binary files /dev/null and b/Day91-100/res/aliyun-dnslist.png differ diff --git a/Day91-100/res/aliyun-domain.png b/Day91-100/res/aliyun-domain.png new file mode 100644 index 0000000..64a1f48 Binary files /dev/null and b/Day91-100/res/aliyun-domain.png differ diff --git a/Day91-100/res/aliyun-keeprecord.png b/Day91-100/res/aliyun-keeprecord.png new file mode 100644 index 0000000..d9d39bc Binary files /dev/null and b/Day91-100/res/aliyun-keeprecord.png differ diff --git a/Day91-100/res/aliyun-resolve-settings.png b/Day91-100/res/aliyun-resolve-settings.png new file mode 100644 index 0000000..3f9363a Binary files /dev/null and b/Day91-100/res/aliyun-resolve-settings.png differ diff --git a/Day91-100/res/app_folder_arch.png b/Day91-100/res/app_folder_arch.png new file mode 100644 index 0000000..7d51e95 Binary files /dev/null and b/Day91-100/res/app_folder_arch.png differ diff --git a/Day91-100/res/builtin-middlewares.png b/Day91-100/res/builtin-middlewares.png new file mode 100644 index 0000000..59acb4b Binary files /dev/null and b/Day91-100/res/builtin-middlewares.png differ diff --git a/Day91-100/res/celery.png b/Day91-100/res/celery.png new file mode 100644 index 0000000..a8d5f7d Binary files /dev/null and b/Day91-100/res/celery.png differ diff --git a/Day91-100/res/click-jacking.png b/Day91-100/res/click-jacking.png new file mode 100644 index 0000000..9d92979 Binary files /dev/null and b/Day91-100/res/click-jacking.png differ diff --git a/Day91-100/res/company_architecture.png b/Day91-100/res/company_architecture.png new file mode 100644 index 0000000..ff3f9cb Binary files /dev/null and b/Day91-100/res/company_architecture.png differ diff --git a/Day91-100/res/deployment_pipelines.png b/Day91-100/res/deployment_pipelines.png new file mode 100644 index 0000000..6db6c2d Binary files /dev/null and b/Day91-100/res/deployment_pipelines.png differ diff --git a/Day91-100/res/django-middleware.png b/Day91-100/res/django-middleware.png new file mode 100644 index 0000000..9885582 Binary files /dev/null and b/Day91-100/res/django-middleware.png differ diff --git a/Day91-100/res/django-mtv.png b/Day91-100/res/django-mtv.png new file mode 100644 index 0000000..76f330e Binary files /dev/null and b/Day91-100/res/django-mtv.png differ diff --git a/Day91-100/res/django_request_response_cycle.png b/Day91-100/res/django_request_response_cycle.png new file mode 100644 index 0000000..3423c9a Binary files /dev/null and b/Day91-100/res/django_request_response_cycle.png differ diff --git a/Day91-100/res/docker_logo.png b/Day91-100/res/docker_logo.png new file mode 100644 index 0000000..19ff797 Binary files /dev/null and b/Day91-100/res/docker_logo.png differ diff --git a/Day91-100/res/docker_vs_vm.png b/Day91-100/res/docker_vs_vm.png new file mode 100644 index 0000000..0eb6fea Binary files /dev/null and b/Day91-100/res/docker_vs_vm.png differ diff --git a/Day91-100/res/er-graph.png b/Day91-100/res/er-graph.png new file mode 100644 index 0000000..c8415be Binary files /dev/null and b/Day91-100/res/er-graph.png differ diff --git a/Day91-100/res/git_logo.png b/Day91-100/res/git_logo.png new file mode 100644 index 0000000..04b0f00 Binary files /dev/null and b/Day91-100/res/git_logo.png differ diff --git a/Day91-100/res/git_repository.png b/Day91-100/res/git_repository.png new file mode 100644 index 0000000..d85bdbd Binary files /dev/null and b/Day91-100/res/git_repository.png differ diff --git a/Day91-100/res/gitignore_io.png b/Day91-100/res/gitignore_io.png new file mode 100644 index 0000000..a305da6 Binary files /dev/null and b/Day91-100/res/gitignore_io.png differ diff --git a/Day91-100/res/greedy.png b/Day91-100/res/greedy.png new file mode 100644 index 0000000..376ceef Binary files /dev/null and b/Day91-100/res/greedy.png differ diff --git a/Day91-100/res/hadoop_ecosystem.png b/Day91-100/res/hadoop_ecosystem.png new file mode 100644 index 0000000..8028fa8 Binary files /dev/null and b/Day91-100/res/hadoop_ecosystem.png differ diff --git a/Day91-100/res/http-request.png b/Day91-100/res/http-request.png new file mode 100644 index 0000000..aca9287 Binary files /dev/null and b/Day91-100/res/http-request.png differ diff --git a/Day91-100/res/http-response.png b/Day91-100/res/http-response.png new file mode 100644 index 0000000..f2b8ae3 Binary files /dev/null and b/Day91-100/res/http-response.png differ diff --git a/Day91-100/res/jenkins_new_project.png b/Day91-100/res/jenkins_new_project.png new file mode 100644 index 0000000..8019ada Binary files /dev/null and b/Day91-100/res/jenkins_new_project.png differ diff --git a/Day91-100/res/mvc.png b/Day91-100/res/mvc.png new file mode 100644 index 0000000..7ba14ba Binary files /dev/null and b/Day91-100/res/mvc.png differ diff --git a/Day91-100/res/oauth2.png b/Day91-100/res/oauth2.png new file mode 100644 index 0000000..7a07bd3 Binary files /dev/null and b/Day91-100/res/oauth2.png differ diff --git a/Day91-100/res/power-designer-pdm.png b/Day91-100/res/power-designer-pdm.png new file mode 100644 index 0000000..5fba456 Binary files /dev/null and b/Day91-100/res/power-designer-pdm.png differ diff --git a/Day91-100/res/pylint.png b/Day91-100/res/pylint.png new file mode 100644 index 0000000..92a9671 Binary files /dev/null and b/Day91-100/res/pylint.png differ diff --git a/Day91-100/res/python-str-join.png b/Day91-100/res/python-str-join.png new file mode 100644 index 0000000..b784ffb Binary files /dev/null and b/Day91-100/res/python-str-join.png differ diff --git a/Day91-100/res/python_jobs_chengdu.png b/Day91-100/res/python_jobs_chengdu.png new file mode 100644 index 0000000..6b18fa9 Binary files /dev/null and b/Day91-100/res/python_jobs_chengdu.png differ diff --git a/Day91-100/res/python_salary_chengdu.png b/Day91-100/res/python_salary_chengdu.png new file mode 100644 index 0000000..fade359 Binary files /dev/null and b/Day91-100/res/python_salary_chengdu.png differ diff --git a/Day91-100/res/python_salary_guangzhou.png b/Day91-100/res/python_salary_guangzhou.png new file mode 100644 index 0000000..e7567e3 Binary files /dev/null and b/Day91-100/res/python_salary_guangzhou.png differ diff --git a/Day91-100/res/redmine_new_issue.png b/Day91-100/res/redmine_new_issue.png new file mode 100644 index 0000000..63971fc Binary files /dev/null and b/Day91-100/res/redmine_new_issue.png differ diff --git a/Day91-100/res/requirements_by_xmind.png b/Day91-100/res/requirements_by_xmind.png new file mode 100644 index 0000000..aac6447 Binary files /dev/null and b/Day91-100/res/requirements_by_xmind.png differ diff --git a/Day91-100/res/selenium_ide.png b/Day91-100/res/selenium_ide.png new file mode 100644 index 0000000..4d2f275 Binary files /dev/null and b/Day91-100/res/selenium_ide.png differ diff --git a/Day91-100/res/shopping-pdm.png b/Day91-100/res/shopping-pdm.png new file mode 100644 index 0000000..2b93a21 Binary files /dev/null and b/Day91-100/res/shopping-pdm.png differ diff --git a/Day91-100/res/uml-graph.png b/Day91-100/res/uml-graph.png new file mode 100644 index 0000000..def109b Binary files /dev/null and b/Day91-100/res/uml-graph.png differ diff --git a/Day91-100/res/uml.png b/Day91-100/res/uml.png new file mode 100644 index 0000000..98168cb Binary files /dev/null and b/Day91-100/res/uml.png differ diff --git a/Day91-100/res/web-application.png b/Day91-100/res/web-application.png new file mode 100644 index 0000000..89d2dec Binary files /dev/null and b/Day91-100/res/web-application.png differ diff --git a/Day91-100/res/web-queue-worker-physical.png b/Day91-100/res/web-queue-worker-physical.png new file mode 100644 index 0000000..81da48d Binary files /dev/null and b/Day91-100/res/web-queue-worker-physical.png differ diff --git a/Day91-100/项目实战 - 开启团队项目.md b/Day91-100/开启团队项目.md similarity index 98% rename from Day91-100/项目实战 - 开启团队项目.md rename to Day91-100/开启团队项目.md index c75375e..1260455 100644 --- a/Day91-100/项目实战 - 开启团队项目.md +++ b/Day91-100/开启团队项目.md @@ -1,4 +1,4 @@ -## 项目实战 - 开启团队项目 +## 开启团队项目 ### 创建项目 diff --git a/Day91-100/电商网站技术要点剖析.md b/Day91-100/电商网站技术要点剖析.md new file mode 100644 index 0000000..64582ba --- /dev/null +++ b/Day91-100/电商网站技术要点剖析.md @@ -0,0 +1,398 @@ +## 电商网站技术要点剖析 + +### 商业模式 + +1. B2B - 商家对商家,交易双方都是企业(商家),最典型的案例就是阿里巴巴。 +2. C2C - 个人对个人,例如:淘宝、人人车。 +3. B2C - 商家对个人,例如:唯品会,聚美优品。 +4. C2B - 个人对商家,先有消费者提出需求,后有商家按需求组织生产,例如: 尚品宅配。 +5. O2O - 线上到线下,将线下的商务机会与互联网结合,让互联网成为线下交易的平台,例如:美团外卖、饿了么。 +6. B2B2C - 商家对商家对个人,例如:天猫、京东。 + +### 需求要点 + +1. 用户端 + - 首页(商品分类、广告轮播、滚动快讯、瀑布加载) + + - 用户(登录(第三方登录)、注册、注销、自服务(个人信息、浏览历史、收货地址、……)) + + - 商品(分类、列表、详情、搜索、添加到购物车) + - 购物车(查看、编辑(修改数量、删除商品、清空)) + - 订单(提交订单(支付)、历史订单、订单详情、订单评价) +2. 管理端 + +> 提示:可以通过思维导图来进行需求的整理,思维导图上的每个叶子节点都是不可再拆分的功能。 + +### 物理模型设计 + +两个概念:SPU(Standard Product Unit)和SKU(Stock Keeping Unit)。 + +- SPU:iPhone 6s +- SKU:iPhone 6s 64G 土豪金 + +![](./res/shopping-pdm.png) + +### 第三方登录 + +第三方登录是指利用第三方网站(通常是知名社交网站)的账号进行登录验证,比如国内的 QQ、微博,国外的Google、Facebook等,第三方登录大部分都是使用[OAuth](),它是一个关于授权的开放网络标准,得到了广泛的应用,目前通常使用的是2.0版本。关于OAuth的基础知识,可以阅读阮一峰老师的[《理解OAuth 2.0》](http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html)。 + +#### OAuth 2.0授权流程 + +1. 用户打开客户端以后,客户端要求用户(资源所有者)给予授权。 +2. 用户(资源所有者)同意给予客户端授权。 +3. 客户端使用上一步获得的授权,向认证服务器申请访问令牌。 +4. 认证服务器对客户端进行认证以后,发放访问令牌。 +5. 客户端使用访问令牌向资源服务器申请获取资源。 +6. 资源服务器确认访问令牌无误,同意向客户端开放资源。 + +![](./res/oauth2.png) + +如果使用微博登录进行接入,其具体步骤可以参考微博开放平台上的[“微博登录接入”](http://open.weibo.com/wiki/Connect/login)文档。使用QQ登录进行接入,需要首先注册成为QQ互联开发者并通过审核,具体的步骤可以参考QQ互联上的[“接入指南”](http://wiki.connect.qq.com/),具体的步骤可以参考[“网站开发流程”](http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0)。 + +> 提示:在Gitbook上面有一本名为[《Django博客入门》](https://shenxgan.gitbooks.io/django/content/publish/2015-08-10-django-oauth-login.html)的书以Github为例介绍了第三方账号登录,有兴趣的可以自行阅读。 + +通常电商网站在使用第三方登录时,会要求与网站账号进行绑定或者根据获取到的第三方账号信息(如:手机号)自动完成账号绑定。 + +### 缓存预热和查询缓存 + +#### 缓存预热 + +所谓缓存预热,是指在启动服务器时将数据提前加载到缓存中,为此可以在Django应用的`apps.py`模块中编写`AppConfig`的子类并重写`ready()`方法,代码如下所示。 + +```Python +import pymysql + +from django.apps import AppConfig +from django.core.cache import cache + +SELECT_PROVINCE_SQL = 'select distid, name from tb_district where pid is null' + + +class CommonConfig(AppConfig): + name = 'common' + + def ready(self): + conn = pymysql.connect(host='1.2.3.4', port=3306, + user='root', password='pass', + database='db', charset='utf8', + cursorclass=pymysql.cursors.DictCursor) + try: + with conn.cursor() as cursor: + cursor.execute(SELECT_PROVINCE_SQL) + provinces = cursor.fetchall() + cache.set('provinces', provinces) + finally: + conn.close() +``` + +接下来,还需要在应用的`__init__.py`中编写下面的代码。 + +```Python +default_app_config = 'common.apps.CommonConfig' +``` + +或者在项目的`settings.py`文件中注册应用。 + +```Python +INSTALLED_APPS = [ + ... + 'common.apps.CommonConfig', + ... +] +``` + +#### 查询缓存 + +```Python +from pickle import dumps +from pickle import loads + +from django.core.cache import caches + +MODEL_CACHE_KEY = 'project:modelcache:%s' + + +def my_model_cache(key, section='default', timeout=None): + """实现模型缓存的装饰器""" + + def wrapper1(func): + + def wrapper2(*args, **kwargs): + real_key = '%s:%s' % (MODEL_CACHE_KEY % key, ':'.join(map(str, args))) + serialized_data = caches[section].get(real_key) + if serialized_data: + data = loads(serialized_data) + else: + data = func(*args, **kwargs) + cache.set(real_key, dumps(data), timeout=timeout) + return data + + return wrapper2 + + return wrapper1 +``` + +```Python +@my_model_cache(key='provinces') +def get_all_provinces(): + return list(Province.objects.all()) +``` + +### 购物车实现 + +问题一:已登录用户的购物车放在哪里?未登录用户的购物车放在哪里? + +```Python +class CartItem(object): + """购物车中的商品项""" + + def __init__(self, sku, amount=1, selected=False): + self.sku = sku + self.amount = amount + self.selected = selected + + @property + def total(self): + return self.sku.price * self.amount + + +class ShoppingCart(object): + """购物车""" + + def __init__(self): + self.items = {} + self.index = 0 + + def add_item(self, item): + if item.sku.id in self.items: + self.items[item.sku.id].amount += item.amount + else: + self.items[item.sku.id] = item + + def remove_item(self, sku_id): + if sku_id in self.items: + self.items.remove(sku_id) + + def clear_all_items(self): + self.items.clear() + + @property + def cart_items(self): + return self.items.values() + + @property + def cart_total(self): + total = 0 + for item in self.items.values(): + total += item.total + return total +``` + +已登录用户的购物车可以放在数据库中(可以先在Redis中缓存);未登录用户的购物车可以保存在Cookie中(减少服务器端内存开销)。 + +```JSON +{ + '1001': {sku: {...}, 'amount': 1, 'selected': True}, + '1002': {sku: {...}, 'amount': 2, 'selected': False}, + '1003': {sku: {...}, 'amount': 3, 'selected': True}, +} +``` + +```Python +request.get_signed_cookie('cart') + +cart_base64 = base64.base64encode(pickle.dumps(cart)) +response.set_signed_cookie('cart', cart_base64) +``` + +问题二:用户登录之后,如何合并购物车?(目前电商应用的购物车几乎都做了持久化处理,主要是方便在多个终端之间共享数据) + +### 集成支付功能 + +问题一:支付信息如何持久化?(必须保证每笔交易都有记录) + +问题二:如何接入支付宝?(接入其他平台基本类似) + +1. [蚂蚁金服开放平台](https://open.alipay.com/platform/home.htm)。 +2. [入驻平台](https://open.alipay.com/platform/homeRoleSelection.htm)。 +3. [开发者中心](https://openhome.alipay.com/platform/appManage.htm#/apps)。 +4. [文档中心](https://docs.open.alipay.com/270/105899/)。 +5. [SDK集成](https://docs.open.alipay.com/54/103419) - [PYPI链接](https://pypi.org/project/alipay-sdk-python/)。 +6. [API列表](https://docs.open.alipay.com/270/105900/)。 + +![](./res/alipay_web_developer.png) + +配置文件: + +```Python +ALIPAY_APPID = '......' +ALIPAY_URL = 'https://openapi.alipaydev.com/gateway.do' +ALIPAY_DEBUG = False +``` + +获得支付链接(发起支付): + +```Python +# 创建调用支付宝的对象 +alipay = AliPay( + # 在线创建应用时分配的ID + appid=settings.ALIPAY_APPID, + app_notify_url=None, + # 自己应用的私钥 + app_private_key_path=os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'keys/app_private_key.pem'), + # 支付宝的公钥 + alipay_public_key_path=os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'keys/alipay_public_key.pem'), + sign_type='RSA2', + debug=settings.ALIPAY_DEBUG +) +# 调用获取支付页面操作 +order_info = alipay.api_alipay_trade_page_pay( + out_trade_no='...', + total_amount='...', + subject='...', + return_url='http://...' +) +# 生成完整的支付页面URL +alipay_url = settings.ALIPAY_URL + '?' + order_info +return JsonResponse({'alipay_url': alipay_url}) +``` + +通过上面返回的链接可以进入支付页面,支付完成后会自动跳转回上面代码中设定好的项目页面,在该页面中可以获得订单号(out_trade_no)、支付流水号(trade_no)、交易金额(total_amount)和对应的签名(sign)并请求后端验证和保存交易结果,代码如下所示: + +```Python +# 创建调用支付宝的对象 +alipay = AliPay( + # 在线创建应用时分配的ID + appid=settings.ALIPAY_APPID, + app_notify_url=None, + # 自己应用的私钥 + app_private_key_path=os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'keys/app_private_key.pem'), + # 支付宝的公钥 + alipay_public_key_path=os.path.join( + os.path.dirname(os.path.abspath(__file__)), + 'keys/alipay_public_key.pem'), + sign_type='RSA2', + debug=settings.ALIPAY_DEBUG +) +# 请求参数(假设是POST请求)中包括订单号、支付流水号、交易金额和签名 +params = request.POST.dict() +# 调用验证操作 +if alipay.verify(params, params.pop('sign')): + # 对交易进行持久化操作 +``` + +支付宝的支付API还提供了交易查询、交易结算、退款、退款查询等一系列的接口,可以根据业务需要进行调用,此处不再进行赘述。 + +### 秒杀和超卖 + +1. 秒杀:秒杀是通常意味着要在很短的时间处理极高的并发,系统在短时间需要承受平时百倍以上的流量,因此秒杀架构是一个比较复杂的问题,其核心思路是流量控制和性能优化,需要从前端(通过JavaScript实现倒计时、避免重复提交和限制频繁刷新)到后台各个环节的配合。流量控制主要是限制只有少部分流量进入服务后端(毕竟最终只有少部分用户能够秒杀成功),同时在物理架构上使用缓存(一方面是因为读操作多写操作少;另外可以将库存放在Redis中,利用DECR原语实现减库存;同时也可以利用Redis来进行限流,道理跟限制频繁发送手机验证码是一样的)和消息队列(消息队列最为重要的作用就是“削峰”和“上下游节点解耦合”)来进行优化;此外还要采用无状态服务设计,这样才便于进行水平扩展(通过增加设备来为系统扩容)。 +2. 超卖现象:比如某商品的库存为1,此时用户1和用户2并发购买该商品,用户1提交订单后该商品的库存被修改为0,而此时用户2并不知道的情况下提交订单,该商品的库存再次被修改为-1这就是超卖现象。解决超卖现象有三种常见的思路: + - 悲观锁控制:查询商品数量的时候就用`select ... for update`对数据加锁,这样的话用户1查询库存时,用户2因无法读取库存数量被阻塞,直到用户1提交或者回滚了更新库存的操作后才能继续,从而解决了超卖问题。但是这种做法对并发访问量很高的商品来说性能太过糟糕,实际开发中可以在库存小于某个值时才考虑加锁,但是总的来说这种做法不太可取。 + - 乐观锁控制:查询商品数量不用加锁,更新库存的时候设定商品数量必须与之前查询数量相同才能更新,否则说明其他事务已经更新了库存,必须重新发出请求。这种做法要求事务隔离级别为可重复读,否则仍然会产生问题。 + - 尝试减库存:将上面的查询(`select`)和更新(`update`)操作合并为一条SQL操作,更新库存的时候,在`where`筛选条件中加上`库存>=购买数量`或`库存-购买数量>=0`的条件。 + +> 提示:有兴趣的可以自己在知乎上看看关于这类问题的讨论。 + +### 静态资源管理 + +静态资源的管理可以自己架设文件服务器或者分布式文件服务器(FastDFS),但是一般的项目中没有必要这样做而且效果未必是最好的,我们建议使用云存储服务来管理网站的静态资源,国内外的云服务提供商如亚马逊、阿里云、腾讯云、七牛、LeanCloud、Bmob等都提供了非常优质的云存储服务,而且价格也是一般公司可以接受的。可以参考《在阿里云OSS上托管静态网站》一文来完成对网站静态资源的管理,代码相关的内容可以参考阿里云的[对象存储 OSS开发人员指南](https://www.alibabacloud.com/zh/support/developer-resources)。 + +### 全文检索 + +#### 方案选择 + +1. 使用数据库的模糊查询功能 - 效率低,每次需要全表扫描,不支持分词。 +2. 使用数据库的全文检索功能 - MySQL 5.6以前只适用于MyISAM引擎,检索操作和其他的DML操作耦合在数据库中,可能导致检索操作非常缓慢,数据量达到百万级性能显著下降,查询时间很长。 +3. 使用开源搜索引擎 - 索引数据和原始数据分离,可以使用ElasticSearch或Solr来提供外置索引服务,如果不考虑高并发的全文检索需求,纯Python的Whoosh也可以考虑。ElasticSearch-Analysis-IK + +#### ElasticSearch + +ElasticSearch是一个高可扩展的开源全文搜索和分析引擎,它允许存储、搜索和分析大量的数据,并且这个过程是近实时的。它通常被用作底层引擎和技术,为复杂的搜索功能和要求提供动力,大家熟知的维基百科、Stack-Overflow、Github都使用了ElasticSearch。 + +ElasticSearch的底层是开源搜索引擎[Lucene](https://lucene.apache.org/)。但是直接用Lucene会非常麻烦,必须自己编写代码去调用它的接口而且只支持Java语言。ElasticSearch相当于是对Lucene进行了封装,提供了REST API的操作接口。搜索引擎在对数据构建索引时,需要进行分词处理。ElasticSearch不支持基于中文分词的索引,需要配合扩展elasticsearch-analysis-ik来实现中文分词处理。 + +ElasticSearch的安装和配置可以参考[《ElasticSearch之Docker安装》](https://blog.csdn.net/jinyidong/article/details/80475320)。除了ElasticSearch之外,也可以使用Solr、Whoosh等来提供搜索引擎服务,基本上Django项目中可以考虑如下两套方案: + +- Haystack(django-haystack / drf-haystack) + Whoosh + Jieba + +- Haystack (django-haystack / drf-haystack)+ ElasticSearch + ElasticSearch-Analysis-IK + +#### Django对接ElasticSearch + +Python对接ElasticSearch的第三方库是HayStack,在Django项目中可以使用django-haystack,通过HayStack可以在不修改代码对接多种搜索引擎服务。 + +```shell +pip install django-haystack elasticsearch +``` + +配置文件: + +```Python +INSTALLED_APPS = [ + ... + 'haystack', + ... +] + +HAYSTACK_CONNECTIONS = { + 'default': { + # 引擎配置 + 'ENGINE': + 'haystack.backends.elasticsearch_backend.ElasticsearchSearchEngine', + # 搜索引擎服务的URL + 'URL': 'http://...', + # 索引库的名称 + 'INDEX_NAME': '...', + }, +} + +# 添加/删除/更新数据时自动生成索引 +HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' +``` + +索引类: + +```Python +from haystack import indexes + + +class HouseInfo(indexes.SearchIndex, indexes.Indexable): + + text = indexes.CharField(document=True, use_template=True) + + def get_model(self): + return HouseInfo + + def index_queryset(self, using=None): + return self.get_model().objects.all() +``` + +编辑text字段的模板(需要放在templates/search/indexes/rent/houseinfo_text.txt): + +``` +{{object.title}} +{{object.detail}} +``` + +配置URL: + +```Python +urlpatterns = [ + # ... + url('search/', include('haystack.urls')), +] +``` + +生成初始索引: + +```Shell +python manage.py rebuild_index +``` + +> 说明:可以参考[《Django Haystack 全文检索与关键词高亮》](https://www.zmrenwu.com/post/45/)一文来更深入的了解基于Haystack的全文检索操作。 \ No newline at end of file diff --git a/Day91-100/网络API接口设计.md b/Day91-100/网络API接口设计.md index a4d51e9..e83a568 100644 --- a/Day91-100/网络API接口设计.md +++ b/Day91-100/网络API接口设计.md @@ -61,32 +61,33 @@ API接口返回的数据通常都是JSON或XML格式,我们这里不讨论后 请求参数: | 参数名 | 类型 | 是否必填 | 参数位置 | 说明 | - | ------ | ------ | -------- | -------- | ------------------------------------ | | + | ------ | ------ | -------- | -------- | ------------------------------------ | | page | 整数 | 否 | 查询参数 | 页码,默认值1 | | size | 整数 | 否 | 查询参数 | 每次获取评论数量(10~100),默认值20 | + | key | 字符串 | 是 | 请求头 | 用户的身份标识 | 响应信息: ```JSON - { "code": 10000, "message": "获取评论成功", "page": 1, "size": 10, + "totalPage": 35, "contents": [ { "userId": 1700095, "nickname": "王大锤", "pubDate": "2018年7月31日", - "content": "小编是不是有病呀" + "content": "小编是不是有病呀", /* ... */ }, { "userId", 1995322, "nickname": "白元芳", "pubDate": "2018年8月2日", - "content": "楼上说得好" + "content": "楼上说得好", /* ... */ } ] @@ -108,16 +109,15 @@ API接口返回的数据通常都是JSON或XML格式,我们这里不讨论后 请求参数: - | 参数名 | 类型 | 是否必填 | 参数位置 | 说明 | - | ------- | ------ | -------- | -------- | ------------------------------------ | - | userId | 字符串 | 是 | 消息体 | 用户ID | - | token | 字符串 | 是 | 消息体 | 用户的令牌或URL的签名 | - | content | 字符串 | 是 | 消息体 | 评论的内容 | + | 参数名 | 类型 | 是否必填 | 参数位置 | 说明 | + | ------- | ------ | -------- | -------- | ---------- | + | userId | 字符串 | 是 | 消息体 | 用户ID | + | key | 字符串 | 是 | 请求头 | 用户的令牌 | + | content | 字符串 | 是 | 消息体 | 评论的内容 | 响应信息: ```JSON - { "code": 10001, "message": "创建评论成功", @@ -129,5 +129,3 @@ API接口返回的数据通常都是JSON或XML格式,我们这里不讨论后 /* ... */ } ``` - - \ No newline at end of file diff --git a/Day91-100/英语面试.md b/Day91-100/英语面试.md new file mode 100644 index 0000000..55af6cb --- /dev/null +++ b/Day91-100/英语面试.md @@ -0,0 +1,89 @@ +## 英语面试 + +以下用I表示面试官(Interviewer),用C表示面试者(Candidate)。 + +### 开场寒暄 + +1. I: Thanks for waiting. (Please follow me.) + + C: It's no problem. + +2. I: How are you doing this morning? + + C: I'm great. / I'm doing fine. Thank you. / How about you? + +3. I: How did you get here? + + C: I took the subway here. / I drove here. + +4. I: Glad to meet you. + + C: Glad to meet you. / It's great to finally meet you in person. (之前电话沟通过的) + +### 正式面试 + +#### 人力面试 + +1. I: Can you tell me a little bit about yourself? (介绍下自己) + + 原则:不要谈私生活和奇怪的癖好(英雄联盟干到钻石),因为别人更想知道的是你的专业技能(qulifications)和工作经验(experience),所以重点在你之前的公司(company name)、职位(title)、时间(years)和主要职责(major responsibilities) + + C: Thank you for having me. My name is Dachui WANG. I'm 25 years old, and I'm single. I have a Bachelor's Degree of Computer Science from Tsinghua University. I was a Junior Java Programmer for ABC Technologies during my college life. Then I become an intermediate Java engineer for XYZ Corporation in last two years. Programming is my everyday life and programming is where my passion is. I think I have a good knowledge of Java enterprise application developement using light-weight frameworks like Spring, Guice, Hibernate and other open source middle-ware like Dubbo, Mycat, rocketmq and so on and so forth. I love reading, travelling and playing basketball in my spare time. That's all! Thank you! + +2. I: How would you describe your personality? (你的性格) + + C: I'm hard working, eager to learn, and very serious about my work. I enjoy working with other people and I love challenges. + +3. I: What do you know about our company? (你对我们公司有什么了解) + + (需要做功课,了解公司的状况和企业文化,该公司在这个行业中的一个状况,有什么核心业务,主要的竞争对手有哪些) + + C: The one thing that I like the most about our company is your core values. I think they're very important in this industry because …(自由发挥的部分)... I personally really believe in the cause as well. Of course, I'm very interested in your products such as …(功课部分)… and the techniques behind them. + +4. I: Why are you leaving your last job? (为什么离职) + + C: I want to advance my career and I think this job offers more challenges and opportunities for me do to that. + +5. I: What do you see yourself in 3 or 5 years? (3-5年职业规划) + + C: My long term goals involve growing with the company, where I can continue to learn, to take on additional responsibilities and to contribute as much value as I can. I intend to take advantage of all of these. + +6. I: What's your salary expectation? (期望薪资) + + C: My salary expectation is in line with my experience and qualifications. I believe our company will pay me and every other employee fairly. (把球踢给对方先看看对方报价是多少,如果对方非要你报价再说后面的内容) I think 15 thousands RMB or above is fitting for me to leave in Chengdu. + +7. I: Do you have any questions for me? (问面试官的问题) + + C: What's the growth potential for this position? + + +#### 技术面试 + +1. I: What's difference between an interface and an abstract class? +2. I: What are pass by reference and pass by value? +3. I: What's the difference between process and threads? +4. I: Explain the available thread state in high-level. +5. I: What's deadlocks? How to avoid them? +6. I: How HashMap works in Java? +7. I: What's the difference between ArrayList and LinkedList? (类似的问题还有很多,比如比较HashSet和TreeSet、HashMap和Hashtable) +8. I: Tell me what you know about garbage collection in Java. +9. I: What're two types of exceptions in Java? +10. I: What's the advantage of PreparedStatement over Statement? +11. I: What's the use of CallableStatement? +12. I: What does connection pool mean? +13. I: Explain the life cycle of a Servlet. +14. I: What's the difference between redirect and forward? +15. I: What's EL? What're implicit objects of EL? +16. I: Tell me what you know about Spring framework and its benefits. +17. I: What're different types of dependency injection. +18. I: Are singleton beans thread safe in Spring framework? +19. I: What're the benefits of Spring framework's transaction management? +20. I: Explain what's AOP. +21. I: What's a proxy and how to implement proxy pattern? +22. I: How Spring MVC works? +23. I: What's the working scenario of Hibernate and MyBatis? +24. I: How to implement SOA? +25. I: Make a brief introduction of the projects you are involved before? + + +上面主要是面试Java程序员的问题,但是整个流程大致如此。 diff --git a/Day91-100/面试中的公共问题.md b/Day91-100/面试中的公共问题.md new file mode 100644 index 0000000..f05e9fa --- /dev/null +++ b/Day91-100/面试中的公共问题.md @@ -0,0 +1,90 @@ +## 面试中的公共问题 + +### 计算机基础 + +1. TCP/IP模型相关问题。 + + > 建议阅读阮一峰的[《互联网协议入门(一)》](http://www.ruanyifeng.com/blog/2012/05/internet_protocol_suite_part_i.html)和[《互联网协议入门(二)》](http://www.ruanyifeng.com/blog/2012/06/internet_protocol_suite_part_ii.html)。 + +2. HTTP和HTTPS相关问题。 + + > 建议阅读阮一峰的[《HTTP 协议入门》](http://www.ruanyifeng.com/blog/2016/08/http.html)和[《SSL/TLS协议运行机制的概述》](http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html)。 + +3. Linux常用命令和服务。 + +4. 进程和线程之间的关系。什么时候用多线程?什么时候用多进程?。 + +5. 关系型数据库相关问题(ACID、事务隔离级别、锁、SQL优化)。 + +6. 非关系型数据库相关问题(CAP/BASE、应用场景)。 + +### Python基础 + +1. 开发中用过哪些标准库和三方库。 + + > 标准库:sys / os / re / math / random / logging / json / pickle / shelve / socket / datetime / hashlib / configparser / urllib / itertools / collections / functools / threading / multiprocess / timeit / atexit / abc / asyncio / base64 / concurrent.futures / copy / csv / operator / enum / heapq / http / profile / pstats / ssl / unitest / uuid + +2. 装饰器的作用、原理和实现。 + +3. 使用过哪些魔法方法。 + + > 建议阅读[《Python魔术方法指南》](https://pycoders-weekly-chinese.readthedocs.io/en/latest/issue6/a-guide-to-pythons-magic-methods.html)。 + +4. 生成式、生成器、迭代器的编写。 + +5. 列表、集合、字典的底层实现。 + +6. 垃圾回收相关问题。 + +7. 并发编程的相关问题。 + +8. 协程和异步I/O相关知识。 + +### Django和Flask + +1. MVC架构(MTV)解决了什么问题。 + +2. 中间件的执行流程以及如何自定义中间件。 + +3. REST数据接口如何设计(URL、域名、版本、过滤、状态码、安全性)。 + + > 建议阅读阮一峰的[《RESTful API设计指南》](http://www.ruanyifeng.com/blog/2014/05/restful_api.html)。 + +4. 使用ORM框架实现CRUD操作的相关问题。 + + - 如何实现多条件组合查询 / 如何执行原生的SQL / 如何避免N+1查询问题 + +5. 如何执行异步任务和定时任务。 + +6. 如何实现页面缓存和查询缓存?缓存如何预热? + +### 爬虫相关 + +1. Scrapy框架的组件和数据处理流程。 +2. 爬取的目的(项目中哪些地方需要用到爬虫的数据)。 +3. 使用的工具(抓包、下载、清理、存储、分析、可视化)。 +4. 数据的来源(能够轻松的列举出10个网站)。 +5. 数据的构成(抓取的某个字段在项目中有什么用)。 +6. 反反爬措施(限速、请求头、Cookie池、代理池、Selenium、PhantomJS、RoboBrowser、TOR、OCR)。 +7. 数据的体量(最后抓取了多少数据,多少W条数据或多少个G的数据)。 +8. 后期数据处理(持久化、数据补全、归一化、格式化、转存、分类)。 + +### 数据分析 + +1. 科学运算函数库(SciPy和NumPy常用运算)。 +2. 数据分析库(Pandas中封装的常用算法)。 +3. 常用的模型及对应的场景(分类、回归、聚类)。 +4. 提取了哪些具体的指标。 +5. 如何评价模型的优劣。 +6. 每种模型实际操作的步骤,对结果如何评价。 + +### 项目相关 + +1. 项目团队构成以及自己在团队中扮演的角色(在项目中的职责)。 +2. 项目的业务架构(哪些模块及子模块)和技术架构(移动端、PC端、后端技术栈)。 +3. 软件控制管理相关工具(版本控制、问题管理、持续集成)。 +4. 核心业务实体及其属性,实体与实体之间的关系。 +5. 用到哪些依赖库,依赖库主要解决哪方面的问题。 +6. 项目如何部署上线以及项目的物理架构(Nginx、Gunicorn/uWSGI、Redis、MongoDB、MySQL、Supervisor等)。 +7. 如何对项目进行测试,有没有做过性能调优。 +8. 项目中遇到的困难有哪些,如何解决的。 \ No newline at end of file diff --git a/Day91-100/项目部署上线指南.md b/Day91-100/项目部署上线指南.md index 54ca90e..ab214e9 100644 --- a/Day91-100/项目部署上线指南.md +++ b/Day91-100/项目部署上线指南.md @@ -1,25 +1,127 @@ ## 项目部署上线指南 -### 更新Python环境到3.x +### 准备上线 -```Shell -yum -y install gcc zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel -wget https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tar.xz -xz -d Python-3.7.0.tar.xz -tar -xvf Python-3.7.0.tar -cd Python-3.7.0 -./configure --prefix=/usr/local/python3 --enable-optimizations -make && make install -cd ~ -vim .bash_profile -export PATH=$PATH:/usr/local/python3/bin -ln -s /usr/local/python3/bin/python3 /usr/bin/python3 -ln -s /usr/local/python3/bin/pip3 /usr/bin/pip3 -``` +1. 上线前的检查工作。 + + ```Shell + python manage.py check --deploy + ``` + +2. 将DEBUG设置为False并配置ALLOWED_HOSTS。 + + ```Python + DEBUG = False + ALLOWED_HOSTS = ['*'] + ``` + +3. 安全相关的配置。 + + ```Python + # 保持HTTPS连接的时间 + SECURE_HSTS_SECONDS = 3600 + SECURE_HSTS_INCLUDE_SUBDOMAINS = True + SECURE_HSTS_PRELOAD = True + + # 自动重定向到安全连接 + SECURE_SSL_REDIRECT = True + + # 避免浏览器自作聪明推断内容类型 + SECURE_CONTENT_TYPE_NOSNIFF = True + + # 避免跨站脚本攻击 + SECURE_BROWSER_XSS_FILTER = True + + # COOKIE只能通过HTTPS进行传输 + SESSION_COOKIE_SECURE = True + CSRF_COOKIE_SECURE = True + + # 防止点击劫持攻击手段 - 修改HTTP协议响应头 + # 当前网站是不允许使用