読者です 読者をやめる 読者になる 読者になる

Anemone&Nokogiriでクローラ作成してスクレイピング

エンジニアインターン浦川です。8ヶ月が過ぎましたが、初めて書きます。

院試明けのタスクとしてスクレイピングを割り当てられ、先程実装終了したので、

スクレイピングについて書こうと思います。

とりあえずgemをインストール。

# Gemfile
gem 'anemone'
gem 'nokogiri'

Anemone
クローリング(サイトの巡回)が簡単に出来てしまう素晴らしいgem

Nokogiri
ノコギリのようにスクレイピング(HTMLを解析しデータ抽出)してくれるこちらも素晴らしいgem

次に文字コードUTF-8に変換してくれるライブラリkconvをロードする。

# crawler.rb
require 'kconv'

これで準備は整いました。
実際のコードを書くと長くなってしまうので、一般的な使い方を掲載します。

# crawler.rb
require 'kconv'

url = "http://example.com/"
Anemone.crawl(url) do |anemone|
  anemone.on_every_page do |page|
    doc = Nokogiri::HTML.parse(page.body.toutf8)
    items = doc.xpath("//div[class='item-list']/ul/li")
    items.each do |item|
      title = item.xpath("div[class='item-title']").text
    end
  end
end

ここでやっているのは、

1. 指定したURLのサイトをAnemoneオブジェクト(*)にする

url = "http://example.com/"
Anemone.crawl(url) do |anemone|
end

2. そのサイトからリンクで辿れる全てのページをAnemoneオブジェクト(*)にする

anemone.on_every_page do |page|
end

3. それをtoutf8でUTF-8に変換したものをNokogiriオブジェクトにする

doc = Nokogiri::HTML.parse(page.body.toutf8)

4. xpathを用いて欲しい要素を取得する

items = doc.xpath("//div[class='item-list']/ul/li")
items.each do |item|
  title = item.xpath("div[class='item-title']").text
end

です。

crawlの引数にはurlの配列やオプションも渡せます。
オプションの例としてdepth_limitがあり、これを0にすると(2.)においてリンクで辿れるページは無視します。

これでスクレイピング完了です。簡単ですね!!

(*)実際にはAnemone::Core, Anemone::Pageとなります。
詳しくはこちらを参照してください。
http://www.rubydoc.info/github/chriskite/anemone/Anemone

rspecを初めて使ってみた

「スキルなし・実績なし」 32歳窓際エンジニアがシリコンバレーで働くようになるまで
これ読んでブログがんばろうかなーという気持ちになっている榊間です。(前回から結構空いちゃいましたが)

今さらながらしっかりとtestできるようにならないとということで、社員さんからrspecの本を薦められて読んでいます。今日はそのことについて書いていきたいと思います。leanpub.com


本を読んで実際に書いたコード

主に使うのは

gem 'rspec-rails'
gem 'factory-girl-rails'
gem 'faker'

です。個人的には、rspecが実際にテストをするために必要なgemで、factorygirlはtestするためのデータ生成に使用、fakerは生成するデータをより現実のデータに近づけるために使用するといったイメージを持ってます。

僕が最初に書いたのは

require 'spec_helper'

describe "Inquiry" do
  it "is valid with contents, email and name" do
    expect(build(:inquiry)).to be_valid
  end

  it "is invalid without contents" do
    inquiry = build(:inquiry, contents: nil)
    inquiry.valid?
    expect(inquiry.errors[:contents]).to include("を入力してください。")
  end

...


  describe "length of contents" do

    it "is invalid with more than 500 words" do
      inquiry = build(:invalid_contents)
      inquiry.valid?
      expect(inquiry.errors[:contents]).to include("は500文字以内で入力してください。")
    end
  end

  describe "format email" do
    it "match regex" do
      inquiry = build(:inquiry)
      expect(inquiry.email).to match(/.+@.+\..+/i)
    end

    it "does not match regex" do
      inquiry = build(:inquiry,
        email: "testexample.com"
      )
      inquiry.valid?
      expect(inquiry.errors[:email]).to include("は不正な値です。")
    end
  end

