Python自动化开发学习25-Django

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python自动化开发学习25-Django相关的知识,希望对你有一定的参考价值。

组合搜索

下面要讲的是基于模板语言的实现方法,完全没有使用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

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: &lt;script src="http://lib.sinaapp.com/js/jquery/1.12.4/jquery-1.12.4.min.js"&gt;&lt;/script&gt;
图片: &lt;img src="https://b.bdstatic.com/boxlib/20180618/2018061813101982596942363.jpg"&gt;
可能是所有的有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×××是通过对网页注入可执行代码且成功地被浏览器执行,达到×××的目的。这里主要讲针对富文本编辑器的情况。
在使用富文本编辑器的时候,尤其要注意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本身是 &lt;class ‘bs4.BeautifulSoup‘&gt;,直接打印这个对象的时候,内部调用的也是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的事务操作

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()

以上是关于Python自动化开发学习25-Django的主要内容,如果未能解决你的问题,请参考以下文章

Python自动化开发学习4-装饰器

25Django for标签详解

Python自动化开发学习4-2

Python range 数据类型 [学习 Python 必备基础知识][看此一篇就够了][range()][range 元素元素检测元素索引查找切片负索引][检测 range 对象是否相等](代码片

Python range 数据类型 [学习 Python 必备基础知识][看此一篇就够了][range()][range 元素元素检测元素索引查找切片负索引][检测 range 对象是否相等](代码片

Python自动化开发学习18-Django基础篇