使用Django框架开发一个网站

96
计科学堂
2019-07-31 17:20:10 浏览次数:0

0 - 重要
有了上一篇文章中对框架的理解,现在来讲一下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形式暂时还不知道,另外能不能做成只能给自己的网站使用的接口呢?有空我再研究研究。

  [错误报告] [推荐] [收藏] [打印] [关闭] [返回顶部]