Escrevendo um Crawler

28 jun 2019 Última atualização: 2 jul 2021

Atenção: este texto é de 2019 e seu conteúdo está desatualizado. Ele é mantido aqui como referência histórica.

Traduzido do en usando google translate

Em primeiro lugar, este será um texto longo, à medida que escrevo cada passo do meu pensamentos ao longo do caminho para escrever este rastreador simples. Se você está com pressa, apenas pule para o [código] (# the_entire_code).

Por que escrever um rastreador? POR QUE NÃO?

Então, basicamente, o que eu quero é um aplicativo que forneça um URL inicial e deve me retornar um mapa do site desenhado em caracteres ascci:

A propósito, vou chamar este projeto de: spidy

$ run spidy http://lewebpag.es
/ (index)
|
|------ /projects
|       |
|       |------ /agrade
|       |
|       |------ /prank
|
|------ /blog
        |
        |------ /hello-world
        |
        |------ /01-2019-report
        |
        |------ /how-to-not-be-productive
        |
        |------ /redux-thuk-utils

Ou algo que se pareça com esta _maestra_de_mapa do site.

Em postagens posteriores do projeto, quero reutilizar este código para fazer mais coisas, como escrever uma ferramenta para me ajudar a verificar a regressão visual em meus projetos ou em alguns dados de páginas, como identificadores de mídia social.

Visão de nível superior do projeto

Nosso código deve fazer o seguinte:

  1. Obtenha o URL inicial e o domínio do site a partir do argumento cli
  2. Rastreie para o próximo URL não visitado
  3. Pesquise os elementos \ <a >
    • Obtenha seu href
    • Armazene o URL href, se o caminho, relativo ou absoluto, residir dentro do mesmo domínio
  4. Obtenha o próximo URL não visitado
  5. Chegue a 2, a menos que não haja mais URLs não visitados
  6. Desenhe o mapa do site com base nos URLs visitados

Escolhendo nossas ferramentas

Usarei nodejs apenas por preguiça, mas talvez no futuro, irei reescrever isso em python ou qualquer outra linguagem.

Existem muitas maneiras de fazer solicitações HTTP no node: vanilla nodejs HTTP, axios, node-fetch, request, etc. Hoje não estou me sentindo louco o suficiente para usar vanilla HTTP e escrever meu próprio wrapper em torno dele para fazer solicitações facilmente, então eu estou indo pela rota solicitada.

Usarei regex para os elementos \ <a > porque não preciso percorrer o DOM, só preciso descobrir esses preciosos hrefs. Apenas como uma nota, se você precisa fazer mais do que apenas descobrir algumas strings em um texto HTML, _ NÃO USE REGEXP_ você vai me agradecer mais tarde. (HTML é uma linguagem de "programação" estruturada, não apenas um monte de texto aleatório para correspondência de padrão. Use um analisador!)

Configurando

Vamos usar a estrutura npm package.json para organizar nosso projeto se você não tem node e npm instalados e configurados, verifique isto antes continuando.

Estou usando o Linux Mint, então use suas células cerebrais para adaptar tudo o que vir aqui para o seu sistema operacional preferido.


cd workspaces/
mkdir spidy
cd spidy/
npm init -y

Mostre-me o código já!

Vá mais devagar, partimos de nossa visão abstrata de nível superior e percorremos cada passo para a implementação. Esta é uma boa maneira de ter uma compreensão de todo projeto.

Escrevendo jargão

Primeiro, vamos escrever um código sem sentido e aprimorá-lo mais tarde.

// 1. Get initial URL and site domain from cli argument
const domain = arguments.domain

// 2. Crawl into next non-visited URL
let page = crawUrl(domain)
let hrefs = getHrefsFromPage(page)
// 3. Search for \<a\> elements
//     * Get its href
//     * Store the href URL, if the path, relative or absolute, resides inside the same domain
let containedUrls = hrefs.filter(url => isPathInsideDomain(url))

// 4. Get next non-visited URL
let newUrl = getNextUrlToVisit()
// 5. Got to 2, unless there are no more non-visited URLs
loop
// 6. Draw sitemap based on visited URLs
drawMap(sitemap)

Escrevendo as funções que encontramos

Embora este código seja totalmente inútil neste estado, já podemos ver alguns dos suas partes, então vamos agora escrever essas funções.

Algumas das funções podem ser refatoradas posteriormente, se necessário.

function crawlUrl(url) {
  // request URL
  // return page
}

// change function name
function getHrefsFromHtml(html) {
  // match a regexp
  // for each match extract href
  // return hrefs
}

function isPathAbsolute(url) {
  // does URL starts with HTTP or https
}

// add domain as an attribute so we don't relay on globals
function isPathInsideDomain(url, domain) {}

function getNextUrlToVisit() {
  // check list of toVisit URLs and return first non-visited URL
}

function drawMap(sitemap) {
  // for each route
  // draw route
  // drawMap (subRoutes)
}

Preenchendo as lacunas

Vamos preencher as funções acima, uma por uma:

crawUrl

Como eu disse antes, vamos usar a biblioteca de solicitações:

npm install request-promise-native@1.0.7
const request = require("request-promise-native")

async function crawlUrl(url) {
  return request.get(url)
}

getHrefsFromHtml

Vamos usar um regex para corresponder a cada elemento âncora que tem um href Atribua e capture apenas a parte href.

const anchorRegex = /<a(?:.|\n)+?href=('.*?'|".*?")/gi

function getHrefsFromHtml(html) {
  let matches
  let hrefs = []

  while ((matches = anchorRegex.exec(html)) !== null) {
    let href = matches[1]
    href = href.replace(/("|')/g, "")

    hrefs.push(href)
  }

  return hrefs
}

isPathAbsolue

function isPathAbsolute(url) {
  return url.startsWith("http://") || url.startsWith("https://")
}

isPathInsideDomain

function isPathInsideDomain(url, domain) {
  // if path is absolute
  if (isPathAbsolute(url)) {
    return url.startsWith(domain)
  }

  return true
}

"main"

Precisaremos de uma função assíncrona principal para chamar, caso contrário, não seríamos capazes de use a sintaxe async / await dentro dela.

let domain = process.argv[2]

console.log(domain)
// Verify if the domain exists and it is an absolute path
if (!domain || !isPathAbsolute(domain)) {
  console.error("Provide a valid initial absolute path")
  exit(1)
}

async function crawl() {
  // Set vector of URLs to visit, starting with the top level website
  let toVisit = [domain]

  // Set vector of already visited websites
  let visited = []

  // While there are URLs to visit...
  while (toVisit.length) {
    // Visit first URL in vector
    let currentUrl = toVisit.shift()

    // Await page html
    let pageHtml = await crawlUrl(currentUrl)

    // Mark URL as visited
    visited.push(currentUrl)

    // Get <a>s href
    let hrefs = getHrefsFromHtml(pageHtml)

    let nonVisitedUrls = hrefs
      // filter only URLs that are inside the domain
      .filter(url => isPathInsideDomain(url, domain))
      // remap all URLs as absolute paths
      .map(url => (url.startsWith(domain) ? url : domain + url))
      // remove last '/' from URL to not visit 'page' and 'page/'
      .map(url => (url.endsWith("/") ? url.slice(0, -1) : url))
      // filter already visited pages
      .filter(url => !visited.includes(url))
      // filter pages that are already marked to be visited
      .filter(url => !toVisit.includes(url))

    // merge toVisit URLs and new nonVisitedUrls
    toVisit = [...toVisit, ...nonVisitedUrls]
  }

  drawMap(visited)
}

crawl(domain)

drawMap

function drawMap(urls, start = domain, tab = 0) {
  let parentUrls = urls
    .map(url => url.replace(new RegExp("^" + start), ""))
    .filter(url => /^\/?[^\/]*$/.test(url))

  if (tab === 0) {
    parentUrls.shift()
    console.log("/ (index)")
  }

  parentUrls.forEach(parent => {
    let prefix = "|   ".repeat(tab)
    console.log(prefix + "|")
    console.log(prefix + "|---- " + parent)

    let childrenUrls = urls.filter(url => url.startsWith(start + parent))

    drawMap(childrenUrls, start + parent + "/", tab + 1)
  })
}

Executando

Agora, para ver nosso script funcionando, precisamos apenas executar:

node spidy.js https: // marceloll.com

Vai levar algum tempo.

O código aqui é apenas um rastreador muito simples, por isso não está otimizado, mas sim o emprego.

O código completo

spidy.js

package.json

Conclusão

Agora temos uma ferramenta simples para rastrear uma página da web e seus links internos, mas ainda carece de alguns recursos:

  • Otimização (realmente precisamos fazer apenas 1 solicitação por vez)
  • Extensibilidade (por exemplo, chamar uma função em cada página)
  • Um sinalizador para reconhecer subdomínios como URLs internos
  • Aceite localhost como URL

Tente adicionar esses recursos por conta própria.

Você encontrou um bug no código?

Você tem sugestões para melhorar este tutorial?

Ou você apenas acha que meu código é uma merda e quer escrever por que você acha isso?

Vá em frente e use a seção de comentários para isso!

Referências