...

end 

長くなるので少し省略していますが、この中でやりたかったことは
1、name,email,contentsのpresent: trueを確かめる。
2、contentsのvalidationを確かめる。
3、emailのvalidationを確かめる。
この三つです。
でも
・ちょっと長くて読みにくい
・テスト内容が細かすぎる(エラー文もテストしている)
・emailのvalidのテストは一番最初のテストで確かめられている(重複している)
などなど問題がありました。
そしてアドバイスを受けて書いたコードがこちら

require 'spec_helper'

describe "Inquiry" do
  context "when valid" do
    subject(:inquiry) { build(:inquiry) }
    it { expect(inquiry).to be_valid }
  end

  context "when no contents" do
    subject(:inquiry) { build(:inquiry, contents: nil) }
    it { expect(inquiry).not_to be_valid }
  end

...

  context "with more than 500 words in contents" do
    subject(:invalid_contents) { build(:invalid_contents) }
    it { expect(invalid_contents).not_to be_valid }
  end

  context "with non-marching format of email" do
    subject(:invalid_email) { build(:inquiry, email: "testexample.com") }
    it { expect(invalid_email).not_to be_valid }
  end

...

end

もう全然違います。contextで状態を表し、全てvalidかinvalidかテストするにとどめました。subujectでまとめることで、すっきりして読みやすくなっています。

テストの書き方は、結構好み・状況に左右されるなーという印象ですが、漏れと重複だけは気をつけてやらないといけないというのが今回の学びです。。

actionmailerを使ってみる

榊間です
お盆とかで少し間が空いてしまったので久しぶりの更新です。


今日はアクションメイラーについて書こうと思います。


今回やりたいことは、inquireページからお問い合わせを送信した時、運営者側と利用者に通知のメールを送ることです。


準備

まずメイラーを生成します(今回はinquirymailerにします)

rails generate mailer InquiryMailer

formで以下のように書いときます。(simpleform使ってますが、要はemailとかもろもろの情報をハッシュ形式で渡してます)

    <%= simple_form_for(@inquiry) do |f| %>
...
      <%= f.input :email %>
...
      <%= f.button :submit, "この内容で送信する" %>
    <% end %>



次にapp/mailerに生成されたinquiry_mailer.rbにdefaultメソッドと、complete_inquiry、notice_inquiry(この二つの名前は自由)メソッドを定義します。

# encoding: utf-8
class InquiryMailer < ActionMailer::Base
  default from: "defaultの送信元メールアドレスを書いてください"

  def complete_inquiry(inquiry)
    @inquiry = inquiry
    mail(to: "利用者のメールアドレス", subject: "題名です")
  end

  def notice_inquiry(inquiry)
    @inquiry = inquiry
    mail(to: "運営側のメールアドレス", subject: "問い合わせ通知")
  end
end



そして、controllerにメールを送りたいタイミングで、inquire_mailer.rbに定義したメソッドを呼び出します。

引数には@inquiryを渡しており、inquire_mailer.rbや、最終的にはview(後述)で使われています。

# encoding: utf-8
class InquiriesController < ApplicationController
  before_filter :set_inquiry, only: [:new, :create]

  def new
  end

  def create
    if @inquiry.save
      InquiryMailer.complete_inquiry(@inquiry).deliver
      InquiryMailer.notice_inquiry(@inquiry).deliver
      redirect_to inquiries_path
    else
      render action: "new"
    end
  end

private

  def set_inquiry
    @inquiry = Inquiry.new(params[:inquiry])
  end
end



あとはメールの内容をviewで生成します。

一番最初に生成されたview/inquiry_mailerに、inquiry_mailerに定義したメソッドに対応するファイルである、complete_inquiry.text.erbとnotice_inquiry.text.erbを生成し編集します。ここでは@inquiryを利用して、formで入力された内容(例えば名前やメールアドレスなど)を利用できます。文面はお好みで。

お名前:<%= @inquiry.name %> 様
メールアドレス:<%= @inquiry.email %>

これでメールが送られるようになったはずです。