-
python全栈开发中Django的输出非HTML内容
到目前为止,我们的注意力都是放在常见 HTML 代码生成上,但是在这一章中,我们将会对使用 Django 生成其它格式的内容进行简要介绍。
python入门教程Django拥有一些便利的内建工具帮助你生成常见的非HTML内容:
§ RSS/Atom 聚合文件
§ 站点地图 (一个XML格式文件,最初由Google开发,用于给搜索引擎提示线索)
我们稍后会逐一研究这些工具,不过首先让我们来了解些基础原理。
基础: 视图和MIME类型
还记得第三章的内容吗?
一个视图函数(view function),或者简称 view ,只不过是一个可以处理一个Web请求并且返回 一个Web响应的Python函数。这个响应可以是一个Web页面的HTML内容,或者一个跳转,或者一个404 错误,或者一个XML文档,或者一幅图片,或者映射到任何东西上。
更正式的说,一个Django视图函数 必须
§ 接受一个 HttpRequest 实例作为它的第一个参数
§ 返回一个 HttpResponse 实例
从一个视图返回一个非 HTML 内容的关键是在构造一个 HttpResponse 类时,需要指定 mimetype 参数。通过改变 MIME 类型,我们可以告知浏览器将要返回的数据是另一种不同的类型。
下面我们以返回一张PNG图片的视图为例。为了使事情能尽可能的简单,我们只是读入一张存储在磁盘上的图片:
from django.http import HttpResponse
def my_image(request):
image_data = open("/path/to/my/image.png", "rb").read()
return HttpResponse(image_data, mimetype="image/png")
python入门教程就是这么简单。如果改变 open() 中的图片路径为一张真实图片的路径,那么就可以使用这个十分简单的视图来提供一张图片,并且浏览器可以正确的显示它。
另外我们必须了解的是”HttpResponse”对象应用了Python标准的文件应用程序界面(API)。这就是说你可以在Python(或第三方库)任何用到文件的地方使用”HttpResponse”实例。
下面将用 Django 生成 CSV 文件为例,说明它的工作原理。
生成 CSV 文件
CSV 是一种简单的数据格式,通常为电子表格软件所使用。它主要是由一系列的表格行组成,每行中单元格之间使用逗号(CSV 是 逗号分隔数值(comma-python入门教程separated values) 的缩写)隔开。例如,下面是以 CSV 格式记录的一些违规航班乘客的数据。
Year,Unruly Airline Passengers
1995,146
1996,184
1997,235
1998,200
1999,226
2000,251
2001,299
2002,273
2003,281
2004,304
2005,203
备注
前面的列表是真实的数据,数据由美国联邦航空管理处提供。具体内容请参见http://www.faa.gov/datastatistics/passengerscargo/unruly_passengers/.
虽然 CSV 看上去简单,以至于简单到这个格式甚至都没有正式的定义。但是不同的软件会生成和使用不同的 CSV 的变种,在使用上会有一些不便。幸运的是, Python 使用的是标准 CSV 库, csv ,所以它更通用。
因为 csv 模块操作的是类似文件的对象,所以可以使用 HttpResponse 替换:
import csv
from django.http import HttpResponse
# Number of unruly passengers each year 1995 - 2005. In a real application
# this would likely come from a database or some other back-end data store.
UNRULY_PASSENGERS = [146,184,235,200,226,251,299,273,281,304,203]
def unruly_passengers_csv(request):
# Create the HttpResponse object with the appropriate CSV header.
response = HttpResponse(mimetype='text/csv')
response['Content-Disposition'] = 'attachment; filename=unruly.csv'
# Create the CSV writer using the HttpResponse as the "file"
writer = csv.writer(response)
writer.writerow(['Year', 'Unruly Airline Passengers'])
for (year, num) in zip(range(1995, 2006), UNRULY_PASSENGERS):
writer.writerow([year, num])
return response
1
代码和注释可以说是很清楚,但还有一些事情需要特别注意:
§ 响应返回的是 text/csv MIME类型(而非默认的 text/html )。这会告诉浏览器,返回的文档是CSV文件。
§ 响应会有一个附加的 Content-Disposition 头部,它包含有CSV文件的文件名。这个头部(或者说,附加部分)会指示浏览器弹出对话框询问文件存放的位置(而不仅仅是显示)。这个文件名是任意的,它会用在浏览器的另存为对话框中。
§ 与创建CSV的应用程序界面(API)挂接是很容易的:只需将”response”作为第一个变量传递给”csv.writer”。csv.writer函数希望获得一个文件类的对象,”HttpResponse”正好能达成这个目的。
§ 调用 writer.writerow ,并且传递给它一个类似 list 或者 tuple 的可迭代对象,就可以在 CSV 文件中写入一行。
§ CSV 模块考虑到了引用的问题,所以您不用担心逸出字符串中引号和逗号。只要把信息传递给 writerow() ,它会处理好所有的事情。
在任何需要返回非 HTML 内容的时候,都需要经过以下几步:创建一个 HttpResponse 响应对象(需要指定特殊的 MIME 类型)。将它作为参数传给一个需要文件的方法,然后返回这个响应。
下面是一些其它的例子
生成 PDF 文件
便携文件格式 (PDF) 是由 Adobe 开发的格式,主要用于呈现可打印的文档,包含有 pixel-perfect 格式,嵌入字体以及2D矢量图像。PDF 文件可以被认为是一份打印文档的数字等价物;实际上,PDF 文件通常用于需要将文档交付给其他人去打印的场合。
可以方便的使用 Python 和 Django 生成 PDF 文档需要归功于一个出色的开源库, ReportLab (http://www.reportlab.org/rl_toolkit.html) 。动态生成 PDF 文件的好处是在不同的情况下,如不同的用户或者不同的内容,可以按需生成不同的 PDF 文件。
下面的例子是使用 Django 和 ReportLab 在 KUSports.com 上生成个性化的可打印的 NCAA 赛程表 (tournament brackets) 。
安装 ReportLab
在生成 PDF 文件之前,需要安装 ReportLab 库。这通常是个很简单的过程:从http://www.reportlab.org/downloads.html 下载并且安装这个库即可。
使用手册(原始的只有 PDF 格式)可以从 http://www.reportlab.org/rsrc/userguide.pdf 下载,其中包含有一些其它的安装指南。
注意
如果使用的是一些新的 Linux 发行版,则在安装前可以先检查包管理软件。多数软件包仓库中都加入了 ReportLab 。
比如,如果使用(杰出的) Ubuntu 发行版,只需要简单的 apt-get install python-reportlab 一行命令即可完成安装。
在 Python 交互环境中导入这个软件包以检查安装是否成功。
import reportlab
如果刚才那条命令没有出现任何错误,则表明安装成功。
编写视图
和 CSV 类似,由 Django 动态生成 PDF 文件很简单,因为 ReportLab API 同样可以使用类似文件对象。
下面是一个 Hello World 的示例:
from reportlab.pdfgen import canvas
from django.http import HttpResponse
def hello_pdf(request):
# Create the HttpResponse object with the appropriate PDF headers.
response = HttpResponse(mimetype='application/pdf')
response['Content-Disposition'] = 'attachment; filename=hello.pdf'
# Create the PDF object, using the response object as its "file."
p = canvas.Canvas(response)
# Draw things on the PDF. Here's where the PDF generation happens.
# See the ReportLab documentation for the full list of functionality.
p.drawString(100, 100, "Hello world.")
# Close the PDF object cleanly, and we're done.
p.showPage()
p.save()
return response
需要注意以下几点:
§ 这里我们使用的 MIME 类型是 application/pdf 。这会告诉浏览器这个文档是一个 PDF 文档,而不是 HTML 文档。如果忽略了这个参数,浏览器可能会把这个文件看成 HTML 文档,这会使浏览器的窗口中出现很奇怪的文字。
§ 使用 ReportLab 的 API 很简单:只需要将 response 对象作为 canvas.Canvas 的第一个参数传入。 Canvas 类需要一个类似文件的对象, HttpResponse 对象可以满足这个要求。
§ 所有后续的 PDF 生成方法需要由 PDF 对象调用(在本例中是 p ),而不是 response 对象。
§ 最后需要对 PDF 文件调用 showPage() 和 save() 方法(否则你会得到一个损坏的 PDF 文件)。
复杂的 PDF 文件
如果您在创建一个复杂的 PDF 文档(或者任何较大的数据块),请使用 cStringIO 库存放临时生成的 PDF 文件。 cStringIO 提供了一个用 C 编写的类似文件对象的接口,从而可以使系统的效率最高。
下面是使用 cStringIO 重写的 Hello World 例子:
from cStringIO import StringIO
from reportlab.pdfgen import canvas
from django.http import HttpResponse
def hello_pdf(request):
# Create the HttpResponse object with the appropriate PDF headers.
response = HttpResponse(mimetype='application/pdf')
response['Content-Disposition'] = 'attachment; filename=hello.pdf'
temp = StringIO()
# Create the PDF object, using the StringIO object as its "file."
p = canvas.Canvas(temp)
# Draw things on the PDF. Here's where the PDF generation happens.
# See the ReportLab documentation for the full list of functionality.
p.drawString(100, 100, "Hello world.")
# Close the PDF object cleanly.
p.showPage()
p.save()
# Get the value of the StringIO buffer and write it to the response.
response.write(temp.getvalue())
return response
其它的可能性
使用 Python 可以生成许多其它类型的内容,下面介绍的是一些其它的想法和一些可以用以实现它们的库。
ZIP 文件 :Python 标准库中包含有 zipfile 模块,它可以读和写压缩的 ZIP 文件。它可以用于按需生成一些文件的压缩包,或者在需要时压缩大的文档。如果是 TAR 文件则可以使用标准库tarfile 模块。
动态图片 : Python 图片处理库 (PIL; http://www.pythonware.com/products/pil/) 是极好的生成图片(PNG, JPEG, GIF 以及其它许多格式)的工具。它可以用于自动为图片生成缩略图,将多张图片压缩到单独的框架中,或者是做基于 Web 的图片处理。
图表 : Python 有许多出色并且强大的图表库用以绘制图表,按需地图,表格等。我们不可能将它们全部列出,所以下面列出的是个中的翘楚。
§ matplotlib (http://matplotlib.sourceforge.net/) 可以用于生成通常是由 matlab 或者 Mathematica 生成的高质量图表。
§ pygraphviz (https://networkx.lanl.gov/wiki/pygraphviz) 是一个 Graphviz 图形布局的工具 (http://graphviz.org/) 的 Python 接口,可以用于生成结构化的图表和网络。
总之,所有可以写文件的库都可以与 Django 同时使用。请相信一切皆有可能。
我们已经了解了生成“非HTML”内容的基本知识,让我们进一步总结一下。Django拥有很多用以生成各类“非HTML”内容的内置工具。
内容聚合器应用框架
Django带来了一个高级的聚合生成框架,它使得创建RSS和Atom feeds变得非常容易。
什么是RSS?什么是Atom?
RSS和Atom都是基于XML的格式,你可以用它来提供有关你站点内容的自动更新的feed。了解更多关于RSS的可以访问http://www.whatisrss.com/, 更多Atom的信息可以访问 http://www.atomenabled.org/.
想创建一个联合供稿的源(syndication feed),所需要做的只是写一个简短的python类。你可以创建任意多的源(feed)。
高级feed生成框架是一个默认绑定到/feeds/的视图,Django使用URL的其它部分(在/feeds/之后的任何东西)来决定输出 哪个feed
要创建一个feed, 您将创建一个 Feed 类, 并在您的 URLconf 中指向它. ( 查看第3章和第8章, 可以获取更多有关URLconfs的更多信息 )
初始化
为了在您的Django站点中激活syndication feeds, 添加如下的 URLconf:
(r'^feeds/(?P<url>.*)/$',
'django.contrib.syndication.views.feed',
{'feed_dict': feeds}
),
这一行告诉Django使用RSS框架处理所有的以 "feeds/" 开头的URL. ( 你可以修改 "feeds/" 前缀以满足您自己的要求. )
URLConf里有一行参数:{‘feed_dict’: feeds}
,这个参数可以把对应URL需要发布的feed内容传递给 syndication framework
特别的,feeddict应该是一个映射feed的slug(简短URL标签)到它的Feed类的字典 你可以在URL配置本身里定义feeddict,这里是一个完整的例子
from django.conf.urls.defaults import *
from myproject.feeds import LatestEntries, LatestEntriesByCategory
feeds = {
'latest': LatestEntries,
'categories': LatestEntriesByCategory,
}
urlpatterns = patterns('',
# ...
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
{'feed_dict': feeds}),
# ...
)
前面的例子注册了两个feed:
§ LatestEntries表示的内容将对应到
feeds/latest/ .
§ LatestEntriesByCategory的内容将对应到
feeds/categories/ .
以上的设定完成之后,接下来需要自己定义 Feed 类
一个 Feed 类是一个简单的python类,用来表示一个syndication feed. 一个feed可能是简单的 (例如一个站点新闻feed,或者最基本的,显示一个blog的最新条目),也可能更加复杂(例如一个显示blog某一类别下所有条目的feed。这里类别 category 是个变量).
Feed类必须继承django.contrib.syndication.feeds.Feed,它们可以在你的代码树的任何位置
一个简单的Feed
例子来自于chicagocrime.org,描述最近5项新闻条目的feed:
from django.contrib.syndication.feeds import Feed
from chicagocrime.models import NewsItem
class LatestEntries(Feed):
title = "Chicagocrime.org site news"
link = "/sitenews/"
description = "Updates on changes and additions to chicagocrime.org."
def items(self):
return NewsItem.objects.order_by('-pub_date')[:5]
要注意的重要的事情如下所示:
子类 django.contrib.syndication.feeds.Feed .
title , link , 和 description 对应一个标准 RSS 里的
items() 是一个方法,返回一个用以包含在包含在feed的
你可以利用 Django models免费实现一定功能,但是 items() 可以返回你想要的任意类型的对象.
还有一个步骤,在一个RSS feed里,每个(item)有一个(title),(link)和(description),我们需要告诉框架 把数据放到这些元素中
如果要指定
RSS系统模板渲染每一个条目,需要给传递2个参数给模板上下文变量:
§ obj : 当前对象 ( 返回到 items() 任意对象之一 )。
§ site : 一个表示当前站点的 django.models.core.sites.Site 对象。 这对于 {{ site.domain }} 或者 {{ site.name }} 很有用。
如果你在创建模板的时候,没有指明标题或者描述信息,框架会默认使用 "{{ obj }}" ,对象的字符串表示。
你也可以通过修改 Feed 类中的两个属性 titletemplate 和 descriptiontemplate 来改变这两个模板的名字。
你有两种方法来指定 的内容。 Django 首先执行 items() 中每一项的getabsoluteurl() 方法。 如果该方法不存在,就会尝试执行 Feed 类中的 item_link() 方法,并将自身作为 item 参数传递进去。
getabsoluteurl() 和 item_link() 都应该以Python字符串形式返回URL。
对于前面提到的 LatestEntries 例子,我们可以实现一个简单的feed模板。 latest_title.html包括:
{{ obj.title }}
并且 latest_description.html 包含:
{{ obj.description }}
这真是 太 简单了!
一个更复杂的Feed
框架通过参数支持更加复杂的feeds。
举个例子,chicagocrime.org提供了一个RSS源以跟踪每一片区域的犯罪近况。如果为每一个单独的区域建立一个 Feed 类就显得很不明智。这样做就违反了DRY原则了,程序逻辑也会和数据耦合在一起。
取而代之的方法是,使用聚合框架来产生一个通用的源,使其可以根据feeds URL返回相应的信息。
在chicagocrime这个例子中,区域信息可以通过这样的URL方式来访问:
§ http://www.chicagocrime.org/rss/beats/0613/ :返回0613号地区的犯罪数据
§ http://www.chicagocrime.org/rss/beats/1424/ :返回1424号地区的犯罪数据
固定的那一部分是 "beats" (区域)。聚合框架看到了后面的不同之处 0613 和 1424 ,它会提供给你一个钩子函数来描述这些URL的意义,以及会对feed中的项产生的影响。
举个例子会澄清一切。下面是每个地区特定的feeds:
from django.core.exceptions import ObjectDoesNotExist
class BeatFeed(Feed):
def get_object(self, bits):
# In case of "/rss/beats/0613/foo/bar/baz/", or other such
# clutter, check that bits has only one member.
if len(bits) != 1:
raise ObjectDoesNotExist
return Beat.objects.get(beat__exact=bits[0])
def title(self, obj):
return "Chicagocrime.org: Crimes for beat %s" % obj.beat
def link(self, obj):
return obj.get_absolute_url()
def description(self, obj):
return "Crimes recently reported in police beat %s" % obj.beat
def items(self, obj):
crimes = Crime.objects.filter(beat__id__exact=obj.id)
return crimes.order_by('-crime_date')[:30]
以下是RSS框架的基本算法,我们假设通过URL /rss/beats/0613/ 来访问这个类:
框架获得了URL /rss/beats/0613/ 并且注意到URL中的slug部分后面含有更多的信息。它将斜杠("/" )作为分隔符,把剩余的字符串分割开作为参数,调用 Feed 类的 get_object() 方法。
在这个例子中,添加的信息是 ['0613'] 。对于 /rss/beats/0613/foo/bar/ 的一个URL请求, 这些信息就是 ['0613', 'foo', 'bar'] 。
get_object() 就根据给定的 bits 值来返回区域信息。
在这个例子中,它使用了Django的数据库API来获取信息。注意到如果给定的参数不合法,getobject() 会抛出 django.core.exceptions.ObjectDoesNotExist 异常。在Beat.objects.get() 调用中也没有出现 try /except 代码块。函数在出错时抛出Beat.DoesNotExist 异常,而 Beat.DoesNotExist 是 ObjectDoesNotExist 异常的一个子类型。 而在 getobject()
System Message: WARNING/2 (<string>, line 798)
Block quote ends without a blank line; unexpected unindent.
中抛出 ObjectDoesNotExist 异常又会使得Django引发404错误。
为产生
-
试图调用一个函数,并且以 get_object() 返回的对象作为参数传递给 obj 参数。
-
如果没有成功,则不带参数调用一个方法。
-
还不成功,则使用类属性。
最后,值得注意的是,这个例子中的 items() 使用 obj 参数。对于 items 的算法就如同上面第一步所描述的那样,首先尝试items(obj) , 然后是 items() ,最后是 items 类属性(必须是一个列表)。
Feed 类所有方法和属性的完整文档,请参考官方的Django文档 (http://www.djangoproject.com/documentation/0.96/syndication_feeds/) 。
指定Feed的类型
默认情况下, 聚合框架生成RSS 2.0. 要改变这样的情况, 在 Feed 类中添加一个 feed_type 属性.
from django.utils.feedgenerator import Atom1Feed
class MyFeed(Feed):
feed_type = Atom1Feed
注意你把 feed_type 赋值成一个类对象,而不是类实例。目前合法的Feed类型如表11-1所示。
表 11-1. Feed 类型
Feed 类 类型
django.utils.feedgenerator.Rss201rev2Feed
RSS 2.01 (default)
django.utils.feedgenerator.RssUserland091Feed
RSS 0.91
django.utils.feedgenerator.Atom1Feed
Atom 1.0
闭包
为了指定闭包(例如,与feed项比方说MP3 feeds相关联的媒体资源信息),使用 itemenclosureurl ,itemenclosurelength , 以及 itemenclosuremime_type ,比如
from myproject.models import Song
class MyFeedWithEnclosures(Feed):
title = "Example feed with enclosures"
link = "/feeds/example-with-enclosures/"
def items(self):
return Song.objects.all()[:30]
def item_enclosure_url(self, item):
return item.song_url
def item_enclosure_length(self, item):
return item.song_length
item_enclosure_mime_type = "audio/mpeg"
当然,你首先要创建一个包含有 songurl 和 songlength (比如按照字节计算的长度)域的 Song 对象。
语言
聚合框架自动创建的Feed包含适当的
URLs
link 方法/属性可以以绝对URL的形式(例如, "/blog/" )或者指定协议和域名的URL的形式返回(例如"http://www.example.com/blog/" )。如果 link 没有返回域名,聚合框架会根据 SITE_ID 设置,自动的插入当前站点的域信息。
Atom feeds需要 指明feeds现在的位置。聚合框架根据 SITE_ID 的设置,使用站点的域名自动完成这些功能。
同时发布Atom and RSS
一些开发人员想 同时 支持Atom和RSS。这在Django中很容易实现:只需创建一个你的 feed 类的子类,然后修改 feed_type,并且更新URLconf内容。下面是一个完整的例子:
from django.contrib.syndication.feeds import Feed
from chicagocrime.models import NewsItem
from django.utils.feedgenerator import Atom1Feed
class RssSiteNewsFeed(Feed):
title = "Chicagocrime.org site news"
link = "/sitenews/"
description = "Updates on changes and additions to chicagocrime.org."
def items(self):
return NewsItem.objects.order_by('-pub_date')[:5]
class AtomSiteNewsFeed(RssSiteNewsFeed):
feed_type = Atom1Feed
这是与之相对应那个的URLconf:
from django.conf.urls.defaults import *
from myproject.feeds import RssSiteNewsFeed, AtomSiteNewsFeed
feeds = {
'rss': RssSiteNewsFeed,
'atom': AtomSiteNewsFeed,
}
urlpatterns = patterns('',
# ...
(r'^feeds/(?P<url>.*)/$', 'django.contrib.syndication.views.feed',
{'feed_dict': feeds}),
# ...
)
Sitemap 框架
sitemap 是你服务器上的一个XML文件,它告诉搜索引擎你的页面的更新频率和某些页面相对于其它页面的重要性。这个信息会帮助搜索引擎索引你的网站。
例如,这是 Django 网站(http://www.djangoproject.com/sitemap.xml)sitemap的一部分:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>http://www.djangoproject.com/documentation/</loc>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>http://www.djangoproject.com/documentation/0_90/</loc>
<changefreq>never</changefreq>
<priority>0.1</priority>
</url>
...
</urlset>
需要了解更多有关 sitemaps 的信息, 请参见 http://www.sitemaps.org/.
Django sitemap 框架允许你用 Python 代码来表述这些信息,从而自动创建这个XML文件。要创建一个 sitemap,你只需要写一个 Sitemap 类然后配置你的URLconf指向它。
安装
要安装 sitemap 应用程序, 按下面的步骤进行:
-
将 'django.contrib.sitemaps' 添加到您的 INSTALLED_APPS 设置中.
-
确保 'django.template.loaders.appdirectories.loadtemplatesource' 在您的 TEMPLATELOADERS 设置中。默认情况下它在那里, 所以, 如果你已经改变了那个设置的话, 只需要改回来即可。
-
确定您已经安装了 sites 框架 (参见第14章).
备注
sitemap 应用程序没有安装任何数据库表. 它需要加入到 INSTALLEDAPPS 中的唯一原因是: 这样 loadtemplate_source 模板加载器可以找到默认的模板.
初始化
要在您的Django站点中激活sitemap生成, 请在您的 URLconf 中添加这一行:
(r'^sitemap.xml$', 'django.contrib.sitemaps.views.sitemap', {'sitemaps': sitemaps})
这一行告诉 Django, 当客户访问 /sitemap.xml 的时候, 构建一个 sitemap.
sitemap文件的名字无关紧要,但是它在服务器上的位置却很重要。搜索引擎只索引你的sitemap中当前URL级别及其以下级别的链接。用一个实例来说,如果 sitemap.xml 位于你的根目录,那么它将引用任何的URL。然而,如果你的sitemap位于/content/sitemap.xml ,那么它只引用以 /content/ 打头的URL。