使用Django框架开发一个网站

2018-07-21  本文已影响556人  小温侯

0 - 重要

有了上一篇文章中对框架的理解,现在来讲一下Django具体的打开方式,最好的方法自然是依附于一个项目。

本文主要是基于Django 博客开发入门教程做的二次开发,因此,你可能需要先完整的做一遍这个教程,再继续读下去

我是先照着教程做了一个一模一样的博客,因为真的是一模一样的,所以就不放出来了。做的过程中也阅读了Django的官方文档,为了确保我对Django的理解都是正确的,以及有些地方对于这个博客我也有点自己的想法,于是我又做了个类似的网站。我所作的改动我会一一列举出来。

我做了个什么?

我的想法是做一个内容展示的平台,主要是用来展示各种爬虫抓取到的内容,具体的文字采用MD格式。虽然我很想直接做个通用平台,但是时间有限,而且也没找到什么好看的页面模板,就先搁下了。这里我还是找了一个类博客的模板,用的内容资源是之前爬取百度贴吧时的帖子内容。

当前的测试内容是从百度贴吧里抓取到的10个帖子,分别来源于来个dota2复仇者联盟吧。在项目里,每个贴吧对应一个Category,每个帖子对应一个Sticker

1 - 改动的地方

关于设计模板和模板标签语言

我们已经知道在Django中,一个网页会由两部分组成:静态文件(js, css, img...)和HTML文档。前者让网站变得美观,而后者则定义了网站的结构和功能。Django则提供了一些方法用来动态生成HTML文档,这些方法被称为标签,标签有很多种,我从两个大括号:{{ xxxxx }}说起。

这类标签官方没有命名,但说明了它返回的是被符号包含的语句的属性值,这个值可以直接来源于某个变量,也可以来源于某个函数。大多数情况,我们传入一个页面的参数是一个模型实例(定义在models.py里)或由若干实例组成的一个QuerySet,后者则通常是由自定义标签返回的。又一般而言,我们会对QuerySet里的对象进行遍历,所有说到底{{ xxxxx }}操作的对象是一个模型实例,因此它可以调用这个模型里所有的成员方法(模型本质上是一个类)。

这段话怎么理解?举个例子,参考base.htmlindex.html

base.html

{% load staticfiles %}
<!DOCTYPE html>
<html lang="en">
<head>
    .....meta contents
</head>
<body>
    <header>
        {% include "common/navigation.html" %}
    </header>

    <div class="widewrapper main">
        <div class="container">
            <div class="row">
                <div class="col-md-8 blog-main">
                    {% block main %}
                    {% endblock main %}
                </div>
                <aside class="col-md-4 blog-aside">
                    
                    <div class="aside-widget">
                        {% include "common/feature.html" %}
                    </div>

                    <div class="aside-widget">
                        {% include "common/tags.html" %}
                    </div>
                </aside>
            </div>
        </div>
    </div>

    <footer>
        {% include "common/footer.html" %}
    </footer>
    
    <script src="{% static 'js/jquery.min.js' %}"></script>
    <script src="{% static 'js/bootstrap.min.js' %}"></script>
    <script src="{% static 'js/modernizr.js' %}"></script>

</body>
</html>
index.html

{% extends 'base.html' %}
{% load staticfiles %}
{% load customTags %}

{% block main %}
<div class="row">
    {% get_all_categories as categories_list %}
    {% for category in categories_list %}
    <div class="col-md-6 col-sm-6">
        <article class=" blog-teaser">
            <header>
                <p>
                <a href="{{ category.get_absolute_url }}">
                <img src="{% static 'images/' %}{{ category.name }}/{{ category.name }}.jpg" alt=""></p>
                <h3><a href="{{ category.get_absolute_url }}">{{ category.name }}</a></h3>
            </header>
            <hr>
        </article>
    </div>
    {% empty %}
    暂无内容!
    {% endfor %}

</div>
{% endblock main %}

当访问的URL为/时,会返回index.html页面,这个页面首先继承了base.html(用{% extends 'base.html' %}标签)。这意味这index.html的内容会和base.html里的内容一起返回给客户端,base.html里会用{% block %}{% endblock %}预留位置,之后在index.html中补足,block也可以命名,这个不难理解。

我这里是将base.html作为最基础的模板,因此我想将它做的尽可能地简洁同时易于阅读,于是我将其进一步的分割开来,剥离出了feature.html, footer.html, navigation.htmltags.html,它们分别对应了页面上的一部分小页面,通过{% include %}标签,index.html可以将它们都包裹进来。

