Ruby
Nokogiri
RubyOnRails
crawling
crawler

RubyとRailsでクローラー開発をしてみた

はじめに

Railsで開発したアプリケーションにタスクとしてクローラーを開発する必要があったので、ほぼ初めてのRubyでクローラーを開発してみました。

今回は海外メディアサイトのRecodeの記事をクローリングしてきてGCPのCloud Translation APIで翻訳後、データベースに保存します。

クローラーはrails_project/lib/tasks/recode_crawler.rbというファイル名で作成しました。

プログラム

recode_crawler.rb
require 'open-uri'
require 'nokogiri'
require 'net/http'
require 'uri'
require 'json'

class Tasks::RecodeCrawler

  # recodeのurlを定数として定義
  TOP_URL = 'https://www.recode.net'.freeze

  @@archives_url = 'https://www.recode.net/archives'

  # 過去の記事(archives)のクローラー
  def self.archives_crawling()
    while true
      doc = Nokogiri.HTML(open(@@archives_url))
      article_url_list = []

      doc.xpath('//h2[@class="c-entry-box--compact__title"]').each do |element|
        element.css('a').each do |a|
          if a[:href] != nil && a[:href].include?('recode')
            article_url_list.push(a[:href])
          end
        end
      end

      article_url_list.each do |article_url|
        articles = Nokogiri.HTML(open(article_url))
        url_split = article_url.split('/')
        date = url_split[3]+ '-' + url_split[4] + '-' + url_split[5]
        title = articles.xpath('//h1[@class="c-page-title"]').inner_text
        body = articles.xpath('//div[@class="c-entry-content"]').inner_text.strip
        puts title + "\n"
        img_url = !articles.xpath('//picture[@class="c-picture"]//img').empty? ?
                      articles.xpath('//picture[@class="c-picture"]//img').first[:srcset].split(',').first.gsub(' 320w', '') : nil


        # クローラー実行して、すでにurl一致するレコードがない場合以下の処理を実行
        unless RecodeArticle.exists?(url: article_url)
          recode_article = RecodeArticle::create(
              url: article_url,
              date: date,
              title: title,
              body: body,
              img_url: img_url,
          )

          parse_body = body.split("\n")
          translate_body = ''

          # 翻訳APIへpostするデータが大きすぎるとエラーになるので細かくパースしてpost
          parse_body.each do |sentences|
            parse_sentences = sentences.split(". ")
            parse_sentences.each do |sentence|
              translate_body += translate(sentence).gsub('"', '')
            end
          end

          TranslateArticle::create(
              recode_article_id: recode_article[:id],
              url: article_url,
              date: date,
              title: translate(title),
              body: translate_body,
              img_url: img_url,
          )
        end

        sleep(rand(2..4))
      end

      pagination_next = doc.css('.c-pagination__next')
      if pagination_next != nil
        pagination_next.each do |n|
          @@archives_url = TOP_URL + n[:href]
        end
      else
        break
      end

    end

  end


  # トップページの記事のクローラー
  def self.crawling

    doc = Nokogiri.HTML(open(TOP_URL))
    article_url_list = []

    doc.xpath('//h2[@class="c-entry-box__title"]').each do |element|
      element.css('a').each do |a|
        if a[:href] != nil && a[:href].include?('recode')
          article_url_list.push(a[:href])
        end
      end
    end

    article_url_list.each do |article_url|

      # 例外処理: ページが存在しなければ次のページ
      begin
        articles = Nokogiri.HTML(open(article_url))
      rescue OpenURI::HTTPError
        sleep(rand(2..4))
        next
      end

      url_split = article_url.split('/')
      date = url_split[3]+ '-' + url_split[4] + '-' + url_split[5]
      title = articles.xpath('//h1[@class="c-page-title"]').inner_text
      body = articles.xpath('//div[@class="c-entry-content"]//p').inner_text.strip
      puts title + "\n"
      img_url = !articles.xpath('//picture[@class="c-picture"]//img').empty? ?
                    articles.xpath('//picture[@class="c-picture"]//img').first[:srcset].split(',').first.gsub(' 320w', '') : nil

      # クローラー実行して、すでにurl一致するレコードがない場合以下の処理を実行
      unless RecodeArticle.exists?(url: article_url)
        begin
          ActiveRecord::Base.transaction do
            recode_article = RecodeArticle::create(
              url: article_url,
              date: date,
              title: title,
              body: body,
              img_url: img_url,
            )

            parse_body = body.split("\n")
            translate_body = ''

            # 翻訳APIへpostするデータが大きすぎるとエラーになるので細かくパースしてpost
            parse_body.each do |sentences|
              parse_sentences = sentences.split('. ')
              parse_sentences.each do |sentence|
                translate_body += translate(sentence).gsub('"', '')
              end
            end

            TranslateArticle::create(
              recode_article_id: recode_article[:id],
              url: article_url,
              date: date,
              title: translate(title),
              body: translate_body,
              img_url: img_url,
            )
          end
        rescue
          next
        end
      end

      sleep(rand(2..4))
    end

  end


  # GCPのCloud Translation APIの翻訳用メソッド
  def self.translate(q)
    url = URI.parse('https://www.googleapis.com/language/translate/v2')
    params = {
        q: q,
        target: 'ja',
        source: 'en',
        key: '****************************'
    }
    url.query = URI.encode_www_form(params)
    res = Net::HTTP.get_response(url)
    JSON.parse(res.body)['data']['translations'].first['translatedText']
  end


end


実行方法

$ bin/rails runner Tasks::RecodeCrawler.crawling
$ bin/rails runner Tasks::RecodeCrawler.archives_crawling

でクローラーを実行します。

最後に

RubyもRailsも初めてなのでお作法だったりがよくわかってない状態でクローラーを開発しましたが、Ruby結構楽しい。