ATC/DDD na Ponta dos Dedos: Extração de Dados com Web Scraping
Como transformar o conteúdo relevante de uma página web em uma base de dados acessível e que pode ser extremamente útil para estudos clínicos?

Na época da escola, se você ainda se lembra, estudamos nas aulas de Biologia uma coisa chamada taxonomia. Você lembra o que é?
Basicamente, a taxonomia é uma ciência que se dedica a organizar os seres vivos de forma hierárquica, considerando caracteres comuns entre eles. A cada nível hierárquico que se avança, mais específico é o grupo (ou, nesse caso, táxon) onde o organismo de interesse estará alocado.
Embora esse conceito seja mais conhecido quando aplicado à classificação dos seres vivos, é possível adotar o mesmo conceito para vários outros grupos. Ao identificarmos padrões, por exemplo, em prefixos do transporte público, placas de carros, números de série de vários utensílios, vemos na prática as possibilidades de organização de tudo o que está ao nosso redor baseando-se em suas características, desde as mais comuns, chegando até o caractere mais específico possível, o mais único para aquele objeto de interesse.
Na pesquisa clínica, também não é diferente. Dentre muitos padrões relacionados às boas práticas preconizadas pelas maiores autoridades na área, alguns sistemas de codificação estão presentes. Um bom exemplo é a classificação ATC, que é o foco deste artigo.
A Classificação ATC (Anatomical Therapeutic Chemical Code) é uma referência internacional utilizada para catalogar substâncias com ação terapêutica. Isso inclui, por exemplo, medicamentos usados diariamente para aliviar ou tratar uma ampla variedade de doenças. Nos estudos clínicos, a classificação ATC é considerada o padrão-ouro para o monitoramento e pesquisa de diversos medicamentos administráveis.
A Taxonomia do ATC
Assim como em uma classificação biológica, o ATC também começa de um universo mais amplo (o grupo anatômico) rumo a grupos mais específicos (classes de medicamentos, grupos de moléculas, entre outros); o que diferencia é que, basicamente, em vez de um nome latino (como Canis lupus familiaris para os cães domésticos), o ATC retorna um código sequencial, refletindo cada grupo o qual aquela substância terapêutica pertence.
Esse código sequencial é dividido em cinco níveis, a saber:
Nível 1 – Domínio ou Grupo Anatômico (letra inicial, ex.: A, B, C)
Nível 2 – Subgrupo terapêutico principal (ex.: A10 – Drogas usadas no diabetes)
Nível 3 – Subgrupo terapêutico/farmacológico (ex.: A10B)
Nível 4 – Subgrupo químico/terapêutico/farmacológico (ex.: A10BA)
Nível 5 – Substância química (ex.: A10BA02 – Metformina, com DDD, dose, via, etc.)
O que será feito aqui
Organizando nossos pensamentos e ações, isto é o que será feito nesse artigo:
Acesso à página da lista ATC;
Reconhecimento dos dados de interesse, através da identificação e entendimento da estrutura em HTML da página;
Web scraping dos dados;
Armazenamento dos dados raspados em um arquivo estruturado (no caso, um arquivo Excel).
Como os dados de interesse estão dispostos na página oficial: entendendo o HTML
Se você chegou até aqui e sabe pouco, ou não tem a mínima ideia do que seja HTML, eis o que você precisa saber para continuar: HTML, sigla para HyperText Markup Language, é, como o nome diz, uma linguagem de marcação. Ela serve para definir a estrutura e o significado de uma página web. Imagine o esqueleto de um ser vivo; assim como os ossos são dispostos a definir o formato de um corpo, as tags HTML ajudam a definir a estrutura de uma página.
A imagem abaixo demonstra exemplos de tags que nós podemos encontrar em um texto HTML; à medida em que formos explorando o conteúdo das páginas web, reconheceremos a função de algumas tags mostradas aqui.
Como acessar a estrutura HTML de um site para ver as tags
Uma vez que sabemos da existência de uma estrutura HTML para um site, antes de partirmos à extração de dados propriamente dita, é interessante que tenhamos conhecimentos de meios de ver a estrutura de um site; assim, quando formos colocar a mão na massa, podemos sempre recorrer a uma fonte contínua que mostre as tags que compõem a página.
Os browsers que são mais utilizados atualmente costumam ter um recurso chamado 'Inspecionar', que pode ser acessado pela lista de opções que aparece quando se pressiona o botão direito do mouse. Como o próprio nome já diz, o recurso serve para fazer uma verificação mais profunda na estrutura da página que está sendo vista.
Uma vez selecionado o 'Inspecionar', aparecerá uma janela à parte em seu navegador. O que interessa para essa prática está em uma aba que tem variados nomes, como 'Inspetor' ou 'Elementos'. É possível identificar do que se trata ao ver as várias linhas de código que saltam aos olhos; compare com os exemplos que já foram dados, e você logo perceberá que se trata da estrutura HTML, ou não.

