5.1 收集网络图片
网上很多用户喜欢的图片,人总是有一种癖好,自己喜欢的就想纳为己有。现在,数字化的东西可以让我们与他人分享,同时自己也不失去。
本节就完成一个功能,允许用户通过图片地址将图片收藏到自己的账户中,并保存在服务器中。
将本章的应用与前面发表文章的应用区分开来,新建一个应用。进入到本项目的根目录中,执行如下命令。

上述代码新建了一个名为image的应用。本章的所有开发代码都放在这个应用中。
新建的应用要在项目中声明,即在./testsite/settings.py文件的INSTALLED_APPS的值列表中增加image应用,代码如下。
1 INSTALLED_APPS = [ 2 'django.contrib.admin', 3 'django.contrib.auth', 4 'django.contrib.contenttypes', 5 'django.contrib.sessions', 6 'django.contrib.messages', 7 'django.contrib.staticfiles', 8 'blog', 9 'account', 10 'password_reset', 11 'article', 12 'image', 13 ]
在正式开发之前,还要做一项准备-------安装Pillow,它是Python中的一个图片支持模块。

准备工作已经完成,下面就开始写实现功能的代码,创建步骤如下图所示。

5.1.1 创建图片相关类
为了能够实现图片的上传和保存,必须要简历数据模型类和表单类。数据模型类用于保存图片的有关信息,对应着数据库表;表单类用于图片上传的数据检验和保存等。
编辑./image/models.py文件,输入如下代码,创建Image类。
1 from django.db import models 2 from django.contrib.auth.models import User 3 from slugify import slugify #① 4 # Create your models here. 5 class Image(models.Model): 6 user = models.ForeignKey(User,on_delete=models.CASCADE,related_name='images') #② 7 title = models.CharField(max_length=300) #③ 8 url = models.URLField() #④ 9 slug = models.SlugField(max_length=500,blank=True) #⑤ 10 description = models.TextField(blank=True) #⑥ 11 created = models.DateField(auto_now_add=True,db_index=True) #⑦ 12 image = models.ImageField(upload_to='images/%Y/%m/%d') #⑧ 13 14 def __str__(self): 15 return self.title 16 17 def save(self, *args, **kwargs): #⑨ 18 self.slug = slugify(self.title) 19 super(Image,self).save(*args,**kwargs)
语句①引入slugify(),这个方法在本书前面已经使用过了,如果一时忘记了,可以返回复习。
Image类和User类属于多对一(一对多)的关系,它们通过语句②的外键实现对应关系,从而确定某张图片的拥有者是这个用户。
语句③定义了图片的标题。
语句④专门用来存储网络图片的URL。这里使用可URLField类,这个类继承了CharField类。它们的区别在于URLField中如果不规定最大长度,则默认是200,即为存储URL做了专门的属性设置。
语句⑤定义了Image对象的slug字段,与本书前面提到的一样,用在URL显示上。
语句⑥用于存储描述图片的文本内容。
语句⑦是Image实例的创建时间(保存该图片的时间),auto_now_add=True表示当作和个类的实例被创建时,时间值会自动创建并写入数据库;db_index=True意味着用数据库的此字段作为索引。
语句⑧规定了图片上传到服务器的物理存储地址。ImageField类继承了FileField类,所以能够接收上传的图片,其中参数upload_to规定了所上传的图片文件的存储路径。
语句⑨重写了父类的save方法,当通过Image实例调用此方法时,自动实现slug的生成(slugify(self.title))和存储。
Image类写好之后,就要迁移数据库,以生成相应的数据库表。分别执行python manage.py makemigrations image和python manage.py migrate命令。

查看数据库表,如下图所示。