回到index.html,我们用{% load %}标签载入了两个模块,staticfiles定义了静态资源的路径,customTags则是我自定义的标签(如果你不知道自定义标签是什么,回头看看文章开始部分提到的博客)。在我的自定义标签里我定义了一个名为get_all_categories的方法(注意这个方法是可以带参数的,参数通过get_all_categories param1 param2 ...的方式传入)用来获取所有的分类,它返回的是一个QuerySet类型的变量,这个后面会说。通过{% for %}, {% empty %}{% endfor %},可以对这个set进行遍历,生成一系列结构一致的HTML结构。for标签是可以多层嵌套的

另一种很有用的物件叫**过滤器 **,用管道符|表示,它相当于一种快捷的自定义标签,用于快速的处理对象内容,过滤器和标签其实是差不多的东西,也可以互相转化。值得一提的是,过滤器具有管道特性,因此它可以像这样使用:{{ content|filter1|filter2|... }}

最后还有一些没提到但是很常用的标签列举在这,仅供浏览:

都很容易理解。Django称这些标签为The Django template language,同时列出了所有可以用的标签Built-in template tags and filters。通过使用模板可以使代码的结构变得清晰,通过使用标签,可以“动态”生成网页的内容。

关于URL的参数传递

上一节中,single.html中使用的category是从自定义标签中的一个方法返回的,我们也可以在生成页面的时候直接给页面传递一个内容。

首先梳理一下一个请求的处理流程,以访问/sticker/a7f85c00424ed045316b7f8eed7e0a04/为例:

Django 如何处理一个请求 - 官方版

Django 如何处理一个请求: https://docs.djangoproject.com/zh-hans/2.0/topics/http/urls/#how-django-processes-a-request 写道:

当一个用户请求Django 站点的一个页面,下面是Django 系统决定执行哪个Python 代码使用的算法:
    1. Django determines the root URLconf module to use. Ordinarily, this is the value of the ROOT_URLCONF setting, but if the incoming HttpRequest object has a urlconf attribute (set by middleware), its value will be used in place of the ROOT_URLCONF setting.
    2. Django loads that Python module and looks for the variable urlpatterns. This should be a Python list of django.urls.path() and/or django.urls.re_path() instances.
    3. Django 依次匹配每个URL 模式,在与请求的URL 匹配的第一个模式停下来。
    4. Once one of the URL patterns matches, Django imports and calls the given view, which is a simple Python function (or a class-based view). The view gets passed the following arguments:
       - 一个 HttpRequest 实例。
       - If the matched URL pattern returned no named groups, then the matches from the regular expression are provided as positional arguments.
       - The keyword arguments are made up of any named parts matched by the path expression, overridden by any arguments specified in the optional kwargs argument to django.urls.path() or django.urls.re_path().
    5. If no URL pattern matches, or if an exception is raised during any point in this process, Django invokes an appropriate error-handling view. See Error handling below.

这里我觉得第四点比较重要,它说在传递URL中的参数给视图时,有可能传入三种不同的参数:

通过传统的”?”传递参数?

那么能不能通过传统的?方式传入参数呢?答案是肯定的,还记得传入的那个HTTPRequest实例吗?它有一个方法为GET,解释为A dictionary-like object containing all given HTTP GET parameters.,如果URL是..../s.html?p1=v1,可以通过request.GET.get('p1')访问。

至于这个参数能不能直接用正则匹配出来,我觉得是可以的,但是我没试过。

为什么用md5?

类视图其实默认是传入pk参数的,也就是主键,这是你在初始化数据库时Django自动生成的。一开始,我只是单纯的想试试能不能传入别的参数,后来发现其实用md5也好,可以起到一定的反爬虫的功效?

sticker还是sticker?

singleDetailView的内容:

class singleDetailView(DetailView):
    model = Sticker
    template_name = 'single.html'
    context_object_name = 'sticker'

    def get_object(self, queryset=None):
        sticker = get_object_or_404(Sticker, md5=self.kwargs['md5'])
        sticker.increase_views()
        sticker.content = markdown.markdown(sticker.content,
                                    extensions=[
                                        'markdown.extensions.extra',
                                        'markdown.extensions.codehilite',
                                        'markdown.extensions.toc',
                                    ])
        return sticker

这里有两个sticker,代码当然没问题,但是改写一些会更好理解:

class singleDetailView(DetailView):
    model = Sticker
    template_name = 'single.html'
    context_object_name = 'sticker'

    def get_object(self, queryset=None):
        obj = get_object_or_404(Sticker, md5=self.kwargs['md5'])
        obj.increase_views()
        obj.content = markdown.markdown(obj.content,
                                    extensions=[
                                        'markdown.extensions.extra',
                                        'markdown.extensions.codehilite',
                                        'markdown.extensions.toc',
                                    ])
        return obj

如上文所述,重写的get_object方法会返回一个Sticker对象或模型,它是根据md5的值筛选出来的。这个对象obj会被赋予另一个名为sticker的变量里,然后传入single.html中,这里只是恰好使用了相同的名字。

