Há algum tempo, os dados baseados em arquivos estão migrando para a web e todos nós estamos acostumados a baixar manualmente o que precisamos e seguir a partir daí em nosso software de desktop como o ArcGIS Pro.<\/P>
Este blog é sobre automatizar<\/STRONG> essa experiência, substituindo as etapas de download e processamento por ferramentas fáceis de entender, bem definidas e compartilháveis que entregam seu produto de informação sob demanda<\/STRONG>.<\/P>Em duas extremidades da escala de dados, tenho divisões políticas mundiais e atividade de permissões de construção em Vancouver, Canadá. O primeiro conjunto de dados é grande<\/STRONG> e proveniente de um bucket AWS S3, o último é pequeno<\/STRONG> e proveniente de um portal de dados abertos. Ambos são ingeridos usando notebooks no ArcGIS Pro com as mesmas ferramentas básicas.<\/STRONG><\/P>Primeiro o conjunto de dados pequeno:<\/P>
Permissões de construção de Vancouver<\/span><\/span><\/P>O conjunto de dados grande:<\/P>
Divisões políticas mundiais<\/span><\/span><\/P>E no meio-termo, as divisões políticas apenas para a Alemanha:<\/P>
Uma visão das divisões políticas para a Alemanha<\/span><\/span><\/P>Todos esses conjuntos de dados foram ingeridos de forma semelhante, mas com diferenças notáveis que discutiremos abaixo.<\/P>Se você não está familiarizado com GeoParquet<\/A>, você vai ficar, e estou fazendo dele meu formato bem conhecido<\/STRONG> preferido para este post. Outros tipos comuns de arquivo na nuvem como CSV, Excel e JSON também funcionariam. Por "nuvem", quero dizer não apenas onde você vai buscá-los, mas formatos que se prestam a consultas remotas sem que você precise baixar e inspecionar uma cópia local.<\/P>Embora eu diga que a mesma ferramenta básica pode acessar os grandes dados no S3 e os pequenos dados em um site<\/STRONG>, as capacidades dos respectivos servidores importam. O S3 sabe como entregar arquivos conforme consultas contra armazenamento hive, enquanto um site só sabe como entregar downloads de arquivos inteiros.<\/P>Vamos ver como abordar primeiro o conjunto de dados menor - permissões de construção, por download automatizado de um site.<\/P>Os dados residem em um portal de dados abertos<\/STRONG><\/A>, se você navegar pelo site verá opções de formato para download.<\/P>Os dados são automatizados no notebook VancouverBuildingPermits<\/STRONG>. Se você inspecionar o código verá que estou usando DuckDB<\/A> para ajudar - é um banco de dados SQL habilitado espacialmente e cliente web. Você verá que o download dos dados e a conversão para feature class é muito simples - a ferramenta infere o esquema da fonte Parquet mais as instruções SQL<\/STRONG>.<\/P> <\/P>import arcpy
from arcgis.features import GeoAccessor, GeoSeriesAccessor
import duckdb
import math
import os
arcpy.env.overwriteOutput = True
url = r"https:\/\/opendata.vancouver.ca\/api\/explore\/v2.1\/catalog\/datasets\/issued-building-permits\/exports\/parquet?lang=en&refine=issuedate%3A%222024%22&timezone=America%2FLos_Angeles"
conn = duckdb.connect()
conn.sql("set force_download=true;")
conn.sql("install httpfs;load httpfs;")
conn.sql("install spatial;load spatial;")
conn.sql(f"create temp view permitsView as select * from read_parquet('{url}');")
bldgPermits = conn.sql("""select permitnumber as Permit_Number,
try_cast (permitnumbercreateddate as date) as Permit_Number_Created_Date,
try_cast (issuedate as date) as Issue_Date,
permitelapseddays::int as Permit_Elapsed_Days,
projectvalue::float as Project_Value,
typeofwork as Type_Of_Work,
address as Address,
projectdescription as Project_Description,
permitcategory as Permit_Category,
applicant as Applicant,
applicantaddress as Applicant_Address,
propertyuse as Property_Use,
specificusecategory as Specific_Use_Category,
buildingcontractor as Building_Contractor,
buildingcontractoraddress as Building_Contractor_Address,
issueyear as Issue_Year,
geolocalarea as Geo_Loc_Area,
yearmonth as Year_Month,
ST_AsText(ST_GeomFromWKB(geom)) as SHAPE
from permitsView;""")
df = bldgPermits.df()
df.spatial.set_geometry("SHAPE",sr=4326,inplace=True)
aprx = arcpy.mp.ArcGISProject("CURRENT")
gdb = aprx.defaultGeodatabase
out_fc = "Issued_Building_Permits"
location = os.path.join(gdb,out_fc)
df.spatial.to_featureclass(location,sanitize_columns=False)<\/code><\/pre><P> <\/P><P>Se você inspecionar o código no download verá algumas etapas extras que usei para arredondar larguras dos campos texto para fatores de 10, um detalhe desnecessário neste caso mas apenas para mostrar como fazer isso se você espera que os dados sejam editados no futuro e não quer truncamento dos valores.<\/P><P>Então esse é um caso simples de ingerir um arquivo pequeno residente na nuvem no ArcGIS com controle mínimo sobre o esquema - note novamente que instruções SQL padrão estão disponíveis para definir sua visão do arquivo originado na nuvem. Compare essa abordagem com definir o esquema usando geoprocessamento em <A title="Refining the schema for local well-known files" href="https:\/\/community.esri.com\/t5\/etl-patterns-blog\/etl-pattern-working-with-local-well-known-files\/ba-p\/1515000" target="_blank">este post relacionado<\/A>.<\/P><P>Vamos avançar para dados em escala global usando o notebook <STRONG>Division_Area<\/STRONG>, usando dados da <A title="Overture" href="https:\/\/overturemaps.org\/download\/" target="_blank" rel="noopener nofollow noreferrer">Overture Maps Foundation<\/A>.<\/P><P> <\/P><pre class="lia-code-sample language-python"><code># Obter a camada Division_Area da Overture Maps Foundation
import arcpy
from datetime import datetime
import duckdb
import os
aprx = arcpy.mp.ArcGISProject("CURRENT")
homeFolder = aprx.homeFolder
gdb = aprx.defaultGeodatabase
release = "2024-07-22.0"
def getNow():
return str(datetime.utcnow().replace(microsecond=0))
arcpy.env.overwriteOutput = True
sR = arcpy.SpatialReference(4326)
conn = duckdb.connect()
conn.sql("install spatial;load spatial;")
conn.sql("install httpfs;load httpfs;")
conn.sql("set s3_region='us-west-2';")
conn.sql("set enable_object_cache=true;")
# Criar a relação DuckDB
# Insira qualquer cláusula where desejada, por exemplo "country = 'GB'"
# A expressão order-by ajuda a exibir pequenas divisões sobre grandes, mas é custosa
print(f"Processamento iniciado em {getNow()}")
whereExp = "1 = 1"
sql = f"""select id,
bbox.xmin as xmin,
bbox.ymin as ymin,
bbox.xmax as xmax,
bbox.ymax as ymax,
version,
subtype,
names.primary as primary_name,
class,
region,
country,
norms.driving_side as driving_side,
geometry
from read_parquet('s3:\/\/_overturemaps-us-west-2\/_release\/_{release}\/_theme=divisions\/_type=division_area\/*',filename=true, hive_partitioning=1)
where {whereExp}
order by ST_Area(ST_GeomFromWKB(geometry)) desc;"""
duckDivisions = conn.sql(sql)
# Criar a feature class de saída com controle preciso do esquema
print('Criando feature class de saída em {}'.format(getNow()))
arcDivisions = arcpy.management.CreateFeatureclass(out_path=gdb,
out_name="Division_Areas",
geometry_type="POLYGON",
spatial_reference=sR).getOutput(0)
arcpy.management.AddField(in_table=arcDivisions,field_name="id",field_type="TEXT",field_length=32)
for f in ["xmin","ymin","xmax","ymax"]:
arcpy.management.AddField(in_table=arcDivisions,field_name=f,field_type="FLOAT")
arcpy.management.AddField(in_table=arcDivisions,field_name="version",field_type="SHORT")
arcpy.management.AddField(in_table=arcDivisions,field_name="subtype",field_type="TEXT",field_length=20)
arcpy.management.AddField(in_table=arcDivisions,field_name="primary_name",field_type="TEXT",field_length=100)
arcpy.management.AddField(in_table=arcDivisions,field_name="class",field_type="TEXT",field_length=6)
arcpy.management.AddField(in_table=arcDivisions,field_name="region",field_type="TEXT",field_length=10)
arcpy.management.AddField(in_table=arcDivisions,field_name="country",field_type="TEXT",field_length=2)
arcpy.management.AddField(in_table=arcDivisions,field_name="driving_side",field_type="TEXT",field_length=10)
# Escreva a saída
print('Escrevendo saída em {}'.format(getNow()))
com arcpy.da.InsertCursor(arcDivisions,["id","xmin","ymin","xmax","ymax","version",
"subtype","primary_name","class","region",
"country","driving_side","shape@"]) como iCursor:
row = duckDivisions.fetchone()
i = 1
enquanto row:
se i % 100000 == 0:
print(f"Inseridas {i} linhas de Division_Areas...")
row = list(row)
row[-1] = arcpy.FromWKB(row[-1])
iCursor.insertRow(row)
i+=1
row = duckDivisions.fetchone()
del iCursor
# Reparar geometrias, remover geometrias nulas
print(f"Corrigindo quaisquer geometrias ruins em {getNow()}")
arcpy.management.RepairGeometry(
in_features=arcDivisions,
delete_null="DELETE_NULL",
validation_method="OGC")
print(f"Finalizado em {getNow()}")
As principais diferenças entre esta ingestão de dados em escala global e aquela para as permissões de construção é que a fonte é obtida por uma consulta curinga no armazenamento hive do S3, e o esquema é definido manualmente. A ferramenta é a mesma.< /P>
Então, essa é uma visão da ingestão focada no usuário de dados em grande e pequena escala, mas e se você for um proprietário de dados e quiser oferecer uma entrega de dados baseada em arquivos well-known na nuvem infinitamente flexível para um amplo público de usuários sem que eles precisem se preocupar em construir ferramentas? É aqui que entra o exemplo de escala média das divisões políticas para a Alemanha.< /P>
A biblioteca cliente que estamos usando - DuckDB - também é um formato de armazenamento, mas com capacidades de banco de dados espacial, incluindo views, então no caso de dados originados na nuvem é simples oferecer um produto de dados que contém apenas uma view que é lida sob demanda por qualquer usuário final. Isso significa que, pelo esforço de definir uma view, um produto de dados de qualquer escala pode ser entregue em um arquivo de banco de dados minúsculo.< /P>
O download do blog contém tal produto de dados - mydivisionview.duckdb - que foi criado pelo notebook MakeViewDatabase e pode ser ingerido por qualquer pessoa usando o notebook ReadViewDatabase. Neste caso, então, um arquivo de apenas 268KB pode entregar dados em escala nacional (ou até global). Este é o poder dos arquivos well-known nativos da nuvem lidos sob demanda da nuvem. Se a fonte do conjunto de dados mudar, o mesmo banco DuckDB sempre acessará os dados mais recentes.< /P>
Abaixo está o código para ReadViewDatabase. Se você copiar mydivisionview.duck db para sua pasta do projeto ele entregará o conteúdo da view no seu geodatabase padrão do projeto em segundos (15 na minha máquina):
import arcpy
from datetime import datetime
import duckdb
import os
def getNow():
return str(datetime.utcnow().replace(microsecond=0))
print(f"Extraindo dados em {getNow()}")
arcpy.env.overwriteOutput = True
aprx = arcpy.mp.ArcGISProject("CURRENT")
homeFolder = aprx.homeFolder
gdb = aprx.defaultGeodatabase
sR = arcpy.SpatialReference(4326)
duckDB = os.path.join(homeFolder,"MyDivisionView.duckdb")
conn = duckdb.connect(duckDB)
conn.sql("install spatial;load spatial;")
conn.sql("install httpfs;load httpfs;")
conn.sql("set enable_object_cache=true;")
duckDivisions = conn.sql(f"""select id,
xmin::float as xmin,
ymin::float as ymin,
xmax::float as xmax,
ymax::float as ymax,
version::short as version,
subtype,
primary_name,
class,
region,
country,
driving_side,
geometry
from MyDivisionView;""")
arqDivisions = arcpy.management.CreateFeatureclass(gdb,
out_name="My_Division_Areas",
geometry_type="POLYGON",
spatial_reference=sR).getOutput(0)
arcpy.management.AddField(in_table=arcDivisions,field_name="id",field_type="TEXT",field_length=32)
para f em ["xmin","ymin","xmax","ymax"]:
arcpy.management.AddField(in_table=arcDivisions,field_name=f,field_type="FLOAT")
arcpy.management.AddField(in_table=arcDivisions,field_name="version",field_type="SHORT")
arcpy.management.AddField(in_table=arcDivisions,field_name="subtype",field_type="TEXT",field_length=20)
arcpy.management.AddField(in_table=arcDivisions,field_name="primary_name",field_type="TEXT",field_length=100)
arcpy.management.AddField(in_table=arcDivisions,field_name="class",field_type="TEXT",field_length=6)
arcpy.management.AddField(in_table=arcDivisions,field_name="region",field_type="TEXT",field_length=10)
arcpy.management.AddField(in_table=arcDivisions,field_name="country",field_type="TEXT",field_length=2)
arcpy.management.AddField(in_table=arcDivisions,field_name="driving_side",field_type="TEXT",field_length=10)
com arcpy.da.InsertCursor(arcDivisions,["id","xmin","ymin","xmax","ymax","version","subtype",
"primary_name","class","region","country",
"driving_side","shape@"]) como iCursor:
row = duckDivisions.fetchone()
i = 1
enquanto row:
#se i % 1000 == 0:
# print('Inseridas {} linhas My_Division_Area'.format(i))
row = list(row)
row[-1] = arcpy.FromWKB(row[-1])
iCursor.insertRow(row)
i+=1
row = duckDivisions.fetchone()
del iCursor
conn.close()
print(f"Dados extraídos em {getNow()}")Vou terminar com alguns pontos resumidos:< /P>
- Pequenos arquivos well-known na nuvem são facilmente ingeridos.< /LI>
- Arquivos well-known na nuvem em escala ilimitada no armazenamento de objetos são facilmente ingeridos.< /LI>
- Dados em arquivos well-known na nuvem de qualquer escala em qualquer definição de view podem ser facilmente compartilhados em contêineres minúsculos.< /LI>
< /P>
< /P>
< /P>