上图再次体现了Django的特点,不需要通过SQL语句建立数据库结构,只需要建立数据模型,就能够生成数据库结构。这种方式的好坏,只有在具体应用场景中才能判断,脱离真实问题谈论并无意义。
图片的数据模型创建完成后,就要编写表单类了。本项目的功能需求是用户提交图片网址,然后由程序将该图片下载并保存到指定的位置。
创建./image/forms.py文件,并在其中增加如下代码。
1 from django import forms 2 from django.core.files.base import ContentFile 3 from slugify import slugify 4 from urllib import request 5 from .models import Image 6 7 class ImageForm(forms.ModelForm): 8 class Meta: 9 model = Image 10 fields = ('title','url','description') 11 12 def clean_url(self): #⑩ 13 url = self.cleaned_data['url'] #⑪ 14 valid_extensions = ['jpg','jpeg','png'] #⑫ 15 extension = url.rsplit('.',1)[1].lower() #⑬ 16 if extension not in valid_extensions: #⑭ 17 raise forms.ValidationError("The given Url does not match valid image extension.") 18 return url #⑮ 19 20 def save(self,force_insert=False,force_update=False,commit=True): #⑯ 21 image = super(ImageForm,self).save(commit=False) #⑰ 22 image_url = self.cleaned_data['url'] 23 image_name = '{0}.{1}'.format(slugify(image.title), image_url.rsplit('.',1)[1].lower()) 24 response = request.urlopen(image_url) #⑱ 25 image.imag.save(image_name,ContentFile(response.read()),save=False) #⑲ 26 if commit: 27 image.save() 28 29 return image
对于表单类,其基本结构和作用在本书前面都已经介绍过了,不过,在上述代码中,又出现了新内容。语句⑩就是新出现的一个函数,其作用主要是处理某个字段值。这个函数名称的命名规则是clean_<fieldname>,其中fieldname就是数据模型类中的字段名称,例如语句⑩函数名称clean_url中的“url”就是Image类中的字段“url”。这个函数除self外,不传入其他参数,更不直接得到表单提交的内容,而是通过self.cleaned_data获取相应字段的值,即语句⑪所示的方式。
语句⑫规定了图片的扩展名,语句⑬则是从得到的图片的网址中分解出其扩展名,然后在语句⑭中进行判断。如果属于所规定的扩展名,就认为提交的URL对象是一个图片,并且符合规定的要求。这种判断对象是否为图片的方法比较简单,因为本书在这里的终极目标不是判别对象是否是图片,所以暂时用这个方法。在真实项目中,建议用专门的方法判断一个对象是否为图片,而不是仅凭其扩展名。
最后,语句⑮将经过验证后的字段值返回。
如读者所知,ModelForm类中有一个save()方法,其作用就是将表单提交的数据保存到数据库。ImageForm类是ModelForm类的子类,当建立了ImageForm类是的实例后,该实例也拥有了save()方法,即实现实例数据的保存。这是我们在前面已经应用过的。但是,语句⑯重写了save()方法,当通过ImageForm类实例调用save()方法时,调用的是语句⑯。
语句⑰中依然执行父类ModelForm的save()方法,commit=False意味着实例虽然被建立了,但并没有保存数据。
语句⑱中request不是视图函数中的参数request,而是Python标准库Urllib中的一部分,是一个很好的爬虫工具。request.urlopen(image_url)的作用是以GET方式访问该图片地址,就是模拟用户把网址输入到浏览器的地址栏中,之后在浏览器中返回相应的内容,这里同样返回一个Request对象。通过该对象得到所访问URL的数据(图片的ASCII字符串),在语句⑲中使用request.read()就是要得到此数据。
语句⑲是将语句⑱中返回的结果保存到本地,并按照约定的名称给该图片文件命名。ContentFile类是Django中File类的子类,它接收字符串为参数。这里直接使用了image属性或字段(第二个image)的save()方法。在ImageField继承了FileField,而FileField具有一个save()方法,其官方文档中显示的样式是FieldFile.save(name,content,save=True)。因此,语句⑲采用这种方式保存图片。
5.1.2 收集和管理图片
要收集网上的图片,就必须提交图片的地址,所以要有视图函数接收所提交的图片URL等信息。此外,仅保存了还不够,用户还要浏览和管理,比如删除图片。所以,要在./image/views.py文件中编写两个视图函数,专门用来处理上述问题,代码如下。
from django.shortcuts import render from django.contrib.auth.decorators import login_required from django.http import JsonResponse from django.views.decorators.http import require_POST from django.views.decorators.csrf import csrf_exempt from .forms import ImageForm from .models import Image # Create your views here. @login_required(login_url='/account/login') @require_POST @csrf_exempt def upload_image(request): form = ImageForm(data=request.POST) #① if form.is_valid(): try: new_item = form.save(commit=False) #② new_item.user = request.user new_item.save() #③ return JsonResponse({'status':"1"}) #④ except: return JsonResponse({'status':"0"})
上述视图函数的作用是处理前端表单提交的图片URL及其相关信息。
语句①利用提交的数据建立了表单类的实例。语句②使用该实例的save()方法,但这里没有保存数据(commit=False)。当执行语句③时,该图片被保存到本地指定目录中。
语句④返回的是JSON数据,这跟前面返回字符串稍有不同,当然也可以返回字符串,只不过这里展示一种返回的数据类型。
upload_image()是接收并处理前端提交的数据的函数,下面还要继续编写展示图片的函数list_images(),代码如下。
1 @login_required(login_url='/account/login') 2 def list_images(request): 3 images = Image.objects.filter(user=request.user) 4 return render(request,'image/list_images.html',{"images":images})
上面两个视图函数编写完之后,为了能够让本应用被项目接收,还要在./testsite/urls.py文件中增加应用的URL配置,代码如下。
1 path('image/',include('image.urls',namespace='image'))
然后在本应用中创建./image/urls.py文件,进行image应用相关的URL配置,代码如下。
1 from django.urls import path 2 from .import views 3 4 app_name="image" 5 urlpatterns = [ 6 path('list-images/', views.list_images,name="list_images"), 7 path('upload-image/',views.upload_image,name="upload_image"), 8 ]
下面做一些零碎的准备工作。
前面对文章管理的部分已经建立了一个后台,此处依然采用类似的方式,用户在后台管理中能够添加、删除图片。
依然使用原来的后台导航和侧边栏功能的模板文件。
将图片列表的功能添加到后台导航模板文件中,即修改./templates/article/header.html文件,添加如下代码。
1<li><a href="{% url 'article:article_column' %}">图片管理</a> </li>
然后修改./templates/article?leftslider.html文件,将前面已有的功能命名为“文章管理”,现在新增加的部分命名为“图片管理”。以下代码放在原有代码的后面。
1 <hr> 2 <div class="text-center" style="margin-top:5px;"> 3 <p><h4>图片管理</h4></p> 4 <p><a href="{% url 'image:list_images' %}">图片管理</a></p> 5 </div>
准备工作完毕,开始创建./templates/image目录,将本应用的模板都放到这个目录中。
在此目录中创建模板文件list_images.html,其代码如下。
1 {% extends "article/base.html" %} 2 {% load staticfiles %} 3 {% block title %}images{% endblock %} 4 {% block content %} 5 <div> 6 <button type="button" class="btn btn-primary btn-lg btn-block" onclick="addImage()">添加图片</button> 7 <div style="margin-top:10px;"> 8 <table class="table table-hover"> 9 <tr> 10 <td>序号</td> 11 <td>标题</td> 12 <td>图片</td> 13 <td>操作</td> 14 </tr> 15 {% for image in images %} 16 <tr> 17 <td>{{ forloop.counter }}</td> 18 <td>{{ image.title }}</td> 19 <td>{{ image.image }}</td> 20 <td><a name="delete" href="javascript:" onclick="del_image(this,{{ image.id }})"> 21 <span class="glyphicon glyphicon-trash" style="margin-left:20px;"></span></a></td> 22 </tr> 23 {% empty %} 24 <p>还没有图片,请点击上面的按钮添加图片</p> 25 {% endfor %} 26 </table> 27 </div> 28 </div> 29 30 31 <script src="{% static 'js/jquery-3.3.1.js' %}"></script> 32 <script src="{% static 'js/layer.js' %}"></script> 33 <script type="text/javascript"> 34 35 function addImage(){ 36 var index=layer.open({ 37 type:1, 38 skin:'layui-layer-demo', 39 closeBtn:0, 40 shift:2, 41 shadeClose:true, 42 title:"Add Image", 43 area:['600px','440px'], 44 content: "<div style='padding:20px'><p>请新增扩展名是.jpg 或.png 的网上照片地址</p><form><div class='form-group'><label for='phototitle' class='col-sm-2 control-label'> 标题</label><div class='col-sm-10'><input id='phototitle' type='text' class='form-control' style='margin-bottom:5px'></div></div><div class='form-group'><label for='photourl' class='col-sm-2 control-label'>地址</label><div class='col-sm-10'><input id='photourl' style='margin-bottom:5px' type='text' class='form-control'></div></div><div class='form-group'><label for='description' class='col-sm-2 control-label'>描述</label> <div class='col-sm-10'><textarea class='form-control' style='margin-bottom:5px' row='2' id='photodescription'></textarea></div></div><div class='form-group'><div class='col-sm-offset-2 col-sm-10'><input id='newphoto' type='button' class='btn btn-default' value='Add It'></div></div></form></div>", 45 success:function(){ 46 $("#newphoto").on('click',function(){ 47 var title = $("#phototitle").val(); 48 var url = $("#photourl").val(); 49 var description = $("#photodescription").val(); 50 var photo = {"title":title,"url":url,"description":description}; 51 $.ajax({ 52 url:'{% url "image:upload_image" %}', 53 type:"POST", 54 data:photo, 55 success:function(e){ 56 var status = e['status'] 57 if(status =="1"){ 58 layer.close(index); 59 window.location.reload(); 60 }else{ 61 layer.msg("图片无法获取,请更换图片"); 62 } 63 }, 64 }); 65 }); 66 }, 67 }); 68 } 69 </script> 70 {% endblock %}
在上面的Javascript代码中,再次使用了弹出层插件。
再次刷新页面,单击“添加图片”按钮,在弹出的对话框中输入有关内容,如下图所示。

