简介:这是一份基于FME的Shapefile转KML工具包,面向GIS数据处理人员与需要对竣工图、地块等空间数据做轻量可视化标注的开发者。该资源可解决shp格式数据无法直接在地图平台中展示名称标签的问题,通过内置模板一键完成格式转换与名称标注,适用于FME入门练习及日常项目中的批量出图场景。压缩包共11个文件,约125KB,主要包含FME工作空间模板(fme/fmw)、Shapefile基础组件(shp、dbf、shx、prj、sbn/sbx)、运行日志、字段属性要求示例图片及xml辅助文件,结构完整便于对照使用。目前已有2022人学习下载。使用者可直接获得可运行的转换工具、配套的模板测试数据以及输入字段属性说明,省去自行搭建FME流程的时间,尤其适合需要将shp数据快速转为带标注KML的工程项目。
1. shp转KML带名称标注:为什么你的转换结果总是“哑巴”?
一个常见场景:你手里有一份从某自然资源平台导出的shp数据,在ArcGIS里看属性表一切正常,但一旦用默认工具转成KML,在Google Earth里打开,图斑是有的,可点上去没有任何地名、地类信息,标签也不显示。这不是KML不支持文字,而是你只转了几何,没转属性,更没做标注映射。shp转kml虽然只隔一层格式,但“含名称标注”和“裸转”完全是两个工作量级。这篇笔记把这套转换拆开讲透,适合测绘、规划、GIS开发岗的从业者,也适合被甲方要求在Google Earth里汇报的兄弟。我们用FME为主方案,同时给出GDAL/Python的轻量替代。
2. 转换原理与工具选型:FME、GDAL与QGIS三选一
2.1 shp与KML的底层数据结构差异
shp(Shapefile)本质上不是一个单一文件,而是由.shp、.dbf、.prj、.shx等若干文件组成。几何坐标在.shp中,属性表在.dbf中,坐标系写在.prj中。KML则是一个基于XML的单文件格式,几何用<Placemark>包裹,属性靠<ExtendedData>承载,显示名称用<name>标签。也就是说,shp转KML时,如果不把.dbf里某个字段映射到<name>,结果就只剩一堆坐标点。很多新手第一次转KML会问“为什么我转出来没有名字?”就是这个原因。
实际处理时,你还会发现,从不同平台下载的省市shp,字段名五花八门,有的叫NAME,有的叫地名,有的根本没有名字字段只有ID。如果你机械地使用默认参数,KML的name就会变成数字编号,或者干脆空着。理解这一点后,你就会明白“名称标注”不是格式本身的功能,而是转换前需要处理的数据映射问题。
我截一段典型KML内容给你看,你就能直观感受到<name>和<ExtendedData>的关系:
<Placemark> <name>某公园</name> <ExtendedData> <Data name="地类"><value>A</value></Data> <Data name="面积"><value>12.35</value></Data> </ExtendedData> <Polygon> <outerBoundaryIs> <LinearRing> <coordinates>104.1,30.6 104.2,30.6 104.2,30.7 104.1,30.7 104.1,30.6</coordinates> </LinearRing> </outerBoundaryIs> </Polygon> </Placemark>这里的<name>决定了你在Google Earth里看到的地标名称,<ExtendedData>里的Data项则会在点击地标时展示为属性卡片。如果你转换出来的KML里<name>是空的,问题一定出在属性映射,而不是KML本身不支持。坐标和属性是两回事,shp转kml虽然把两者都带出来了,但使用什么字段作为名称、哪些字段进入ExtendedData,必须由转换工具或脚本显式指定。
另外,坐标系统差异也会影响KML的展示。国内很多shp用的是CGCS2000或1980西安坐标系,而Google Earth底层是WGS84坐标。如果直接硬转,图斑可能漂移到海里或偏离街道几百米。这部分细节放在第5章避坑里详细说,这里先记住:转换前必须知道源数据的坐标系,并且优先统一到WGS84再输出KML。
2.2 为什么我最终选了FME做这个转换
工具选择上,常见的有FME、GDAL/ogr2ogr、QGIS另存为。我个人的习惯是FME作为主方案,GDAL作为备选和批处理底层。原因有三:第一,FME的可视化流程让我能直观看到每一步的数据流,字段映射错了马上能发现;第二,FME对中文属性、复杂嵌套属性、坐标系转换都有成熟的处理模块;第三,FME写出的KML自带样式控制,不用手动去改XML。
GDAL的优势在于免费、脚本化、无界面依赖,非常适合放在服务端定时任务里。但它的命令行参数比较隐晦,比如-dsco NameField这个选项,如果不知道,你就只能得到默认的字母数字地标名。QGIS虽然免费,但批量处理和样式控制上不如FME灵活,特别是你要在同一个工作空间里对多个图层做不同名称字段映射时,QGIS的“另存为”每次都要重复设置。
下面是一个简单的工具对比,方便你快速决策:
| 工具 | 学习成本 | 批量能力 | 中文支持 | 样式控制 | 授权成本 |
|---|---|---|---|---|---|
| FME | 中 | 强 | 好 | 强 | 商业 |
| GDAL/ogr2ogr | 中 | 强 | 需注意编码 | 弱 | 开源 |
| QGIS | 低 | 弱 | 好 | 中 | 开源 |
如果你是经常处理各种格式的从业者,建议值得用FME;如果你只是偶尔转一次,或者要集成到后端管道,GDAL更合适。这份资源里,工作空间模板用FME做的,脚本用GDAL/Python做的,两端都有得用。
FME中我们常用的转换器组合是:读模块选择ESRI Shapefile,写模块选择Google KML,中间用AttributeManager做字段重命名,再用FeatureHolder? 不需要。坐标转换用CsmapReprojector,编码问题在读模块参数里解决。之所以这么组合,是因为FME里面处理<name>字段不是靠猜,而是靠写模块参数里的“名称字段”下拉框。这个下拉框只有在读模块完成属性读取后才会出现,所以搭建工作空间时流程顺序不能乱。
2.3 用GDAL实现最简转换(代码)
在讲FME之前,先看命令行有多简单。我用一个最典型的命令举例:
ogr2ogr -f KML output.kml input.shp -dsco NameField=地类名称-f KML指定输出格式为KML,-dsco NameField=地类名称意思是把源字段地类名称的值写入KML的<name>标签。如果源字段是英文NAME,那就对应改过来。默认情况下ogr2ogr会把第一个字符串字段作为名称,但国内shp的第一个字段往往是ID或OBJECTID,所以你需要显式指定。
如果你从第三方平台下载的shp是GBK编码,而你的命令在没有设置环境中直接跑,中文很容易变成问号。解决方式是在当前终端设置环境变量,或者用Python包装:
export SHAPE_ENCODING=GBK ogr2ogr -f KML output.kml input.shp -dsco NameField=地类名称逻辑说明:SHAPE_ENCODING是GDAL读取shp的dbf属性时使用的编码。KML输出端默认是UTF-8,所以只要读取端正确,写入端不会再乱。参数说明:这个环境变量只影响shp驱动,不影响其他矢量格式;如果你确定源数据是UTF-8,可以不要设置。
用这种命令转出来的KML,每个要素会有一个<name>,但样式是默认的,颜色、透明度、图标都要后续手动编辑XML。所以GDAL适合“能用”,不适合“好看”。如果你想做出能在汇报里直接用的KML,还是得靠FME或后期脚本清洗样式。
这里还有一个隐藏参数:-dsco DescriptionField。默认情况下,GDAL会把所有属性打包到Description字段里,KML里会生成一大段HTML表格。这在某些场景下挺好用,但如果你的属性里有超长字符串,KML文件会迅速膨胀。我的做法是用-dsco DescriptionField=none关掉这个行为,只保留name字段。命令变成:
ogr2ogr -f KML output.kml input.shp -dsco NameField=地类名称 -dsco DescriptionField=none逻辑说明:DescriptionField=none告诉KML驱动不要为每个要素生成详细的描述HTML,这样文件更小,打开更快。参数说明:如果你的业务要求点击要素时看到完整属性,就不要加这个参数,或者把特定字段指定为描述字段。
3. 用FME实现shp转kml并保留名称标注:逐步拆解
3.1 工作空间搭建:读模块与写模块配置
在FME中新建工作空间,我一般从“生成工作空间”向导开始。读模块选择ESRI Shapefile,浏览到你的shp文件。注意,这里有一个隐藏很深的选项叫“字符编码”,在读模块参数里。国内来的数据大多是GBK或GB18030,而FME默认按UTF-8读取,结果就是属性表里中文全部变成乱码。你需要在读模块参数里手动把“字符编码”改为GBK或者系统默认,再点击确定。
写模块选择Google KML。输出数据集直接填写目标kml文件路径。关键点是写模块参数中的“文档属性”,里面有一个“名称字段”下拉框。这个下拉框会列出源shp的所有属性字段,你选择地类名称或NAME。这一选,FME就会自动把该字段的值写入KML的<name>标签。如果下拉框是空的,说明读模块没有正确读到属性,或者编码有问题,先回去修读模块。
为了保险起见,我会在读写中间加一个AttributeManager转换器,把源字段统一重命名为name。这样做的好处是,即使你后面把写模块换成其他格式,比如转3dtiles或WKT,字段映射逻辑也不会乱。具体配置:在AttributeManager里选择源字段“地类名称”,在“输出属性”列填name,其他字段保持原样。
还有一个容易忽略的点:FME中KML写模块默认会输出两个图层,一个点/线/面图层,一个标签图层。如果你只需要地标名称,不需要单独的文字标签图层,可以在写模块参数里关闭“标签”生成。否则输出的KML里会多出一堆以_label结尾的Placemark,谷歌地球里会看到重复的名称标注。
我这里给出一组常用参数表,你在做的时候可以直接对照:
| 参数项 | 推荐值 | 说明 |
|---|---|---|
| 读模块字符编码 | GBK或系统默认 | 避免中文乱码 |
| 写模块名称字段 | name 或源名称字段 | 决定<name>内容 |
| 坐标转换 | 自动转换为WGS84 | 谷歌地球需要 |
| 描述字段 | 关闭或指定字段 | 控制KML体积 |
| 标签生成 | 关闭 | 避免重复标注 |
3.2 名称标注是怎么进KML的:属性映射与标签设置
也许你会问,直接用FME的“设置名称字段”不就行了吗?为什么还要处理?因为实战中,源数据的名称往往不是单一字段,而是分散在多个字段里的。比如某规划项目里,地块名称在一个字段NAME,行政区名称在XZQ,需要把两者拼接成“某区-某地块”才能作为标注。这种情况下,靠下拉框选一个字段就不够了。
我一般会在读模块后用AttributeCreator创建新字段name,公式写成类似:
@Value(XZQ)-@Value(NAME)这样生成的name就包含了层级信息,KML里名称标注更完整。注意,如果某些要素的NAME字段为空,最终name会变成“某区-”这种半截字符串。需要加一个条件判断:如果NAME为空,则只使用XZQ。在FME里可以用AttributeCreator的条件逻辑实现,或者用PythonCaller做更灵活的字符串处理。
如果你已经安装了FME的Python环境,可以使用PythonCaller来统一处理名称。下面是一个最小示例:
# 在FME的PythonCaller中处理中文字段映射 import fmeobjects class FeatureProcessor: def __init__(self): pass def input(self, feature): name = feature.getAttribute('XZQ') sub = feature.getAttribute('NAME') if sub: name = name + '-' + sub if name: feature.setAttribute('name', name) self.pyoutput(feature)逻辑说明:这个脚本读取XZQ和NAME两个字段,拼接成一个name字段,供KML写模块使用。FME内部使用的是Unicode字符串,拼接时不需要额外编码处理。参数说明:如果源字段名是英文或包含空格,getAttribute的写法要完全一致,否则拿到的是None。
拼接完成后的name,在写模块里始终映射到KML的<name>标签。如果你还想在Google Earth里看到标签文字直接显示在地物旁边,而不是鼠标悬停才出现,需要在写模块的“样式/注释”部分打开“显示标签”,并把标签大小、颜色、透明度设置好。这一点很多从事GIS的兄弟会忽略,结果转出来后只有点击图斑才显示名称,汇报时客户对着屏幕一脸迷茫。
3.3 样式与图标:让KML在Google Earth里不裸奔
裸转出来的KML没有样式,图斑在Google Earth里是默认的黄颜色或半透明填充,看起来像一堆半成品。如果你是要拿给甲方看,必须处理样式。FME的KML写模块支持在写模块参数里设置图层级样式,或者通过Style转换器给每个要素添加样式信息。
我通常的做法是:在工作空间中加入一个KMLStyleSetter转换器,对点、线、面分别设定颜色、线宽、透明度。比如面要素设置为浅蓝色填充、透明度0.5、蓝色边界线;点要素设置一个圆形地标图标;线要素设置红色实线,线宽2像素。KML的颜色格式是ABGR十六进制,不是常见的RGB,这点特别容易记混。比如ff0000ff是纯红色,但实际写出来是Alpha=ff、Blue=00、Green=00、Red=ff,也就是调整字节顺序。
如果你希望一个图层的所有要素共享同一个样式,可以在FME里对每个要素添加一个公共的kmldocstyle标签,或者用写模块的“图层名”参数统一设置。共享样式的好处是KML文件体积小,打开流畅。如果每个要素都单独内嵌样式,文件会膨胀几十倍,特别是大块用地的数据。
点要素的名称标注位置也是一个坑。<name>标签默认显示在几何点的上方,但如果点密度高,名称会互相遮挡。FME里可以用LabelPointReplacer或者偏移设置,让标注自动避让。不过Google Earth并没有真正的避让算法,只能通过调节摆放位置缓解。我的经验是:点要素转KML时,给名称加一个小的偏移向量,比如南北方向偏移0.0002度,能减少叠加概率。
4. 用Python/GDAL实现轻量级转换:不带FME也能干
4.1 ogr2ogr命令深挖:NameField之外还有哪些细节
第2章里的最简命令只是开胃菜。实际工作中,你很可能在一个没有FME授权的服务器上处理数据,这时候Python+GDAL就是救命稻草。ogr2ogr除了NameField,还有几个值得关注的参数。
第一个是-t_srs,用来做坐标系转换。如果源shp是CGCS2000,可以直接在命令里指定目标坐标系:
ogr2ogr -f KML output.kml input.shp -dsco NameField=地类名称 -t_srs EPSG:4326-t_srs EPSG:4326把输出坐标统一成WGS84经纬度。这个操作很重要,因为CGCS2000和WGS84在平面坐标上数值差异是米级,但KML要求经纬度,如果不转,Google Earth会完全不识别,图斑飞到哪里都不知道。注意,只改变数字范围是不够的,必须通过正确的投影变换,否则面积和位置都会失真。
第二个是-lco与-dsco的区别。很多人混用这两个参数,其实-lco是图层创建选项,-dsco是数据源创建选项。KML驱动中NameField属于数据源创建选项,所以用-dsco而不是-lco。如果你写错了,命令不会报错,但参数不生效,名称标注还是老样子。
第三个是-skipfailures。如果某个要素几何非法,整个转换会中断。加上这个参数,跳过错误要素继续转其他要素。我的建议是调试阶段先不添加,让它暴露问题;生产环境再加,避免半夜任务崩溃。
4.2 Python脚本实现名称自动落到地标名
如果想在批处理场景里动态指定名称字段,用Python包一层会更灵活。下面是一个基础脚本,它调用ogr2ogr子进程,并统一设置环境编码:
# -*- coding: utf-8 -*- # 用subprocess包装ogr2ogr,支持动态指定名称字段 import subprocess import os src_file = 'd:/data/地块.shp' dst_file = 'd:/data/地块.kml' name_field = '地类名称' cmd = [ 'ogr2ogr', '-f', 'KML', dst_file, src_file, '-dsco', 'NameField=' + name_field, '-t_srs', 'EPSG:4326', ] env = os.environ.copy() # 强制shp属性按GBK读取 env['SHAPE_ENCODING'] = 'GBK' subprocess.run(cmd, env=env, check=True)逻辑说明:subprocess.run会在Python进程里启动ogr2ogr命令,env=env把当前环境变量复制出去,并额外设置SHAPE_ENCODING为GBK。这样做的好处是,你不用手动在终端导变量,脚本放到任何机器上都能保证读取编码一致。参数说明:check=True表示如果ogr2ogr返回非零退出码,脚本直接抛异常,方便你捕获错误。
如果你不想依赖子进程,希望完全用Python库操作,可以使用osgeo.ogr直接读写。核心代码段是:
# 用ogr库直接在内存中处理要素并写出KML from osgeo import ogr src_ds = ogr.Open('地块.shp') src_layer = src_ds.GetLayer(0) name_field = '地类名称' # 创建KML数据源 drv = ogr.GetDriverByName('KML') if os.path.exists('地块.kml'): drv.DeleteDataSource('地块.kml') dst_ds = drv.CreateDataSource('地块.kml') dst_layer = dst_ds.CreateLayer('地块', geom_type=ogr.wkbUnknown, options=['NameField=name']) # 先创建name字段,再把其他字段复制过去 name_defn = ogr.FieldDefn('name', ogr.OFTString) dst_layer.CreateField(name_defn) # 复制源字段定义 src_defn = src_layer.GetLayerDefn() for i in range(src_defn.GetFieldCount()): field_defn = src_defn.GetFieldDefn(i) if field_defn.GetName() != name_field: dst_layer.CreateField(field_defn) src_layer.ResetReading() feat = src_layer.GetNextFeature() while feat: new_feat = ogr.Feature(dst_layer.GetLayerDefn()) new_feat.SetGeometry(feat.GetGeometryRef().Clone()) # 设置name name_value = feat.GetField(name_field) or '' new_feat.SetField('name', name_value) # 复制其他字段 for i in range(src_defn.GetFieldCount()): src_fname = src_defn.GetFieldDefn(i).GetName() if src_fname != name_field: new_feat.SetField(src_fname, feat.GetField(i)) dst_layer.CreateFeature(new_feat) feat = src_layer.GetNextFeature()逻辑说明:第一步创建KML数据源和图层,指定NameField=name,这样KML驱动会把要素的name字段自动写入<name>标签。第二步创建name字段并填充源字段值,其他属性原样保留,会自动进入ExtendedData。参数说明:ogr.GetDriverByName('KML')必须确认你的GDAL编译包含KML驱动,否则返回空指针;另外,源字段名如果是中文,在创建字段时理论上没问题,但如果遇到老版本GDAL可能出现字符集问题,最好把字段名统一改成拼音或英文。
4.3 批处理:多文件循环与编码处理
如果你有一整个目录的shp要转,脚本就更有价值了。我的习惯是用glob.glob读取目标目录下所有.shp文件,然后在循环里拼接命令。注意,一个坑是文件名可能包含空格或中文,在子进程命令里不能直接字符串拼接,必须用参数列表形式传给subprocess,避免shell转义问题。
另一个坑是输出文件重命名。shp文件名是“地块.shp”,KML输出如果是“地块.kml”,同一个文件夹里源和目标混在一起,后续再次扫描目录时会把KML也当成要处理的对象。我通常会建一个独立的输出目录,比如kml_output。
编码处理上,我建议把源文件的编码检测放在循环外,避免每个文件都重复检测。大多数数据是GBK,少数是UTF-8。最稳妥的方式是写一个函数,尝试用utf-8解码属性,抛异常就改回GBK。但ogr2ogr的SHAPE_ENCODING环境变量只接受一个值,无法逐文件设置。这种情况可以每个文件单独设置环境变量,或者直接用osgeo.ogr库在内存中做,因为那样可以逐图层设置SetField。
我一般会在批处理脚本里增加一个信息输出,打印每个文件的要素数、名称字段非空数量。这样转完后可以快速检查哪些文件名称标注丢失。例如:
地块.shp -> 地块.kml 要素数 128 名称非空 120这行日志看起来简单,但排查问题时能省大量时间。
5. 避坑指南:shp转KML常见的五个坑
5.1 中文乱码:源数据编码不一致
现象:转换后KML在Google Earth里打开,<name>显示成问号或方块,属性表里的中文也全是乱码。 原因:shp的dbf文件存储属性时,本身不强制使用哪种编码。国内传统工具如CAD、部分GIS软件导出时用的是GBK,而GDAL/FME默认按UTF-8读取,导致字节流解析错误。 解决:在读模块或环境变量中明确指定编码。FME里在读模块参数修改“字符编码”为GBK;GDAL里设置SHAPE_ENCODING=GBK。如果数据来源是ArcGIS导出的UTF-8,那就不用改。判断方法是:在ArcGIS属性表里选中一个中文要素,看导出后的dbf字节;或者直接用文本编辑器打开dbf,无效。我自己的习惯是,拿到数据先看.prj和元数据说明,再决定编码。
5.2 坐标漂移:坐标系不一致
现象:图斑在Google Earth里位置差几百米,或者完全落在海里。 原因:源shp是CGCS2000投影坐标或1980西安坐标系,而KML必须使用WGS84经纬度。直接转换仅改变坐标格式,不做投影变换,坐标数值被当作经纬度解释,自然偏移。 解决:在转换命令或FME里必须做坐标系转换。GDAL用-t_srs EPSG:4326,FME用CsmapReprojector或写模块自动转换。特别注意,如果源shp没有.prj文件,GDAL会默认猜测为未知坐标系导致转换失败,你需要手动设置源坐标系-s_srs EPSG:4490之类的参数。
5.3 名称字段丢失:属性映射没有生效
现象:转换后的KML没有<name>,或者全是None。 原因:最常见的是写模块名称字段选错,或者源字段名中包含空格、小写字母,导致映射失效。另一个原因是,数据本身存在空值,所有要素的名称字段都是空。 解决:先用QGIS或Python读取属性表,确认字段名和值。然后在FME中用AttributeManager统一重命名为name,并增加一个条件判断,如果为空则用ID兜底。用GDAL时,检查-dsco NameField=字段名是否和实际字段名完全一致,包括大小写。
5.4 KML文件过大:几何细节过多导致卡顿
现象:一个面图层转换出来的KML有几十MB,Google Earth打开要卡几秒,甚至直接崩溃。 原因:shp里的几何坐标精度过高,或者图斑边界非常复杂,KML会保留全部坐标点。 解决:在转换前用FME的Generalizer或GDAL的-simplify参数对几何做抽稀。例如GDAL命令ogr2ogr -simplify 0.0001表示经纬度容差为0.0001度。注意,抽稀会改变几何面积,如果业务有面积精度要求,需要先计算原始面积,再决定容差。另外,关闭DescriptionField也可以大幅减小体积,因为每个要素的HTML描述占了很多字节。
5.5 名称标注错位:点要素与面要素的处理差异
现象:面要素转出的KML,标注位置跑到图斑中心点,但多个面靠得近时标签互相重叠;点要素则出现标签偏离点位很远的情况。 原因:KML对名称的默认锚点是几何的“中心点”或“起点”,对于不规则多边形,中心点可能落在外部;点要素如果标了图标,锚点可能是图标底部,导致标签偏移。 解决:建议在转换前对面要素提取真正的内部点(PointOnAreaOverlayer),用它作为名称标注锚点。点要素则在FME里设置LabelPoint或偏移量。如果你用文本编辑器直接修改KML,也可以在<name>内嵌入坐标偏移,但不建议手工改大量文件。
6. 验证与进阶:从“能转”到“转得好”的三个技巧
6.1 用Google Earth验证:不只是点开看
转完KML后,别只满足于能打开。我会做三件事:先看总文件大小,超过5MB就考虑抽稀和精简;再随机抽取5个要素,对比源shp属性表中的名称和面积是否一致;最后把KML缩放到市区尺度,看标注是否互相覆盖。这些检查基本能在两分钟内发现问题。
6.2 进阶:用KML的ExtendedData保留属性
如果你不想让DescriptionField生成一堆杂乱HTML,而是有结构地保留属性,可以手动构造ExtendedData节点。ogr2ogr的KML驱动已经自动做了这个事,但字段顺序不可控。用FME的话,可以在AttributeManager里调整字段顺序,或者用ListTransformers控制。将来要用数据做分析时,从ExtendedData里解析出属性比解析描述HTML简单得多。
6.3 提速:批量转换的内存与性能调优
处理几百个图斑时性能不是问题,但处理几十万要素时,KML会变得很庞大。我的习惯是在批处理前先把源shp按地理范围切成小块,分块转换再合并KML,这样方便分块debug。另外,GDAL的GML_INCLUDE_MANDATED等选项未必需要,但-lco ENCODING=UTF-8不能忘。最后,转换完以后可以写一段Python脚本,用xml.dom.minidom解析KML,统计<Placemark>数量,与你源文件要素数对比,验证完整性。
从那以后,我每次转KML都会强制走一遍:先检查.srs和编码,再检查字段映射,最后抽稀和体积检查。哪怕是最简单的单文件任务,也至少确认一次名称非空数量。这一步“确认”的工作,能省下你在Google Earth里手动点击几十个图斑的时间。希望这套流程和踩坑记录能帮到你。
本文还有配套的精品资源,点击获取