使用Django框架开发一个网站

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|... }}

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

  • {% if condition %}, {% elif condition2{% else %}{% endif %}
  • {% ifequal val1 val2 %}{% ifnotequal val1 val2 %}
    • 配合else: {% ifequal val1 val2 %}, {% else %}{% endifequal %},ifnotequal同理。
  • {# #}注释

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

关于URL的参数传递

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

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

  • 浏览器发起请求,URL为http://127.0.0.1:8000/sticker/a7f85c00424ed045316b7f8eed7e0a04/

  • urls.py中会匹配到url(r'^sticker/(?P<md5>[0-9a-z]+)/$', views.singleDetailView.as_view(), name='single')这一条正则表达式。这里我们抓取的是sitcker/????/????的值,因为我知道这是一个md5值,所以过滤的规则是所有小写字母和数字。通过?P可以对抓取出来的内容命名,因此这里会得到一个类似{'md5': a7f85c00424ed045316b7f8eed7e0a04}的字典对象,有多个?P的话字典中自然也有多个值。然后这个字典会被作为参数传入singleDetailView的一个实例对象,在这个对象中,你可以使用self.kwargs['md5']访问到这个值。在这个对象中,默认定义了三个变量:

    • model = Sticker :这个视图(本质上就是个类)对应的模型,此处是Sticker(在models.py中定义)。
    • template_name = 'single.html'context_object_name = 'sticker'表示这个实例要作为名为sticker的参数传入single.html中。
  • 如果你没有什么特殊需求,那么singleDetailView中只要制定这三个变量即可。但是我们这里想要返回的其实是md5值为a7f85c00424ed045316b7f8eed7e0a04sticker对象,因此我们要重写get_object方法,根据md5值找到其对应的sticker对象,此处不表。

  • 这个sticker对象会作为参数传入single.html,之后你就可以在single.html中通过类似{{ sticker.title }}{{ sticker.author }}的标签来生成特定的页面了。

  • 最后自然就是将这个定制过的single.html返回给客户端了。

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实例,如果你足够聪明,很容易就可以猜到这个实例的名字叫request因此,在这个视图对应的模板文件里,可以使用类似{{ request.xxx }}来访问HTTPRequest对象的所有方法,这有时候很有用。
  • 正则中使用()来代表需要抓取的内容,如上文所述,我们使用?P来命名这个抓取到的内容,但是如果不命名呢?它会按照顺序传回抓取到的内容,但是官方表示不推荐这种用法,所以我也不用。这是第二种参数。
  • 第三种参数,就是我们上文中所述的参数,应该也是最常用的方法。

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

那么能不能通过传统的?方式传入参数呢?答案是肯定的,还记得传入的那个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的模型:

  • Sticker(name=yourname).save()
    • Sticker的参数列表需要对应数据结构中的字段名,save()将其生成的对象写入数据库中。
  • Sticker.objects.create(title = title, author = author, content = content, category = c, md5 = md5)
    • 同样create的参数列表需要对应数据结构中的字段名。

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

关于图片外链

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

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

  • 将图片全部下载到本地,这是我目前使用的方法,也是我最不赞成使用的方法。这不仅使得本地文件过多,而且也不容易管理。

    不过我仍然解释下我这里是怎么做的:

    首先,用爬虫去爬取每个帖子,过滤出所有的图片,并将其下载到本地,命名为tid_cnt.jpgtid是这个帖子的唯一id,cnt则是一个简单的递增值。之后将所有的图片都处理成md的语义:[图片上传失败...(image-d1bc88-1532185791647)]。这样其实已经可以了,如果要分细一点,可以在images后再加入不容的目录。

    之后,再将帖子刷入数据库后,将下载下来的图片都放到static/images/下面,也就是网站的静态资源目录下,这样就可以正常访问图片了。

  • 第二种方法就是使用云主机,有一些现成的在线图片外链生成站,最有名的就是七牛云了。

    这里我在推荐一个轻量级的站,名为SM.MS, 但是会有上传数量限制,不符合我的需求。这里提供一个Python版的代码,最后的data是一个字典,里面包含了生成的外链。

    import requests
    import json
    
    image_path = 'mvey0559.jpg' # 要上传的图片路径
    url = 'https://sm.ms/api/upload'
    files = {'smfile' : open(image_path, 'rb')}
    
    content = None
    try:
        response = requests.post(url, files=files)
    
    
        if response.status_code == requests.codes.ok:
            content = response.text
            
    except Exception as e:
        print (e)
    
    data = json.loads(content)
    print (data)
    
    
  • 最后一种方法自然就是自己部署一个图床服务器了。目前我搜集到的信息是使用FastDFS + Python的方法:FastDFSClient_Python。但是能不能再封装成Web形式暂时还不知道,另外能不能做成只能给自己的网站使用的接口呢?有空我再研究研究。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容