Pythoner 的Flask项目实践-在web页面实现矢量数据转换工具集功能(附源码)

本文中的项目是接着上几篇中的Python flask项目做的;要想从头创建flask项目的,请翻阅我上几篇pythoner文章。

本文是在现有 Flask + 前端页面 的框架里,扩展成一个 常见矢量数据格式转换工具集。

1、支持的数据格式

支持以下常见格式互转:

  • Shapefile (.shp/.zip)

  • GeoJSON (.geojson / .json)

  • KML / KMZ

  • GPKG (GeoPackage)

2、实现的功能

  1. 单个文件转换
  2. 批量转换

一个页面,使用tab进行切换。

  • 一次可上传多个文件(Shapefile ZIP、GeoJSON、KML、GPKG 等);

  • 选择目标格式;

  • 逐个转换;

  • 最后打包成一个 .zip 压缩包,统一下载。

3、前端 (templates/map.html)

html 复制代码
{% extends "home.html" %} {% block title %}矢量数据转换工具集{% endblock %} {% block content %}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>矢量数据转换工具集</title>
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <link href="https://api.mapbox.com/mapbox-gl-js/v3.15.0/mapbox-gl.css" rel="stylesheet" />
    <script src="https://api.mapbox.com/mapbox-gl-js/v3.15.0/mapbox-gl.js"></script>
    <style>
      body {
        margin: 0;
        padding: 0;
        display: flex;
        height: 100vh;
        font-family: sans-serif;
      }
      #sidebar {
        width: 350px;
        background: #f5f5f5;
        border-right: 1px solid #ccc;
        padding: 10px;
        overflow-y: auto;
        font-size: 14px;
      }
      #map {
        flex: 1;
      }

      .tab {
        overflow: hidden;
        border-bottom: 1px solid #ccc;
      }
      .tab button {
        background-color: inherit;
        float: left;
        border: none;
        outline: none;
        cursor: pointer;
        padding: 10px 16px;
        transition: 0.3s;
        font-size: 14px;
      }
      .tab button:hover {
        background-color: #ddd;
      }
      .tab button.active {
        background-color: #bbb;
      }
      .tabcontent {
        display: none;
        padding: 10px 0;
      }
    </style>
  </head>
  <body>
    <div id="sidebar">
      <h3>矢量数据转换工具集</h3>

      <!-- Tab 按钮 -->
      <div class="tab">
        <button class="tablinks active" onclick="openTab(event, 'singleTab')">单文件转换</button>
        <button class="tablinks" onclick="openTab(event, 'batchTab')">批量转换</button>
      </div>

      <!-- 单文件转换 -->
      <div id="singleTab" class="tabcontent" style="display: block;">
        <form id="convertForm">
          <label>选择文件:</label><br />
          <input type="file" id="inputFile" accept=".zip,.shp,.geojson,.json,.kml,.kmz,.gpkg" /><br /><br />

          <label>目标格式:</label><br />
          <select id="targetFormat">
            <option value="geojson">GeoJSON</option>
            <option value="shp">Shapefile (zip)</option>
            <option value="kml">KML</option>
            <option value="kmz">KMZ</option>
            <option value="gpkg">GeoPackage</option> </select
          ><br /><br />

          <button type="submit">转换并下载</button>
        </form>
      </div>

      <!-- 批量转换 -->
      <div id="batchTab" class="tabcontent">
        <form id="batchForm">
          <label>选择多个文件:</label><br />
          <input type="file" id="inputFiles" multiple accept=".zip,.shp,.geojson,.json,.kml,.kmz,.gpkg" /><br /><br />

          <label>目标格式:</label><br />
          <select id="targetFormatBatch">
            <option value="geojson">GeoJSON</option>
            <option value="shp">Shapefile (zip)</option>
            <option value="kml">KML</option>
            <option value="kmz">KMZ</option>
            <option value="gpkg">GeoPackage</option> </select
          ><br /><br />

          <button type="submit">批量转换并下载</button>
        </form>
      </div>
    </div>
    <div id="map"></div>

    <script>
      mapboxgl.accessToken = "pk.eyJ1IjoidGlnZXJiZ3AyMDIwIiwiYSI6ImNsaGhpb3Q0ZTBvMWEzcW1xcXd4aTk5bzIifQ.4mA7mUrhK09N4vrrQfZA_Q";

      var map = new mapboxgl.Map({
        container: "map",
        style: "mapbox://styles/mapbox/streets-v12",
        center: [55.2744, 25.1972],
        zoom: 6,
      });

      // Tab 切换函数
      function openTab(evt, tabName) {
        var i, tabcontent, tablinks;
        tabcontent = document.getElementsByClassName("tabcontent");
        for (i = 0; i < tabcontent.length; i++) {
          tabcontent[i].style.display = "none";
        }
        tablinks = document.getElementsByClassName("tablinks");
        for (i = 0; i < tablinks.length; i++) {
          tablinks[i].className = tablinks[i].className.replace(" active", "");
        }
        document.getElementById(tabName).style.display = "block";
        evt.currentTarget.className += " active";
      }

      // 单文件转换
      document.getElementById("convertForm").addEventListener("submit", function (e) {
        e.preventDefault();
        var fileInput = document.getElementById("inputFile");
        var target = document.getElementById("targetFormat").value;

        if (fileInput.files.length === 0) {
          alert("请选择一个文件");
          return;
        }

        var formData = new FormData();
        formData.append("file", fileInput.files[0]);
        formData.append("target_format", target);

        fetch("/convert", { method: "POST", body: formData })
          .then((res) => {
            if (res.ok) return res.blob();
            return res.json().then((err) => {
              throw new Error(err.error);
            });
          })
          .then((blob) => {
            var url = window.URL.createObjectURL(blob);
            var a = document.createElement("a");
            a.href = url;
            a.download = fileInput.files[0].name.split(".")[0] + "." + target;
            document.body.appendChild(a);
            a.click();
            a.remove();
          })
          .catch((err) => alert("转换失败: " + err.message));
      });

      // 批量转换
      document.getElementById("batchForm").addEventListener("submit", function (e) {
        e.preventDefault();
        var files = document.getElementById("inputFiles").files;
        var target = document.getElementById("targetFormatBatch").value;

        if (files.length === 0) {
          alert("请选择至少一个文件");
          return;
        }

        var formData = new FormData();
        for (var i = 0; i < files.length; i++) {
          formData.append("files", files[i]);
        }
        formData.append("target_format", target);

        fetch("/convert_batch", { method: "POST", body: formData })
          .then((res) => {
            if (res.ok) return res.blob();
            return res.json().then((err) => {
              throw new Error(err.error);
            });
          })
          .then((blob) => {
            var url = window.URL.createObjectURL(blob);
            var a = document.createElement("a");
            a.href = url;
            a.download = "converted_all.zip";
            document.body.appendChild(a);
            a.click();
            a.remove();
          })
          .catch((err) => alert("批量转换失败: " + err.message));
      });
    </script>
  </body>
