Desenvolvimento - Visual Basic

Como Implementar Classes no VB

A classe SAX pode ser uma maneira mais eficaz de acessar dados XML. Compare a SAX com o objeto DOMDocument e veja porquê.

por A. Russel Jones



As versões mais recentes do XML Parser da Microsoft (MSXML) permitem que você analise o conteúdo dos documentos XML de duas maneiras. Na primeira (já familiar), você cria um objeto DOMDocument e usa o método Load ou LoadXML para analisar o documento inteiro na memória. Na segunda (menos conhecida), usa o analisador SAX (Simple API for XML). Nesta coluna, mostrarei como usar o analisador SAX.

O analisador SAX não carrega um documento inteiro na memória; em vez disso, inicia a análise pelo início do documento e gera eventos para os diversos elementos que vai encontrando no arquivo. Você escreve o código para testar os valores dos documentos durante a ocorrência dos eventos. Ao contrário do DOM (Document Object Model), o SAX não oferece acesso à estrutura do documento, apenas aos seus conteúdos. Embora a abordagem DOM funcione melhor em documentos XML menores ou em documentos em que se planeje executar várias operações, você constatará que o SAX funciona muito melhor em documentos maiores ou naqueles em que deseja executar apenas uma operação, como uma busca.

A Microsoft chamou a implementação atual do SAX de SAX2. Infelizmente, o uso do analisador SAX2 da Microsoft a partir do Visual Basic não é tão fácil como deveria ser. Grande parte do analisador SAX2 é formada de interfaces (em vez de objetos), e isso faz com que o usuário tenha de criar suas próprias classes para implementar as interfaces antes de poder receber os eventos.

Primeiro, crie um objeto SAXXMLReader. Esse objeto sozinho faz muito pouco, mas funciona como um ponto central de coleta para vários outros objetos de análise de conteúdo (IVBSAXContentHandler), tratamento de erros (IVBSAXErrorHandler), tratamento de Document Type Definitions (IVBSAXDTDHandler) etc. Este artigo analisará apenas as duas interfaces iniciais; você poderá usar a mesma técnica para implementar as outras interfaces. O objeto SAXXML.Reader expõe os métodos parse e parseURL, mas esses métodos delegam a tarefa de geração de eventos para as classes em que as interfaces são implementadas. Tudo isso parece complicado - e, na verdade, é mais complicado do que criar um objeto DOMDocument e chamar seu método Load. O código de exemplo apresentado a seguir mostra o processo usado para analisar um documento com o SAX2:

Listagem 1: Analisando um document com SAX 2

Dim reader as New SAXXMLReader
Dim handler as New _
	CSAXContentHandlerDefault
Dim errHandler as New _
	CSAXErrorHandlerDefault
Set reader.contentHandler = handler
Set reader.errorHandler = errHandler
On Error Resume Next
reader.parseURL <some file>
If Err.Number <> 0 then
	" trate o erro aqui
End If

SAX2 vs. DOMDocuments

Se observar alguns dos exemplos, você poderá ver uma justificativa para o uso dos complexos SAXs. Criei um documento XML com 100.000 itens. O documento é regular (ou seja, os itens se repetem em uma seqüência previsível) e contém apenas nomes e números de telefone fictícios.

Listagem 2: Trecho do arquivo XML utilizado

<people>
	<person id="anID">
		<name>person1</name>
		<tel>568-846-8430</tel>
	</person>
	<!-many more <person> entries ->
</people>

