矢量篇 - kml&kmz转shp

@ 20240908 & lth

目标:从kml或kmz带属性转成shp

逻辑:主要是对kml的description字段的处理,他的格式是html的

复制代码
目前我搜了一下没有现成的工具,要想将kml带属性转成shp,我这里工具选的是fme或python

FME

复制代码
用fme的话,关键点就是StringSearcher转换器,(?<=<td>).+?(?=</td>),然后用AttributeExposer暴露出来把获取的字段


PYTHON

复制代码
import xml.etree.ElementTree as ET
import os
from osgeo import ogr


def parse_kml_description(html_content):
    """
    解析 KML description 字段中的 HTML 表格,提取属性字典。
    
    参数:
        html_content (str): 描述字段的 HTML 内容(如 '<table>...</table>')
    
    返回:
        dict: 提取的属性字典,例如 {'bh': '40339', 'name': '831774/2023', 'FID': '40338'}
    """
    # 尝试修复不完整的 HTML(比如缺少根标签)
    if not html_content.strip().startswith('<table'):
        # 查找第一个 <table> 开始位置
        start = html_content.find('<table')
        end = html_content.find('</table>')
        if start == -1 or end == -1:
            return {}
        html_content = html_content[start:end + 8]  # 截取完整 table

    try:
        # 使用 XML 解析器解析 HTML 表格
        root = ET.fromstring(html_content)
        
        # 如果根节点不是 <table>,尝试找子节点中的 <table>
        if root.tag != 'table':
            table = root.find('.//table')
            if table is not None:
                root = table
            else:
                return {}

        attr_dict = {}
        # 遍历所有表格行
        for row in root.findall('.//tr'):
            ths = row.findall('th')
            tds = row.findall('td')
            if len(ths) > 0 and len(tds) > 0:
                key = ths[0].text
                value = tds[0].text
                if key:  # 确保字段名不为空
                    attr_dict[key.strip()] = value.strip() if value else ''
        return attr_dict
    except ET.ParseError as e:
        print(f"HTML 解析失败: {e}")
        return {}


def convert_kml_to_shp(in_file, out_file):
    """
    将 KML 文件转换为 Shapefile,并从 description 中提取嵌套属性。

    参数:
        in_file (str): 输入 KML 文件路径。
        out_file (str): 输出 SHP 文件路径。
    """
    # 打开输入 KML 文件
    ds_in = ogr.Open(in_file)
    if ds_in is None:
        print(f"无法打开输入文件:{in_file}")
        return

    layer = ds_in.GetLayer(0)
    srs = layer.GetSpatialRef()  # 获取空间参考

    # 创建输出 Shapefile
    driver = ogr.GetDriverByName('ESRI Shapefile')
    # 删除已存在的输出文件(OGR 不会自动覆盖)
    if os.path.exists(out_file):
        driver.DeleteDataSource(out_file)
    ds_out = driver.CreateDataSource(out_file)
    if ds_out is None:
        print(f"无法创建输出文件:{out_file}")
        return

    # 获取输入图层定义
    layer_defn = layer.GetLayerDefn()
    geom_type = layer_defn.GetGeomType()

    # 创建输出图层(暂时无字段,后面动态添加)
    layer_out = ds_out.CreateLayer('output', srs=srs, geom_type=geom_type)

    # 存储已创建的字段名,避免重复创建
    created_fields = set()

    # === 第一步:遍历所有要素,提取 description 中的所有唯一字段名 ===
    print("正在扫描所有要素以提取字段...")
    all_attributes = set()
    features_data = []  # 临时存储每个要素的 geometry 和属性字典

    for feat_in in layer:
        desc = feat_in.GetField('description')  # 获取 description 字段
        if desc is not None:
            attrs = parse_kml_description(desc)
            all_attributes.update(attrs.keys())
            features_data.append({
                'geometry': feat_in.GetGeometryRef().Clone(),
                'attributes': attrs
            })
        else:
            features_data.append({
                'geometry': feat_in.GetGeometryRef().Clone(),
                'attributes': {}
            })

    # === 第二步:根据提取出的所有字段,创建 Shapefile 的字段 ===
    print(f"发现以下属性字段: {sorted(all_attributes)}")
    for field_name in sorted(all_attributes):
        # 检查字段名是否合法(Shapefile 字段名不能太长,且只能用字母数字下划线)
        safe_name = field_name.strip()
        if not safe_name.isidentifier():
            safe_name = ''.join(c if c.isalnum() or c == '_' else '_' for c in safe_name)
        if len(safe_name) > 10:  # Shapefile 字段名最多 10 字符
            safe_name = safe_name[:10]
        # 避免重复
        if safe_name not in created_fields:
            field_defn = ogr.FieldDefn(safe_name, ogr.OFTString)
            field_defn.SetWidth(254)
            layer_out.CreateField(field_defn)
            created_fields.add(safe_name)

    # 获取输出图层的要素定义
    feat_defn = layer_out.GetLayerDefn()

    # === 第三步:写入所有要素 ===
    for data in features_data:
        feat_out = ogr.Feature(feat_defn)
        feat_out.SetGeometry(data['geometry'])

        # 填充从 description 中提取的属性
        for key, value in data['attributes'].items():
            safe_key = key.strip()
            if not safe_key.isidentifier():
                safe_key = ''.join(c if c.isalnum() or c == '_' else '_' for c in safe_key)
            if len(safe_key) > 10:
                safe_key = safe_key[:10]
            if safe_key in created_fields:
                feat_out.SetField(safe_key, value)

        layer_out.CreateFeature(feat_out)
        feat_out = None  # 释放内存

    # 清理
    ds_out = None
    ds_in = None
    print(f"✅ 转换完成!已将 {len(features_data)} 个要素写入 {out_file}")

插入一个打包的知识点用Nuitka比pyinstaller要好很多,生成的exe要小很多大概是5倍,然后要注意的是pyqt5不太兼容Nuitka