Com esse conhecimento em mãos, já podemos começar a usar o Python para acessar as páginas de interesse, e extrair os dados que estão nelas.
Utilizando Python para fazer o web scraping
Para esta parte, precisaremos importar uma série de bibliotecas que serão as responsáveis por acessarmos as páginas, extrairmos os dados e, por fim, adicionarmos tudo a uma planilha, que dará sentido a tudo o que fizermos.
import requests
from bs4 import BeautifulSoup as BS
import string, re
import pandas as pd
import numpy as np
import gc
from time import sleep
Requests - HTTP para seres humanos
re - Operações com REGEX (Expressões regulares)
string - Operações com strings (sequência de caracteres)
pandas - A biblioteca de análise de dados mais usada em Python
numpy - A principal biblioteca Python para computação científica
BeautifulSoup - Para extrair dados em HTML
gc - O garbage collector do Python, para administrar a memória utilizada
time - Para tudo que envolve tempo
Reconhecendo o que existe no site do ATC
A primeira coisa que faremos antes de começarmos a extrair os dados é entender como o site do ATC/DDD Index está estruturado. Para tanto, será preciso acessar o site https://atcddd.fhi.no/atc\_ddd\_index/ . A imagem abaixo indica o que vamos encontrar ao acessar o link.

Ao usarmos o recurso 'Inspecionar' do browser, teremos acesso a cada detalhe da estrutura HTML, que é o que realmente importa aqui. Com isso já em mãos, podemos seguir para a escrita do código.
Construindo uma função de requisição (e evitando possíveis problemas)
Para fazer a raspagem dos dados, a construção do script será orientada por funções; isso será feito baseado no princípio de que a modularização facilita a legibilidade, manutenção, e reusabilidade do código.
A primeira função a ser criada é uma das mais importantes do script inteiro:
def get_page_content(url, retries=3, delay=0.5):
"""Função para fazer requisições com retry e delay"""
for attempt in range(retries):
try:
response = requests.get(url, timeout=10)
sleep(delay) # Evita sobrecarregar o servidor
return response.content
except requests.RequestException as e:
if attempt == retries - 1:
print(f"Erro ao acessar {url}: {e}")
return None
sleep(delay * (attempt + 1))
return None
O que temos aqui é uma função responsável por fazer as requisições ao site do ATC/DDD, construída de uma maneira que previna alguns problemas que podem ocorrer quando exploramos de forma automática páginas web.
requests.getcomtimeout: evita que o código fique travado indefinidamente.Retry com backoff (
delay * (attempt + 1)): trata instabilidades temporárias da rede ou do servidor, insistindo no código apenas depois de um tempo.Pequenos
sleeps: respeita o servidor, reduz risco de um possível bloqueio por parte do servidor; isso poderia acontecer se fossem feitas muitas requisições em um período curto de tempo, o que poderia gerar uma sobrecarga no servidor, tornando o conteúdo do site indisponível para acesso.
Identificando os grupos anatômicos (nível 1)
O primeiro nível do ATC usa letras simples de A a Z, mas nem todas são válidas. A partir disso, uma função que itera por todo o alfabeto (usando o string.ascii_uppercase) é construída, e usa a presença de uma tag <h2> na resposta como sinal de que aquela letra não corresponde a um grupo existente.