O arquivo tem 11,5 MB e leva apenas alguns segundos para carregar em um Pentium III de 800 MHz que utilize um objeto DOMDocument. É possível estimar o requisito de memória do DOMDocument como três vezes o tamanho do XML bruto (nesse caso, 33 MB). Em contrapartida, a análise do arquivo todo com o SAX2 leva menos de três segundos e utiliza menos de 1 MB de memória - ou seja, uma diferença de 1.000% a menos de recursos. Infelizmente, esses tempos de análise representam apenas o tempo que essas duas tecnologias levam para carregar o arquivo. Um teste mais realista seria carregar o arquivo e solicitar informações sobre ele (por exemplo, contar o número de elementos person contidos no arquivo). Um DOMDocument executa esse teste em aproximadamente 4,5 segundos, ao passo que o SAX2 requer 6,5 segundos. Em outras palavras, no caso de operações que envolvam muitos nós, o uso do DOMDocument será mais indicado. Já em um terceiro teste, que localiza o primeiro elemento person e recupera o valor de um atributo ID, o SAX2 leva enorme vantagem sobre o DOCDocument. Os números mostrados são medidos a cada 100 iterações, por meio um aplicativo compilado com todas as otimizações ativadas (consulte a Tabela 1).

Tabela 1: Quanto tempo leva para analisar um documento. Compare na tabela abaixo os tempos médios exigidos para analisar e recuperar informações em um documento que contém 100.000 elementos por meio destes três métodos: objeto DOMDocument, analisador SAX2 com uma classe IVBSAXContentHandler implementada para cada operação e analisador SAX2 com implementação de classe IVBSAXContentHandler genérica. Todos os tempos são exibidos em milissegundos.

Operação executada Objeto DOMDocument SAX2 Parser - Implementação Específica SAX2 Parser - Implementação Genérica
Carregar (apenas) 3,784 2,945 2,975
Carregar e contar todos os nós person 4,495 6,671 7,962
Carregar e recuperar a ID do primeiro elemento person 3,851

À medida que o tamanho do arquivo aumentava, constatamos a vantagem do arquivo grande SAX2 também em relação à análise (provavelmente porque o DOMDocument precisa criar grande buffers de memória, o que exige que o sistema armazene os dados em spool durante o aumento de tamanho dos arquivos).

Se você dobrar o número de entradas no arquivo e aumentar o tamanho do arquivo para 23 MB, a versão DOMDOcument exigirá 65 MB de memória e levará oito segundos para carregar. A versão SAX2, por outro lado, levará menos de seis segundos para carregar e ainda usará menos de 1 MB de memória. As principais diferenças entre os dois ocorrem quando se tenta recuperar um valor específico do arquivo. Se o valor estiver posicionado no final do arquivo, o objeto DOMDocument será mais rápido. No entanto, se o valor estiver localizado próximo ao início do arquivo, o SAX2 será mais rápido porque você poderá interrompê-lo assim que localizar o valor. Fiquei surpreso com os números; esperava que a versão SAX fosse mais rápida para análises em geral porque não requeria a criação de uma hierarquia de objetos durante o processo de análise. Na verdade, de acordo com meus testes, a diferença entre usar o SAX2 e um objeto DOMDocument (pelo menos nesta versão) é basicamente uma questão de localização dos dados exigidos e de disponibilidade da memória exigida pelo objeto DOMDocument. A não ser que você saiba com antecedência a localização dos dados em um documento XML, a principal razão para usar o SAX em vez de DOM é poupar recursos.

Como Implementar as Interfaces

Você cria uma classe que trate eventos SAX implementando tanto a interface IVBSAXContentHandler como a interface IVBSAXError. No entanto, a não ser que precise expor os eventos durante a validação, você não precisará implementar a interface IVBSAXDTDHandler. Coloque esse código na seção Declarations da classe:

Listagem 3: Implementando a interface IVBSAXErrorHandler

Implements IVBSAXContentHandler
Implements IVBSAXErrorHandler

Você deve implementar todos os eventos expostos por essas interfaces, mas não precisa escrever o código em nenhum deles. Basta um clique em cada evento no IDE e o VB adicionará a definição do método através de um evento stub. Por exemplo, o IVBSACContentHandler expõe um evento startElement. O evento é acionado depois que o analisador completa a análise de uma tag de elemento de início; desse modo, ele expõe um objeto que fornece acesso a todos os atributos, além do nome da tag:

Listagem 4: Implementação do evento startElement da interface

