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.html
和index.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.html
和tags.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同理。
- 配合else:
-
{# #}
注释
都很容易理解。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
值为a7f85c00424ed045316b7f8eed7e0a04
的sticker
对象,因此我们要重写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.jpg
,tid
是这个帖子的唯一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形式暂时还不知道,另外能不能做成只能给自己的网站使用的接口呢?有空我再研究研究。