def extract_domain_codes():
"""Extrai códigos de domínio (nível 1)"""
print("Extraindo códigos de domínio...")
results = []
for letter in string.ascii_uppercase:
content = get_page_content(f'https://atcddd.fhi.no/atc_ddd_index/?code={letter}&showdescription=no')
if not content:
continue
soup = BS(content, 'html.parser')
if soup.find('h2'):
print(f'{letter} inválido')
else:
print(f'{letter} válido')
try:
link = soup.find('b').find('a')
code = re.search('[A-Z]', link['href']).group(0)
name = link.text.strip()
results.append({'code': code, 'name': name, 'level': 1})
except (AttributeError, TypeError):
print(f"Erro ao processar {letter}")
return pd.DataFrame(results)
O BeautifulSoup faz o parsing do HTML bruto retornado pelo requests, e o re (regex) extrai o código ATC do atributo href do link. Esse padrão de usar regex nos hrefs ao invés de depender do texto visível é mais robusto, porque o texto pode variar, mas a estrutura da URL tende a ser consistente. Ainda vale destacar a atribuição de um nível hieráquico para cada resultado, assim como é estabelecido no ATC/DDD; nesse caso, o nível atribuído é o número 1. A mesma coisa será feita para os níveis seguintes.
O resultado final é uma lista de dicionários que, usando o pandas, se tornará um DataFrame, que estará pronto para ser exportado no final de tudo.
Identificando os grupos terapêuticos (nível 2)
Aqui entra um ponto de autoridade importante: a preservação da hierarquia. Isso quer dizer que, para alcançar os códigos seguintes, será preciso considerar o código do nível anterior. Assim:
Para cada domínio (nível 1), você acessa novamente o site. Uma vez que já temos os domínios válidos, serão acessados apenas estes.
Usa regex para encontrar códigos
[A-Z][0-9]+dentro dos links. Isso porque, em cada link de domínio, aparece uma lista com os códigos do nível seguinte.Relaciona cada código de nível 2 com o parent_code (a letra do domínio). Exemplo: para o domínio
A, estarão relacionados os códigos de nível 2A01,A02, e assim por diante.

