Rotular arquivos KMZ pode ser uma tarefa desafiadora, especialmente porque o Google Earth impõe algumas limitações — principalmente no que diz respeito à exibição de rótulos para linhas e polígonos. Normalmente, apenas pontos são rotulados automaticamente. Já no ArcGIS Earth, esse tipo de visualização funciona sem maiores problemas.
Mas e se você quiser converter uma pasta cheia de shapefiles para o formato KMZ e rotular os polígonos com base em um dos campos da tabela de atributos? E mais: incluir essa tabela como um popup clicável no KMZ?
A boa notícia é que isso é totalmente possível com um simples script em Python.
O que você vai precisar
Antes de rodar o script, certifique-se de que o Python está instalado em seu computador. Em seguida, abra o Prompt de Comando (no Windows) e execute o seguinte comando para instalar as bibliotecas necessárias:
pip install geopandas pandas pathlib
Depois disso, siga estes passos:
- Crie um novo arquivo de texto;
- Cole o código Python que fornecemos abaixo;
- Salve o arquivo com a extensão
.py(por exemplo,converter_para_kmz.py); - Clique com o botão direito no arquivo e escolha “Abrir com” >
python.exe(ou execute via terminal).
Pronto! O script deve rodar automaticamente, convertendo seus shapefiles em arquivos KMZ rotulados e com popups interativos.
import geopandas as gpd
import pandas as pd
from pathlib import Path
import zipfile
import math
NAME_FIELD = "TALHAO" # mude para o campo que desejar rotular
def xml_escape(text):
s = "" if text is None else str(text)
return (s.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace('"', """)
.replace("'", "'"))
def is_null(v):
try:
return pd.isna(v) or (isinstance(v, float) and math.isnan(v))
except Exception:
return v is None
def coords_str(ring):
# lon,lat,alt(0)
return " ".join(f"{c[0]},{c[1]},0" for c in ring.coords)
def polygon_xml(poly):
outer = f"""
<outerBoundaryIs>
<LinearRing>
<coordinates>{coords_str(poly.exterior)}</coordinates>
</LinearRing>
</outerBoundaryIs>"""
inner_xml = ""
if poly.interiors:
inner_xml = "\n".join(
f""" <innerBoundaryIs>
<LinearRing>
<coordinates>{coords_str(r)}</coordinates>
</LinearRing>
</innerBoundaryIs>""" for r in poly.interiors
)
return f"<Polygon>{outer}{inner_xml}\n</Polygon>"
def geometry_with_label_xml(geom):
# MultiGeometry: all polygon parts + one representative point for on-map label
if geom.geom_type == "Polygon":
polys = [geom]
elif geom.geom_type == "MultiPolygon":
polys = [p for p in geom.geoms if not p.is_empty]
else:
return "" # ignore non-polygons
poly_parts = "\n".join(polygon_xml(p) for p in polys)
rp = geom.representative_point() # label point inside polygon
pt = f"<Point><coordinates>{rp.x},{rp.y},0</coordinates></Point>"
return f"<MultiGeometry>\n{poly_parts}\n{pt}\n</MultiGeometry>"
def extended_data_xml(row, columns):
items = []
for col in columns:
val = "" if is_null(row[col]) else xml_escape(row[col])
items.append(f'<Data name="{xml_escape(col)}"><value>{val}</value></Data>')
return "<ExtendedData>" + "".join(items) + "</ExtendedData>"
def pick_name(row, columns):
if NAME_FIELD in columns and not is_null(row[NAME_FIELD]):
return str(row[NAME_FIELD])
for col in columns:
v = row[col]
if not is_null(v):
return str(v)
return str(row.name)
# ---- Runtime inputs ----
print ("BEM VINDO AO CONVERSOR EM MASSA DE SHP EM UMA PASTA PARA KMZ ROTULADO.")
src = Path(input("Entre o caminho da pasta com os shps: ").strip('"'))
dst = Path(input("Entre a pasta de destino dos kmz: ").strip('"'))
exclude_text = input("Pule arquivos que contenham isto no nome do arquivo. Deixe em branco para nenhum: ").strip()
label_scale_in = input("Tamanho do Label (e.g., 0.8 pequeno, 1 normal, 1.5 grande) [padrão 0.8]: ").strip()
try:
label_scale = float(label_scale_in) if label_scale_in else 0.8
except ValueError:
print("Escala inválida. Usando 0.8.")
label_scale = 0.8
dst.mkdir(parents=True, exist_ok=True)
# Build KML header with chosen label size
KML_HEADER = f"""<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<Style id="polyWithLabel">
<LineStyle>
<color>ff00ffff</color> <!-- Yellow (aabbggrr) -->
<width>1</width>
</LineStyle>
<PolyStyle>
<fill>0</fill> <!-- No fill -->
<outline>1</outline>
</PolyStyle>
<IconStyle>
<scale>0</scale> <!-- hide icon; keeps only text label -->
</IconStyle>
<LabelStyle>
<color>ffffffff</color> <!-- white label text -->
<scale>{label_scale}</scale>
</LabelStyle>
</Style>
"""
KML_FOOTER = "</Document></kml>"
for shp in src.glob("*.shp"):
# Skip by filename substring if requested
if exclude_text and exclude_text.lower() in shp.name.lower():
print(f"[SKIP] {shp.name} (devido a texto excluidor entrado: '{exclude_text}')")
continue
print(f"Processando {shp.name}...")
gdf = gpd.read_file(shp)
# Reproject to WGS84 (KML requirement)
if gdf.crs is not None and gdf.crs.to_string() != "EPSG:4326":
gdf = gdf.to_crs("EPSG:4326")
elif gdf.crs is None:
print(f" [WARN] {shp.name}: projecao faltando; Usando WGS84.")
attr_cols = [c for c in gdf.columns if c != gdf.geometry.name]
parts = [KML_HEADER, f"<Folder><name>{xml_escape(shp.stem)}</name>"]
for _, row in gdf.iterrows():
geom = row.geometry
if geom is None or geom.is_empty:
continue
if not geom.is_valid and geom.geom_type in ("Polygon", "MultiPolygon"):
geom = geom.buffer(0)
geom_xml = geometry_with_label_xml(geom)
if not geom_xml:
continue
name_text = xml_escape(pick_name(row, attr_cols))
data_xml = extended_data_xml(row, attr_cols)
parts.append(f"""
<Placemark>
<name>{name_text}</name>
<styleUrl>#polyWithLabel</styleUrl>
{data_xml}
{geom_xml}
</Placemark>""")
parts.append("</Folder>")
parts.append(KML_FOOTER)
kml_content = "\n".join(parts)
# Write KMZ (doc.kml inside)
kmz_path = dst / f"{shp.stem}.kmz"
with zipfile.ZipFile(kmz_path, "w", zipfile.ZIP_DEFLATED) as zf:
zf.writestr("doc.kml", kml_content)
print("Concluido.")
Se encontrar qualquer problema ou tiver dúvidas, fique à vontade para comentar abaixo — vou tentar ajudar!
Aqui está nosso conversor, usando um código parecido com o acima, confira, use e compartilhe

Avançado, mas consegui fazer funcionar. Muito útil !