Python 包构建教程
本文内容纲要:
-setuptools和setup.py
-你所需要做的事&一些概念
-基础概念
-关于源码分发文件和二进制分发文件
-示例和分发选择
-purepythonmodule
-package
-extensionmodule
-package元信息参数
-package内容参数
-py_modules列举每个模块
-package列举每个包
-package_dir重新映射package和目录的关系
-install_requires和dependency_links安装依赖模块
-ext_modulePython调用C/C++
-name扩展模块名字
-sources和include_dirs
-define_macros和undef_macros
-libraries和library_dirs
-extra_compile_args
-创建源码分发
-sdist命令
-MANIFEST.in模版文件
目录
-
setuptools和setup.py
-
你所需要做的事&一些概念
- 基础概念
- 关于源码分发文件和二进制分发文件
-
示例和分发选择
- purepythonmodule
- package
- extensionmodule
-
package元信息参数
-
package内容参数
-
py_modules列举每个模块
-
package列举每个包
-
package_dir重新映射package和目录的关系
-
install_requires和dependency_links安装依赖模块
-
ext_modulePython调用C/C++
- name扩展模块名字
- sources和include_dirs
- define_macros和undef_macros
- libraries和library_dirs
- extra_compile_args
-
-
创建源码分发
- sdist命令
- MANIFEST.in模版文件
setuptools和setup.py
Setuptools
和distutils
都是用于编译、分发和安装python包的一个工具,特别是在包依赖问题场景下非常有用,它是一个强大的包管理工具。Setuptools是distutils的加强版。编译、分发和安装python包的一个关键的事就是编写setup脚本。setup脚本的主要作用在于向包管理工具Setuptools
或distutils
说明你的模块分发细节,所以Setuptools
支持大量的命令以操作你的包。setup脚本主要调用一个setup()
方法,许多提供给Setuptools
的信息都以keywordarguments
的参数形式提供给setup()
方法。
你所需要做的事&一些概念
对于包开发者和使用者,所需要做的事:
- 编写setup.py脚本,用于处理你的包
- (可选)编写setup配置文件
- 创建源码分发文件,
pythonsetup.pysdist
, - (可选)创建二进制分发文件,
pythonsetup.pybdist
对于包使用者,只需要pythonsetup.pyinstall
,便可以成功安装python包。
基础概念
- module模块:module是python中代码重用的基本单元,一个module可以通过
import
语句导入到另一个module;module分为:purepythonmodule
(纯python模块)、extensionmodule
(扩展模块)和package
(包) - purepythonmodule:纯python模块是用纯python语言编写的模块,单一的
.py
文件作为一个模块使用,也就是一个.py
可以称为模块了 - extensionmodule:扩展模块是用底层的C/C++、Objective-C或Java编写的模块,通常包含了一个动态链接库,比如so、dll或Java,目前
distutils
只支持C/C++和Objective-C,不支持Java编写扩展模块;但是python提供了一个JCC
这样一个用于生成访问Java类接口的C++代码的胶水模块,应该也是可以使用Java编写模块的。 - package:包是一个带有
__init__.py
文件的文件夹,用于包含其他模块 - rootpackage:rootpackage是包的最顶层,它不是实质性的包,因为它不包含
__init__.py
文件。大量的标准库位于rootpackage
,因为它们不属于一个任何更大的模块集合了。实际上,每一个sys.path
列举出来的文件夹都是rootpackage
,你可以在这些文件夹中找到大量的模块。 - distribution:模块分发,一个归档在一起的python模块集合,它作为一个可下载安装的资源,方便用户使用,作为开发者便需要努力创建一个易于使用的
distribution
。 - distributionroot:源代码树的最顶层,也就是
setup.py
所在的位置。
关于源码分发文件和二进制分发文件
源码分发文件是将包分享给其他人更为推荐的一种形式,因为源码分发文件比二进制分发文件更适合跨平台,这样使用者可以在自己的机器上通过编译得到自己的机器相关的包代码并且进行安装。
示例和分发选择
- 如果你只是发布几个脚本文件而已,特别是它们逻辑上不属于同一个包,你可以使用
py_modules
选项一个一个地指定; - 如果你需要发布的模块文件太多,使用
py_modules
一个一个指定比较麻烦,特别是模块位于多个包中,那么你可以使用packages
指定整个包,另外只需要另外指定package_dir
,位于distributionroot下的模块文件也可以被处理; - setuptools帮助文档声明,
package_dir
和py_modules
也可以支持分发任何没有包含__init__.py
文件夹下的模块,经测试安装过程没有报错,但是没有包含__init__.py
的文件夹下的模块是没有被正确安装的!因此,如果python模块分布在不同的文件夹,最好是在该文件夹下创建一个__init__.py
文件,以表示它是一个包。
purepythonmodule
举个简单的例子,你需要发布两个模块foo以及bar.bar,以供别人使用(importfoo和importbar.bar)。
其目录树如下:
pure_module
├──bar
│└──bar.py
│└──__init__.py
├──foo.py
└──setup.py
如上图所示,pure_module目录下包含了一个foo模块以及一个bar包,同时在bar包下还包含了一个bar模块。
一个仅使用py_modules
的setup脚本可以这样写:
fromsetuptoolsimportsetup
NAME='foo'
VERSION='1.0'
PY_MODULES=['foo','bar.bar']
setup(name=NAME
,version=VERSION
,py_modules=PY_MODULES)
py_modules指定了foo模块以及bar.bar模块。
通过pythonsetup.pyinstall--user--prefix=
进行安装后,便可以直接通过importfoo和importbar.bar
直接使用了。
- 注意:经测试,如果
.py
文件位于其他文件夹,该文件夹需要创建一个允许为空的__init__.py
文件,表示为一个package,否则安装后不能正常使用其他文件夹的模块。
package
上一节的例子中,bar.bar属于bar包,foo位于distributionroot,安装后属于rootpackage,在Setuptools中""可以用于表示rootpackage。所以下面展示两种setup脚本的写法:
pure_module
├──bar
│└──bar.py
│└──__init__.py
├──foo.py
└──setup.py
仅使用packages
的setup.py文件如下:
fromsetuptoolsimportsetup
NAME='foo'
VERSION='1.0'
PACKAGES=['','bar']
setup(name=NAME
,version=VERSION
,packages=PACKAGES
)
如上,packages包含了package的列表rootpackage以及bar,这样便能轻松覆盖到distributionroot下的foo.py和bar文件夹下的bar.py了,在存在大量模块的情况下,省去像py_modules
一样穷举模块的麻烦。在python中,默认的情况下,包的名字和目录的名字是一致的,比如bar包对应了bar目录,且包的路径表示是相对于distributionroot的(也就是setup.py所在目录)。比如packages=['foo']
,Setuptools会在setup.py所在目录下寻找foo/__init__.py
,并将foo/下的所有模块包含进去。
另外一个关键字是package_dir
,它的作用是将package映射到其他目录,这样的一个好处是方便将package移到其他目录而不用修改packages
的参数值。举个例子,假设我们现在需要把bar移到foobar目录下,按照原来的脚本,Setuptools是无法成功找到bar包的。
package_dir
├──foobar
│├──bar.py
│└──__init__.py
├──foo.py
└──setup.py
通过package_dir=={'bar':'foobar'}
,将原来的barpackage映射到foobar下。完整的脚本如下:
fromsetuptoolsimportsetup
NAME='foo'
VERSION='1.0'
PACKAGE_DIR={'bar':'foobar'}
PACKAGES=['','bar']
setup(name=NAME
,version=VERSION
,package_dir=PACKAGE_DIR
,packages=PACKAGES
)
package_dir是一个字典,它的key是package名(""表示rootpackage),value是相对于distributionroot的目录名。在上面的例子中,package_dir=={'bar':'foobar'}
改变了packages
中package对应的目录位置,这样当Setuptools在找barpackage时,会在foobar目录下找相应的__init__.py
文件。
- 注意,package_dir会影响packages下列出的所有package,比如,
packages=['bar','bar.lib']
,package_dir不仅会影响所有和bar有关的package,bar.lib
也会相应被映射到foorbar.lib
。
extensionmodule
扩展模块需要使用ext_modules
参数。上面所说的package_dir
和packages
都是针对纯python模块,而ext_modules
针对的是使用C/C++底层语言所写的模块。下面举个最简单的例子,扩展模块仅包含一个foo.cpp文件,其中定义了可供python调用的myPrint函数。
#include<iostream>
#include<string>
usingnamespacestd;
voidmyPrint(stringtext)
{
cout<<text<<endl;
}
.
├──setup.py
└──src
├──foo.cpp
├──foo.h
└──PythonWrapAPI.cpp
现在,我们想要发布扩展模块供别人使用我们的myPrint方法。想要python中成功导入你的包,需要利用额外的代码封装将被调用的方法。这里的PythonWrapAPI.cpp的作用就是使用Python提供的库封装你所写的接口,它是处在python和C++间的胶水库,当python调用你的C++方法时,由于语言类型的差别,需要做转换。
PythonWrapAPI.cpp如下:
#include"foo.h"
#include<string>
#include<python2.6/Python.h>
usingnamespacestd;
/*
Notice:PythonInterfaceWrap
*/
staticPyObject*_myPrint(PyObject*self,PyObject*args)
{
char*text;
//解析Python传过来的参数
if(!PyArg_ParseTuple(args,"s",&text))
returnNULL;
myPrint(text);
returnPy_None;
}
staticPyMethodDefExtestMethods[]=
{
{"myPrint",_myPrint,METH_VARARGS},
{NULL,NULL},
};
PyMODINIT_FUNCinitmyprint(void){
(void)Py_InitModule("myprint",ExtestMethods);
}
- wrapper函数_myPrint。它负责将Python的参数转化为C/C++的参数(PyArg_ParseTuple),然后调用实际的myPrint,并处理myPrint的返回值,最终返回给Python环境。需要注意的是,C/C++中无返回值时,不能直接返回NULL,而是需要返回Py_None。
- 导出表ExtestMethods。它负责告诉Python这个模块里有哪些函数可以被Python调用。导出表的名字可以随便起,每一项有4个参数:第一个参数是提供给Python环境的函数名称,第二个参数是_myPrint,即wrapper函数。第三个参数的含义是参数变长,第四个参数是一个说明性的字符串。导出表总是以{NULL,NULL,0,NULL}结束。说明,第3和4个参数可以省略。
- 导出函数initmyprint。这个的名字不是任取的,是你的module名称添加前缀init。导出函数中将模块名称与导出表进行连接。
扩展模块和纯python模块有点不太一样,我们导入的package名字与setup()的packages
或者package_dir
参数是一致的,但是扩展模块的名字是由Extension
实例的name
参数决定的,且需要和导出函数对应的initxxx名字以及Py_InitModule
方法对应的第一个参数相同。
最后,我们需要编写setup脚本编译我们的cpp文件为so动态链接库,并进行相应的封装。运行pythonsetup.pyinstall
,setuptools会帮我们自动编译。
fromsetuptoolsimportsetup,Extension,find_packages
#packagename,importNAME
NAME="foo"
VERSION='1.0.0'
#anExtensioninstancelist
EXT_MODULES=[
Extension(
name='myprint'
,sources=['src/foo.cpp','src/PythonWrapAPI.cpp']
,include_dirs=['src']
)
]
setup(name=NAME
,version=VERSION
,ext_modules=EXT_MODULES
,)
ext_modules
是一个Extension
实例列表,Extension的参数sources用于指定所有源文件位置,include_dirs指定头文件位置,同时还可以使用library_dirs和libraries指定外部链接库,以及extra_compile_args指定额外的编译参数。
package元信息参数
在编写一个package的时候,尽量提供更多的元信息,这样使用者更加能够了解到package的相关信息,并且有些信息会被PyPi使用。
- 必填
- 如果为了兼容2.2.3或2.3版本,不建议使用此字段
- 如果提供了maintainer,那么distutils会将其加入PKG-INFO
package内容参数
py_modules列举每个模块
py_modules是一个字符串列表,用于指定所有的模块,即py文件模块。如果你只是发布几个脚本文件而已,特别是它们逻辑上不属于同一个package。
比如,py_modules=['mod1','pkg.mod2']
。这指定了两个模块,一个位于rootpackage
,而另一个位于pkgpackage
。如果没有使用package_dir
重新映射package和目录的关系的话,那么这两个模块分别对应了mod1.py
以及pkg/mod2.py
文件,并且在pkg文件夹下还存在__init__.py
文件。
package列举每个包
如果你需要发布的模块文件太多,使用py_modules
一个一个指定比较麻烦,特别是模块位于多个包中,那么你可以使用packages
指定整个包。
packages是一个包名列表,packages参数告诉Setuptools处理列举出的package下所有纯python模块。在文件系统中,默认地,package的名字与目录是一一对应的,也就是说,packages=['foo']
,Setuptools会去查找foo/__init__.py
文件。
package_dir重新映射package和目录的关系
当你想要重命名你的package所在的文件夹,或者想要移动整个package到其他目录下,一般情况下,一旦你的源代码布局改变,你需要重新修改packages。但是package_dir可以重新映射package和目录的关系。比如你将rootpackage下的模块和package移到lib目录下,那么你只需要在package_dir中将rootpackage映射到lib下。比如package_dir={'':'lib'}
再举个例子,比如一下目录结果,当我使用package_dir={'bar':'foobar'}
和packages=['bar']时,Setuptools根据packages
参数查找barpackage时,会在foobar文件夹下找相应的__init__.py
文件。
package_dir
├──foobar
│├──bar.py
│└──__init__.py
└──setup.py
- 注意,package_dir会影响packages下列出的所有package,比如,
packages=['bar','bar.lib']
,package_dir不仅会影响所有和bar有关的package,bar.lib
也会相应被映射到foorbar.lib
。
install_requires和dependency_links安装依赖模块
install_requires
可以声明package安装时所需的依赖模块及其版本。安装package时,Setuptools便能够从PyPi上自动下载其所依赖的模块,并且将依赖信息包含进PythonEggs中。
比如我们在自己的package中用到了一个python非标准库pycurl和xmltodict,当我们的package在别的机器上使用时便会报错。为了解决这个问题,我们可以使用install_requires=['pycurl','xmltodict']
将pycurl和xmltodict加入package依赖。
install_requires
可以是string或stringlist,指明所需要依赖的模块。当install_requires
为string类型,并且依赖多于1个时,每个依赖的声明都要另起一行。
最新版本的Setuptools的install_requires
有另外两个作用:
- 在运行时,任何脚本都会检查其依赖模块的正确性,并且确保正确的依赖版本都加入到sys.path中(假如有多个版本的话)。
- PythonEggdistributions将会包含依赖相关的元信息。
前面说到,Setuptools便能够从PyPi上自动下载其所依赖的模块,但是在某些环境下无法正常访问Pypi下,我们也可以通过dependency_links
参数指定到自己的python源,这样便可以解决下载问题。
比如,dependency_links=['http://xxx/xmltodict','http://xxx/pycurl']
。
dependency_links是一个字符串列表,包含了依赖的下载URL。
Setuptools对链接的支持比较强大!
下载的资源可以满足以下条件:
- 通过pythonsetup.pysdist进行分发的压缩文件,默认情况下在linux为.tar.gz,在windows为zip
- 单一的py文件
- VCS仓库(Subversion,Mercurial,Git)
URL链接可以是:
- 可以直接下载的URL
- 包含资源下载链接的网页URL
- 仓库URL
当包含资源下载链接的网页URL中存在多个版本时,Setuptools会根据版本要求下载合适的版本。
一般,比较好的方式是网页URL方式。我们也可以使用SourceForge的showfiles.php链接来下载我们所依赖的模块。
如果依赖的模块是一个py文件时,你必须在URL添加"#egg=project-version"
后缀,以指出模块的名字和版本,另外需要确保将模块名和版本中出现的-
替换为_
。EasyInstall将会识别这个后缀并且自动创建一个setup.py脚本,将我们的py文件包装为egg文件。如果为VCS,将会checkout对应的版本,创建一个临时文件夹并执行setup.pybdist_egg,安装所需的依赖。
在使用VCS的情况下,你也可以使用#egg=project-version
指定要使用的版本。你可以通过在#egg=project-version
前加入@REV
来指定要checkout的版本。另外你也可以通过在URL前加上以下标识显式声明URL使用的是VCS:
- Subversion:
svn+URL
- Git:
git+URL
- Mercurial:
hg+URL
因此使用VCS更复杂的一个示例为:vcs+proto://host/path@revision#egg=project-version
ext_modulePython调用C/C++
Python的可扩展性特别强,不仅支持python语言的扩展模块,而且支持其他语言的扩展。
Python调用C++的详细文档可以查看https://docs.python.org/2/extending/building.html
这里假设已经懂得怎么调用C++方法了,接下来只需要使用ext_module参数,使Setuptools能够编译和安装扩展模块了。
ext_module
参数是一个Extension实例列表,Extension类似于gcc/g++的所需参数,包含了指定源文件、头文件、依赖的静态库或动态库、额外的编译参数、宏定义等功能。
name扩展模块名字
name
是一个字符串,用于指定扩展模块的名字。
packages
和package_dir
用于支持python语言编写模块,其import语句使用的包名与packages
和package_dir
中所指定的名字是一致的。但是扩展模块的名字是由Extension
实例的name
参数决定的,且需要和导出函数对应的initxxx名字以及Py_InitModule
方法对应的第一个参数相同。定义好模块的名字xxx后,我们便可以使用importxxx
使用我们自己的模块了。
sources和include_dirs
sources
为用于指定要编译源文件的字符串列表,比如,sources=['foo/foo.cpp','bar/bar.cpp']
,Setuptools支持C/C++以及Objective-C。
include_dirs
为用于指定编译需要的头文件目录的字符串列表,比如,include_dirs=['foo/include','bar/include']
。如果头文件位于distributionroot目录,需要使用'.'
表示头文件位于当前目录,不能为''
,否则将找不到头文件。
另外还支持extra_objects
向链接程序传递object文件,比如.o
文件。
define_macros和undef_macros
gcc支持在编译的时候定义新的宏变量和取消某个宏变量的定义,具体的选项[-Dmacro[=defn]...][-Umacro]
。Extension也支持这样的选项。
你可以使用define_macros
和undef_macros
定义新的宏变量和取消某个宏变量的定义。
define_macros
是一个(name,value)
元组列表,其中的name为宏变量名字符串,value为对应的值,可以为字符串、数字或为None类型(说明:官方文档没有声明value
可以为数字,但是经过测试,只要是python支持的数字类型都可以用于value
,但是最好还是使用字符串的形式,这样脚本的兼容性会更好).
比如,define_macros=[('DEBUG',None),('FOO','1'),('BAR',2),('FOOBAR','"abc"')]
,gcc对应的编译选项结果为-DDEBUG-DFOO=1-DBAR=2-DFOOBAR="abc"
。
undef_macros
比define_macros
简单得多,它就是一个宏变量字符串列表,举个例子,我们想要取消以上定义的宏变量,对应的undef_macros值为undef_macros=['DEBUG','FOO','BAR','FOOBAR']
。
libraries和library_dirs
Setuptools对C/C++库的引用方法和gcc一样,具体的规则可以参考gcc。
libraries为要添加的库的名字字符串列表,而library_dirs为要添加的库所在的目录,举个例子:
.
├──setup.py
└──curl
├──include
├──curl.h
├──test.h
├──lib
├──libcurl.a
├──libtest.a
其对应的参数为libraries=['curl','test']
、library_dirs=['curl/lib']
、include_dirs=[‘curl/include’]
注意:在实际的使用过程中碰到过一个链接错误的坑,Setuptools在编译的时候报错:
libcurl.a:relocationagainst.rodatacannotbeusedwhenmakingasharedobject:recompilewith-fPIC
libcurl.a:couldnotreadsymbols:Badvalue
前面提到,python在创建扩展模块时会将源文件编译为动态链接库,动态链接库在加载的时候,内存位置是不固定的,所以我们链接的外部库代码也需要全部使用相对地址,这样代码便可以加载到内存的任意位置。因为有的库没有使用-fPIC
选项进行编译,导致库最终在链接到so文件时报错。
解决方案是使用-fPIC
重新编译libcurl.a
库。
extra_compile_args
在编译扩展模块时,Setuptools会自动指定编译参数,比如下面一个模块的编译:
gcc-pthread-fno-strict-aliasing-O2-g-pipe-Wall-Wp,-D_FORTIFY_SOURCE=2-fexceptions-fstack-protector--param=ssp-buffer-size=4-m64-mtune=generic-D_GNU_SOURCE-fPIC-fwrapv-DNDEBUG-O2-g-pipe-Wall-Wp,-D_FORTIFY_SOURCE=2-fexceptions-fstack-protector--param=ssp-buffer-size=4-m64-mtune=generic-D_GNU_SOURCE-fPIC-fwrapv-fPIC-DDEBUG-DFOO=1-DBAR=2-DFOOBAR="abc"-Isrc-I/usr/include/python2.6-csrc/foo.cpp-obuild/temp.linux-x86_64-2.6/src/foo.o
gcc-pthread-fno-strict-aliasing-O2-g-pipe-Wall-Wp,-D_FORTIFY_SOURCE=2-fexceptions-fstack-protector--param=ssp-buffer-size=4-m64-mtune=generic-D_GNU_SOURCE-fPIC-fwrapv-DNDEBUG-O2-g-pipe-Wall-Wp,-D_FORTIFY_SOURCE=2-fexceptions-fstack-protector--param=ssp-buffer-size=4-m64-mtune=generic-D_GNU_SOURCE-fPIC-fwrapv-fPIC-DDEBUG-DFOO=1-DBAR=2-DFOOBAR="abc"-Isrc-I/usr/include/python2.6-csrc/PythonWrapAPI.cpp-obuild/temp.linux-x86_64-2.6/src/PythonWrapAPI.o
g++-pthread-sharedbuild/temp.linux-x86_64-2.6/src/foo.obuild/temp.linux-x86_64-2.6/src/PythonWrapAPI.o-L/usr/lib64-lpython2.6-obuild/lib.linux-x86_64-2.6/myprint.so
这么多的编译参数绝大部分是Setuptools自动指定的,但是如果我们还想要在每个文件的编译再加上额外的编译选项,可以使用extra_compile_args
和extra_link_args
,其中extra_link_args
选项用于链接。
extra_compile_args
是一个编译选项字符串列表,每个编译选项都要单独作为一个字符串,不能并在一起,否则会报错。
创建源码分发
建议使用源码分发的形式发布你的包,而不是二进制发布形式,这样包将更方便跨平台。
sdist命令
创建源码分发的命令为:pythonsetup.pysdist
,命令执行后会创建dist目录,收集一些必要的文件以及setup脚本,生成一个压缩文件,用户安装时,只需要解压,然后执行pythonsetup.pyinstall
命令,将进行编译和安装,将相应的文件存放到python第三方库目录下。
sdist比较常用的一个选项是--format
,选择压缩的格式。比如,使用zip进行压缩,pythonsetup.pysdist--format=zip
。
说明:pythonsetup.pysdist--format=zip,tar
,Setuptools会分别使用zip和tar进行压缩,将同时产生两个压缩文件。
setuptools和distutils对于文件查找的算法是一样的:
- 所有在
py_modules
和packages
指定的对应模块文件 - 所有在
ext_modules
和libraries
选项指定的源文件和库 scripts
选项指定的脚本文件- 所有类似测试脚本的文件,比如:test/test*.py(低版本的包管理工具可能不支持)
- README.txt(或README),setup.py以及setup.cfg(README文件目前无法支持更多的后缀格式)
package_data
选项指定的文件data_files
选项指定的文件
另外在使用过程中,遇到Setuptools的一个巨坑,确实可以包含文件,但是它并不总能包含文件,这是有前提的。
bdist是发布二进制文件,sdist是发布源文件。而在旧版本的python中(2.7以前),package_data只有在使用bdist时候才有用,也就是如果使用sdist,是无法正确包含文件的。而在新版本中,会自动把package_data里面的内容添加到MANIFEST文件中。
MANIFEST.in模版文件
当我们使用sdist进行分发包时,如果需要包含额外的文件,可以使用MANIFEST.in
文件,在该文件中列举出需要包含的文件。当我们执行sdist时,将会对MANIFEST.in
文件进行检查,读取解释并生成MANIFEST文件,该文件列举了所有需要包含进包的文件。位于distributionroot下的MANIFEST.in文件每行对应一条包含一系列文件的命令。
本文内容总结:setuptools和setup.py,你所需要做的事&一些概念,基础概念,关于源码分发文件和二进制分发文件,示例和分发选择,purepythonmodule,package,extensionmodule,package元信息参数,package内容参数,py_modules列举每个模块,package列举每个包,package_dir重新映射package和目录的关系,install_requires和dependency_links安装依赖模块,ext_modulePython调用C/C++,name扩展模块名字,sources和include_dirs,define_macros和undef_macros,libraries和library_dirs,extra_compile_args,创建源码分发,sdist命令,MANIFEST.in模版文件,
原文链接:https://www.cnblogs.com/cposture/p/9029023.html