1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
require 'rubygems' require 'bundler/setup' Bundler.require(:default) require 'typhoeus/adapters/faraday' # documentations: # https://api.slack.com/methods/files.list # https://api.slack.com/methods/files.delete api_token = ENV['SLACK_API_TOKEN'] or abort "no api token" per_page = 500 uri = "/api/files.list?token=%s&ts_from=0&ts_to=now&types=all&count=%s&page=%s" conn = Faraday.new(:url => 'https://slack.com/') do |faraday| faraday.request :url_encoded # form-encode POST params faraday.response :logger # log requests to STDOUT faraday.adapter :typhoeus end response = conn.get( uri % [api_token, per_page, 1] ) json = Oj.load(response.body) total_pages = json["paging"]["pages"].to_i responses = [] conn.in_parallel do (1..total_pages).each do |page| responses << conn.get( uri % [api_token, per_page, page] ) end end files_ids = [] errors = [] Parallel.each(responses, in_threads: 10) do |response| begin json = Oj.load(response.body) Parallel.each(json["files"], in_threads: 10) do |file| files_ids << file["id"] end rescue => e errors << e end end uri_delete = "https://slack.com/api/files.delete?token=%s&file=%s" conn.in_parallel do files_ids.each do |file_id| responses << conn.get( uri_delete % [api_token, file_id] ) end end puts responses.map do |response| Oj.load(response.body)["ok"] end |
Dica 1
Mesmo em pequenos scripts, use Bundler!
Dica 2
Para acessar páginas Web, use a gem "Faraday" junto com a gem "Typhoeus".
Por que Faraday?
Porque ela abstrai diversos clientes HTTP. Você pode mudar entre Net::HTTP ou Excon ou Typhoeus com mínimo esforço e pode ser útil para diferenciar cenários de produção com um adapter e testes com outro adapter, por exemplo.
Por que Typhoeus?
Porque ela é capaz de realizar diversas requisições em paralelo e o que mais queremos num crawler é paralelizar o máximo possível (vide linhas 25..29 e 45..48 para exemplos). O único cuidado é não puxar coisa demais e estourar o acumulador de retornos (no exemplo, o array responses).
Para evitar estourar memória em vez de acumular internamente num array como no exemplo, já use a gem "Dalli" e vá acumulando num Memcached ou acumule num Redis ou MongoDB ou qualquer outro sistema de armazenamento que suporte inserções assíncronas (não bloqueantes, como um RDBMS normal).
Dica 3
Se estiver recebendo respostas JSON, use um parser rápido como a gem "Oj". Em conjunto com a gem "oj_mimic_json" - que você deve colocar junto na Gemfile se for um projeto Rails - ela substitui a padrão gem JSON que o ActiveSupport usa. A Oj é de 2 a 4 vezes mais rápido do que a gem JSON por isso vale a pena usá-la. Claro, ela é rápida assim porque possui uma extension feita em C, ou seja não funciona com JRuby, apenas com MRI.
Dica 4
Uma vez que recebemos dezenas de respostas, queremos processá-las o mais rápido possível e fazer um mesmo responses.each { |response| ... } é lento porque é linear.
Para resolver isso podemos recorrer à gem "Parallel" (vide exemplos nas linhas 33..42). Ela vai iterar pela coleção paralelamente usando threads.
Em Ruby MRI, lembrar que temos o Global Interpreter Lock (GIL) que bloqueia a execução paralela porque a mudança de estados em código C sem mutexes e outras proteções poderia corromper a execução. Então não precisamos nos preocupar com estado global compartilhado sendo alterado em paralelo por múltiplas threads mas por outro lado não temos multithreading real o tempo todo.
Mas temos como usar threads em operações bloqueantes, I/O bound, ou que tem muita espera (operações com arquivos ou rede). Nesse caso, quando uma thread bloquear esperando a resposta de algum I/O, outra thread pode de fato executar em paralelo. E daí o valor de usar threads no caso de crawlers ou scripts que façam import/export em banco de dados, etc.
Para o caso de I/O bound, usar threads. Mas se o caso for algo CPU-bound, de processamento de dados, cálculos, mais pesados, threads não vão ajudar. Para isso em vez da opção :in_threads você usa a opção :in_processes que vai fazer um fork() do seu processo e vai rodar realmente em paralelo em processos independentes e isolados, sem estado compartilhado.
Como desde o Ruby 2.0 ele suporta "Copy on Write" (ou "COW") de tal forma que mesmo duplicando um processo Ruby, ele não vai duplicar a memória, eles vão compartilhar a memória que não muda e cada processo isola somente a memória que ele mudar. Isso torna rodar processos em paralelo não tão caros em termos de consumo de memória e muito mais fáceis de escrever (sem precisar se preocupar com mutexes e semáforos, já que tudo rodará garantidamente isolado).
Ou seja, para casos justamente como um crawler ou um ETL, a gem "Parallel" pode ser uma mão na roda. Mas sempre meça para saber se as coisas estão realmente paralelas ou se você não está bloqueando suas threads e caindo no caso de execução linear sem saber.