Esse parent_code é crucial: ele permite, mais tarde, reconstruir a árvore completa sem precisar “chutar” ligações. Essa preocupação com hierarquia torna seu scraping analiticamente útil, não apenas uma extração bruta de texto.
def extract_therapeutic_subgroups(domain_codes):
"""Extrai subgrupos terapêuticos (nível 2)"""
print("Extraindo subgrupos terapêuticos...")
results = []
for _, row in domain_codes.iterrows():
letter = row['code']
content = get_page_content(f'https://atcddd.fhi.no/atc_ddd_index/?code={letter}&showdescription=no')
if not content:
continue
soup = BS(content, 'html.parser')
for b_tag in soup.find_all('b'):
try:
link = b_tag.find('a')
if not link:
continue
code_match = re.search('[A-Z][0-9]+', link['href'])
if code_match:
code = code_match.group(0)
name = link.text.strip()
# Relaciona com o domínio pai
parent_code = code[0] # Primeiro caractere é o código do domínio
results.append({
'code': code,
'name': name,
'parent_code': parent_code,
'level': 2
})
except (AttributeError, TypeError):
continue
return pd.DataFrame(results).drop_duplicates(subset=['code'])
Identificando subgrupos farmacológicos (nível 3) e químicos (nível 4)
Para os próximos dois níveis, 3 e 4, a lógica de preservação e relação com hierarquias segue sendo utilizada, mas com algumas diferenças para cada subgrupo, devido a suas peculiaridades. Vejamos:
def extract_pharmacological_subgroups(therap_subgroups):
"""Extrai subgrupos farmacológicos (nível 3)"""
print("Extraindo subgrupos farmacológicos...")
results = []
for _, row in therap_subgroups.iterrows():
code = row['code']
content = get_page_content(f'https://atcddd.fhi.no/atc_ddd_index/?code={code}&showdescription=no')
if not content:
continue
soup = BS(content, 'html.parser')
for b_tag in soup.find_all('b'):
try:
link = b_tag.find('a')
if not link:
continue
code_match = re.search('[A-Z][0-9]{2}[A-Z]', link['href'])
if code_match:
new_code = code_match.group(0)
name = link.text.strip()
parent_code = new_code[:3] # ex.: A10
results.append({
'code': new_code,
'name': name,
'parent_code': parent_code,
'level': 3
})
except (AttributeError, TypeError):
continue
gc.collect()
return pd.DataFrame(results).drop_duplicates(subset=['code'])
def extract_chemical_subgroups(pharm_subgroups):
"""Extrai subgrupos químicos (nível 4)"""
print("Extraindo subgrupos químicos...")
results = []
for _, row in pharm_subgroups.iterrows():
code = row['code']
content = get_page_content(f'https://atcddd.fhi.no/atc_ddd_index/?code={code}&showdescription=no')
if not content:
continue
soup = BS(content, 'html.parser')
for b_tag in soup.find_all('b'):
try:
link = b_tag.find('a')
if not link:
continue
code_match = re.search('[A-Z][0-9]{2}[A-Z]{2}', link['href'])
if code_match:
new_code = code_match.group(0)
name = link.text.strip()
parent_code = new_code[:4] # ex.: A10B
results.append({
'code': new_code,
'name': name,
'parent_code': parent_code,
'level': 4
})
except (AttributeError, TypeError):
continue
gc.collect()
return pd.DataFrame(results).drop_duplicates(subset=['code'])
Aqui, o que vale destacar é o seguinte:
Há expressões regulares específicas para cada nível. Nesta altura, se sobressai o conhecimento da estrutura do ATC/DDD, indicando exatamente como cada código está disposto conforme seu nível.
Construção sistemática do
parent_codepor fatiamento de string.Uso de
gc.collect()para aliviar memória, uma preocupação que aparece em scrapers que percorrem muitas páginas. Isso se torna essencial quando vamos lidar com scripts como esse em máquinas cuja memória é bastante limitada. O que não está sendo mais utilizado é excluído com segurança, de modo que, a cada etapa passada, não seja comprometida tanta memória a ponto de travar a atividade.
Essa disciplina de padrões e relacionamentos é o que transforma o código em infraestrutura de dados, não apenas em um script pontual para extrair informação de um dado local.

Identificando as substâncias químicas (nível 5) e dados complementares

