こんにちは、コロンビア系の大学院生エンジニアgaruです。
今回はスクレイピングやクローリングをしていると度々出てくるCookieとの触れ合い方について確認していきましょう。

Cookieの定義を確認してみます。

HTTP cookieは、RFC 6265などで定義されたHTTPにおけるウェブサーバとウェブブラウザ間で状態を管理する通信プロトコル、またそこで用いられるウェブブラウザに保存された情報のことを指す。ユーザ識別やセッション管理を実現する目的などに利用される。 (参照:wikipedia)

噛み砕くと、ブラウザ側にログイン情報やアクセスの履歴を保存する事で、次のアクセスの際にログインを省略したり、表示する広告の種類を変えたりする事もできます。
一言で言うならば、「個人情報」です。
スクレイピング(クローリング)ではCookie情報をDBに保存したりする事があるため、扱いには十分に気をつけましょう。

今回はそんなCookieの扱い方を確認します。

まず始めに、「Net::HTTP, Mechanize, Selenium」はどのような形式でcookieを管理しているのか確認してみましょう。

require 'net/http'
require 'mechanize'
require 'selenium-webdriver'

# Net::HTTP
uri = URI('https://www.google.com')
res = Net::HTTP.get_response(uri)
res['set-Cookie']
# => "1P_JAR=2018-05(略

# クラスを確認してみましょう。
res['set-Cookie'].class
# => String


# Mechanize
agent = Mechanize.new
agent.get('https://www.google.com')
agent.cookies
# => [#<HTTP::Cookie:(略

# クラスを確認してみましょう。
agent.cookies.class
# => Array
agent.cookies[0].class
# => HTTP::Cookie
# つまり、以下のような形式で保存されている事がわかります
# => [HTTP::Cookie, ...]

# Selenium
driver = Selenium::WebDriver.for :chrome
driver.navigate.to('https://www.google.com')
driver.manage.all_cookies
# => [{:name=>"1P_JAR"

# クラスを確認してみましょう。
driver.manage.all_cookies.class
# => Array
driver.manage.all_cookies[0].class
# => Hash
# SeleniumもMechanizeと似たような形式で保存されている事がわかります
# => [Hash, ...]

ここまでをまとめると、各gemのcookieは以下のような形式で扱われている事がわかります。

gem Cookie
Net::HTTP String
Mechanize [ HTTP:Cookie ]
Selenium [ Hash ]

何となくお察しかもしれませんが、SeleniumとMechanizeはこのままでは扱えません。
そこで保存できる形式にパースする必要があります。

DBにCookieを保存するには、以下の2通りの形式にパースする必要があります。

  • String
  • Yaml

一般的にはYamlで保存した方が扱いやすいですが、Stringで保存する事にもメリットがあります。
パースするにも手間があるため、簡単にパースできるかどうかで判断すると、以下のようになります。

gem String Yaml
Net::HTTP ✖️
Mechanize
Selenium

各gemごとにコードで確認してみましょう。

require 'net/http'
# Stringに変換
uri = URI('https://www.google.com')
res = Net::HTTP.get_response(uri)
cookie_str = res['set-Cookie']
puts cookie_str
# そもそもStringなので変換の必要はありません

# Stringを読み込み
# そのまま渡すだけでokです
uri = URI.parse('https://www.google.com/')
req = Net::HTTP::Get.new(uri)
req["Cookie"] = cookie_str

# Yamlに変換
# 頑張れば出来ますが、メソッドが用意されていないので出来ないものとする

# Yamlを読み込み
# 頑張れば(ry
require 'mechanize'
require 'yaml'
# Stringに変換
agent = Mechanize.new
agent.get('https://www.google.com')
# HTTP::Cookieクラスにはset_cookie_valueメソッドが用意されている
# Array形式なのでmapで変換してjoinする
cookie_str =  agent.cookies.map{|e| e.set_cookie_value}.join(", ")
puts cookie_str

# Stringを読み込み
# cookie_jarメソッドに追加していきます
agent2 = Mechanize.new
HTTP::Cookie.parse(cookie_str, "https://www.google.com").each{ |cookie| agent2.cookie_jar << cookie }

# Yamlに変換
agent = Mechanize.new
agent.get('https://www.google.com')
# HTTP::Cookieクラスのyamを生成出来ます
cookie_yml = agent.cookies.map{ |e| e.to_yaml }.join
puts cookie_yml

# Yamlを読み込み
agent2 = Mechanize.new
agent2.cookie_jar << YAML.load(cookie_yml)
require 'selenium-webdriver'
require 'yaml'
# Stringに変換
driver = Selenium::WebDriver.for :chrome
driver.navigate.to('https://www.google.com')
# Hashを、HTTP::Cookieクラスに変換する
# e[:expires]にはDateTimeクラスで入っており、HTTP::Cookieのインスタンスを作成する際にエラーになるので文字列にしています
http_cookies = driver.manage.all_cookies.each{ |e| e[:expires]=e[:expires].to_s }.map{ |e| HTTP::Cookie.new(e) }
cookie_str = http_cookies.map{|e| e.set_cookie_value}.join(", ")
puts cookie_str

# Stringを読み込み
# 頑張れば出来ますが、メソッドが用意されていないので出来ないものとする

# Yamlに変換
driver = Selenium::WebDriver.for :chrome
driver.navigate.to('https://www.google.com')
# Hashクラスのyamlを生成出来ます
cookie_yml = driver.manage.all_cookies.to_yaml
puts cookie_yml

# Yamlを読み込み
driver2 = Selenium::WebDriver.for :chrome
driver2.navigate.to('https://www.google.com')
YAML.load(cookie_yml).each{ |e| driver2.manage.add_cookie(e) }

よく出来ました!

yaml形式の文字列にした時に、MechanizeとSeleniumでクラス形式が違いました。
そのため、他のgemでcookieを読み込むには別途パースする必要がありますが、メソッドが用意されているわけではないので今回はやりません。

gem間でcookieを受け渡したい時
-> Stringを使う

cookieを保存して同じgemで再利用する時
-> Yamlを使う