O guia para iniciantes em Web Scraping com Rust
Mihai Maxim em 17 de outubro de 2022
O Rust é adequado para a recolha de dados da Web?
Rust é uma linguagem de programação concebida para ser rápida e eficiente. Ao contrário do C ou do C++, o Rust possui um gestor de pacotes e uma ferramenta de compilação integrados. Tem também uma excelente documentação e um compilador amigável com mensagens de erro úteis. Demora algum tempo a habituar-se à sintaxe. Mas assim que o fizer, aperceber-se-á de que pode escrever funcionalidades complexas com apenas algumas linhas de código. A recolha de dados da Web com Rust é uma experiência estimulante. Ganha-se acesso a poderosas bibliotecas de scraping que fazem a maior parte do trabalho pesado por si. Como resultado, você pode passar mais tempo nas partes divertidas, como projetar novos recursos. Neste artigo, eu vou guiá-lo através do processo de construção de um web scraper com Rust.
Como instalar o Rust
A instalação do Rust é um processo bastante simples. Visite Install Rust - Rust Programming Language (rust-lang.org) e siga o tutorial recomendado para o seu sistema operativo. A página apresenta conteúdos diferentes consoante o sistema operativo que estiver a utilizar. No final da instalação, certifique-se de que abre um novo terminal e executa rustc --version. Se tudo correu bem, deve ver o número da versão do compilador Rust instalado.
Since we will be building a web scraper, let’s create a Rust project with Cargo. Cargo is Rust’s build system and package manager. If you used the official installers provided by rust-lang.org, Cargo should be already installed. Check whether Cargo is installed by entering the following into your terminal: cargo --version. If you see a version number, you have it! If you see an error, such as command not found, look at the documentation for your method of installation to determine how to install Cargo separately. To create a project, navigate to the desired project location and run cargo new <project name>.
Esta é a estrutura de projeto predefinida:
- O código é escrito em ficheiros .rs.
- As dependências são geridas no ficheiro Cargo.toml.
- Visite crates.io: Registo de Pacotes Rust para encontrar pacotes para o Rust.
Construir um raspador da Web com Rust
Vamos agora ver como pode utilizar o Rust para construir um raspador. O primeiro passo é definir um objetivo claro. O que é que eu quero extrair? O passo seguinte é decidir como quer armazenar os dados extraídos. A maioria das pessoas guarda-os como .json, mas deve considerar o formato que melhor se adequa às suas necessidades individuais. Com estes dois requisitos definidos, pode avançar com confiança para a implementação de qualquer scraper. Para ilustrar melhor este processo, proponho que criemos uma pequena ferramenta que extraia dados sobre a Covid do sítio Web COVID Live - Coronavirus Statistics - Worldometer (worldometers.info). Esta ferramenta deve analisar as tabelas de casos registados e armazenar os dados em formato .json. Iremos criar este scraper em conjunto nos capítulos seguintes.
Obtenção de HTML com pedidos HTTP
Para extrair as tabelas, primeiro é necessário obter o HTML que está dentro da página web. Usaremos a caixa/biblioteca "reqwest" para obter o HTML bruto do site.
Primeiro, adicione-o como uma dependência no arquivo Cargo.toml:
reqwest = { version = "0.11", features = ["blocking", "json"] }
Em seguida, defina o seu url de destino e envie o seu pedido:
let url = "https://www.worldometers.info/coronavirus/";
let response = reqwest::blocking::get(url).expect("Could not load url.");
A funcionalidade de "bloqueio" garante que o pedido é síncrono. Como resultado, o programa aguardará a sua conclusão e depois continuará com as outras instruções.
let raw_html_string = response.text().unwrap();
Utilizar selectores CSS para localizar dados
Tem todos os dados brutos necessários. Agora tem de encontrar uma forma de localizar as tabelas de casos registados. A biblioteca Rust mais popular para este tipo de tarefa chama-se "scraper". Permite a análise e consulta de HTML com selectores CSS.
Adicione essa dependência ao seu arquivo Cargo.toml:
raspador = "0.13.0"
Adicione estes módulos ao seu ficheiro main.rs.
use scraper::Seletor;
use scraper::Html;
Agora, utilize a cadeia HTML em bruto para criar um fragmento HTML:
let html_fragment = Html::parse_fragment(&raw_html_string);
Seleccionaremos as tabelas que apresentam os casos comunicados para hoje, ontem e há dois dias.