在网上找一些图片,通过此方式提交给本应用,如下图所示。

当然,这里在显示上还有一些问题,后面需要继续完善,读者重点关注是否把图片提交并保存了。
查看数据模型类Image中的字段image=models.ImageField(upload_to='images/%Y/%m/%d')所设置的物理存储位置,是否已经有了刚才上传的图片,如下图所示。

数据库也要检查一下,看看是否按照预设把数据都保存了下来,如下图所示。

最终结果实现了预期目标。
5.1.3 完善图片管理功能
图片已经实现了上传,如果有图片不喜欢怎么办?删除!所以,列表中的删除功能是必须有的。利用已经学习过的知识就能够完成此功能,编辑./image/views.py文件,增加如下代码。
1 @login_required(login_url='/account/login') 2 @require_POST 3 @csrf_exempt 4 def del_image(request): 5 image_id = request.POST['image_id'] 6 try: 7 image = Image.objects.get(id=image_id) 8 image.delete() 9 return JsonResponse({'status':"1"}) 10 except: 11 return JsonResponse({'status':"2"})
这个视图函数与前文删除文章的视图函数相差无几。这里再次提醒读者,可以思考并尝试写一个更具有通用性的删除函数(或者类),以实现所有类似的删除功能。
之后不要忘记在./image/urls.py文件中配置URL,在列表中增加下列值。
1 path('del-image/',views.del_image,name="del_image"),
然后在./templates/image/list_images.html文件中编写早已经为删除图标设定的JavaScript函数del_image(),代码如下。
1 function del_image(the,image_id){ 2 var image_title = $(the).parents("tr").children("td").eq(1).text(); 3 layer.open({ 4 type:1, 5 skin:"layui-layer-rim", 6 area:["400px","200px"], 7 title:"删除图片", 8 content:'<div class="text-center" style="margin-top:20px"><p>是否确定删除《'+image_title+'》</p></div>', 9 btn:['确定','取消'], 10 yes:function(){ 11 $.ajax({ 12 url:'{% url "image:del_image" %}', 13 type:"POST", 14 data:{"image_id":image_id}, 15 success:function(e){ 16 var status = e['status'] 17 if(status=="1"){ 18 parent.location.reload(); 19 layer.msg("has been deleted.") 20 }else{ 21 layer.msg("删除失败"); 22 } 23 }, 24 }) 25 }, 26 }); 27 }
这个函数的写法与前文类似函数的写法一样,这里不再解释。
这样就实现了图片的删除功能。