Essa é a parte final de extração de dados da página do ATC/DDD, e a que possui maior riqueza de detalhes. Aqui, além do código ATC por inteiro, faremos a extração do DDD, da unidade da dose, a via de administração, além de possíveis observações. Para tanto, precisamos considerar primeiramente um ponto em específico:
Alguns códigos não possuirão dados como a dose, a unidade de dose, e a via de administração. Para esses casos, precisaremos fazer um tratamento de dados faltantes, visando evitar possíveis quebras de linha que bagunçariam o resultado final.
Isso será feito da seguinte maneira:
Trataremos explicitamente células vazias que vêm como
'\xa0'(espaço não separável em HTML).Substituiremos essas células por
np.nan(do NumPy), que é o padrão industrial para representar missing values em análises numéricas.Manteremos
dose,unit,admin,notebem separados – isso facilita desde análise estatística até construção de dicionários de medicamentos. A célula que tiver dado ausente será representada por umnan, assim garantindo a representação de todas as colunas.
def extract_chemical_substances(chem_subgroups):
"""Extrai substâncias químicas (nível 5)"""
print("Extraindo substâncias químicas...")
results = []
for _, row in chem_subgroups.iterrows():
code = row['code']
print(f'Processando dados de {code}')
content = get_page_content(f'https://atcddd.fhi.no/atc_ddd_index/?code={code}&showdescription=no')
if not content:
continue
soup = BS(content, 'html.parser')
table = soup.find('table')
if table:
for tr in table.find_all('tr'):
tds = tr.find_all('td')
if len(tds) >= 6: # Garante que temos todas as colunas
try:
atc_match = re.search('[A-Z][0-9]{2}[A-Z]{2}[0-9]{2}', tds[0].text)
if atc_match:
substance_code = atc_match.group(0)
name = tds[1].text.strip()
dose = tds[2].text.strip() if tds[2].text.strip() != '\xa0' else np.nan
unit = tds[3].text.strip() if tds[3].text.strip() != '\xa0' else np.nan
admin = tds[4].text.strip() if tds[4].text.strip() != '\xa0' else np.nan
note = tds[5].text.strip() if tds[5].text.strip() else np.nan
parent_code = substance_code[:5] # ex.: A10BA
results.append({
'code': substance_code,
'name': name,
'dose': dose,
'unit': unit,
'admin': admin,
'note': note,
'parent_code': parent_code,
'level': 5
})
except (IndexError, AttributeError):
continue
gc.collect()
return pd.DataFrame(results).drop_duplicates(subset=['code'], keep='last')
Juntando tudo e construindo a árvore final: das páginas do ATC/DDD para uma estrutura de dados
Uma vez realizadas as coletas em todos os níveis de código ATC e dados complementares, chegamos à parte mais crítica do código: juntar todos os resultados em um DataFrame estruturado, observando sempre a preservação das hirearquias entre níveis.
Isso será feito usando os seguintes passos:
Performando joins hierárquicos em pandas usando colunas derivadas (
str[:4],str[:3], etc.).Uso de
how='left'para não perder nenhuma substância mesmo que alguma descrição de nível superior falte.Renomear colunas para nomes sem ambiguidade (
chemical_name,therapeutic_name, etc.).
O resultado será um DataFrame onde cada linha representa uma substância química, acompanhada de:
Domínio (nível 1)
Subgrupo terapêutico (nível 2)
Subgrupo farmacológico (nível 3)
Subgrupo químico (nível 4)
Dados de DDD, unidade, via, nota (nível 5)
def build_hierarchical_structure(level1, level2, level3, level4, level5):
"""Constrói a estrutura hierárquica de forma eficiente"""
print("Construindo estrutura hierárquica...")
# Para o nível 5, fazemos joins hierárquicos diretos
result = level5.copy()
# Join com nível 4
level4_slim = level4[['code', 'name']].rename(columns={'code': 'parent_code', 'name': 'chemical_name'})
result = result.merge(level4_slim, on='parent_code', how='left')
# Join com nível 3
result['pharm_parent'] = result['parent_code'].str[:4]
level3_slim = level3[['code', 'name']].rename(columns={'code': 'pharm_parent', 'name': 'pharmacological_name'})
result = result.merge(level3_slim, on='pharm_parent', how='left')
# Join com nível 2
result['therap_parent'] = result['pharm_parent'].str[:3]
level2_slim = level2[['code', 'name']].rename(columns={'code': 'therap_parent', 'name': 'therapeutic_name'})
result = result.merge(level2_slim, on='therap_parent', how='left')
# Join com nível 1
result['domain_parent'] = result['therap_parent'].str[:1]
level1_slim = level1[['code', 'name']].rename(columns={'code': 'domain_parent', 'name': 'domain_name'})
result = result.merge(level1_slim, on='domain_parent', how='left')
# Limpa colunas auxiliares
result = result.drop(['parent_code', 'level', 'pharm_parent', 'therap_parent', 'domain_parent'], axis=1)
return result
O DataFrame que surge de resultado é, basicamente, uma tabela bem estruturada, que considera todos os níveis de código do ATC e os junta de uma forma facilmente entendível. Esse resultado está pronto para ser utilizado de várias formas; vejamos alguns exemplos:
Modelos de classificação de medicamentos
Consolidação de bases de prescrição
Integrações com sistemas regulatórios
Dashboards de consumo/uso de medicamentos
La grande finale: o pipeline completo em Python
No final de tudo, todos as funções que já foram criadas serão incluídas em um único pipeline, na função main(), dividido em etapas lógicas que invocam cada nível do código em seu momento oportuno. Além disso, a função conta com logs que informam diretamente no console quantos registros foram extraídos em cada nível percorrido.
Todos os dados extraídos e organizados são finalmente consolidados em um arquivo Excel (.xlsx), valendo-se da função do pandas apropriada para a atividade; esse arquivo estará pronto para o consumo e verificação de qualquer usuário. Ainda, há o tratamento de possíveis exceções em uma escala global: basicamente, o script avisa qual exatamente foi o erro que ocorreu durante a sua execução, tornando a resolução mais rápida e assertiva.
def main():
"""Função principal otimizada"""
try:
# Extrai dados nível por nível
level1 = extract_domain_codes()
print(f"Nível 1: {len(level1)} registros")
level2 = extract_therapeutic_subgroups(level1)
print(f"Nível 2: {len(level2)} registros")
level3 = extract_pharmacological_subgroups(level2)
print(f"Nível 3: {len(level3)} registros")
level4 = extract_chemical_subgroups(level3)
print(f"Nível 4: {len(level4)} registros")
level5 = extract_chemical_substances(level4)
print(f"Nível 5: {len(level5)} registros")
# Constrói estrutura final
final_result = build_hierarchical_structure(level1, level2, level3, level4, level5)
# Salva resultado
output_file = "PATH/Lista_ATC_COMPLETA_Otimizada.xlsx" #Adapte para o caminho em sua máquina onde o arquivo será salvo.
final_result.to_excel(output_file, index=False)
print(f"Dados salvos em: {output_file}")
print(f"Total de registros finais: {len(final_result)}")
return final_result
except Exception as e:
print(f"Erro durante execução: {e}")
return None
Insights valiosos sobre web scraping em contextos como o apresentado
Scraping não é só “raspar HTML” — é modelar o domínio de negócio
Ao preservar a hierarquia (parent codes, níveis 1 a 5) e organizar os dados em um modelo coerente, estamos traduzindo a lógica regulatória do sistema ATC para um modelo de dados. Isso permite análises complexas (por domínio terapêutico, por via de administração, por dose definida diária) com rastreabilidade total até a fonte oficial.Requests + BeautifulSoup + pandas + numpy formam um stack poderoso para dados regulatórios
requests: acesso controlado, com timeout e retries.BeautifulSoup: extração precisa e legível de conteúdo HTML.pandas: manipulação de tabelas, joins hierárquicos, limpeza.numpy: representação rigorosa de valores ausentes e numéricos.
Dominar esse conjunto significa ir além do Excel e ter controle total sobre a linha de produção dos dados farmacêuticos.
Eficiência e respeito ao servidor fazem parte da reputação técnica
O uso de pausa entre requisições, retries controlados, limpeza de memória e filtragem por regex minimiza carga no servidor, reduz falhas e aumenta a confiabilidade do processo. Em contextos sensíveis como saúde, isso é mais do que uma boa prática de código — é um requisito ético e profissional, ainda mais em contextos em que ainda usamos recursos limitados para fazer análises mais complexas.
Agora, imagine: Se você tivesse hoje a hierarquia ATC/DDD completa e atualizada nesse formato tabular, qual seria a primeira análise ou automação de alto impacto que você implementaria com esses dados na sua realidade (por exemplo, na área clínica, regulatória, de mercado ou de pesquisa)? E, com os conhecimentos sobre web scraping passados aqui, quais possibilidades você já enxerga de extração de dados para auxiliar em outras análises, considerando o tempo e energia economizados para suas análises do dia-a-dia?