关于帖子主题内容的格式

网站的测试内容,我是从网站直接抓过来的,它被转化为Markdown格式,之后再通过Markdown库渲染成HTML格式。因此这里其实有一个HTML -> MD -> HTML的过程。私以为这不是多余的,这样做可以过滤掉很多杂乱的HTML标签。如果网站的内容来源是很多不同的网站,优势会更加明显。

长URL换行问题

有时候帖子内容中会包含一个很长的URL,这个URL不会自动换行,因此可能会超出当前的CSS block,影响美观。我在网上查到了两种解决方法:(django问题)处理换行和空格,第二种我这没生效。

关于ORM和数据操作

虽然Django后端使用的是sqlite3(也可以换成其他的),但是我们并不需要编写SQL语句来操作它(但是这也是可以的)。

Django对数据操作进行了封装,这使得我们可以通过使用Python语言直接操作数据,这个过程就叫做ORM,即Object-relational mapping。

存入数据

这里我们已经知道,Django中的数据模型其实就是继承了db.models的类,在默认文件models.py中定义。 这个类中定义的属性即对应了表中的一个字段,同时db.models中也定义了所有可用的字段类型。注意一对一、一对多、多对多关系的表示方法。

需要注意的是,只要你对models.py中的内容 进行了修改,就需要执行:

python manage.py makemigrations
python manage.py migrate

完成数据库的迁移,可以理解为刷新。

完成了数据库迁移后,你就可以使用代码直接向数据库中写入数据了,下面会提到。

读取数据

从数据库中读取数据,最重要的内容就是QuerySet 对象。值得一提的是,QuerySet 对象是Lazy的(想一想生成器),这个怎么理解呢?QuerySet 对象的一个特性就是它可以进行链式操作,考虑一下代码:

r = CustomM.objects.all().filer(pk__gt=12).filter(...). ..........filert(...)

理论上你可以无限进行链式操作,这里的trick就是虽然这行代码被执行了,但实际上这里面没有设计到数据库的行为。那么什么时候会产生数据库行为呢?文档上说的是`当这个对象被evaluated的时候。哈哈,下个问题自然是对象什么时候会被evaluated呢?答案是When QuerySets are evaluated。这个文档里列举所有会造成数据库行为的操作,这对写代码会有帮助。

在同一篇文档里:QuerySet API reference, 里面列举了所有QuerySet 对象的操作,在我的代码里,我其实只用到了很有限的几个:all(), order_by, filter()values()。关于这些API,我想我会另开文档说一说,毕竟实在是太多了。

理论上,通过这些API可以完成所有的数据库操作,不管是简单的还是复杂的SQL语句。事实上呢?

关于批量导入数据库

通过合理的开发,我们当然可以在Admin页面中手动添加数据。但是第一次启动网站时,我们可能会需要导入一些初始数据。Django提供了两种方法用来向数据库写入数据,假如models.py中定义了一个名为Sticker的模型:

在Django项目的根目录下,可以执行python manage.py shell进入一个交互界面,然后就可以使用上述方法了。但是,我们更想做的应该是在一个Python文件中执行这些代码。

你只需要在项目根目录下新建一个python文件,然后执行它就行了。这里放上我的代码:

# 这个一定要,不然会报错,但是错误很明显,容易定位。
import os 
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gallery.settings") 

# django版本大于1.7时需要这两句
import django
django.setup()

import hashlib

# 导入你需要写入的两个模型类
from sticker.models import Category, Sticker

# 第一种方法写入数据库
dirname = '分类1'
c = Category(name=dirname)
c.save()

file_dir = r"D:\OneDrive\Download\"

# 这里我是遍历了file_dir中的所有文件,并依次解析它们
for file in os.listdir(file_dir): 
    with open(file_dir+'\\'+file, 'r', encoding='utf-8') as file:
        title  = file.readline().split(':')[1].strip() # 获取第一行title值
        author = file.readline().split(':')[1].strip() # 获取第二行author值

        content = file.read().strip() # 读取剩余内容,作为content,即post的内容
    
        # 根据content生成md5
        m = hashlib.md5()
        m.update(content.encode('utf-8'))
        md5 = m.hexdigest()
    
    # 第二种方法写入数据库
    Sticker.objects.create(title = title,
                       author = author,
                        content = content,
                        category = c,
                        md5 = md5
                        )    

关于图片外链

虽然抓取到的内容中的图片的链接都是绝对路径,但是如果直接在网站上使用,会被认为是盗链导致图片无法显示。(怎么识别盗链的?)

关于这一点,我想到了三种解决方法:

上一篇下一篇

猜你喜欢

热点阅读