Private Sub _
	IVBSAXContentHandler_startElement _
	(strNamespaceURI As String, _
	strLocalName As String, _
	strQName As String, ByVal oAttributes As _
	MSXML2.IVBSAXAttributes)
" seu código aqui
End Sub

Use o evento startElement para localizar uma instância de um elemento ou para recuperar um valor de atributo. Use o evento characters para recuperar os valores de texto do elemento. Por exemplo, para reunir os nomes e IDs de cada pessoa do documento em um objeto Dictionary, você precisa testar o argumento strLocalName passado para o evento startElement do elemento . Quando o evento startElement for acionado, o analisador fornecerá um objeto Attributes contendo os nomes e valores do atributo, classificados por índice, para que você possa recuperar o valor da ID imediatamente e usá-lo para a chave no objeto Dictionary, denominada dNames neste código:

Listagem 5: Complementação do evento startElement

" no evento startElement
If strLocalName = "person" Then
	" obtenha o valor do primeiro atributo (a id)
	s = oAttributes.getValue(0)
	dNamesIDs.Add s, ""
	lastID = s
End If

No código anterior, a variável lastID contém o valor que você usou para atribuir o nome à chave correspondente no objeto Dictionary. O evento startElement não oferece acesso ao valor de texto de um elemento. Para recuperar o valor de texto do elemento, é necessário definir um sinalizador no evento startElement e, em seguida, recuperar o valor no próximo evento characters:

Listagem 6: Definindo o sinalizador isName

" no evento startElement
If strLocalName = "name" Then
	isName = True
Else
	isName = False
End If

No evento characters, se o sinalizador isName for True, os caracteres fornecidos pertencerão à tag , e você poderá atribuí-los durante o evento:

Listagem 7: Implementação do evento characters

Private Sub _
	IVBSAXContentHandler_characters (text As String)
	If isName Then
		dNamesIDs(lastID) = text
		isName = False
	End If
End Sub

Se estiver codificando eventos apenas para maior agilidade, crie uma nova classe que implemente a interface IVBSAX para cada documento XML ou tipo de operação e escreva seu código diretamente nos eventos gerados pela interface IVBSAXContentHandler. Se estiver disposto a abrir mão de um pouco da velocidade, crie uma classe individual genérica que gere todos os eventos de volta para o programa. Esse procedimento, além de remover o código personalizado do módulo de classe e de colocá-lo de volta em seus formulários e módulos, facilita muito o uso do analisador SAX2. Para cada evento de interface, declare um Public Event correspondente na seção Declarations:

Listagem 8: Declaração dos eventos

Implements IVBSAXContentHandler
Implements IVBSAXErrorHandler

Public Event StartElement (sNamespaceURI As String, _
	strLocalName As String, sQName As String, attributes As _
	MSXML2.IVBSAXAttributes)

Public Event FatalError (oLocator As MSXML2.IVBSAXLocator, _
	strError As String, nErrorCode As Long)

"etc.

Como Implementar a Classe SAXHandler

Implementei a classe SAXHandler no código dessa maneira genérica. A vantagem de uma classe genérica é a possibilidade de escrever o código para os eventos no seu programa principal, em vez de no arquivo de classe. Por exemplo, você pode usar a classe SAXHandler para coletar os nomes e IDs do arquivo XML de amostra (consulte a Listagem 1).

A classe SaxHandler implementa tanto a interface IVBSAXContentHandler como IVBSAXErrorHandler. À medida que essas interfaces gerarem eventos, a classe gerará novamente os eventos para que o usuário possa tratá-los em um formulário ou módulo. Esse exemplo mostra como codificar os eventos para coletar IDs de valores no arquivo de amostra.

Listagem 9: Codificação dos eventos para recuperar valores do arquivo

" insira o código em um formulário ou módulo
" não no arquivo de classe SaxHandler
Private WithEvents handler As SaxHandler
Private isName As Boolean
Private lastID As String
Private dNamesIDs As New Dictionary