</html>
{% endblock %}

4、后端 (app.py)

两个接口:

  • /convert → 单文件转换

  • /convert_batch → 批量转换

前端根据 tab 切换调用不同接口即可。

具体代码:

python 复制代码
@app.route("/vectorcovertertools")
def vectorcovertertools():
    return render_template("vectorcovertertools.html")

@app.route("/convert", methods=["POST"])
def convert():
    try:
        file = request.files["file"]
        target_format = request.form.get("target_format", "").lower()
        if not file:
            return jsonify({"error": "没有上传文件"})

        # 临时保存文件
        tmpdir = tempfile.mkdtemp()
        filepath = os.path.join(tmpdir, file.filename)
        file.save(filepath)

        # 如果是 zip(Shapefile)
        if filepath.lower().endswith(".zip"):
            with zipfile.ZipFile(filepath, "r") as zip_ref:
                zip_ref.extractall(tmpdir)
            shp_files = [f for f in os.listdir(tmpdir) if f.lower().endswith(".shp")]
            if not shp_files:
                return jsonify({"error": "压缩包中没有找到 .shp 文件"})
            input_path = os.path.join(tmpdir, shp_files[0])
        else:
            input_path = filepath

        # 读取矢量数据
        gdf = gpd.read_file(input_path)

        # 生成输出路径
        base = os.path.splitext(file.filename)[0]
        out_path = os.path.join(tmpdir, base + "." + target_format)

        # 转换逻辑
        if target_format == "geojson":
            gdf.to_file(out_path, driver="GeoJSON")
        elif target_format == "shp":
            shp_dir = os.path.join(tmpdir, base + "_shp")
            os.makedirs(shp_dir, exist_ok=True)
            gdf.to_file(os.path.join(shp_dir, base + ".shp"), driver="ESRI Shapefile")
            # 打包 zip
            zip_path = os.path.join(tmpdir, base + ".zip")
            with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
                for f in os.listdir(shp_dir):
                    zipf.write(os.path.join(shp_dir, f), f)
            out_path = zip_path
        elif target_format == "kml":
            gdf.to_file(out_path, driver="KML")
        elif target_format == "kmz":
            kml_path = os.path.join(tmpdir, base + ".kml")
            gdf.to_file(kml_path, driver="KML")
            kmz_path = os.path.join(tmpdir, base + ".kmz")
            with zipfile.ZipFile(kmz_path, "w", zipfile.ZIP_DEFLATED) as kmz:
                kmz.write(kml_path, os.path.basename(kml_path))
            out_path = kmz_path
        elif target_format == "gpkg":
            gdf.to_file(out_path, driver="GPKG")
        else:
            return jsonify({"error": f"不支持的目标格式: {target_format}"})

        return send_file(out_path, as_attachment=True)

    except Exception as e:
        return jsonify({"error": str(e)})