Abra a consola do programador e identifique os ids da tabela:

No momento da redação deste artigo, a identificação para o dia de hoje é: "main_table_countries_today".
Os outros dois id's de tabela são:
"main_table_countries_yesterday" e "main_table_countries_yesterday2"
Agora vamos definir alguns selectores:
let table_selector_string = "#main_table_countries_today, #main_table_countries_yesterday, #main_table_countries_yesterday2";
let table_selector = Seletor::parse(table_selector_string).unwrap();
let head_elements_selector = Seletor::parse("thead>tr>th").unwrap();
let row_elements_selector = Seletor::parse("tbody>tr").unwrap();
let row_element_data_selector = Seletor::parse("td, th").unwrap();
Passe a table_selector_string para o método de seleção html_fragment para obter as referências de todas as tabelas:
let all_tables = html_fragment.select(&table_selector);
Utilizando as referências das tabelas, crie um ciclo que analise os dados de cada tabela.
for table in all_tables{
let head_elements = table.select(&head_elements_selector);
for head_element in head_elements{
//parse the header elements
}
let head_elements = table.select(&head_elements_selector);
for row_element in row_elements{
for td_element in row_element.select(&row_element_data_selector){
//parse the individual row elements
}
}
}
Analisar os dados
O formato em que armazena os dados dita a forma como os analisa. Para este projeto, é .json. Consequentemente, precisamos de colocar os dados da tabela em pares de valores chave. Podemos usar os nomes de cabeçalho da tabela como chaves e as linhas da tabela como valores.
Utilize a função .text() para extrair os cabeçalhos e armazená-los num Vetor:
//for table in tables loop
let mut head:Vec<String> = Vec::new();
let head_elements = table.select(&head_elements_selector);
for head_element in head_elements{
let mut element = head_element.text().collect::<Vec<_>>().join(" ");
element = element.trim().replace("\n", " ");
head.push(element);
}
//head
["#", "Country, Other", "Total Cases", "New Cases", "Total Deaths", ...]
Extrair os valores das linhas de forma semelhante:
//for table in tables loop
let mut rows:Vec<Vec<String>> = Vec::new();
let row_elements = table.select(&row_elements_selector);
for row_element in row_elements{
let mut row = Vec::new();
for td_element in row_element.select(&row_element_data_selector){
let mut element = td_element.text().collect::<Vec<_>>().join(" ");
element = element.trim().replace("\n", " ");
row.push(element);
}
rows.push(row)
}
//rows
[...
["", "World", "625,032,352", "+142,183", "6,555,767", ...]
...
["2", "India", "44,604,463", "", "528,745", ...]
...]
Utilize a função zip() para criar uma correspondência entre os valores do cabeçalho e da linha:
for row in rows {
let zipped_array = head.iter().zip(row.iter()).map(|(a, b)|
(a,b)).collect::<Vec<_>>();
}
//zipped_array
[
...
[("#", ""), ("Country, Other", "World"), ("Total Cases", "625,032,352"), ("New Cases", "+142,183"), ("Total Deaths", "6,555,767"), ...]
...
]
Agora armazene os pares (chave, valor) do zipped_array num IndexMap:
serde = {version="1.0.0",features = ["derive"]}
indexmap = {version="1.9.1", features = ["serde"]} (adicionar estas dependências)
use indexmap::IndexMap;
//use this to store all the IndexMaps
let mut table_data:Vec<IndexMap<String, String>> = Vec::new();
for row in rows {
let zipped_array = head.iter().zip(row.iter()).map(|(a, b)|
(a,b)).collect::<Vec<_>>();
let mut item_hash:IndexMap<String, String> = IndexMap::new();
for pair in zipped_array{
//we only want the non empty values
if !pair.1.to_string().is_empty(){
item_hash.insert(pair.0.to_string(), pair.1.to_string());
}
}
table_data.push(item_hash);
//table_data
[
...
{"Country, Other": "North America", "Total Cases": "116,665,220", "Total Deaths": "1,542,172", "Total Recovered": "111,708,347", "New Recovered": "+2,623", "Active Cases": "3,414,701", "Serious, Critical": "7,937", "Continent": "North America"}
,
{"Country, Other": "Asia", "Total Cases": "190,530,469", "New Cases": "+109,009", "Total Deaths": "1,481,406", "New Deaths": "+177", "Total Recovered": "184,705,387", "New Recovered": "+84,214", "Active Cases": "4,343,676", "Serious, Critical": "10,640", "Continent": "Asia"}
...
]
O IndexMap é uma óptima escolha para armazenar os dados da tabela porque preserva a ordem de inserção dos pares (chave, valor).
Serialização dos dados
Agora que você pode criar objetos do tipo json com dados de tabela, é hora de serializá-los para .json. Antes de começarmos, certifique-se de ter todas essas dependências instaladas:
serde = {version="1.0.0",features = ["derive"]}
serde_json = "1.0.85"
indexmap = {version="1.9.1", features = ["serde"]}
Armazenar cada table_data num vetor tables_data:
let mut tables_data: Vec<Vec<IndexMap<String, String>>> = Vec::new();
For each table:
//fill table_data (see previous chapter)
tables_data.push(table_data);
Definir um contentor struct para as tabelas_dados:
#[derive(Serialize)]
struct FinalTableObject {
tables: IndexMap<String, Vec<IndexMap<String, String>>>,
}
Instanciar a estrutura:
let final_table_object = FinalTableObject{tables: tables_data};
Serializa a struct para uma cadeia de caracteres .json:
let serialized = serde_json::to_string_pretty(&final_table_object).unwrap();
Escreve a cadeia de caracteres .json serializada num ficheiro .json:
use std::fs::File;
use std::io::{Write};
let path = "out.json";
let mut output = File::create(path).unwrap();
let result = output.write_all(serialized.as_bytes());
match result {
Ok(()) => println!("Successfully wrote to {}", path),
Err(e) => println!("Failed to write to file: {}", e),
}
Aaaand, you're done. Se tudo deu certo, seu .json de saída deve ser parecido com:
{
"tables": [
[ //table data for #main_table_countries_today
{
"Country, Other": "North America",
"Total Cases": "116,665,220",
"Total Deaths": "1,542,172",
"Total Recovered": "111,708,347",
"New Recovered": "+2,623",
"Active Cases": "3,414,701",
"Serious, Critical": "7,937",
"Continent": "North America"
},
...
],
[...table data for #main_table_countries_yesterday...],
[...table data for #main_table_countries_yesterday2...],
]
}
You can find the whole code for the project at [Rust][A simple <table> scraper] (github.com)
Fazer ajustes para se adaptar a outros casos de utilização
Se me seguiu até aqui, provavelmente já se apercebeu de que pode utilizar este raspador noutros sítios Web. O raspador não está vinculado a uma contagem de colunas de tabela específica ou a uma convenção de nomenclatura. Além disso, não depende de muitos selectores CSS. Por isso, não deve ser necessário fazer muitos ajustes para que funcione noutras tabelas, certo? Vamos testar esta teoria.

We need a selector for the <table> tag.

Se class="wikitable sortable jquery-tablesorter", pode alterar o table_selector para:
let table_selector_string = ".wikitable.sortable.jquery-tablesorter";
let table_selector = Seletor::parse(table_selector_string).unwrap();
This table has the same <thead> <tbody> structure, so there is no reason to change the other selectors.
O raspador deve funcionar agora. Vamos fazer um teste:
{
"tables": []
}
A recolha de dados da Web com Rust é divertida, não é?
Como é que isto pode falhar?
Vamos aprofundar um pouco mais:
A maneira mais fácil de descobrir o que deu errado é olhar para o HTML que é retornado da solicitação GET:
let url = "https://en.wikipedia.org/wiki/List_of_countries_by_population_in_2010";
let response = reqwest::blocking::get(url).expect("Could not load url.");
et raw_html_string = response.text().unwrap();
let path = "debug.html";
let mut output = File::create(path).unwrap();
let result = output.write_all(raw_html_string.as_bytes());

O HTML devolvido pelo pedido GET é diferente do que vemos no sítio Web real. O navegador oferece um ambiente para que o Javascript seja executado e altere o layout da página. No contexto do nosso scraper, obtemos a versão inalterada da mesma.
Our table_selector did not work because the “jquery-tablesorter” class is injected dynamically by Javascript. Also, you can see that the <table> structure is different. The <thead> tag is missing. The table head elements are now found in the first <tr> of the <tbody>. Thus, they will be picked up by the row_elements_selector.
Removing “jquery-tablesorter” from the table_selector is not enough, we also need to handle the missing <tbody> case:
let table_selector_string = ".wikitable.sortable";
if head.is_empty() {
head=rows[0].clone();
rows.remove(0);
}// take the first row values as head if there is no <thead>
Agora vamos dar outra volta:
{
"tables": [
[
{
"Rank": "--",
"Country / territory": "World",
"Population 2010 (OECD estimate)": "6,843,522,711"
},
{
"Rank": "1",
"Country / territory": "China",
"Population 2010 (OECD estimate)": "1,339,724,852",
"Area (km 2 ) [1]": "9,596,961",
"Population density (people per km 2 )": "140"
},
{
"Rank": "2",
"Country / territory": "India",
"Population 2010 (OECD estimate)": "1,182,105,564",
"Area (km 2 ) [1]": "3,287,263",
"Population density (people per km 2 )": "360"
},
...
]
]
Assim está melhor!
Resumo
Espero que este artigo forneça um bom ponto de referência para o web scraping com Rust. Mesmo que o rico sistema de tipos e o modelo de propriedade do Rust possam ser um pouco esmagadores, ele não é, de forma alguma, inadequado para o web scraping. Tem um compilador amigável que o aponta constantemente na direção certa. Também se encontra muita documentação bem escrita: A linguagem de programação Rust - A linguagem de programação Rust (rust-lang.org).
Criar um raspador da Web nem sempre é um processo simples. Vai deparar-se com renderização de Javascript, bloqueios de IP, captchas e muitos outros contratempos. Na WebScraping API, fornecemos-lhe todas as ferramentas necessárias para combater estes problemas comuns. Está curioso para saber como funciona? Pode experimentar o nosso produto gratuitamente em WebScrapingAPI - Produto. Ou pode contactar-nos em WebScrapingAPI - Contacto. Teremos todo o gosto em responder a todas as suas questões!
Notícias e actualizações
Mantenha-se atualizado com os mais recentes guias e notícias sobre raspagem da Web, subscrevendo a nossa newsletter.
We care about the protection of your data. Read our <l>Privacy Policy</l>.Privacy Policy.

Artigos relacionados

Explore a comparação aprofundada entre o Scrapy e o Selenium para raspagem da Web. Desde a aquisição de dados em grande escala até o tratamento de conteúdo dinâmico, descubra os prós, os contras e os recursos exclusivos de cada um. Saiba como escolher a melhor estrutura com base nas necessidades e na escala do seu projeto.


Aprenda a extrair sites dinâmicos renderizados em JavaScript usando o Scrapy e o Splash. Desde a instalação até à escrita de um spider, à manipulação da paginação e à gestão das respostas do Splash, este guia abrangente oferece instruções passo a passo tanto para principiantes como para especialistas.


Explore o poder transformador da recolha de dados da Web no sector financeiro. Desde dados de produtos a análises de sentimentos, este guia oferece informações sobre os vários tipos de dados da Web disponíveis para decisões de investimento.