Private Sub handler_StartElement _
	(sNamespaceURI As String, _
	strLocalName As String, sQName As String, _
	oAttributes As MSXML2.IVBSAXAttributes)
	Dim s As String
	If strLocalName = "person" Then
		s = oAttributes.getValue(0)
		dNamesIDs.Add s, ""
		lastID = s
	End If
	If strLocalName = "name" Then
		isName = True
	Else
		isName = False
	End If
End Sub
Private Sub handler_Characters (text As String)
	If isName Then
		dNamesIDs(lastID) = text
		isName = False
	End If
End Sub

O mecanismo de geração de eventos VB é notoriamente lento. Sendo assim, evite gerar eventos que não sejam de seu interesse. Você pode vincular a classe SAXHandler para gerar apenas eventos específicos, criando um conjunto de propriedades booleanas (Boolean), uma para cada evento:

Listagem 10: Propriedades booleanas para auxiliar o tratamento de eventos

Public handleStartElement As Boolean
Public handleEndElement As Boolean
"etc. 

O VB inicializa essas propriedades como False, a fim de que a classe SAXHandler não gere eventos por padrão. Defina a propriedade desejada como True para indicar o seu interesse em um evento. A classe verificará internamente o valor da propriedade associada antes de gerar o evento:

Listagem 11: Verificação das variáveis para tratamento dos eventos

If handleStartElement Then
	RaiseEvent startElement _
		(strNamespaceURI, strLocalName, _
		strQName, oAttributes)
End If

O objeto SAXXMLReader não possui um método para interromper a análise. Para interromper a análise, você precisará gerar um erro. Dentre os três métodos IVBSAXErrorHandler (Warning, Error e FatalError), o analisador SAX2 atual implementa apenas o evento FatalError. O analisador gerará esse evento sempre que encontrar um erro.

Para solucionar esse problema, a classe SAXHandler adiciona um método StopParsing. Esse método simplesmente define uma variável booleana interna mStopParsing como False quando você chama o método. A não ser que seja absolutamente necessário interromper imediatamente o analisador, você poderá continuar verificando o valor da variável mStopParsing em mais alguns eventos. A classe SaxHandler verificará apenas os eventos startElement, endElement e documentLocator. Se o sinalizador for True, a classe gerará um erro com um número de erro de SAX_ERROR_STOP_PARSING, que será definido como um valor Enum público. A geração desses erros acionará o evento FatalError, que também verificará o valor de erro e ignorará os erros de SAX_ERROR_STOP_PARSING; no caso de outros erros, o evento gerará outra vez o erro. Para interromper a análise de um objeto SAXHandler, basta chamar o método StopParsing. Por exemplo, o aplicativo de exemplo interromperá a análise após encontrar o primeiro nó do tipo ID utilizando este código (observe a Figura 1):

Exemplo - Test SAX/DOM

Figura 1:Exemplo - Test SAX/DOM

Listagem 12: Interrupção do método após localização do ID

If strLocalName = "person" Then
	For i = 0 To oAttributes.length
		If oAttributes.getLocalName(i) = "id" Then
			firstNodeID = oAttributes.getValue(i)
			generichandler.stopParsing
			Exit For
	End If
	Next
End If

É possível tornar seu código mais legível e de fácil manutenção empacotando implementações de interfaces e classes. Você também pode adicionar funcionalidades que a classe ou interface originais não implementam. No entanto, os empacotadores de classe prejudicam o desempenho. O empacotador do exemplo nesta coluna reduz o desempenho em 15%. Esse é o preço que você paga pela conveniência de usar classes individuais (em vez de implementar classes personalizadas para cada tipo de arquivo). Você deve decidir se a economia no desenvolvimento e na manutenção compensa esse preço.

Sobre o Autor

A. Russel Jones, Ph.D. é engenheiro sênior de Web. Ex-criador de répteis e músico profissional, atualmente compõe aplicativos de software. Autor do Visual Basic Developer’s Guide to ASP and IIS e Mastering Active Server Pages 3 (Sybex).

A. Russel Jones

A. Russel Jones