@app.route("/convert_batch", methods=["POST"])
def convert_batch():
    try:
        files = request.files.getlist("files")
        target_format = request.form.get("target_format", "").lower()

        if not files:
            return jsonify({"error": "没有上传文件"})

        tmpdir = tempfile.mkdtemp()
        output_dir = os.path.join(tmpdir, "converted")
        os.makedirs(output_dir, exist_ok=True)

        for file in files:
            filename = file.filename
            filepath = os.path.join(tmpdir, filename)
            file.save(filepath)

            # 如果是 zip(Shapefile)
            if filepath.lower().endswith(".zip"):
                with zipfile.ZipFile(filepath, "r") as zip_ref:
                    zip_ref.extractall(tmpdir)
                shp_files = [f for f in os.listdir(tmpdir) if f.lower().endswith(".shp")]
                if not shp_files:
                    continue
                input_path = os.path.join(tmpdir, shp_files[0])
                base = os.path.splitext(shp_files[0])[0]
            else:
                input_path = filepath
                base = os.path.splitext(filename)[0]

            try:
                gdf = gpd.read_file(input_path)
            except Exception:
                continue

            out_path = os.path.join(output_dir, base + "." + target_format)

            # 转换逻辑
            if target_format == "geojson":
                gdf.to_file(out_path, driver="GeoJSON")
            elif target_format == "shp":
                shp_dir = os.path.join(output_dir, base + "_shp")
                os.makedirs(shp_dir, exist_ok=True)
                gdf.to_file(os.path.join(shp_dir, base + ".shp"), driver="ESRI Shapefile")
                # 打包 zip
                zip_path = os.path.join(output_dir, base + ".zip")
                with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf:
                    for f in os.listdir(shp_dir):
                        zipf.write(os.path.join(shp_dir, f), f)
            elif target_format == "kml":
                gdf.to_file(out_path, driver="KML")
            elif target_format == "kmz":
                kml_path = os.path.join(output_dir, base + ".kml")
                gdf.to_file(kml_path, driver="KML")
                kmz_path = os.path.join(output_dir, base + ".kmz")
                with zipfile.ZipFile(kmz_path, "w", zipfile.ZIP_DEFLATED) as kmz:
                    kmz.write(kml_path, os.path.basename(kml_path))
            elif target_format == "gpkg":
                gdf.to_file(out_path, driver="GPKG")

        # 最终打包所有结果
        result_zip = os.path.join(tmpdir, "converted_all.zip")
        with zipfile.ZipFile(result_zip, "w", zipfile.ZIP_DEFLATED) as zipf:
            for root, dirs, files_in_dir in os.walk(output_dir):
                for f in files_in_dir:
                    full_path = os.path.join(root, f)
                    rel_path = os.path.relpath(full_path, output_dir)
                    zipf.write(full_path, rel_path)

        return send_file(result_zip, as_attachment=True, download_name="converted_all.zip")

    except Exception as e:
        return jsonify({"error": str(e)})

5、总结

上传 .zip (Shapefile)、.geojson、.kml、.gpkg 等

选择目标格式(GeoJSON / Shapefile / KML / KMZ / GPKG)

点击"转换并下载" → 自动生成目标文件并下载。


"人的一生会经历很多痛苦,但回头想想,都是传奇"。


相关推荐
lypzcgf2 小时前
Coze源码分析-资源库-编辑工作流-前端源码-核心流程/API/总结
前端·工作流·coze·coze源码分析·智能体平台·ai应用平台·agent平台
lypzcgf2 小时前
Coze源码分析-资源库-编辑工作流-前端源码-核心组件
前端·工作流·coze·coze源码分析·智能体平台·agent平台
有梦想的攻城狮2 小时前
从0开始学vue:vue和react的比较
前端·vue.js·react.js
FIN66682 小时前
昂瑞微,凭啥?
前端·科技·产品运营·创业创新·制造·射频工程
学习的学习者2 小时前
CS课程项目设计19:基于DeepFace人脸识别库的课堂签到系统
人工智能·python·深度学习·人脸识别算法
哈里谢顿3 小时前
flask中的 Blueprint总结
flask
悠哉悠哉愿意3 小时前
【数据结构与算法学习笔记】双指针
数据结构·笔记·python·学习·算法
kura_tsuki3 小时前
[Web网页] Web 基础
前端
MoRanzhi12033 小时前
5. Pandas 缺失值与异常值处理
数据结构·python·数据挖掘·数据分析·pandas·缺失值处理·异常值处理