单击“确定”按钮,弹窗关闭,删除该图片信息。

请读者仔细观察,这种删除并不彻底,因为该图片并没有从磁盘上删除,只是从本项目的数据库中删除了。
虽然完成了删除功能,但还有需要改进之外,就是在页面中不显示图片,仅列出图片的标题和地址。因此,要修改./templates/image/list_images.html文件的代码,将下面的代码:
<td>{{ image.image }}</td>
替换为类似以下代码:
<td><imge src="url of image"></imge></td> #url of image是占位符
然后在./testsite/settings.py文件中增加如下设置。
MEDIA_URL = '/media' MEDIA_ROOT = os.path.join(BASE_DIR,'media/')
MEDIA_ROOT声明了相对项目跟目录的文件保存地址。前面我们已经将图片保存到了相对项目根目录的images中,即./images。考虑到以后还可以能上传视频等媒体文件,所以还是设置一个统一的目录较好,于是这里设置了一个名为media的目录,图片将被保存到./media/images里面。
MEDIA_URL用于设置URL映射中的路径,下面就编辑./testsite/urls.py文件,引入settings和static,代码如下。
from django.conf import settings from django.conf.urls.static import static
在本文件的urlpatterns之后增加如下代码。
urlpatterns += static(settings.MEDIA_URL,document_root=settings.MEDIA_ROOT)
这样就为每个上传的静态图片配置了URL路径。做好上面的配置之后,再将上面提到的./templates/image/list_images.html文件中将要替代的部分用下面代码替代。
<td><img src="{{ image.image.url }}"width="100px" height="100px"></imge></td>
确保Django服务运行,访问http://127.0.0.1:8000/image/list-images/,原来提交的图片肯定不能显示了,因为按照上面的设置,图片的访问地址已经发生了变化,需要重新提交,并且在提交之后,注意观察文件是否被保存到了./media/images里面。
最终的展示效果如下图所示。

