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的库。

 

posted @ 2019-06-06 14:33  taoziya  阅读(268)  评论(0)    收藏  举报