美文网首页
使用Django框架开发一个网站

使用Django框架开发一个网站

作者: 小温侯 | 来源:发表于2018-07-21 23:09 被阅读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|... }}

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

    • {% 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形式暂时还不知道,另外能不能做成只能给自己的网站使用的接口呢?有空我再研究研究。

    相关文章

      网友评论

          本文标题:使用Django框架开发一个网站

          本文链接:https://www.haomeiwen.com/subject/qjbkmftx.html