这样,就实现了对上传图片的可视化管理。
这里展示的图片虽然已经进行了长宽的设置,但仅仅是在显示的时候对图片尺寸进行来了更改,实际上仍然要读取原图。说到这里,读者可能也想到了,能不能使用缩略图呢?当然可以!
sorl-thumbnail就是很好的缩略图Django应用,其光放网站是https://sorl-thumbnail.readthedocs.io这里再次显示了Django快速开发的特点,“不需要重复造轮子”。可以用下面的方法安装此应用。

既然是Django的一个应用,要让其在本项目中发挥作用,就要在./testsite/settings.py的INSTALLED_APPS中注册名为sorl.thumbnail的应用。

接下来进行数据迁移,代码如下。

然后修改./templates/image/list_images.html文件。
在文件第一行后引入thumbnail。
{% load thumbnail %}
再将原来显示图片的代码替换为下面的代码。
{% thumbnail image.image "100*100" as im %} <td><img src="{{ im.url }}"></img></td> {% endthumbnail %}
重新启动Django服务,然后刷新页面,这时候查看到的图片就是缩略图,如下图所示。

到./media/目录中查看缩略图文件的保存地址。
当然,sorl-thumbnail在数据库中也有体现,可以去查看,如下图所示。

读者还可以继续创建一个分页功能,因为这个功能和前面文章管理中的分页功能一致,所以笔者就省略了,特别建议读者能够补上。
本节中我们收集了网络图片,并对保存到项目本地的图片进行了管理,还使用了缩略图。因为前面已经奠定了开发基础,所以本节中并没有遇到难点,但并不意味着开发过程会一帆风顺,如果在某些细节上不注意也会有Bug出现。随着功能越来越多,重复代码也日渐增多,所以读者要充分使用在Python中已经学会的知识,将重复的功能进行抽象,写成能够被公用的类(或函数),虽然笔者在本书中没有这样做。
5.1.4 知识点
1、模型:ImageField
ImageField专门用来声明某字段是图片类型,其类的完全调用形式如下。
class ImageField(upload_to=None, height_field=None, width_field=None, max_length=100, **options)
当字段被设置为ImageField类型之后,提交图片,Django会自动检测所提交的是否是图片类型,不需要用户写检验方法。
在创建ImageField实例时,可以指明upload_to参数,说明图片保存在服务器上的位置,通常还在settings.py中配置MEDIA_ROOT和MEDIA_URL的值,例如在本书的项目中,代码如下。
MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media/')
其实,ImageField继承了FileField,如果理解了ImageField,后面再遇到FileField也好解决了。阅读下面这段代码:

当然,对这个文件也可以继续按照文件对象的方式进行操作,比如:

只有在执行了实例的save()方法后,保存在服务器物理空间中的文件名才会改变,否则知识数据库表记录中的文件名被修改了,物理空间中的文件名并没有变化,在调用时会找不到对应的文件。
不要忘记,如果要上传图片,还有依赖一个名为Pillow的库。
浙公网安备 33010602011771号