Python自动化开发学习25-Djan

发布时间:2019-08-29 07:36:04编辑:auto阅读(1574)

    组合搜索

    下面要讲的是基于模板语言的实现方法,完全没有使用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: <script src="http://lib.sinaapp.com/js/jquery/1.12.4/jquery-1.12.4.min.js"></script>
    图片: <img src="https://b.bdstatic.com/boxlib/20180618/2018061813101982596942363.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×××是通过对网页注入可执行代码且成功地被浏览器执行,达到×××的目的。这里主要讲针对富文本编辑器的情况。
    在使用富文本编辑器的时候,尤其要注意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的事务操作

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

关键字