Ruby - UNIX MBOX メールヘッダ「Date」検証!
Updated:
先日は、Ruby でメールの UNIX MBOX データの読み込みを試してみました。
今後、この読み込んだデータを MySQL に保存することを考えていますが、何万件とあるデータを一気に取り込もうとすると少なからず不正なデータ存在します。
そこで、少しずつ Ruby でデータの検証をしてみようと考えました。
今回は、メールヘッダの「Date」属性を検証してみました。
このメールヘッダの「Date」は「RFC 5322」(以前の「RFC 2822」・「RFC 822」)に準拠した書式でなければなりません。
Fri, 14 Oct 2011 23:59:59 +0900
というような書式です。
参考サイト
※「RFC 822」の改訂版が「RFC 2822」で「RFC 2822」の改訂版が「RFC 5322」ですが、今でも「RFC 822」や「RFC 2822」に準拠した記述をしているものがあるようです。(実際、多数あります) ※「RFC」の詳細はWeb等で検索してみてください。
補足すると、
- 曜日部分 “Fri, “ はなくてもよい。
- 秒部分 “:59” はなくてもよい。
- タイムゾーン部分は “+0900” というような “+” か “-“ と4桁の数字でなく、 “UT”, “GMT”, “EST”, “EDT”, “CST”, “CDT”, “MST”, “MDT”, “PST”, “PDT”, “Z”, “A”, “M”, “N”, “Y” でもよい。( RFC 822 準拠の場合 )
この「Date」属性が「RFC 5322」(「RFC 2822」・「RFC 822」)に準拠した書式となっているかどうかを「正規表現」を使用してチェックしてみました。
Rubyサンプルスクリプト
「Ruby - UNIX MBOXデータ読み込み!」で紹介したRubyスクリプトを流用しています。 以下のスクリプトでは今後のために正規表現でのマッチング処理時に値を取得できるようにしています。 ●ファイル名:ana_mbox_check_date.rb
require 'find'
require 'kconv'
require 'time'
class AnaMboxCheckDate
# MBOXディレクトリ
DIR_MBOX = "D:\01_Mail\Thunderbird"
# 正規表現 ( Date )
REG_DATE = /(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun), )?(\d{1,2}) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}|\d{2}) (\d{2}):(\d{2})(?::(\d{2}))? (UT|GMT|[ECMP][SD]T|[ZAMNY]|[+-]\d{4})/
# [CLASS] 解析
class Analyze
# INTIALIZE
def initialize
# 読み込みシーケンスNo
@seq = 0
# 正規表現にマッチしないものの件数
@cnt_notmatch = 0
end
# Mailbox名一覧取得
def get_mailbox_list
begin
# 指定ディレクトリ配下のディレクトリ一覧
res = Array.new
Dir::entries( DIR_MBOX ).each do |d|
if File::ftype( DIR_MBOX + "/" + d ) == "directory"
res << d unless ( d.to_s == "." || d.to_s == ".." || d.to_s == "local" )
end
end
return res
rescue => e
# エラーメッセージ
str_msg = "[EXCEPTION][" + self.class.name + ".get_mailbox_list] " + e.to_s
STDERR.puts( str_msg )
exit 1
end
end
# MBOXファイル一覧取得
def get_mbox_list( mailbox )
begin
# 指定ディレクトリ配下のファイル一覧
res = Array.new
Find.find( DIR_MBOX + "/" + mailbox ) do |f|
# ファイルのみ抽出
unless File::ftype( f ) == "directory"
# ファイルを読み込み1行目の先頭がFromならMBOXと判定
open( f ) do |file|
l = file.gets
if l =~ /^From/
res << f
end
end
end
end
return res
rescue => e
# エラーメッセージ
str_msg = "[EXCEPTION][" + self.class.name + ".get_mbox_list] " + e.to_s
STDERR.puts( str_msg )
exit 1
end
end
# MBOXファイル解析
def ana_mbox( mbox )
begin
# 1つのMBOXファイルを開く
open( mbox ) do |f|
f.slice_before do |line|
# "From "を1グループの先頭とみなす。
line.start_with? "From "
end.each do |mail|
header = {}
# HEADER読み込み
mail.each do |row|
line = row.kconv( Kconv::UTF8, Kconv::AUTO )
# 行最後の改行文字を削除
line.chop!
# "From "で始まる行を読み飛ばし
next if /^From / =~ line
# HEADERの終了
break if /^$/ =~ line
# 属性と値を取得
if /^(\S+?):\s*(.*)/ =~ line
( @attr = $1 ).capitalize!
header[@attr] = $2
elsif @attr
# 複数行にわたる場合は結合
line.sub!( /^\s*/, '' )
if @attr == "Subject"
# Subjectの場合は改行せずに結合
header[@attr] += line
else
header[@attr] += "\n" + line
end
end
end
# 読み込みSEQインクリメント
@seq += 1
# nilなら""を設定
header['Date'] ||= ""
header['Subject'] ||= ""
# 以下の正規表現マッチング処理で取得できるもの
# $1 : 曜日, $2 : 日 $3 : 月, $4 : 年, $5 : 時, $6 : 分, $7 : 秒, $8 : タイムゾーン
unless header['Date'] =~ REG_DATE
@cnt_notmatch += 1
puts "SEQ.#{@seq}"
puts "\tDate : #{header['Date']}"
puts Kconv.tosjis( "\tSubject: #{header['Subject']}" )
end
end
end
rescue => e
# エラーメッセージ
str_msg = "[EXCEPTION][" + self.class.name + ".ana_mbox] " + e.to_s
STDERR.puts( str_msg )
exit 1
end
end
def seq
return @seq
end
def cnt_notmatch
return @cnt_notmatch
end
end
##############
#### MAIN ####
##############
begin
puts "====< START >===="
# 解析クラスインスタンス化
obj_ana = Analyze.new
# Mailbox名一覧取得
mailbox_list = obj_ana.get_mailbox_list
# Mailbox分ループ
mailbox_list.each do |mailbox|
# MBOXファイル一覧取得
mbox_list = obj_ana.get_mbox_list( mailbox )
# MBOXファイル分ループ
mbox_list.each do |mbox|
# MBOXファイル解析
obj_ana.ana_mbox( mbox )
end
end
puts "TOTAL COUNT = #{obj_ana.seq}"
puts "NO REGEXP = #{obj_ana.cnt_notmatch}"
puts "====< E N D >===="
rescue => e
# エラーメッセージ
str_msg = "[EXCEPTION] " + e.to_s
STDERR.puts( str_msg )
exit 1
end
end
検証結果
当方のメールデータ(Thunderbird)で検証した結果、「RFC 2822」 (「RFC 822」)に準拠していなかったものは、61,655件中 22件 でした。 内訳は以下のとおり。
1.時刻の「時」部分が2桁でなく1桁のもの | 17件 |
2.「日」と「月」の間に半角空白が2個あるもの | 2件 |
3.「Date」属性値がないもの | 3件 |
それほど、深刻なものではありませんでした。 1と2は Time.parse で問題なく変換できます。 3は何かしら値を設定してやればよいです。
参考書籍
今回は、メールヘッダの「Date」属性をチェックしましたが、次回も何か検証するつもりです。
それにしても「正規表現」でのマッチング処理って面白いし、便利ですね。
Ruby では文字列を比較チェックするより断然「正規表現」でのマッチングが高速だし。。。
以上。
Comments