Python自动化开发学习25-Djan
下面要讲的是基于模板语言的实现方法,完全没有使用js。讲的时候有点混乱,把其他与效果实现无关的小知识点也顺带讲了一下。不过我最后做了小结。
准备表结构
这里讲组合搜索,所以要2个搜索条件。这里用一个选项保存在内存中的type和一个保存在数据库中的section:
# models.py 文件中的表结构
class Article(models.Model):
"""文章信息"""
title = models.CharField(verbose_name="文章标题", max_length=128)
create_time = models.DateTimeField(verbose_name="创建时间", auto_now_add=True)
author = models.ForeignKey('UserInfo', models.CASCADE, related_name='author', verbose_name="作者")
section = models.ForeignKey('Section', models.CASCADE, verbose_name="所属板块")
type_choices = [(1, "原创"), (2, "转载"), (3, "翻译")]
type = models.IntegerField(choices=type_choices, verbose_name="文章类型")
class Section(models.Model):
"""文章所属的板块"""
name = models.CharField(verbose_name="板块", max_length=32)
def __str__(self):
return self.name
动态的根据url处理筛选
urls里使用捕获参数的方法,这里的名字不能随便取,要取一个和数据库表的字段名一样的名字:
path('search-<int:section>-<int:type>/', views.Search.as_view()),
因为这里字典的key就是字段名,这样处理函数里就可以直接使用**kwargs来筛选了:
def search(request, **kwargs):
article_obj = models.Article.objects.filter(**kwargs)
这里还有个问题,一般搜索的条件会有一个全部。这里可以用0来表示全部,因为数据库的id是从1开始的。但是这样的话按照上面的代码,将什么也搜索不到。这条命令可以搜索到全部的数据:
article_obj = models.Article.objects.filter(**{}) # 就是空字典,相当于就是.all()
最终写成下面这样来实现:
def search(request, **kwargs):
condition = {}
for k, v in kwargs.items():
if v == 0:
pass
else:
condition[k] = v
article_obj = models.Article.objects.filter(**condition).order_by('-id')
types = models.Article.type_choices
section_obj = models.Section.objects.all()
return render(request, 'search.html', {'article_obj': article_obj, 'types': types, 'section_obj': section_obj})
上面的实现的好处是,处理函数里对于搜索条件没有写死。urls直接和数据库的字典名对应,之后如果要增减或者修改搜索条件,处理函数也不用做修改。
生成url的方法
上面只解决了通过url来获取到筛选的数据,但是首先得有url。如果是单个的筛选条件,那么一个a标签就能解决问题:
<a href="detail-{{ row.id }}"></a>
但是对于多个筛选条件的组合搜索,另外一个值就无法动态的保留了。
获取当前url的方法
先给url加个名字
path('detail-<int:hid>-<int:uid>.html', views.detail, name='detail'),
下面的2个方法都可以在处理函数里获取到当前的url:
print(request.path_info)
from Django.urls import reverse
url = reverse('detail', kwargs=kwargs)
print(url)
# reverse是生成url,如果传入一个别的字典,就能动态的生成url
url = reverse('detail', kwargs={'hid': '1', 'uid': '2'})
print(url)
所以url的信息全部在kwargs里了,把这个kwargs也传给前端:
def search(request, **kwargs):
# print(kwargs)
# print(reverse('search', args=kwargs.values()))
condition = {}
for k, v in kwargs.items():
if v == 0:
pass
else:
condition[k] = v
# print(condition)
article_obj = models.Article.objects.filter(**condition).order_by('-id')
types = models.Article.type_choices
section_obj = models.Section.objects.all()
return render(request, 'search.html', {'article_obj': article_obj, 'types': types, 'section_obj': section_obj, 'kwargs': kwargs})
上面顺便讲了2种生成当前url的方法。这里最后是在后端获取到了当前url的参数,然后再返回给前端
在前端用模板语言实现
现在后端传来的kwargs参数,就是当前url动态的内容的,所以当前的url是这样的:
href="/search-{{ kwargs.section }}-{{ kwargs.type }}/"
获取到上面的这个动态的url的式子,这小段的重点也就讲完了。
剩下的就是熟练运用之前掌握的只是了,前端htlm的代码如下:
<style>
div.search-area>div {margin: 5px; font-size: large;}
div.search-area a {display: inline-block; padding: 3px 5px; border: 1px solid gray;}
div.search-area a:hover {display: inline-block; padding: 3px 5px; border: 1px solid red; text-decoration:none;}
div.search-area a.active {background-color: blue; color: white;}
</style>
<div class="container">
<div class="search-area">
<h2>搜索条件</h2>
<div>
<span>版块:</span>
<a {% if kwargs.section == 0 %} class="active" {% endif %} href="/search-0-{{ kwargs.type }}/">全部</a>
{% for section in section_obj %}
<a {% if kwargs.section == section.id %} class="active" {% endif %} href="/search-{{ section.id }}-{{ kwargs.type }}/">{{ section.name }}</a>
{% endfor %}
</div>
<div>
<span>类型:</span>
<a {% if kwargs.type == 0 %} class="active" {% endif %} href="/search-{{ kwargs.section }}-0/">全部</a>
{% for type in types %}
<a {% if kwargs.type == type.0 %} class="active" {% endif %} href="/search-{{ kwargs.section }}-{{ type.0 }}/">{{ type.1 }}</a>
{% endfor %}
</div>
</div>
<div>
<h2>查询结果</h2>
<div class="list-group">
{% for article in article_obj %}
<a href="/article-{{ article.id }}/" class="list-group-item">
{{ article.title }}
</a>
{% endfor %}
</div>
</div>
</div>
上面还对选中的项目加了一个样式,同样是判断当前动态的url,如果url判断后该项目是被选中的,则加上 class="active"
的样式。
小结
- 在 urls.py 里,路由的捕获参数不能随便写,最好是和表的字段名一致(这样之后都是直接引用,不用修改变量名了)
- 后端处理函数里要写一个for循环,处理一下选择全部传入参数是0的问题。
- 把kwargs这个url的参数也return给前端处理
href="/search-{{ kwargs.section }}-{{ kwargs.type }}/"
,在这个动态的url上修改
最后,上面的代码比较长,看着也比较乱。可以用模板语言的自定义函数封装一下,这样前端只需要写一行就好了,而更加复杂的逻辑则放到 templatetags/*.py
自定义的模板函数里来实现。课上是这么做了,不过我
JSONP是一种请求方式,解决浏览器的同源策略阻止跨域请求的问题。
准备
准备里了可以跳过,这里通过后端转发请求,浏览器端不存在跨域的问题。但是这样多了一个中间环节。
这里需要用到requests模块,所以先安装一下(或者不要装了,直接看下面用浏览器直接发请求会报错的情况):
pip install requests
然后去网上找一个api接口来请求,比如天气api的接口:Http://www.weather.com.cn/data/sk/101020100.html
如下写一个处理函数:
import requests
def get_res(request):
response = requests.get('http://www.weather.com.cn/data/sk/101020100.html') # 发起get请求
# print(response.content) # 返回的二进制内容
response.encoding = 'utf-8' # 设置编码格式,否则中文会是乱码
print(response.text) # 返回的文本内容
return render(request, 'demo/jsonp.html', {'res': response.text})
然后记得配好urls.py的对应关系,开启服务,页面获取一下内容:
<div>
{{ res }}
</div>
这样,页面请求后有返回的内容的。但是上面的请求过程是前端往后端发请求,然后后端再去找api接口请求,把api接口返回的结果再返回给前端。但是前端也是可以直接给api接口发请求的,而不用经过后端的中转。
直接使用浏览器发请求
直接从浏览器发请求,就会出现跨域的问题了。下面先来触发这个问题。
直接修改前端代码:
<h2>后台获取的结果:</h2>
<p>{{ res }}</p>
<h2>js直接获取结果</h2>
<input type="button" value="获取结果" onclick="getContent();" />
<p id="container"></p>
<script>
function getContent() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://www.weather.com.cn/data/sk/101020100.html');
xhr.onreadystatechange = function () {
console.log(xhr.responseText); // 这里不能alert看结果
};
xhr.send();
}
</script>
打开后台,查看控制台的信息,就是下面这句报错信息:
SEC7120: [CORS] 原点“http://127.0.0.1:8000”未在“http://www.weather.com.cn/data/sk/101020100.html”的 cross-origin 资源的 Access-Control-Allow-Origin response header 中找到“http://127.0.0.1:8000”。
这里的情况是,数据已经发出了,并且服务器也处理并返回了。报错的信息是由于浏览器的同源策略,拒绝接收。
本地重现跨域的问题
上面是会有出现问题的场景,现在本地来重现一下跨域的场景。
处理函数很简单:
def jsonp(request):
return HttpResponse('OK')
全端页面只需要把请求的url参数修改一下:
xhr.open('GET', 'http://127.0.0.1:8000/demo/jsonp/');
如果用默认的 127.0.0.1:8000
这个本地域名访问,是不跨域的。用这个地址 localhost:8000
来访问,也是访问本地,然后再向 http://127.0.0.1:8000
发请求,就被认为跨域了。
另外还有一个方法,去settiongs.py里修改设置一下下面这个参数:
ALLOWED_HOSTS = []
这里提一下,就不展开了。
通过JSONP支持跨域
浏览器有同源策略,但是其实并不是所有的请求都会被同源策略阻止。比如:
CDN: <script src="http://lib.sinaapp.com/js/Jquery/1.12.4/jquery-1.12.4.min.js"></script>
图片: <img src="/file/imgs/upload/202301/31/3aemkbpo2yb.jpg">
可能是所有的有src属性的标签,都不受同源策略的影响。
这里就要通过script标签来绕过浏览器的同源策略,把前端的按钮事件绑定到下面这个新的函数上:
<script>
function getJSONP() {
var tag = document.createElement('script');
tag.src = 'http://127.0.0.1:8000/demo/jsonp/';
document.head.appendChild(tag)
}
</script>
上面这个函数的效果是,创建一个script标签,设置了src后,追加到head标签里。浏览器处理的时候,就会添加这个script标签,并且会去src的地址获取内容,并且由于这是一个script标签,所以获取到的内容,浏览器更当做js语句来处理。这里由于获取到的是 return HttpResponse('OK')
,js语法错误,所以还是会有个错误信息。修改一下处理函数,返回一句js语句看看:
def jsonp(request):
return HttpResponse("alert('OK');")
然后现在再看看效果,点击按钮后,会解析并执行返回的 alert('OK');
这句js语句。
现在修改一下处理函数,返回一个复杂一点的JSON字符串,并且使用一个自定义个函数名,字符串作为函数的参数:
import json
def jsonp(request):
res = {'status': True, 'data': 'Test123'}
return HttpResponse("callback(%s);" % json.dumps(res))
然后前端也要定义好这个自定义的js函数:
<input type="button" value="获取结果" onclick="getJSONP();" />
<script>
function getJSONP() {
var tag = document.createElement('script');
tag.src = 'http://127.0.0.1:8000/demo/jsonp/';
document.head.appendChild(tag)
}
function callback(arg) {
alert(JSON.stringify(arg))
}
</script>
现在的效果就是,前端通过script标签,跨域接收到了一个callback函数调用的命令,并且参数就是我们需要的数据。自己通过在页面里定义这个callback函数,就可以获取到返回的数据了。如此成功的绕开了浏览器的同源策略,实现了跨域请求。
继续优化JSONP
上面还有2个问题:
- 回调函数的函数名写死了,可能会和本地的函数名重名
- 每请求一次,都会生成一个script标签
先把处理函数修改一下解决第一个问题:
import json
def jsonp(request):
func = request.GET.get('callback', 'callback')
res = {'status': True, 'data': 'Test123'}
return HttpResponse("%s(%s);" % (func, json.dumps(res)))
现在发送get请求的时候可以通过callback参数来指定需要函数的回调函数的函数名。之前的前端不用修改,依然可以使用。
一般约定这个指定返回的函数的函数名的key就是callback
然后修改前端,这次回调函数换一个名字试试。另外还要解决第二个问题,就是获取回复数据之后,把之前生成的script标签移除掉:
<input type="button" value="获取结果" onclick="getJSONP();" />
<script>
function getJSONP() {
var tag = document.createElement('script');
tag.src = 'http://127.0.0.1:8000/demo/jsonp/?callback=myJSONP'; // get请求加一个callback参数
document.head.appendChild(tag);
document.head.removeChild(tag); // 移除创建的标签
}
function myJSONP(arg) {
alert(JSON.stringify(arg))
}
</script>
JSONP只能发get请求。使用jQuery的话,就算指定method是POST,jQuery内部也是转成GET处理的。
jQuery发送JSONP
这里主要看一下jQuery的用法。基本上使用了jQuery之后,和发送普通的ajax请求形式差不多:
<input type="button" value="获取结果" onclick="jqJSONP();" />
<script src="http://lib.sinaapp.com/js/jquery/1.12.4/jquery-1.12.4.min.js"></script>
<script>
function jqJSONP() {
$.ajax({
url: 'http://127.0.0.1:8000/demo/jsonp/',
type: 'POST', // 没用,因为发的还是GET
dataType: 'jsonp', // 指定使用jsonp来发送这个请求
jsonp: 'callback', // 就是指定回调函数的参数的key
jsonpCallback: 'myJSONP' // 指定回调函数的函数名,和上面的和起来就是 ?callback=myJSONP
})
}
function myJSONP(arg) {
alert(JSON.stringify(arg))
}
</script>
CORS(跨站资源共享)
解决跨域的问题,除了上面的JSONP,还有这个CORS。
讲师的博客:https://www.cnblogs.com/wupeiqi/p/5703697.html
在最后有介绍,课上没展开讲。
XSS×××是通过对网页注入可执行代码且成功地被浏览器执行,达到×××的目的。这里主要讲针对富文本编辑器的情况。
在使用富文本编辑器的时候,尤其要注意XSS×××。因为别的地方还可以过滤html标签,但是富文本编辑器本身就要使用html标签,如果全部过滤掉,就无法正常显示文档格式了。
防范的手段就是把特定的标签过滤掉,比如script标签。最安全的做法就是设置白名单,留着编辑器使用的标签,其他的全部过滤。编辑器可能会自带过滤,不过前端XSS过滤都会被绕过,只有在后端过滤才能万无一失。
通用的手段就是,在收到数据提交之后进行过滤,然后把过滤后的数据保存到数据库。保存后的数据就认为是安全的,之后页面显示的时候,就一律放行。
过滤标签的方法当然可以通过正则匹配来实现。不过这里推荐一个模块,beatifulsoup4。安装模块:
pip install beautifulsoup4
另外这个模块貌似也是爬虫利器,都是要处理html标签嘛。
查找标签-清空、清除
下面是BeautifulSoup的基本用法,使用find()方法找到指定的标签,然后清除掉:
content = """
<h1>测试页面</h1>
<p class="c1">
第一个段落<span class="color" style="background-color: red">这里是红色的</span>
<script>alert('p1');</script>
</p>
<p class="c2 p2" id="i2">
第二个段落<strong id="click" onclick="alert('p2');">点我看看</strong>
</p>
<p class="c3" id="i3">
第三个段落
<script>alert('p3');</script>
</p>
"""
from bs4 import BeautifulSoup
# 下面第二个参数是指定解析器,这个是python标准库内置的。也支持其他第三方的解析器(需安装)
soup = BeautifulSoup(content, "html.parser")
tag = soup.find('script') # 查找第一个标签
while tag: # 这个循环应该是能把所有的标签都查找出来了
print(tag)
# tag.hidden = True # 去掉注释,可以把整个空标签也去掉,否则就是去掉标签的内容,保留标签
tag.clear() # 清空标签里的内容
tag = tag.find_next('script') # 查找后一个标签
content = soup.decode() # 转成字符串
print(type(content), content)
HTML解析器,这里用了Python自带的,就不用另外安装了。也有其他第三方更好的,但是需要安装,就看怎么取舍了。
如歌直接打印soup,print(soup)
,显示的效果也是一样的。但是soup本身是 <class 'bs4.BeautifulSoup'>
,直接打印这个对象的时候,内部调用的也是return self.encode()
。
查找标签的属性-清除
还是上面的html,进一步处理以下标签中的属性
from bs4 import BeautifulSoup
# 下面第二个参数是指定解析器,这个是python标准库内置的。也支持其他第三方的解析器(需安装)
soup = BeautifulSoup(content, "html.parser")
tag = soup.find('script') # 查找第一个标签
while tag: # 这个循环应该是能把所有的标签都查找出来了
print(tag)
tag.hidden = True
tag.clear() # 清空标签里的内容
tag = tag.find_next('script') # 查找后一个标签
span = soup.find('span')
print(span.attrs) # 打印这个标签的所有的属性
del span.attrs['style'] # 删除特定的属性
strong = soup.find('strong')
print(strong.attrs)
del strong.attrs # 删除所有属性
content = soup.decode() # 转成字符串
print(content, type(content), type(soup))
标签白名单
这次设置一个白名单,只保留白名单中的标签的内容:
from bs4 import BeautifulSoup
soup = BeautifulSoup(content, "html.parser")
tags = ['p', 'span', 'strong'] # 设置一个白名单,下面只保留白名单的里的标签内容
# 下面的这个循环,遍历一遍所有的标签
for tag in soup.find_all():
if tag.name not in tags:
tag.hidden = True
tag.clear()
content = soup.decode() # 转成字符串
print(content)
包含标签属性的白名单。上面的做法,只处理了标签,没有处理标签中的属性。这里需要一个更加复杂的白名单:
from bs4 import BeautifulSoup
soup = BeautifulSoup(content, "html.parser")
tags = {
'p': ('class', 'id'), # 只允许class 和 id 这2个属性
'span': ('class',),
'strong': (), # 值允许标签,不能带任何属性
}
# 下面的这个循环,遍历一遍所有的标签
for tag in soup.find_all():
if tag.name not in tags:
tag.hidden = True
tag.clear()
else: # 处理白名单的属性,再遍历一遍标签的属性
# 下面的list()相当于再复制了一份列表,然后遍历这个列表。防止下面在迭代过程中禁止把迭代的元素删除
for attr in list(tag.attrs):
if attr not in tags[tag.name]:
del tag.attrs[attr]
content = soup.decode() # 转成字符串
print(content)
一个类,每次实例化都会生成一个对象:
class Foo(object):
def __init__(self):
pass
c1 = Foo()
c2 = Foo()
print(c1, c2)
# 结果如下:
# <__main__.Foo object at 0x0000018AAF5C8A20> <__main__.Foo object at 0x0000018AAF76A2B0>
上面的情况,生成了2个对象,每个对象分别占用各自的内存空间。
下面自定义了一个方法,用这个方法生成对象时候,只有对一次会创建实例,之后用的都是第一次的对象:
class Foo(object):
__instance = None
def __init__(self):
pass
@claSSMethod
def get_instance(cls):
if not Foo.__instance:
Foo.__instance = Foo()
return Foo.__instance
def process(self):
return 'Foo.process'
c1 = Foo.get_instance()
c2 = Foo.get_instance()
print(c1.process(), c2.process())
print(c1, c2)
# 结果如下:
# Foo.process Foo.process
# <bound method Foo.process of <__main__.Foo object at 0x0000022276B0A320>> <bound method Foo.process of <__main__.Foo object at 0x0000022276B0A320>>
为了更加直观的说明问题,我这个类里还定义了一个process方法,返回的结果也是不变的。所以这种情况下,这个类不需要多个实例,因为每个实例返回的结果都是一样的。也就是说,这种类,只需要一个实例,即只有在第一次实例化的时候需要创建对象,之后每次都只需要用之前创建的对象就好了,不用另外再创建对象了。
最LOW的做法大概就是,自己再实例化这个类后,把创建的对象保存下来,之后不要再进行实例化操作了。上面的例子中使用了特定的方法来进行实例化,之后再第一次实例化的时候才会创建对象。从打印的结果来看,c1 和 c2 的内存地址是一样的。
上面的例子算是实现效果,但是改变了调用的方法。并且依然是可以用标准的方法来创建不同的对象的。下面的例子通过定义new方法,实现了真正的单例模式:
class Foo(object):
__instance = None
def __init__(self):
pass
# 单例模式,就是处在类里加上这个new方法和上面的__instance静态属性
def __new__(cls, *args, **kwargs):
if not cls.__instance:
cls.__instance = object.__new__(cls, *args, **kwargs)
return cls.__instance
def process(self):
return 'Foo.process'
c1 = Foo()
c2 = Foo()
print(c1.process(), c2.process())
print(c1, c2)
# 结果如下:
# Foo.process Foo.process
# <__main__.Foo object at 0x000001DF3132A2E8> <__main__.Foo object at 0x000001DF3132A2E8>
上面如果注释掉new方法,process方法返回的结果是一样的,但是每个对象占用的内存就是不同的了(浪费资源)。如果一个类,它的每个对象里封装的内容都是一样的,就可以使用单例模式。
所以实现了单例模式后,调用类中的方法可以实例化之后直接调用方法或属性:
res = Foo().process()
DjanGo提供了单独API来控制事务:
atomic(using=None, savepoint=True)[source]
原子性是数据库事务的一个属性。使用atomic,我们就可以创建一个具备原子性的代码块。一旦代码块正常运行完毕,所有的修改会被提交到数据库。反之,如果有异常,更改会被回滚。
被atomic管理起来的代码块还可以内嵌到方法中。这样的话,即便内部代码块正常运行,如果外部代码块抛出异常的话,它也没有办法把它的修改提交到数据库中。
一般还是用下面例子中的方法来使用把。
作为装饰器来使用的例子
from django.db import transaction
@transaction.atomic
def viewfunc(request):
# This code executes inside a transaction.
do_stuff()
作为上下文管理器来使用的例子:
from django.db import transaction
def viewfunc(request):
# This code executes in autocommit mode (Django's default).
do_stuff()
with transaction.atomic():
# This code executes inside a transaction.
do_more_stuff()
相关文章