Nginx + uWSGI + Python + Django构建必应高清壁纸站
写在前面
做这个网站的初衷是因为,每次打开必应搜索搜东西的时候都会被上面的背景图片吸引,我想必应的壁纸应该是经过专业人员精选出来的,我甚至会翻看以前的历史图片,唯一美中不足的是必应的首页只能查看最多7天的壁纸。所以我萌生出自己建一个网站,每天定时搜集必应的壁纸,将壁纸信息保存在数据库中,这样就可以看到很久之前的壁纸图片了。网站使用的是python的django框架,前端接入使用了nginx+uwsgi。没什么特别的考虑,其实网站本身没什么技术含量,使用python的django入手还是很快的,当然用nodejs+express也是不错的(其实我的node比python熟,想折腾点新东西哈哈)。我在这里写出步骤来,给刚刚上手的朋友们一个参考。服务器是直接购买的阿里云的,第一年很便宜才100不到好像,1核2G的基础配置带宽也低,不过够用了。域名直接在万网购买,其实也是阿里旗下,备案也很方便,按照提示步骤操作,差不多2周以内就下来了。最终的成果可以到这里先睹为快:必应高清壁纸,必应每日一图
安装python环境
我们使用的是最新的django框架,需要比较新版本的python环境,阿里云的服务器我购买的时候选择的是centos 6.9,不过我觉得操作系统版本对后面的操作影响不大,服务器内置都已经安装有python环境,只不过是python2.6.6比较老的版本了。所以为了使用最新的django 3.0.1,我干脆安装最新的python 3.8.1。这里我自己下载python源码进行编译,安装。
wget https://www.python.org/ftp/python/3.8.1/Python-3.8.1.tgz tar zxvf Python-3.8.1.tgz cd Python-3.8.1 ./configure make make install
安装好之后,python2.6与python3.8是共存的,我们可以使用python3命令来使用3.8.1版本的python,通过下面的命令可以看到我们安装的python3.8被安装到的目录:
$ which python3 /usr/local/bin/python3 $ ll /usr/local/bin/python3 /usr/local/bin/python3 -> python3.8
跟随python3一起安装的还有pip3,是python的包管理工具,python2.x用的是pip,python3.x用的是pip3。接下来我们安装django框架
安装django框架
pip3 install django
结果报告下面的错误:
看提示是ssl的版本过低,而python需要更高的版本,使用命令 openssl version 查看得到当前系统安装的openssl的版本是1.0.1e,而python3.8需要openssl版本为1.0.2或者以上的版本。这里我们安装openssl 1.1.1的版本:
# 下载源码编译安装 wget https://www.openssl.org/source/openssl-1.1.1a.tar.gz tar -zxvf openssl-1.1.1a.tar.gz cd openssl-1.1.1a ./config --prefix=/usr/local/openssl no-zlib make make install # 替换系统目录中低版本的库 mv /usr/bin/openssl /usr/bin/openssl.bak mv /usr/include/openssl/ /usr/include/openssl.bak ln -s /usr/local/openssl/include/openssl /usr/include/openssl ln -s /usr/local/openssl/lib/libssl.so.1.1 /usr/local/lib64/libssl.so ln -s /usr/local/openssl/bin/openssl /usr/bin/openssl ln -s /usr/local/openssl/lib/libssl.so.1.1 /usr/lib64/libssl.so # 加入库搜索路径并使之生效 echo "/usr/local/openssl/lib" >> /etc/ld.so.conf ldconfig -v # 查看openssl版本输出:OpenSSL 1.1.1a 20 Nov 2018 openssl version
这样新版openssl就安装好了,我们再重新编译并安装Python3,如下:
cd Python-3.8.1 ./configure --with-openssl=/usr/local/openssl make make install
这里的3.8.1编译的时候是添加 --with-openssl=/usr/local/openssl 这个选项,而不是网上的资料说的 --with-ssl 网上说的这个选项是错的,会报错 unrecognized options 。除此以外还有一个问题让我费了点时间值得注意一下,就是在openssl进行make install之后,重新编译python3.8的时候,编译结束总是有下面的错误提示:
Could not build the ssl module! Python requires an OpenSSL 1.0.2 or 1.1 compatible libssl with X509_VERIFY_PARAM_set1_host().
就是提示找不到ssl,该错误提示还是因为编译python的时候 -ssl 选项没有找到正确的 libssl.so 的动态库。我们可以通过下面的命令来确定当前系统中的ssl动态库的情况:
ldconfig -v | grep ssl # 在其输出中我们要确定是否还有老的ssl库在里面,实际上在编译提示找不到ssl的时候 # 我通过命令查看的结果发现 /usr/lib64/libssl.so 软链的还是 1.0.1 的老版本 # 于是我改成连接到新编译的 1.1.1 版本之后,最后看到的是下面的结果 libssl.so.1.1 -> libssl.so.1.1 libssl3.so -> libssl3.so libssl.so.10 -> libssl.so.1.0.1e
之后再重新编译python就没有报ssl not found的错误了,说明新版ssl已经编译到python3.8.1中,此时我们再用上面的命令安装django,最后得到下面的提示,表示安装成功完成:
Installing collected packages: sqlparse, pytz, asgiref, Django Successfully installed Django-3.0.5 asgiref-3.2.7 pytz-2019.3 sqlparse-0.3.1
这里要注意的是,网络很慢的情况下会经常超时,我之后是通过wget手工下载之后再进行安装的:
wget https://files.pythonhosted.org/packages/a9/4f/8a247eee2958529a6a805d38fbacd9764fd566462fa0016aa2a2947ab2a6/Django-3.0.5-py3-none-any.whl pip3 install ./Django-3.0.5-py3-none-any.whl
安装成功之后,我们通过下面的方法验证是否正常:
python3 Python 3.8.1 (default, Apr 18 2020, 00:10:11) [GCC 4.4.6 20120305 (Red Hat 4.4.6-4)] on linux Type "help", "copyright", "credits" or "license" for more information. >>> import django >>> print(django.get_version()) 3.0.5
就是直接运行python3,打开python3的交互输入,引用django模块,打印出其版本,正确打印出了我们安装的3.0.5版本,说明安装正常了
配置django
django框架安装正常之后,我们建立一个简单的django工程testdj如下:
django-admin.py startproject testdj
创建完成之后,会在当前目录下创建一个目录与工程名同名 testdj,我们看下该目录的结构:
这样我们的django项目就创建好了,我们通过其提供的管理脚本 manage.py 来启动他:
# 注意这里要用python3来运行 python3 manage.py runserver 0.0.0.0:8000
运行之后报错了:
由于django3.0.5版本默认引用了sqlite3的数据库,我们并没有安装sqlite3,所以报错,我们先在 settings.py 中把这个配置去掉:
再次运行之后正常,但是此时我们还不能在浏览器中访问他,我们需要编写对应的页面,因为此时项目中什么都没有,连主页都没有,运行成功如下图:
编写视图以及设置url路由
下面我们为主页的请求编写一个view并配置一下路由规则,让主页请求路由到我们编写的view上,在上面的目录结构里面,与 settings.py 同一目录下添加文件 view.py 内容如下:
# view.py from django.http import HttpResponse def hello(request): return HttpResponse("Hello world ! ")
修改urls.py的内容如下:
# urls.py from django.contrib import admin from django.urls import path from django.conf.urls import url from . import view urlpatterns = [ url(r'^$', view.hello), ]
将 settings.py 中的 ALLOWED_HOSTS 配置修改为如下:
# 如果不修改会报类似的错误 # Invalid HTTP_HOST header: 'ip:8000'. You may need to add 'ip' to ALLOWED_HOSTS. ALLOWED_HOSTS = ['*']
再次启动项目之后,在浏览器通过地址访问得到正确输出:
此时我们的diango顺利启动,下面我们在django中使用数据库,django中默认配置sqlite3,我们这里为演示方便,也使用sqlite3作为例子,相信换成其他的数据库不难
在django中使用sqlite3
我们使用sqlite3这个小巧简单的单文件数据库来做演示,sqlite3虽然结构简单,但是功能可一点不简单,支持大多数常见的sql语法,首先我们需要安装sqlite3:
wget https://www.sqlite.org/2018/sqlite-autoconf-3240000.tar.gz tar -xvzf sqlite-autoconf-3240000.tar.gz cd sqlite-autoconf-3240000/ ./configure --prefix=/usr/local/sqlite make make install
编译完成之后,我们需要重新编译与安装python3.8.1,按照上面编译python3的流程重新跑一遍(包括 ./configure --with-openssl=/usr/local/openssl
) ,重新编译python3之后,我们可以打开python3的输入交互环境,直接在里面 import sqlite3 没有报错,即sqlite3可以在python3中正常使用了,如果有报错的话,按照我们上面解决 openssl 的思路去定位类似的问题,相信应该很好解决。在调用configure配置python3的时候,会生成 config.log 里面也可以查看到一些出错信息。接下来我们首先生成一个sqlite3的数据库文件,并创建一个表,插入测试数据,过程如下:
# 我们的数据库文件的名字取 db.sqlite3 sqlite3 db.sqlite3 create table "tb_message" ( "message_id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "message_user" varchar(255) NOT NULL, "message_content" text default '' ); insert into tb_message(message_user,message_content)values('张三','这是张三发送的消息'); insert into tb_message(message_user,message_content)values('李四','这是李四发送的消息');
将生成的数据库文件的名字放在想要放置的目录下,为了测试方便,我们将其放在django工程的根目录,我们将在django项目中读出表的数据并展示到网页上,为了使用django的model,我们需要先创建一个app,在django工程的根目录下使用下面的命令创建一个名字为djapp的app:
django-admin startapp djapp
我们看到创建了一个djapp的文件夹,我们看看djapp的目录结构:
我们前面为了顺利运行django项目,将settings.py中关于sqlite的配置注释了,现在我们需要将这些配置正常化,同时我们刚才创建的djapp我们要在 settings.py 配置中添加进来:
# settings.py # 这里配置了数据库文件所在的路径 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } ... # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'djapp', ]
生成model定义,也就是 djapp/models.py 的内容,在django中,我们可以自己编写models.py的内容,然后使用相关的命令生成数据库的表结构,反过来我们可以自己定义好表结构,使用相关命令由表结构去生成model的类定义。我一般习惯于自己定义好数据库的表结构,然后使用命令生成model的类定义,下面我们生成model类定义到文件 djapp/models.py 中:
# 我们在manage.py所在的目录运行下面的命令 python3 manage.py inspectdb > djapp/models.py
我们可以在 djapp/models.py 文件中看到生成的model定义的内容,一个数据库表对应一个class,我们只创建了一个表,所以文件中只有一个模型类,要注意的是这一步骤的前提是必须在 settings.py 中设置sqlite3的相关配置:
from django.db import models class TbMessage(models.Model): message_id = models.AutoField(primary_key=True) message_user = models.CharField(max_length=255) message_content = models.TextField(blank=True, null=True) class Meta: managed = False db_table = 'tb_message'
接着我们修改之前 view.hello 的实现,在里面读取数据库表的记录,并将数据呈现到模板文件中,再编写模板文件之前,我们需要在 settings.py 中设置模板文件所在的目录:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR+"/templates"], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
我们将模板文件放在项目根目录下的 templates 目录下,我们在此目录下新建一个模板文件 test.tmp 内容如下:
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>django test</title> </head> <body> {% for msg in msg_list %} <div>用户名:{{ msg.message_user }} 消息:{{ msg.message_content }} </div> {% endfor %} </body> </html>
django的模板提供各种包括if、for、过滤器等丰富的模板标签来控制模板的呈现逻辑,使用过MVC方式编写Web应用程序的应该很容易理解,具体的用法很容易在网络上找到,我也不再说明了,上面的模板实际上就是将后端返回的数据中对应的model数据呈现到模板文件中,然后返回最终呈现的HTML内容,我们的 testdj/view.py 的内容如下:
# view .py from django.http import HttpResponse from django.shortcuts import render from django.http import JsonResponse from djapp.models import TbMessage def hello(request): xlist = TbMessage.objects.all().order_by('message_id') msg_list = [] context={"msg_list":msg_list} for msg in xlist: msg_list.append({"message_user":msg.message_user,"message_content":msg.message_content}) return render(request, 'test.tmp', context)
django生成的模板类提供了丰富的访问数据库的能力,包括按各种条件去查询数据库以及进行排序等,另外通过 Model.objects.raw() 函数可以直接写自定义的sql,数据访问这部分更多的函数功能可以查询django的文档,这里也不在浪费篇幅说明了,上述操作好之后,我们再启动django的项目,输入网址,在网页上就能看到结果了:
至此我们的django项目已经运行起来了,并且可以访问sqlite3数据库中的数据,我们列出目前为止整个项目目录的结构以及其说明,方便大家参考:
到目前为止我们运行django项目都是通过项目目录下的 manage.py 进行运行,此种方式仅仅是django为方便调试而提供的一个简易的web服务器,在线上环境我们肯定不能这么使用,那么接下来我们需要通过 Nginx、uWSGI与django结合的方式搭建可以在线上部署的方案
安装uWSGI
在开始这个话题之前,我们首先来说明几个比较重要的概念:
首先要说明的是WSGI,他的全称叫做Web服务器网关接口(Python Web Server Gateway Interface,缩写为WSGI)是为Python语言定义的Web服务器和Web应用程序或框架之间的一种简单而通用的接口。WSGI 不是框架,也不是一个模块而是一个接口规范,是Web服务器与Web应用之间的一种通信规范,该规范不仅仅局限于Python语言,任何语言编写的Web服务器和Web应用程序之间都可以使用WSGI来通信。WSGI标准在 PEP 333 中定义并被许多框架实现,其中包括现我们这里的django框架
说了这么多到底WSGI是什么呢,WSGI只是一种规范,是用于Web服务器和Web应用程序之间通信用的,Web服务器我们知道是提供HTTP服务的服务端程序,例如Nginx、Apache Http Server、Windows IIS 都是常见的Web服务器程序,那么Web应用程序又是什么呢。讲这个之前我们需要先回顾一下Web程序的发展历史,HTTP协议出来之后,作为服务端的实现,Web服务器开始只能提供对静态资源的访问,例如html、图片资源等等,像下面的形式:
人们不满足于只获取静态的网页内容,就出现了动态的网页技术,由Web服务器调用一个外部的程序来生成动态的HTML内容返回给浏览器端。而这个外部的程序可以使用shell,也可以使用perl等脚本编写,或者干脆使用C语言编写,这些程序可以访问外部数据源来生成HTML。那么Web服务器程序与这个生成动态HTML内容的外部程序之间通过什么来传递请求参数呢,Web服务器又通过什么方式来获得并返回由外部程序生成的动态HTML内容呢。由此定义了一个规范,也即是Common Gateway Interface,公共网关接口规范,就是我们常说的CGI了,这些被Web服务器调用的外部程序也因此被称为CGI程序。CGI规范规定:CGI程序从他的标准输入中,接受Web服务器的输入(例如请求参数),并通过标准输出将生成的HTML内容返回给Web服务器。HTTP请求中的一些变量(例如Host,客户端ip地址,浏览器的Agent信息等)则是由Web服务器设置在环境变量中,再由CGI程序从环境变量中读取,这种请求模式变成下面的图:
之后出现了CGI规范的改进版,也即FastCGI规范,FastCGI程序与Web服务器之间的通信并不是通过标准输入与输出,也不通过环境变量来与Web服务器交换HTTP的变量信息,FastCGI程序本身是一个独立的服务端程序,其通过socket监听端口,与Web服务器通信,只不过通信协议使用FastCGI协议而已。那么我们这里说的CGI程序与FastCGI程序是用来处理动态的Web请求并动态生成HTML内容返回给Web服务器的,他们就是我们所说的Web应用程序。随着动态语言的发展,涌现出了越来越多的动态网页技术,但是总归其模式与上面讲的差异不大,例如我们常见的语言的请求结构如下图所示:
之所以需要Web服务器与Web应用程序这种架构,是因为Web服务器本身不处理动态内容,动态内容的处理交给Web应用程序去处理,Web应用程序由广大的程序员根据自己的业务需求使用各种各样的语言去编写,而Web服务器与Web应用程序之间通过某种规范或者说协议来规定怎样去通信,这就出现了各种语言框架下实现的CGI、FastCGI、Servlet、Rack、WSGI等协议或规范,Web服务器与Web应用程序之间如果都遵循相同的协议或实现相同的规范,理论上应该是与语言无关的,由于各种各样的原因,有些规范或者协议,只在特定的语言的web框架中出现或者说多数情况下是这样,例如Servlet在Java中使用,Rack在Ruby中使用
在一些情况下Web服务器与Web应用程序是两个独立的进程,他们通过socket进行通信,通信协议是双方都支持的协议规范(例如FastCGI)。还有一些情况Web服务器与Web应用程序之间的通信是紧耦合的,比如IIS是通过ISAPI的方式,实际上是加载一个dll,调用dll中的相关功能与.NET Web应用交互的。当Nginx与Python Django结合的时候他支持这两种方式,一种是 Nginx+mod_wsgi 方式,这种方式下由Nginx去调用 mod_wsgi 模块中的功能,进而调用python的Web应用程序功能动态生成HTML,其运行的上下文环境在 Nginx 进程中:
另一种方式就是上面截图中展示的通过socket与uWSGI进程交互的方式,Web服务器与uWSGI通信的协议可以是 HTTP 协议,也可以是二进制的 uwsgi 协议,uWSGI程序支持这两种方式与之通信,我们这里使用二进制 uwsgi 协议的方式,不过我们首先要安装 uWSGI 程序(终于说到本章的正题了,前面讲原理都是做铺垫):
pip3 install uwsgi ModuleNotFoundError: No module named '_ctypes'
报错找不到模块 '_ctypes' 经查询我们需要安装 libffi-devel 命令如下:
yum install libffi-devel -y
安装完成之后,我们需要重新配置Python3.8.1并重新make以及make install,命令在上面都有,这里就不重复说了,完成之后继续安装 uwsgi 没有报错,安装成功,我们尝试用下面的命令通过uwsgi启动django应用:
uwsgi --http :8000 --wsgi-file testdj/wsgi.py
我们之前说过uWSGI程序支持两种方式接受请求,一种是使用 HTTP 协议,另一种是使用二进制的 uwsgi 协议,这里我们需要在浏览器中直接访问,所以我们这里运行 uwsgi 的时候通过 --http 选项指定 http 协议,并指定 wsgi 应用文件所在的路径为django项目根目录下的 testdj/wsgi.py 文件,启动之后在浏览器中输入地址直接访问即可正确出结果,截图与前面一样。接下来我们需要使用 Nginx 来作为Web服务器并通过uwsgi进程与wsgi应用来处理动态请求。为什么我们需要Nginx呢,上面运行uwsgi不是已经可以直接使用了吗,我们在之前已经说过了,现在的网站的结构一般都是 Web服务器程序 + 动态语言程序 的结构,是因为各个模块应该各自做自己擅长的事,wsgi应用擅长处理动态的http请求并生成动态的HTML内容,Nginx是专业的Web服务器擅长其他的HTTP请求的处理,事实上uWSGI程序中实现了一个简陋的HTTP服务,所以我们才可以通过 HTTP 的方式直接在浏览器中访问他,然而如果静态资源也用他来处理,其性能肯定比Nginx差很多,所以这里要引入Nginx,引入Nginx的另一个好处是还可以做负载均衡
安装Nginx
首先我们需要安装Nginx服务,Nginx是使用非常广泛的高性能Web服务器,在CentOS下安装也非常简单:
yum install nginx
没有任何报错,直接安装完成,当然自己下载源码安装想要的版本也很方便,下面我们需要配置Nginx,安装之后的nginx的配置文件地址在 /etc/nginx/conf.d/default.conf
server { ... listen 80; # 静态文件请求 location /static { alias /data/testdj/static/; } # uwsgi_pass 转向到uWSGI的8000端口 location / { uwsgi_pass 127.0.0.1:8000; include /etc/nginx/uwsgi_params; } ... }
这里我列出了比较关键的配置,第一个static匹配用于请求静态资源,我们的静态资源可以放在一个单独的 static 目录中,这样静态资源请求不会转发到 uWSGI 去处理,而是直接由 Nginx 处理返回,其他的请求我们通过 uwsgi_pass 配置转发到本机的 8000 端口上,实际上这个配置表明我们的 uWSGI 进程开启的是以 uwsgi 二进制协议进行 socket 通信的8000端口,而不是上面的 HTTP 协议的 8000 端口,如果是 HTTP 协议的8000端口,这里直接配置一个普通的 http 代理即可,配置完成之后 ,我们以下面的命令启动 uWSGI程序:
uwsgi --socket 127.0.0.1:8000 --wsgi-file testdj/wsgi.py
我们这里使用的是 --socket 而不是 --http 并且监听的是 127.0.0.1 这个地址,因为我们的Nginx与uWSGI进程运行在同一台机器上,这个端口就不用暴露到外面了,启动 uWSGI 之后,我们启动 Nginx进程,直接在浏览器中输入 http://ip 即可以正常访问了
在后端运行uWSGI进程
我们前面通过 uwsgi 命令启动uWSGI的方式,程序跑在我们shell环境的前端,如果我们的终端连接断掉则程序会退出,我们需要以后端服务的形式运行uWSGI,这里我们需要将 uWSGI 运行所需要的参数放到一个配置文件中,我们将这些参数放在 uwsgi.ini 中,当然这个文件名字以及放置的具体路径并没有特殊要求,文件内容如下:
# uwsgi.ini [uwsgi] # uWSGI的端口 socket = 127.0.0.1:8000 # 直接作为web服务器使用 # http=ip:port # django项目目录 chdir = /data/testdj # 以django项目名命名的wsgi文件,实际上这个文件是不存在的 # 但是配置必须这么配 module = testdj.wsgi master = true #进程数 processes = 4 threads = 2 vacuum = true # 保存启动之后主进程的pid pidfile=uwsgi.pid # 设置uwsgi后台运行,uwsgi.log保存日志信息 daemonize=uwsgi.log
设置好之后,我们在 uwsgi.ini 文件所在的目录运行下面的命令即可(Nginx不用重启):
# -d 参数表示从后台运行 uwsgi -d --ini uwsgi.ini
再次访问网页,大功告成
收集必应壁纸
文章开头我们说我们要建立一个必应壁纸的收集展示网站,其实主要是介绍 Nginx+uWSGI+Django 的使用,因为网站使用的是 Python语言和Django框架,所以搭建网站也费了一些周折,这个是耗时最多的,其实环境搭建好之后,代码的编写反而很简单,在django中编写程序很方便。收集必应壁纸本身并没有什么特别要说明的,就是下载每天的必应美图,将图片保存到自己的网站,并将相关信息入库,在网站上展示出来。不过呢这里简单说一下怎么获取必应的背景壁纸,有一种很容易想到的方法是直接在代码里面请求必应的网站,然后查看网页结构,获取必应壁纸的url,不过必应的壁纸并没有这么麻烦,通过在Chrome浏览器中访问必应网站发现,必应其实有提供接口,如下:
通过在Chrome浏览器中请求必应网站,选择 "XHR" 可以看到必应的壁纸是通过一个 Ajax 请求获取的数据,我们找到这个请求即可,我贴出来:
https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&nc=1587287726147&pid=hp
这个接口里面的参数是可以设置的,format表示返回的数据的格式 js 返回的是json格式的数据,n 应该是返回的图片的张数,idx 应该是分页的参数,具体的参数含义,大家可以去网上搜索一下,已经有一些人研究过了。不过好像无论怎么设置,并没有办法获取太久的历史图片数据,必应的官网上也只能看到最近一周的图片。也因如此,才有了本篇文章,必应壁纸收集站的实现完全使用上面提到的技术,网站也会持续完善中,欢迎大家来这里反馈留言:关于必应高清壁纸。另外服务器我直接从阿里云买的低配版本机器,带宽和性能都比较基础,目前来说是够用的,域名是直接从万网购买并备案的,其实整个流程还是很便捷的,而且第一年的价格很便宜。 到此本篇介绍就完结了,希望对大家使用django搭建web网站有所帮助