QA@IT

CronでRubyのNokogiriを動かそうとするとエラーする

2152 PV

レンタルサーバーを利用して、CronでRubyのスクリプトを動かしたいのですが、エラーして困っております。
ターミナルからRubyのスクリプトを叩く分には、正しく動作するため、権限またはパスの問題かと考えましたが、
落ちている箇所がそうでは無いようなので、解決できずにおります。

cronを用いた場合の、rubyのエラーの吐き出し方が不明なため、怪しい箇所にtxtファイルを生成する処理を入れて、デバッグしております。

質問1
cronを用いた場合、エラーはどのように出力すればよいのでしょうか?
質問2
cronで動かす場合、権限、パス以外に、問題になる要素とはどのようなものでしょうか?
質問3
下記のスクリプトの問題を教えていただきたいです。

require 'rbconfig'
require 'openssl'
require "open-uri"
require "rubygems"
require "nokogiri"
require 'net/https'
require 'fileutils'

# スクレイピングするURL
url = 'https://play.google.com/store/apps/collection/topgrossing'

#SSl対応
http = Net::HTTP.new(url, 443)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
http.cert_store = OpenSSL::X509::Store.new
http.cert_store.set_default_paths
abc = File.dirname(__FILE__) + '/cacert.pem'
http.cert_store.add_file(abc)

#ファイル出力で処理がどこまで進んだかチェックしています。
#ファイルは正しく存在しているので、ここまでは問題ありません。
if  File.exist?(abc) then
    File.open("check01.txt","w") do |file|
      file.puts(abc)
    end
end

#スクレイピング
charset = nil
html = open(url) do |f| 
  charset = f.charset               
  f.read   
end

#ファイル出力で処理がどこまで進んだかチェックしています。
#このファイルは出力されておらず、この箇所以前で処理がとまっています
File.open("check02.txt","w") do |file|
  file.puts(charset)
  file.puts(html)
  file.puts(url)
end


#要素格納用配列 utf-8
new_posts = Array.new()
arRankTitle = Array.new()
arRankUrl = Array.new()
arRankNum = Array.new()
rankNum = 1
quTitle = ""

# htmlをパース(解析)してオブジェクトを作成
doc = Nokogiri::HTML.parse(html, nil, charset)

#タイトル
quTitle = doc.title
puts quTitle

#アプリタイトルとURLを
doc.xpath('//div[@class = "details"]/a').each do |node|
    #変数
    tt = nil
    print "--------------------\n"
    title2 = node.inner_text

    #からオブジェクトが居るので間引く
    if title2.empty?
    else 
        #print  title2.gsub(" ", ""),"<=空白削除\n"  #空白削除
        tt = title2.gsub(" ", "")
        #print tt,"tt1です。\n" 
        #p tt


        #順位表示の文字位置を取得
        tilteNum = title2.dup    
        tilteNum = /\./ =~ tilteNum    #何位までの文字を削除
        num = tilteNum.to_i
        #p num


        #testMoji = "abcdefg"
        testMoji = tt
        set =  testMoji.slice!(num..-1)
        #print set,"\n"


        #URL
        hrAp ="https://play.google.com" + node[:href]
        #puts hrAp

        #順位
        puts rankNum,"<=順位です。\n" 

        #配列に格納する
        #new_post = [set,hrAp,rankNum]
        #new_posts << new_post        
        arRankTitle << set
        arRankUrl << hrAp
        arRankNum << rankNum


        #順位の値を増やしていく
        rankNum = rankNum + 1
    end 



end



#配列を確認
#puts new_posts
#puts arRankTitle
#puts arRankUrl
#puts arRankNum


#日時
#SQLに入れるべき形 2014-01-11 00:55:29
tNum = Time.now.strftime("%Y-%m-%d %H:%M:%S") 
puts tNum

puts quTitle
#sqlに挿入する形に変更
quRankTitle = arRankTitle.join(", ")
quRankUrl = arRankUrl.join(", ")
quRankNum = arRankNum.join(", ")

====追記(10/11 15:50)====

下記の方法でエラーを出力したところ、証明書のエラーが吐き出されておりました。
そのために以降の処理で、エラーしていたようです。

SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed

レンタルサーバーのため、証明書がなく、また証明書をおくディレクトリにアクセス出来ないため、
SSl対応の部分で、対応していたつもりでしたが、
記述が間違っているでしょうか?
(cacert.pemのパーミッションは755に一応しており、File.exist?により、存在している事は確認しております)

#スクレイピング
charset = nil
begin
  html = open(url) do |f| 
    charset = f.charset               
    f.read   
  end
rescue => e
  File.open("check_scraping.txt","w") do |file|
    file.puts(e.message)
    file.puts(e.backtrace)
  end  
end

回答

url = 'https://play.google.com/store/apps/collection/topgrossing'

このURLにブラウザから接続して、証明書パスを見てみると次のようになっていました。

  • GeoTrust Global CA
  • Google Internet Authority G2
  • *.google.com

ですので証明書パスを検証するには、ローカルなトラストストアにGeoTrust Global CAのルートCA証明書(シリアル番号0x23456)が入っている必要があります。

まず下の箇所ではシステム標準のトラストストアを使わせようとしていますが、これが使えないということでしょうか?

http.cert_store.set_default_paths

次に下の箇所でcacert.pemをトラストストアに追加しようとしていますが、これは上の設定が効かないためにやっている、という理解ですが、それでよいでしょうか?

abc = File.dirname(__FILE__) + '/cacert.pem'
http.cert_store.add_file(abc)

ちなみにcacert.pemの中身はどうなっていますか?

cacert.pemはシステムによってはテスト用のCA証明書のことがありますが、それをトラストストアとして指定しても意味はありません。

cacert.pemに信頼するルートCA証明書の束が入っている場合はOKです。ただしその中にGeoTrust Global CAのルートCA証明書(シリアル番号0x23456)があることが前提です。

手動だとうまく行って、cronからだと失敗する理由がいまいちわからないのですが、ちょっと想像してみたのは、.bashrcなどで環境変数SSL_CERT_DIRやSSL_CERT_FILEを指定しているのだろうかとか...

追記
上の最後の文は推測なんですが、一応根拠を書いておくと、RubyのマニュアルOpenSSL::X509モジュールによると、環境変数SSL_CERT_DIRやSSL_CERT_FILEを指定していると、set_default_pathsで読み込まれる証明書ディレクトリや証明書ファイルを変更できるとあるからです。手動のときだけこれらの環境変数が有効になっているとすると、そういうこともありうると考えました。

編集 履歴 (1)
  • SSL_CERT_PATHは書き間違いだったので修正。あと説明を若干補足しました。 -
  • ありがとうございますご指摘のとおり、.bashrcに記入していたため、手動では成功していました。
    cacert.pemには、GeoTrust Global CAも存在しており、.bashrcで指定していたファイルもこちらになります。
    https://gist.github.com/fnichol/867550
    SSl対応の記述が、そもそも間違っていたことになりますので、改めて調べてみます。
    -

crontabがどうなっているかわかりませんが(レンタルサーバーだからそもそも見れないですかね?)、
cronでエラーが起きた場合、標準では実行ユーザーのメールボックスにメールが飛んでいるはずです。

レンタルサーバーだとレンタルサーバー次第ですが(わたしが使っているとこだと、あらかじめcronのログを受け取るメールアドレスを指定しておけて、出力があった場合にメールが飛んできます)、
「レンタルサーバー名 cron エラー 出力」とかで検索すれば説明しているサイトが見つかるかもしれませんね。

質問 1 ですが、

自分の環境のcronならメールファイル見たり、 crontabでファイルに出力してしまう (> /tmp/my_cron_dev_debug.log 2>&1 みたいな)コマンドにしてみたりでしょうか。

しかしレンタルサーバーのcronとなるといろいろとレンタルサーバー次第になるので、
とりあえず、以下の様にbegin-rescue でエラーメッセージを拾ってみるとエラーメッセージが見れるかもしれません。

#スクレイピング
charset = nil
begin
  html = open(url) do |f| 
    charset = f.charset               
    f.read   
  end
rescue => e
  File.open("check_scraping.txt","w") do |file|
    file.puts(e.message)
    file.puts(e.backtrace)
  end  
end

または

