苦い抗生物質を小さい子供に飲ませるのは
難しいと感じたよ。
なかなか飲んでくれない(;ω;)
チョコレートなどで、口の中に油膜をはれるものが
好まれる理由がわかりました。
前回に続き、R言語のプログラムをcronを用いた定時処理を利用して、
レポートを作成し、メールで知らせてくれる
サンプルプログラムを作成してみました。
(もちろんKali Linux環境のはなしです!)
具体的には、
1.指定した時間になるとプログラムが起動する。
2.R言語の分析プログラムが実行され、データファイルを基にダイレクトメール(DM)送信後の来客数や、サイト閲覧数を計測レポートを作成する。
3.作成したレポートを、指定した送信先にメールで送る。
というものです。
本日は、全般的に技術的な話となりますので、
ご興味のある方だけご覧ください。
■環境構築
1.インストール
R言語をインストールしていなければ、
こちらの手順を参考にR言語をインストールしてください。
作業に必要なパッケージをインストールします。
# 管理者権限で、日本語漢字フィルター(日本語の取扱いができるようにする)のインストール
apt-get install nkf
2.ユーザおよびディレクトリの作成
本来管理者権限で自動処理を行うことは、
あまり好ましいことではないので、自動処理を実行する
専用の作業ユーザを作成する方針で話を進めます。
※今回は特に作業ユーザの所属グループについては触れません。
# 管理者権限で、作業ユーザを作成する(今回はappleboyユーザを作成)
useradd -d /home/appleboy -m -s /bin/bash appleboy
# 管理者権限で、作業ユーザのパスワードを設定する(今回はtestに設定)
passwd appleboy
# 環境変数を引き継がずにappleboyユーザでログイン
su - appleboy
# ユーザがappleboyとなっていることを確認
whoami
# プログラムファイルを配置するディレクトリを作成します。
mkdir -p /home/appleboy/app/sample/
# 使用する生のCSVデータ(サンプルデータ)を置くディレクトリを作成
mkdir -p /home/appleboy/app/sample/baseinput/
# 読み込み用に加工したCSVデータ(サンプルデータ)を置くディレクトリを作成
mkdir -p /home/appleboy/app/sample/input/
# 処理結果レポートを置くディレクトリを作成
mkdir -p /home/appleboy/app/sample/report/
■プログラムの配備
以下の手順で対応します。
1.サンプルプログラム配置
2.プログラムの実行権限の設定
3.環境依存チェック
1.サンプルプログラム配置
以下4ファイルを配備します。
配置はすべて同じ作業ユーザ(ここでは、appleboy)
を使用してください。
a.csvファイル(データファイル)
b.メイン処理ファイル(bashシェルスクリプト)
c.データ解析処理ファイル(Rscriptプログラム)
d,メール送信ファイル(rubyスクリプト)
プログラムの処理の流れは、以下の流れとなります。
1.CRON(定時処理)が起動し、bのメイン処理が実行される。
2.bのメイン処理からcのデータ解析処理が呼び出され、レポートの文面および添付画像ファイルを作成する。
3.bのメイン処理からdのメール送信処理が呼び出され、メールを送信する。
※別々のプログラミング言語を使ってすみません。。。
ソースコードをのせるのにはアメブロは向いてないですね。。。
ソース記述に向いているCMSを公開サーバ上にのせるか、gitを検討してます。。。
a.csvファイル(データファイル)
以下の参考サイトをブラウザで開き、例題のCSVをダウンロードし、
「/home/appleboy/app/sample/baseinput/」に保存してください。
[参考サイト:アデリー]
http://xica-inc.com/adelie/sample/restaurant.html
※開いたページ下の「サンプルデータをダウンロードする」で入手した
圧縮ファイル中に存在する「restaurant.csv」が対象のデータとなります。
# csv(comma separated value=カンマ区切り)のデータファイルの配置確認
ls -la /home/appleboy/app/sample/baseinput/restaurant.csv
ちなみにファイルの中身はこんな感じですね。
日付,レストランの集客,サイトG:総PV数,サイトH:総PV数,サイトT:総PV数,最高気温,雨天,DM送信,土日祝前日$
2012/4/1,66,135,78,81,14.7,0,1,0
2012/4/2,44,139,66,78,14.9,0,0,0
2012/4/3,67,72,72,70,17.7,1,0,0
2012/4/4,69,113,75,58,15.2,0,0,0
b.メイン処理ファイル(bashシェルスクリプト)
メイン処理のプログラムが以下の内容となります。
コピー&ペーストでプログラムを[main.sh]のファイル名で
「/home/appleboy/app/sample」ディレクトリに保存してください。
#!/bin/bash
### start.sh 統計解析レポート送信処理
### author mikimiku
### エラーログは標準であれば、/var/log/messagesに出力
###
# 環境変数の設定
export PWD="/home/appleboy/app/sample"
# 処理開始ログを記録
logger -ip user.notice 'program start:main.sh'
### ファイル名を読み取るための日付生成
# yyyymmddで本日の日付を生成
senddate=`date +"%Y%m%d"`
# yyyymmddで前日の日付を生成
datadate=`date --date "-1days" +"%Y%m%d"`
# ディレクトリ名として使用する、前日のyyyymmを生成
dataym=${datadate:0:6}
# レポート出力先ディレクトリが存在しなければ作成
cur=${PWD}
if [ -e ${cur}/report/${dataym} ]
then
logger -ip user.notice 'proccess レポート作成先のディレクトリは存在します。'
else
mkdir -p ${cur}/report/${dataym}
fi
# メール本文と添付するレポート画像ファイル名を定義
mailbody="${cur}/report/${dataym}/${datadate}-report.txt"
report="${cur}/report/${dataym}/${datadate}-report.png"
# 解析処理実行、レポート及びメール本文を生成
${PWD}/sample.R 1>${mailbody} 2>&1
# 解析処理でエラーが発生したら、エラーメッセージをログに出力
if [ $? -ne 0 ]
then
logger -ip user.warn "Error 解析処理でエラーが発生しました。${mailbody}を確認してください。"
fi
# 解析処理でレポート画像ファイルが存在しなければ、エラーメッセージをログにを出力
if [ -e ${report} ]
then
logger -ip user.notice 'proccess レポートの作成に成功しました。'
else
logger -ip user.warn "Error 解析処理でエラーが発生しました。${report}が作成されていません。"
fi
# メール送信処理
result=`"${PWD}"/sendmail.rb ${mailbody} ${report} 2>&1`
# メール送信処理に失敗すればエラーメッセージをログに出力
if [ $? -ne 0 ]
then
logger -ip user.warn "Error ${result}"
fi
# 処理終了ログ記録
logger -ip user.notice 'program terminate:main.sh'
# ファイルの配置確認
ls -la /home/appleboy/app/sample/main.sh
c.データ解析処理ファイル(Rscriptプログラム)
データ解析処理のプログラムが以下の内容となります。
コピー&ペーストでプログラムを[sample.R]のファイル名で
「/home/appleboy/app/sample」ディレクトリに保存してください。
#!/usr/bin/Rscript
### レストラン集計データ検証 ####
# author mikimiku
# #←これはコメントです
# プログラム実行はこのファイルの下の方から開始されます
#################################
require(grDevices)
require(graphics)
# 相関分析関数
correl <- function(retdata, i, msg) {
# 相関分析
report <- cor(retdata)
### 相関分析結果が「高い相関あり(0.7以上1.0未満)」となるものを表示###
# 相関分析データ1件ずつ抽出し、高い相関のデータを抽出する
for(k in 1:nrow(report)) {
# 高い相関のデータであれば内部の処理を行う
if (report[k] >= 0.7 & report[k] < 1) {
# 相関が高い旨のメッセージを表示する
print(gsub("%s%", i, msg))
# 相関状況のデータを表示する
print(report[report[k] >= 0.7 & report[k] < 1,])
break
}
}
}
# グラフの色を取得する関数
getcolors <- function() {
return(
c(
"green",
"blue",
"red",
"yellow",
"orange",
"brown",
"pink",
"magenta",
"lightblue",
"lightgreen",
"black",
"violet"
)
)
}
# 平均値グラフ描画関数
draw <- function(retdata, i, msg) {
# 平均値を算出
line <- colMeans(retdata)
# グラフの色を取得
cols = getcolors()
# グラフを描画
lines(line, type="l", col=cols[i])
}
# メイン処理関数
main <- function(date) {
# 処理日に前日日付のcsv(comma separated value=カンマ区切り)データファイルを分析結果対象とする
reportymd <- format(date - 1, '%Y%m%d')
reportym <- format(date - 1, '%Y%m')
# paste0で現在のディレクトリからのファイルパスを整形する(例:./input/201505/restaurant-20150528.csv)
inputfilepath <- paste0("./input/", reportym, "/restaurant-", reportymd, ".csv")
# 対象データのcsvファイルがなければ処理中断
if(file.access(inputfilepath) != 0) {
warning(paste(inputfilepath, "が存在しないため、データ分析処理を中断しました。", sep=""))
}
### 対象データのcsvファイル取り込み###
# 文字コードUTF-8でファイルを読み込む
fp <- file(inputfilepath, encoding="UTF-8")
# 項目ヘッダー行が先頭に存在し、","(カンマ)区切りのcsvファイルとして、データフレームとして取り込む
data <- read.csv(fp, header=T, sep=",")
# 比較を容易にするために、日付フォーマットを文字列型に変換
data[1] <- apply(data[1], 1, function(x) {return(format(as.Date(x) , "%Y/%m/%d"))})
# グラフ描画設定(ping画像ファイルデバイスオープン)
png(paste0("./report/",reportym, "/", reportymd , "-report.png"), width = 400, height = 300)
# グラフ描画の範囲やラベル設定
plot(0, 0, type = "n", xlim = range(1:8), ylim = range(0,110),
xlab = "1:レストランの集客, 2:サイトG.総PV数, \n3:サイトH.総PV数, 4:サイトT.総PV数", ylab = "平均集客・PV数")
# 汎用ラベル名称設定ベクトルを作成
legendlabel <- c()
### DMを送った当日ー10日後に集客数が増えているか
# DM送信日付のベクトルを取得
dmday <- data[data$DM送信 == 1, ][1]
# 調査対象は、DM送信日付の翌日から10日後までの日付
maxday = 10
afterday <- c(1:maxday)
tmp <- "afterdm"
### DM送信1日後から10日後までのデータについて、来客数と各サイト閲覧の相関を調査 ###
for (i in 1:length(afterday)) {
# DMを送った日からプラスi日した日付のベクトルを取得
tmpday = eval(
parse(
text=paste(
tmp, i, " <- c(apply(dmday, 1, function(x) {return(format((as.Date(x) + i), '%Y/%m/%d'))}))", sep=""
)
)
)
# 抽出結果データ格納ベクトルを初期化
retdata <-c()
# DMを送信したi日後の日付のデータ(length(tmpday)回DMを送信したと仮定)を取得する
for(j in 1:length(tmpday)) {
# DMを送信したi日後の日付データの集客とサイト訪問数を1件抽出する
tmpdata <- data[data$日付 == tmpday[j], c("レストランの集客","サイトG.総PV数", "サイトH.総PV数", "サイトT.総PV数") ]
# 行列retdataに、抽出したデータを追加する
retdata <- merge(retdata, tmpdata, all=TRUE)
}
# DMを送信したi日後のデータに対して、集客とサイト訪問数の相関分析を実行する
msg <- "=====DM送信 %s% 日後のサイト閲覧と客数における相関====="
# このプログラムファイル上で定義した、自作関数correl()関数を呼び出す
correl(retdata, i, msg)
### 平均集客数とサイト訪問数のグラフ描画を行う ###a
# 汎用のラベル文字を設定
msg <- "DM送信%s%日後平均"
legendlabel[length(legendlabel) + 1] <- gsub("%s%", i, msg)
# グラフを描画
draw(retdata, i)
}
### 平均集客数とサイト訪問数のグラフ描画を行う ###
# 平日平均のグラフ描画を行う
legendlabel[length(legendlabel) + 1] <- "平日平均"
draw(data[data$土日祝前日 == 0, c("レストランの集客","サイトG.総PV数", "サイトH.総PV数", "サイトT.総PV数") ] , maxday + 1)
# 土日祝前日平均のグラフ描画を行う
legendlabel[length(legendlabel) + 1] <- "土日祝前日平均"
draw(data[data$土日祝前日 == 1, c("レストランの集客","サイトG.総PV数", "サイトH.総PV数", "サイトT.総PV数") ] , maxday + 2)
# グラフ凡例の描画
cols <- getcolors()
legend("bottomright", legend = legendlabel, lty = 1, col = cols)
# ping画像ファイルデバイスクローズ
dev.off()
# グラフィクスのバッファクリア
graphics.off()
}
### ここから処理開始 ###
# 作業ディレクトリを設定(このプログラムファイルを置いたディレクトリを指定します)
setwd("/home/appleboy/app/sample/")
# 本プログラムの終了時のステータス変数を定義
code <- 1
# tryCatchブロックで通常処理、異常処理、終了処理を設定し、メイン処理を実行します
tryCatch({
# 自作メイン処理関数のmain()を実行します(main()のパラメータに本日のシステム日付を渡します)
main(Sys.Date())
code <- 0
},
# stop()で中断が発生したら、実行される処理(変数eは、stop()のメッセージが入ります)
error = function(e) {
message(e)
code <- 1
},
# warning()で警告が発生したら、実行される処理(変数eは、warning()のメッセージが入ります)
warning = function(e) {
message(e)
code <- 1
},
# 最後に必ず実行する処理(error, warningの処理より先に実行されます)
finnaly = {
},
silent = TRUE
)
# 処理終了(セーブをせずに、exitステータスは1:異常、0:正常、終了処理は実行しない)
quit(save = "no", status = code, runLast = FALSE)
# ファイルの配置確認
ls -la /home/appleboy/app/sample/sample.R
d,メール送信ファイル(rubyスクリプト)
メール送信処理のプログラムが以下の内容となります。
コピー&ペーストでプログラムを[sendmail.rb]のファイル名で
「/home/appleboy/app/sample」ディレクトリに保存してください。
(今回、初めてRuby書いたので、ソースが汚いかもしれませんが、ご容赦ください。。。)
#!/usr/bin/ruby -Ku
# encoding: utf-8
### package: sample
### author mikimiku
### memo: 一行目、linux実行プログラムパス、二行目文字コード
###
### usage(実行例): ./sendmail.rb body.txt attachfile
### ARGV[0]: body.txt メール本文テキストファイル
### ARGV[1]: attachfile 添付ファイル
###
###
# メールライブラリの読み込み
require "mail"
# rubyライブラリの読み込み
require "date"
# パラメータチェック
unless ARGV.length == 2
puts "パラメータは2つとし、一つ目が本文、二つめが添付ファイルとなります。"
exit(1)
end
# メール本文に利用するテキストファイルを指定
bodytext = ARGV[0]
# メール本文テキストファイルが存在しない場合は、
# エラーメッセージを出力し、処理終了
if FileTest.exist?(bodytext) then
else
puts "指定したメール本文テキストファイル#{bodytext}が存在しません。"
exit(1)
end
# メール本文に添付するファイルを指定
attachfile = ARGV[1]
# メール本文に添付するファイルが存在しない場合は、
# エラーメッセージを表示し、処理終了
if FileTest.exist?(attachfile) then
else
puts "指定した添付ファイル#{attachfile}が存在しません。"
exit(1)
end
# mail送信情報を設定
today = Date.today.strftime("%Y%m%d")
mail = Mail.new do
# 送信元メールアドレス
from "from@example.desu.com"
# 送信先メールアドレス
to "to@example.desu.com"
# メール件名
subject "report-" << today
# メール本文(テキストファイルの内容を使用)
body File.read(bodytext)
# 添付ファイル
add_file attachfile
end
# mail送信の設定
mail.delivery_method(
# SMTPサーバを指定してメールを送信する
:smtp,
# SMTPサーバ
address: "smtp.example.desu.com",
# SMTPサーバのポート番号
port: 587,
# 送信ドメイン名
domain: "localhost.localdomain",
# 認証:ログイン認証
authentication: :login,
# 認証ユーザ名
user_name: "user@example.desu.com",
# 認証パスワード名
password: "password",
# SSLによる暗号化を有効
ssl: true,
# 自動的にSSLを開始する
enable_starttls_auto: true
)
# mail送信を実行
mail.deliver
# ファイルの配置確認
ls -la /home/appleboy/app/sample/sendmail.rb
2.プログラムの実行権限の設定
管理者権限で作業します。
# root(管理者)権限に変更(あるいは、sudoを各コマンドの頭につけて実行)
su
# ユーザがrootであることを確認(sudoを使う場合は確認不要)
whoami
# プログラムファイルのディレクトリへ移動
cd /home/appleboy/app/sample/
# プログラムファイルのオーナ/グループを実行ユーザに設定
chown appleboy:appleboy sample.R main.sh sendmail.rb
# プログラムファイルのパーミッションをユーザのみ実行可能に設定
chmod 764 sample.R main.sh sendmail.rb
# 確認コマンドでこんな感じになっていればOKです
ls -la sample.R main.sh sendmail.rb
-rwxrw-r-- 1 appleboy appleboy 2093 6月 5 23:35 main.sh
-rwxrw-r-- 1 appleboy appleboy 7167 6月 5 22:39 sample.R
-rwxrw-r-- 1 appleboy appleboy 2464 6月 5 22:00 sendmail.rb
3.環境依存チェック
a.拡張ライブラリインストール
管理者権限で作業します。
・プログラミング言語rubyライブラリ
# メールライブラリをインストール
gem install mail
# 日本語メールライブラリをインストール
gem install mail-iso-2022-jp
# 日付ライブラリをインストール
gem install date
b.文字コードの確認・設定
作成したユーザ権限で作業します。
# プログラムファイルのディレクトリへ移動
cd /home/appleboy/app/sample/
# 文字コードを確認します。
nkf -g /home/appleboy/app/sample/main.sh
nkf -g /home/appleboy/app/sample/sample.R
nkf -g /home/appleboy/app/sample/sendmail.rb
nkf -g /home/appleboy/app/sample/baseinput/restaurant.csv
文字コードがLinuxの文字コード(通常はUTF-8)を確認し、
文字コード環境を合わせて、入力データファイルをinputディレクトリに配備する
# 現在年月のディレクトリが存在するか確認する
ls input/`date +%Y%m`
ls report/`date +%Y%m`
# 年月ディレクトリが存在しなければディレクトリを作成する
mkdir -p input/`date +%Y%m`
mkdir -p report/`date +%Y%m`
以下文字コード設定になります。
vi(vim)エディタを使っていることとして話を進めます。
###Linuxの文字コードがutf-8の場合###
# 対象のCSVデータファイルの文字コードをUTF-8に変換し、処理日時のファイル名で保存する(例:input/201505/restaurant-20150530.csv)
nkf -w baseinput/restaurant.csv > input/`date +%Y%m`/restaurant-`date +%Y%m%d`.csv
# vi(テキストエディタ)で文字コードをUTF-8に変更して保存する
vi sample.R
# 文字コードの変更
:set fenc=utf-8
# プログラムソース内encodingの変更
fp <- file(inputfilepath, encoding="UTF-8")
# (viで編集している場合は以下コマンドで)保存して終了
:wq!
# 他のファイルについても文字コードをUTF-8に合わせてください。
###Linuxの文字コードがeuc-jpの場合###
# 対象のCSVデータファイルの文字コードをEUC-JPに変換し、処理日時のファイル名で保存する
nkf -e baseinput/restaurant.csv > input/`date +%Y%m`/restaurant-`date +%Y%m%d`.csv
# vi(テキストエディタ)で文字コードをEUC-JPに変更して保存する
vi sample.R
# 文字コードの変更
:set fenc=euc-jp
# プログラムソース内encodingの変更
fp <- file(inputfilepath, encoding="EUC-JP")
# (viで編集している場合は以下コマンドで)保存して終了
:wq!
c.プログラムソースコードの確認
異なるユーザディレクトリなどに配置した場合は。
変更する必要があります。
変更箇所は以下のとおりです。
・main.sh
プログラムの配置ディレクトリが異なる場合は
以下の変更が必要となる。
export PWD="/home/appleboy/app/sample"
・sample.R
プログラムの配置ディレクトリが異なる場合は
以下の変更が必要となる。
setwd("/home/appleboy/app/sample/")
・sendmail.rb
メール送信情報をプロバイダから付与された情報に書き換える。
# メール送信元の変更
from "from@example.desu.com"
# メール送信先の変更
to "to@example.desu.com"
# メール送信サーバのSMTPサーバ変更
address: "smtp.example.desu.com",
# メール送信サーバのポート番号変更
port: 587,
# メール送信サーバの送信ドメイン名変更(必要があれば)
domain: "localhost.localdomain",
# メール送信サーバの認証方式(必要があれば)
authentication: :login,
# メール送信サーバの認証ユーザ(login認証の場合)
user_name: "user@example.desu.com",$
# メール送信サーバの認証パスワード(login認証の場合)
password: "password",
# メール送信サーバのSSL暗号(暗号を無効化する場合)
ssl: true,
■CRONのスケジューリングと起動確認
1.CRON(定時処理バッチ)にプログラム実行の記述追加
# 定時処理のcronを編集
vi /etc/crontab
時間の指定の仕方はこちら
# 以下のようにaplleboyユーザで、起動したい時刻とプログラムファイルを指定する。
# 以下例では、毎月1日の22:52にプログラムを実行する
52 22 1 * * appleboy /home/appleboy/app/sample/main.sh
2.cronの起動を確認
# 処理起動のログの確認(ログ出力先を変更した場合はログファイルを変更する)
tail -n 50 -f /var/log/messages
cronのトラブルシューティング
※尚、送信されたメールが迷惑メールに振り分けられる場合もありますので
ご注意ください。
最後に、
クラウドなどで、データを預ける考え方もあるかもしれませんが
自社に必要なDWHのように意思決定に必要な、
集計データを定期的にエクスポートするのも良いですよね!
そして、Rstudioで解析を試行的に行い、
分析価値があるものを自動処理に落とし込み、改善や新しい統計を積み上げる。
この流れで、データという資産を有効活用したいですね!
そして、情報資産を生かした素敵な産業につながったり、
幸せや希望のあふれる日本の未来が切り開かれることを願っています。
■反省
・本来であれば、はじめにデータベースからデータを抽出する処理も
入れるのが好ましいと思います。
・レポートの通知方法ですが、
画像を添付するのではなく、メール本文にwebリンク載せて、
ブラウザで解析結果を閲覧できる仕様の方が、
メールの負荷も軽くなるし、あとからも確認できるので便利ですね。
・定期的に統計解析するプログラムを、あとから追加で載せるのであれば、複数のプログラムを逐次実行することを想定して、統計解析プログラムをもっと疎結合に機能分割してドメインや処理レイヤを意識したいですね。あとは日付の生成処理のような重複を排除!メール送信モジュールをサブクラス化して、外部から設定をインジェクション!。。。などなど
・データ量が多い場合の負荷などを特に考えない処理になっているため、
データ量が大きい場合は
集計途中のデータを、一旦中間データとして保持する仕組みが必要かもしれません。