本文中的项目是接着上几篇中的Python flask项目做的;要想从头创建flask项目的,请翻阅我上几篇pythoner文章。
本文是在现有 Flask + 前端页面 的框架里,扩展成一个 常见矢量数据格式转换工具集。
1、支持的数据格式
支持以下常见格式互转:
-
Shapefile (.shp/.zip)
-
GeoJSON (.geojson / .json)
-
KML / KMZ
-
GPKG (GeoPackage)
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)

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

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