begin
  #スクレイピング
  charset = nil
  html = open(url) do |f| 
    charset = f.charset               
    f.read   
  end
rescue => e
  scraping_error = e.message
  scraping_backtrace = e.backtrace
ensure
  # エラーでもそうでなくても通る
  File.open('check_scraping.txt', 'w') do |file|
    file.puts(scraping_error) if defined?(scraping_error)
    file.puts(scraping_backtrace) if defined?(scraping_backtrace)

    file.puts(charset) if defined?(charset)
    file.puts(html) if defined?(html)
    file.puts(url) if defined?(url)
  end
end

上記のスクリプトで、エラーでファイルが出るかどうかは

begin
  puts 1 + 'a'
rescue => e
  ...

などで一旦確認してみるといいでしょう
プロセスがkillされてたりすると、これだと出ないかもしれませんね。

質問 2 ですが、

それ以外だと、環境変数(cronで実行中のもの$PATHだけでなく)、起動シェル(素のshだったり)、プロファイル(.bash_profile等)、rubyバージョン。
私ならこのあたりを気にします。
環境変数全部見れれば他のはそれで大体わかると思います。

あとは直接スクリプト(.rbファイル)を指定するのか、rubyスクリプトを実行するシェルスクリプトを書くのかも検討するかも知れません。

質問 3 は手動では実行できているとのことなのでエラーの内容次第ですね。

編集 履歴 (0)
  • コメントありがとうございます。
    教示していただいた内容で、エラーを確認することが出来ました。
    証明書の部分でエラーしていたので、以降すべてでエラーしていたようです。

    SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed
    -

斜め読みした感じでは、crontabからの実行による影響は無さそうですが、あくまで斜め読みなので。

質問1.
crontabから実行されるコマンドから、何らかの出力があった場合、または終了コードが0でない場合は、その出力が、crontabのユーザー宛にメールされますので、普通はメールで実行結果を知ります。
メールで無くファイルに残したい場合は、crontabで、
0 6 * * * ruby foo >foo.log 2>&1
のようにします。

質問2.
多くの場合(ほとんどの場合)は、環境変数です。
Rubyスクリプトの場合は、先頭でp ENVしておくと環境変数が分かるのでいいかと。

権限は、crontabのユーザーで実行されるので、共用レンタルサーバーの場合だと、コマンドラインからの実行の場合と同じです。専用サーバーの場合は、どのユーザーで実行しているかは自分で分かっていると思うので。

あとは、起動時のカレントディレクトリはcrontabのユーザーのホームディレクトリです。これもコマンドラインからの場合と普通は同じでしょう。上に書いた例だと、foo と foo.log はホームディレクトリのファイルです。

質問3.
なんかゴチャゴチャしているので、多分こういう結果が欲しいんだろうという物に単純化してみました。

require "open-uri"
require "nokogiri"

# スクレイピングするURL
url = 'https://play.google.com/store/apps/collection/topgrossing'

charset = nil
html = open(url) do |f|
  charset = f.charset
  f.read
end

File.open("check02.txt","w") do |file|
  file.puts(charset)
  file.puts(html)
  file.puts(url)
end

#要素格納用配列 utf-8
arRankTitle = []
arRankUrl = []
arRankNum = []

# htmlをパース(解析)してオブジェクトを作成
doc = Nokogiri::HTML.parse(html, nil, charset)

#タイトル
quTitle = doc.title
puts quTitle

#アプリタイトルとURLを
doc.xpath("//div[@class = 'details']/a").each do |node|
    title = node.text
    #からオブジェクトが居るので間引く
    next if title.empty?

    puts "--------------------"

    rank,title = title.gsub(" ", "").split(".",2)

    url ="https://play.google.com" + node[:href]

    puts "#{rank} <=順位です。"
    puts title

    arRankTitle << title
    arRankUrl << url
    arRankNum << rank
end
編集 履歴 (1)
  • コメントありがとうございます。
    お見苦しいソースでしたので、書きかたの教示は参考になります。
    #SSl対応 の部分は、レンタルサーバーのため証明書が存在していないため、記述しておりますた。ただ、↓の方法でエラーを確認すると、証明書でエラーを起こしていたようです。
    -
ウォッチ

この質問への回答やコメントをメールでお知らせします。