Usando Python e o Geoprocessing Framework #1
Boa noite pessoal,
Hoje quero falar um pouquinho do framework de geoprocessamento do ArcGIS, disponível em Python. Python, como sabem, é uma linguagem de alto nível, orientada à objetos e muito - mas muito fácil de se aprender.
Existem tarefas extremamente repetitivas que podem ser facilmente automatizadas com um pouquinho de Python e o tal Geoprocessing. O ArcGIS, basicamente faz uso do Python em toda a command-line e em algumas ferramentas da toolbox. Tudo que é feito no ModelBuilder é convertido em código Python.
Bem, vamos à proposta: imagine que você tenha necessidade de atualizar dados disponibilizados como shapefiles, por um órgão do governo, de forma periódica e em um banco de dados (ArcSDE, Personal GDB, File GDB, etc.). Será possível?
Bem, isto é possível pois o Python conta com milhares de bibliotecas para acesso à páginas da web, compressão/descompressão de arquivos .zip, entre outras. Este foi um requerimento real de um trabalho antigo.
Bem, imaginemos o site do DNPM, que disponibiliza shapefiles dos direitos minerários, de tempos em tempos (o DNPM também publica um serviço web, mas nem sempre é um bom caminho). No site SIGMINE você pode puxar separado por estados o shape atualizado e alimentar um banco. Mas é muito trampo para ser feito toda semana. Ainda mais se tua base não for centralizada. Todo mundo tem atualizar milhares de mxds, mapas, etc.
Quais são nossos passos?
- Fazer o download do shapefile em .zip
- Descompactar nosso arquivo em disco
- Atualizar o banco de dados
Podemos realizar o passo número 3 de várias formas, deletando a Feature Class antiga ou então atualizando registro por registro. Para nosso exercício, vamos deletar todos os registros e inseri-los novamente. Em ambientes mais controlados, com outras necessidades, talvez isto não seja o ideal.
Para contemplar nosso objetivo, iremos criar algumas classes:
- leecherHandler - responsável pelo download de arquivos da web;
- folderHandler - responsável pela criação/deleção de pastas;
- zipHandler - responsável pela compressão/descompressão de arquivos .zip;
- logger - responsável por guardar mensagens importantes para controle do que está acontecendo;
- geoprocessor - responsável por realizar as operações e interfaces com o ArcGIS;
Vamos começar pela classe leecherHandler, que é o coração de nosso pequeno sistema:
import sys, urllib2, logHandler
class leecherHandlerClass():
def __init__(self,webAddress,tempFolder):
self.logs = logHandler.logHandlerClass()
# Startup the log object.
self.webAddress = webAddress
self.tempFolder = tempFolder
self.localFile = self.buildLocalFile(self.downloadWebFile())
def buildFileName(self,webAddress):
fileName = "\\" + webAddress.split(r"/")[-1]
return fileName
def downloadWebFile(self):
try:
self.logs.newLogMessage(self,"Starting download...","Information")
# @eventoLogged: Start download.
webFile = urllib2.urlopen(self.webAddress)
# Opening the URL chosen.
self.logs.newLogMessage(self,"Finished download.","Information")
# @eventLogged: Download finished. Return a file object type.
return webFile
except:
self.logs.newLogMessage(self,"It was not possible to download file.\n" + str( sys.exc_info()[0]),"Error")
# @eventLogged: Error downloading file from URL
def buildLocalFile(self,webFile):
try:
self.logs.newLogMessage(self,"Writing file to disk...","Information")
# @eventLogged: Start writing to a local file.
fileName = self.buildFileName(self.webAddress)
localFile = open(self.tempFolder + fileName,"wb")
# Get the filename and open a localfile.
localFile.write(webFile.read())
# Write to brand new file.
# @todo: find a better way to write the file to disk
webFile.close()
localFile.close()
# Close both files. Clean-up action.
Esta classe é bastante simples. Ela usa o módulo urllib2 para fazer os downloads, além de construir um novo arquivo em disco, na pasta especificada. Vejam que o código da função downloadWebFile é muito simples. Apenas precisamos apontar qual é o arquivo que queremos puxar e ela já o constrói em disco, com um nome alterado com data - para não nos perdemos.
Você não precisa fazer mais nada, pois no construtor da classe, ela já dispara todas as ações de download e construção de arquivo em disco. Pode levar um tempo para puxar os arquivos, mas em geral é bastante rápido. Caso seja necessário, você terá de montar e inicializar um proxy - que também será mostrado.
A forma de uso é bastante simples:
shapeLeecher = leecherHandler.leecherHandlerClass('endereco do arquivo web','pasta temporaria de destino')
# para acessarmos o arquivo local em disco, utilzamos a seguinte sintaxe:
# shapeLeecher.localFile
Teremos agora um arquivo .zip em disco. Precisamos descompactá-lo. Crie uma nova classe, com a seguinte definição:
import sys, zipfile, logHandler
class zipHandlerClass():
def __init__(self,tempFolder,zipFilePathname,watchFileFormat="shp"):
self.logs = logHandler.logHandlerClass()
# Startup the log object
self.tempFolder = tempFolder
self.zipFilePathname = zipFilePathname
self.watchFileFormat = watchFileFormat
# Basic properties
self.containedFiles = []
# Generated file list inside zip archive
self.outputWatchFile = watchFileFormat
# outputInformation
def testZipFile(self):
if self.zipFilePathname == None or zipfile.is_zipfile == False:
self.logs.newLogMessage(self,"File pointed is not a valid zipfile" + str(sys.exc_info()[0]),"Error")
return False
else:
return True
def readContainedFiles(self,zipFile):
return zipFile.namelist()
def extractFiles(self):
try:
if self.testZipFile()== True:
extractingFile = zipfile.ZipFile(self.zipFilePathname,"r")
self.containedFiles = self.readContainedFiles(extractingFile)
# Defines a zipFile object using pathname for further manipulation. Read the files inside archive.
for zippedFile in self.containedFiles:
unpackedFile = open(self.tempFolder + "\\" + zippedFile, "wb")
unpackedFile.write(extractingFile.read(zippedFile))
unpackedFile.close()
# Unpack and write file to disk
self.logs.newLogMessage(self,"File " + zippedFile + " unpacked and written to disk.","Information")
# @eventLogged: Zip file unpacked and written in disk with success.
if zippedFile[-3:] == self.watchFileFormat:
self.outputWatchFile = zippedFile
else:
self.outputWatchFile = self.tempFolder
# Test to see if any of these files is of type watched.
# @todo: make this classe output a list of watched files.
extractingFile.close()
# close extracted file. CleanUp.
return True
else:
return False
# Test to see if it is a valid zipfile. If not, return false.
except zipfile.BadZipfile:
self.logs.newLogMessage(self,"Corrupted zipfile. Please download it again.","Error")
return False
Esta classe é um pouco mais complicada. Temos de monitorar um arquivo mestre (em nosso caso, um shapefile) para que possamos iniciar os próximos passos. Olhem a definição da classe - ela pede uma pasta temporária (a mesma que você usou com o leecher), uma localização do arquivo .zip e um formato. Este formato, tem como default a extensão .shp, mas você pode especificar outro.
# a idéia é que se use os resultados armazenados na classe leecher para alimentar
# o construtor da classe zipHandler, criando um processo encadeado
zipH = zipHandler.zipHandlerClass('pasta temporaria','arquivo.zip');
zipH.extractFiles()
No frigir dos ovos, esta classe testa o arquivo zip, confere se o mesmo é válido, extrai os arquivos para a pasta selecionada, e guarda uma referência ao arquivo com a extensão escolhida.
Vamos mostrar agora a clase folderHandler. Ela irá criar nossas pastas para nós:
import sys, os,datetime, shutil, logHandler
class folderHandlerClass():
def __init__(self,tempFolder):
self.logs = logHandler.logHandlerClass()
self.tempFolder = tempFolder
def generateTempFolderName(self):
dataHora = datetime.date.today()
folderName = self.tempZipFile[:-4] + "_" + str(dataHora)
return folderName
def createTempFolder(self):
try:
if os.path.exists(self.tempFolder):
if os.path.isdir(self.tempFolder):
return True
else:
self.logs.newLogMessage(self,"The specified folder is not a valid folder.","Error")
# @eventLogged: Folder is not valid.
return False
else:
os.mkdir(self.tempFolder,222)
self.logs.newLogMessage(self,"Folder " + self.tempFolder + " created with success.","Information")
# @eventLogged: Folder create with success.
return True
except:
self.logs.newLogMessage(self,"An unexpected error occurred while creating the specified folder.\n" + str(sys.exc_info()[0]),"Error")
# @eventLogged: Error while creating folder. More info on sys.exc_info()
def deleteTempFolder(self):
try:
shutil.rmtree(self.tempFolder)
except:
self.logs.newLogMessage(self,"An unexpected error occurred while deleting the specified folder.\n" + str(sys.exc_info()[0]),"Error")
# @eventLogged: Error while deleting folder. More info on sys.exc_info()
Esta classe tem dois métodos principais: um para criar pastas e outro para deletá-las. A classe executa uma verificação básica para saber se a pasta existe ou é uma pasta. Em caso de positivo a pasta não é criada, apenas usada como destino dos arquivos. Caso ela não exista, a classe tenta criá-la. Isto é feito para economizarmos uma operação e evitar de criar uma pasta com um nome já existente, o que nos daria um erro.
O método que deleta a pasta é radical. Ele remove tudo que existe dentro da pasta. Sub-pastas, arquivos, não importa. Ele irá deletar tudo. Cuidado ao usá-lo com outros arquivos. O ideal para se trabalhar com este script é uma pasta separada só para ele. Para usar esta classe, faça assim:
# como usar:
folderH = folderHandler.folderHandlerClass('pasta temporaria')
folderH.createTempFolder()
# ele irá tentar criar a pasta
folderH.deleteTempFolder()
# ele irá tentar deletar a pasta e seu conteúdo
Bem, com estas classes conseguimos construir um pequeno framework para atualizarmos os dados em um banco do ArcGIS. São coisas simples, mas que ajudam bastante. No próximo post, teremos a classe que cuida das operações de geoprocessamento, interfaceando com o ArcGIS - e uma classe "mestra" que faz todas estas classes conversarem.
Como utilizar as três classes juntas?
import sys,folderHandler,zipHandler,leecherHandler
# primeiro tentamos acessar/criar a pasta que queremos
# lembrem-se que um r antes de uma string, significa raw
# não sendo necessário escapar os caracteres \
folderH = folderHandler.folderHandlerClass(r"C:\temporario")
folderH.createTempFolder()
# é só inicializar o leecher Handler. seu construtor cuida do restante para nós
shpLeecher = leecherHandler.leecherHandlerClass("ftp://sigmine.dnpm.gov.br/Brasil.zip",folderH.tempFolder)
# podemos inicializar o zipHandler de duas maneiras
# com uma chamada explícita
zipH = zipHandler.zipHandlerClass(folderH.tempFolder,shpLeecher.localFile,"shp")
# ou implicita, caso a extensao seja shapefile - pois é um parametro com uma entrada
# default. se nada for especificado, .shp irá cair em seu lugar
# zipH = zipHandler.zipHandlerClass(folderH.tempFolder,shpLeecher.localFile)
zipH.extractFiles()
Com o código acima e as três classes descritas, já conseguimos puxar um arquivo da web e descompactá-lo para uma pasta qualquer.
Vocês podem notar que existem outras classes envolvidas aqui, especialmente o tal de LogHandler. Irei disponibilizar todo o projeto no próximo post
Quaisquer dúvidas, estou à disposição!
Abraços
George