kusano

初投稿となります、クライゼルグループの kusano です。

何を書こうか考えたのですが、PHP の文字列チェック関数について少し気になったことがあったので、この機会に簡単な調査をしてみました。

PHP で文字列がどういった文字で構成されているかを確認する関数として有名なのは preg_match でしょうか。正規表現によるマッチングを行えるので万能ですね。

一方、文字列が「0 から 9 の数字」のみで構成されているかどうかのみをチェックする代わりに速い (らしい) 関数として ctype_digit といったものもあると聞きました。

以下では、ctype_digit 関数とはそもそもどういった関数かをまとめた上で、preg_match との速度比較を行ってみようと思います。

ctype_digit とは

ctype_digit 関数の仕様については こちら に書いてある通りで、「与えられた文字列 text のすべての文字が数字であるかどうかを調べます。」となります。

ただ、リンク先ページの「注意」にも書いてあるとおり、気をつけるべき点があります。以下に引用します。

  • 注意:
    • この関数を活用するには string を渡さなければなりません。 たとえば integer を渡すと、期待する結果にならない可能性があります。 HTML フォームに入力された整数値は、integer ではなく string 型で返されます。 マニュアルの 型 についての節を参照ください。
    • -128 から 255 までの整数値を渡すと、ひとつの文字の ASCII 値とみなします (負の値には 256 を足して、拡張 ASCII の範囲に収まるようにします)。 それ以外の整数値は、10 進数を含む文字列とみなします。

つまり、-128 から 255 までの整数値を ctype_digit に渡してしまうと、文字列が 0 から 9 の数字のみで構成されているかどうかではなく、その ASCII 値に対応する文字列が 0 から 9 の数字かどうか、がチェックされるというわけですね。

実際に簡単なプログラムを動かして ctype_digit の振る舞いを見てみます。

<?php

for ($i = -512; $i <= 512; $i++) {
    if ($i < 256 and $i > -129) {
        echo "ASCII Code: ";
        echo chr($i);
    } else {
        echo "Non ASCII Code";
    }
    
    echo " ($i): ";
    var_dump ( ctype_digit($i) );
}

上記のコードを実行すると、以下のような結果が出ます。 (長いので一部割愛します)


Non ASCII Code (-512): bool(false)   ← ASCII 値とは扱わないが 
Non ASCII Code (-511): bool(false)      "-" が含まれるので false
             :
Non ASCII Code (-129): bool(false)
ASCII Code: <80> (-128): bool(false) ← ASCII 値として扱い、
ASCII Code: <81> (-127): bool(false)    0-9 の文字ではないので false
             :
ASCII Code:  (-2): bool(false)
ASCII Code:  (-1): bool(false)
ASCII Code: ^@ (0): bool(false)
             :
ASCII Code:   (32): bool(false)
ASCII Code: ! (33): bool(false)
             :
ASCII Code: . (46): bool(false)
ASCII Code: / (47): bool(false)
ASCII Code: 0 (48): bool(true)  ← ASCII 値として扱い、
             :                     0-9 の文字なので true
ASCII Code: 1 (49): bool(true)
ASCII Code: 9 (57): bool(true)
ASCII Code: : (58): bool(false) ← ASCII 値として扱い、
ASCII Code: ; (59): bool(false)    0-9 の文字ではないので false
ASCII Code: < (60): bool(false)
             :
ASCII Code:  (254): bool(false)
ASCII Code:  (255): bool(false)
Non ASCII Code (256): bool(true)  ← ASCII 値と扱わず、0-9 の文字だけで
Non ASCII Code (257): bool(true)     構成されると見なされ true
             :

実際にこういったプログラムを書いてみると、どういった動きになるかひと目で分かりますね。

なお、float 型や bool 型はどの値を与えても false が返ります。

したがって、ctype_digit を利用する場合、あえて「ASCII 値に対応する文字列が 0 から 9 の数字かどうかをチェック」するのでない限り、値を必ず string 型にしてから渡すようにするよう注意が必要ということですね。

ちなみに、preg_match に string 型以外を渡すと、string 型にキャストして判定されるようです。

ctype_digit と preg_match との速度比較

ctype_digit と preg_match との速度比較 としては、ctype digit と preg matchでの文字列比較速度 で比較検証がされていますね。こちらの結果によるとほとんど両者の速度は変わらないそうです。

ただ、こちらで紹介している方法ですと、それぞれの関数による処理時間に加え、以下の処理時間も計測に含まれているようです。

  • 文字列の生成処理
  • echo による出力処理

仮に上の 2 つの処理が ctype_digit と preg_match に比べて数倍以上時間がかかってるとすると、2つの関数の処理時間の差が埋もれてしまっているかもしれませんね。

そんなわけで、ctype digit と preg matchでの文字列比較速度 で紹介されているコードから、echo による出力を省き、文字列生成と文字列チェックのそれぞれの処理の前後に microtime 関数を入れて、それぞれにかかる時間を計測できるようにしたコードで改めて計測を行ってみました。

検証に使用したコード

【string_generator2.php】

<?php

function generate_random_strings($type='letters')
{
    switch($type){
      case 'digits':
        $asset = '0123456789';
        break;
      case 'letters':
      default:
        $asset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        break;
    }

    $length = strlen($asset);
    $max = rand(4, 32);
    $result = array();
    for($i = 0; $i < $max; $i++){
        array_push($result, $asset[rand(0, $length-1)]); 
    }

    return implode($result);
}

function generate_list($max=100, $type='letters')
{
  $start_time = microtime(true);
  $result = array();
  for($i = 0; $i < $max; $i++){
    array_push($result, generate_random_strings($type));
  }
  echo "str_gen:\t";
  echo microtime(true) - $start_time . " sec\n";
  return $result;
}

function generate_letters_list($max=100)
{
  return generate_list($max);
}

function generate_digits_list($max=100)
{
  return generate_list($max, 'digits');
}

【ctype_digit_bench.php】

<?php

require_once('string_generator2.php');

function validate($variables)
{
  $start_time = microtime(true);
  foreach($variables as $v){
    if(ctype_digit($v)){
      $valid++;
    }
    else{
      $invalid++;
    }
  }

  echo "ctype_digit:\t";
  echo microtime(true) - $start_time . " sec\n";

  echo "# valid:   $valid\n";
  echo "# invalid: $invalid\n";
}

validate(generate_letters_list(100000));
validate(generate_digits_list(100000));

【preg_match_bench.php】

<?php

require_once('string_generator2.php');

function validate($variables)
{
  $start_time = microtime(true);
  foreach($variables as $v){
    if(preg_match('/^[0-9]+$/', $v)){
      $valid++;
    }
    else{
      $invalid++;
    }
  }

  echo "preg_match:\t";
  echo microtime(true) - $start_time . " sec\n";

  echo "# valid:   $valid\n";
  echo "# invalid: $invalid\n";
}

validate(generate_letters_list(100000));
validate(generate_digits_list(100000));

上記のコードによる計測内容は要約すると以下のようになります。こちらの 1,2 をそれぞれ時間計測しています。

  1. 10万 x 2 行のランダム文字列を生成(generate_letters_list および generate_digits_list 関数)し、validate 関数に渡す
  2. validate 関数内で、それぞれ ctype_digit と preg_match で文字列の検査を実施

検証した環境

上記のコードを、以下の環境で 10 回実施し、その平均値をとってみました。

  • OS : CentOS 6.2
  • CPU : Intel Core2Duo 3.33GHz
  • Memory : 512MB
  • PHP : Ver.5.3.3

検証結果

  • ctype_digit_bench.php
    • A-Z, a-z, 0-9 で構成されたランダム文字列 10 万行を生成・チェック
      • 文字列生成: 6.28 秒
      • ctype_digit: 0.175 秒
    • 0-9 で構成されたランダム文字列 10 万行を生成・チェック
      • 文字列生成: 6.29 秒
      • ctype_digit: 0.170 秒
  • preg_match_bench.php
    • A-Z, a-z, 0-9 で構成されたランダム文字列 10 万行を生成・チェック
      • 文字列生成: 6.32 秒
      • preg_match: 0.274 秒
    • 0-9 で構成されたランダム文字列 10 万行を生成・チェック
      • 文字列生成: 6.46 秒
      • preg_match: 0.289 秒

この結果から、以下のように考えられるのではないかと思われます。

  • 文字列生成は文字列チェック (ctype_digit と preg_match) に比べ、10 倍以上時間がかかる
  • ctype_digit の方が preg_match より 1.5 倍程度速い

参考にさせていただいたサイトでの検証ですと、前者の点で ctype_digit と preg_match の差が文字列生成側の誤差の範囲内になってしまっていたのかもしれませんね。

ctype_digit の方が単機能なので当たり前といえば当たり前ですが、一応これで ctype_digit と preg_match との正味のチェックの処理時間の差が確認できたかと思います。

まとめ

計測の結果、ctype_digit は preg_match の 1.5 倍程度速いようでした。ただ、preg_match のように int 型で渡した場合に string 型と同じようには判定してくれませんので、利用する際は注意が必要になりますね。

以上、今更ネタかもしれませんが、何かのお役に立てれば幸いです。

Tags: , ,

black

明けましておめでとうございます。black です。

今回は業務に使うツール作成をしてみました。このツールは複数行にリスト化された文字列に対して、各行ごとに含まれる文字種を判定するものです。
テキストデータに意図しない文字種のデータが含まれていないかを確認したい場合があり、業務効率化と勉強がてらに作ってみました。

目次

  1. ツール作り
  2. 使い方
  3. ソース全文

ツール作り

ツールはインターフェイスの作成が不要な EXCEL の VBA で作ることにします。

まずはざっくりと仕様を以下のように決めました。

  • 1 列目のセルを、1 行目から入力行数分まで処理する
  • 入力文字を「全角文字」「半角英数」「全角半角の混在」の種別ごとにおおまかに分ける
  • 全角ひらがな、全角カタカナ、半角文字、半角カナ、半角記号に分類して抽出する
  • 各列の説明を一行目に挿入する (既に説明行がある場合はスキップ)

*完成イメージ

lab_img2

まずは半角か全角か混合かの判定を作成します。

'ANSI文字列に変換した文字列を取得
  set_text_ansi = StrConv(set_text, vbFromUnicode)
  
  If Len(set_text) = LenB(set_text_ansi) Then  '半角のみ
      ~
  ElseIf Len(set_text) * 2 = LenB(set_text_ansi) Then  '全角のみ
      ~
  Else  '半角と全角の混合

ここは文字数とバイト数で比較判定する定番の手法を採用しました。

まずは入力文字が半角である場合を作成。

For l_int_1 = 1 To Len(set_text)
    my_temp = Mid(set_text, l_int_1, 1)
    If my_temp Like "[a-z]" Or my_temp Like "[A-Z]" Then
        han_temp = han_temp & my_temp
    ElseIf my_temp Like "[。-゚]" Then '半角カナの場合
        kana_temp = kana_temp & my_temp
    Else 'アルファベット以外
        kigo_temp = kigo_temp & my_temp
    End If
Next

入力文字を一文字ずつ抽出しLike演算子で判定、
英字、半角カナであればそれぞれの専用の変数に格納、
それ以外は半角記号と見なすようにしました。
注意点は、半角カナのLike演算子の範囲を[ア-ン]にしてしまうと、
濁点半濁点が無視されてしまうので、そこまで含んだ指定にするところでしょうか。

しかしここで数字を失念していたことが発覚。

ElseIf my_temp Like "#" Then
    ~

なのでLIKE演算子で0~9の場合の条件分岐を追加しました。
さらに数字のみの場合や、日付の場合にも処理も分けた方がよさそうだと思い分岐を追加。

If VarType(set_text) = 8 Then '項目が文字型の時
    ~
ElseIf VarType(set_text) = 7 Then '項目が日付型の時
    ~
Else '項目がそれ以外の型の時(数字とみなす)
    If InStr(set_text, ".") Then '小数を含む数字の場合
        result = "数字(小数)"
        Cells(i, int_cell).Value = "<ALL(小数)>"
    Else
        result = "数字"
        Cells(i, int_cell).Value = "<ALL>"
    End If
End If

ここではVarType関数を使いました。戻り値で変数の型などを返してくれるので、文字、日付、数字などを大別するには便利です。

半角の対応は以上です。

次は全角の場合の対応です。

If set_text = StrConv(set_text, vbHiragana) Then '全てひらがな
    ~
ElseIf set_text = StrConv(set_text, bvkatakana) Then '全てカタカナ
    ~
Else '全角混合

各入力値を、元の値と、ひらがな変換したものと比較して一致していればひらがなと判定、
カタカナも同様に、という手段を取ろうと思いましたが、これには落とし穴が。。。
テストしてみると、全角の記号や長音が含まれていた時それを識別できないことが判明。
結局はここもLIKE演算子で対応することに。

'検証文字をばらす
For l_int_1 = 1 To Len(set_text)
        my_temp = Mid(set_text, l_int_1, 1)
    If my_temp Like "[あ-ん]" Then '五十音ひらがな
        hira_temp = hira_temp & my_temp
    ElseIf my_temp Like "[ア-ン]" Then  '五十音カタカナ
        zenkana_temp = zenkana_temp & my_temp
    ElseIf my_temp Like "ー" Then '長音対応
        hira_temp = hira_temp & my_temp
        zenkana_temp = zenkana_temp & my_temp
    Else 'それ以外(全角記号など)
        str_temp = my_temp
    End If
Next

やることは半角の時と同様ですが、長音のみはひらがなとカタカナどちらにも属するようにしています。

これで全角の場合も完成です。

全角と半角が混合している場合は上記を組み合わせて対応。
さて、これで基礎は完成しました。

次は空白文字の対応です。
ここまでの対応で、全角、半角それぞれの空白文字も記号として振り分けはされるのですが、
ぱっと見た目には分かりません。
そこで、空白文字判定用のカラムを追加し、空白文字あれば”○”が入るように対応。

あとは結果表示の調整です。
分類したデータが続き文字でない場合に、わかりやすいように間にカンマを挟むモジュールを追加します。

Function add_comma(temp As Variant) As Variant
    If temp <> "" And Right(temp, 1) <> "," Then
        temp = temp & ","
    End If
    add_comma = temp
End Function

これを文字判定の都度実行させます。

それと、一行目に各列の説明を挿入するコードも入れておきます。

    '各列の説明行の追加
    chk_str_header = "検証文字列"
    If Cells(1, 1) <> chk_str_header Then
        Application.CutCopyMode = False
        Range("1:1").Insert
        With Cells(1, 1)
            .Value = chk_str_header
            .Interior.Color = RGB(255, 255, 0)
        End With
        With Cells(1, hantei_cell)
            .Value = "型判定"
            .Interior.Color = RGB(0, 255, 255)
        End With
        Cells(1, zenkaku_cell).Value = "他全角"
        Cells(1, zenhira_cell).Value = "ひらがな"
        Cells(1, zenkana_cell).Value = "全角カナ"
        Cells(1, hankaku_cell).Value = "半角文字"
        Cells(1, hankana_cell).Value = "半角カナ"
        Cells(1, kigo_cell).Value = "半角記号"
        Cells(1, int_cell).Value = "数字"
        Cells(1, space_cell).Value = "全角空白"
        With Range("A1:J1")
            .Font.Bold = True
            .Borders.LineStyle = xlContinuous
            .HorizontalAlignment = xlCenter
        End With
    End If

これで完成です。

使い方

Excel を開き、一番左の列に判別させたい文字列を入力します。

lab_img3

Visual Basic Editor に以下の ソース全文 を貼り付けて実行します。

実行した結果は以下のようになります。

lab_img1

カンマが含まれていた場合に見づらかったり、VBAなので処理速度がお察しの通りだったりと、
まだまだ問題もありますが、とりあえずは要求を満たすものができました。

専門職ではないので作りの甘い部分はありますが、
同じようなものが欲しい人の参考になればと思います。

ソース全文

Sub text_judge()
    Dim chk_str_header, my_temp, str_temp, hira_temp, zenkana_temp, num_temp, han_temp, kana_temp, kigo_temp As String
    Dim i, max_row, zenkaku_cell, zenhira_cell, zenkana_cell, hankaku_cell, hankana_cell, kigo_cell, int_cell, space_cell As Integer
    hantei_cell = 2     '型判定結果のcolumn位置
    zenkaku_cell = 3    '全角文字のcolumn位置
    zenhira_cell = 4    '全角ひらがなのcolumn位置
    zenkana_cell = 5    '全角カタカナのcolumn位置
    hankaku_cell = 6    '半角文字のcolumn位置
    hankana_cell = 7    '半角カナのcolumn位置
    kigo_cell = 8       '半角記号のcolumn位置
    int_cell = 9        '数字のcolumn位置
    space_cell = 10     '全角空白のcolumn位置
    
    '各列の説明行の追加
    chk_str_header = "検証文字列"
    If Cells(1, 1) <> chk_str_header Then
        Application.CutCopyMode = False
        Range("1:1").Insert
        With Cells(1, 1)
            .Value = chk_str_header
            .Interior.Color = RGB(255, 255, 0)
        End With
        With Cells(1, hantei_cell)
            .Value = "型判定"
            .Interior.Color = RGB(0, 255, 255)
        End With
        Cells(1, zenkaku_cell).Value = "他全角"
        Cells(1, zenhira_cell).Value = "ひらがな"
        Cells(1, zenkana_cell).Value = "全角カナ"
        Cells(1, hankaku_cell).Value = "半角文字"
        Cells(1, hankana_cell).Value = "半角カナ"
        Cells(1, kigo_cell).Value = "半角記号"
        Cells(1, int_cell).Value = "数字"
        Cells(1, space_cell).Value = "全角空白"
        With Range("A1:J1")
            .Font.Bold = True
            .Borders.LineStyle = xlContinuous
            .HorizontalAlignment = xlCenter
        End With
    End If
    
    max_row = ActiveSheet.UsedRange.Find("*", , xlFormulas, , xlByRows, xlPrevious).Row
    i = 2
    
    For i = 2 To max_row   'A列の最後まで繰り返し
        set_text = Cells(i, 1)
         
        '値がない場合はスキップ
        If Len(set_text) = 0 Then
            GoTo Continue
        End If
         
        'ANSI文字列に変換した文字列を取得
        set_text_ansi = StrConv(set_text, vbFromUnicode)
        
        'まず全角のみ、半角のみか判定
        
        If Len(set_text) = LenB(set_text_ansi) Then
        '半角だけの時
            If VarType(set_text) = 8 Then
                result = "半角英数"
                
                    '変数初期化
                    han_temp = ""
                    kana_temp = ""
                    kigo_temp = ""
                    num_temp = ""
                    
                    If set_text Like "* *" Then '半角スペース検索
                        Cells(i, space_cell).Value = "○"
                    End If
                    
                    '検証文字をばらす
                    For l_int_1 = 1 To Len(set_text)
                        my_temp = Mid(set_text, l_int_1, 1)
                        If my_temp Like "[a-z]" Or my_temp Like "[A-Z]" Then '半角英字の場合
                            han_temp = han_temp & my_temp
                            num_temp = add_comma(num_temp)
                            kana_temp = add_comma(kana_temp)
                            kigo_temp = add_comma(kigo_temp)
                            
                        ElseIf my_temp Like "[。-゚]" Then        '半角カナの場合
                            kana_temp = kana_temp & my_temp
                            num_temp = add_comma(num_temp)
                            han_temp = add_comma(han_temp)
                            kigo_temp = add_comma(kigo_temp)
                        
                        ElseIf my_temp Like "#" Then    '数字の場合
                            num_temp = num_temp & my_temp
                            han_temp = add_comma(han_temp)
                            kana_temp = add_comma(kana_temp)
                            kigo_temp = add_comma(kigo_temp)
                            
                        Else        'アルファベット以外
                            kigo_temp = kigo_temp & my_temp
                            han_temp = add_comma(han_temp)
                            kana_temp = add_comma(kana_temp)
                            num_temp = add_comma(num_temp)
                        
                        End If
                    Next
                                                                                       
                    Cells(i, hankaku_cell).Value = han_temp
                    Cells(i, hankana_cell).Value = kana_temp
                    Cells(i, kigo_cell).Value = kigo_temp
                    Cells(i, int_cell).Value = num_temp

                
            ElseIf VarType(set_text) = 7 Then   '項目が日付の時
                result = "日付"
                
            Else    '数字のみの場合、小数と整数に分ける
                If InStr(set_text, ".") Then
                    result = "数字(小数)"
                    Cells(i, int_cell).Value = "<ALL(小数)>"
                Else
                    result = "数字"
                    Cells(i, int_cell).Value = "<ALL>"
                End If
            End If
            
        ElseIf Len(set_text) * 2 = LenB(set_text_ansi) Then '全角
        
            my_temp = ""
            str_temp = ""
            hira_temp = ""
            zenkana_temp = ""
            
            '検証文字をばらす
            For l_int_1 = 1 To Len(set_text)
                my_temp = Mid(set_text, l_int_1, 1)
                    'ひらがな、カタカナ、長音、その他で処理を分ける
                    If my_temp Like "[あ-ん]" Then
                        hira_temp = hira_temp & my_temp
                        zenkana_temp = add_comma(zenkana_temp)
                        str_temp = add_comma(str_temp)
                    ElseIf my_temp Like "[ア-ン]" Then
                        zenkana_temp = zenkana_temp & my_temp
                        hira_temp = add_comma(hira_temp)
                        str_temp = add_comma(str_temp)
                    ElseIf my_temp Like "ー" Then
                        hira_temp = hira_temp & my_temp
                        zenkana_temp = zenkana_temp & my_temp
                        str_temp = add_comma(str_temp)
                    Else
                        str_temp = str_temp & my_temp
                        hira_temp = add_comma(hira_temp)
                        zenkana_temp = add_comma(zenkana_temp)
                    End If
            Next
                                           
            If set_text Like "* *" Then    '全角スペース検索
                Cells(i, space_cell).Value = "○"
            End If
            
            Cells(i, zenkaku_cell).Value = str_temp
            Cells(i, zenhira_cell).Value = hira_temp
            Cells(i, zenkana_cell).Value = zenkana_temp
            result = "全角文字"
            
        Else
            '半角全角混合の処理
            
            '初期化
            my_temp = ""
            str_temp = ""
            hira_temp = ""
            zenkana_temp = ""
            han_temp = ""
            kana_temp = ""
            kigo_temp = ""
            num_temp = ""
            
            '検証文字をばらす
            For l_int_1 = 1 To Len(set_text)
                my_temp = Mid(set_text, l_int_1, 1)
                If LenB(StrConv(my_temp, vbFromUnicode)) = 2 Then   '全角の場合
                
                    If my_temp Like "[あ-ん]" Then
                        hira_temp = hira_temp & my_temp
                        zenkana_temp = add_comma(zenkana_temp)
                        str_temp = add_comma(str_temp)
                    ElseIf my_temp Like "[ア-ン]" Then
                        zenkana_temp = zenkana_temp & my_temp
                        hira_temp = add_comma(hira_temp)
                        str_temp = add_comma(str_temp)
                    ElseIf my_temp Like "ー" Then
                        hira_temp = hira_temp & my_temp
                        zenkana_temp = zenkana_temp & my_temp
                        str_temp = add_comma(str_temp)
                    Else
                        str_temp = str_temp & my_temp
                        hira_temp = add_comma(hira_temp)
                        zenkana_temp = add_comma(zenkana_temp)
                    End If
                            
                    
                    han_temp = add_comma(han_temp)
                    han_temp = add_comma(han_temp)
                    kana_temp = add_comma(kana_temp)
                    kigo_temp = add_comma(kigo_temp)
                Else        '半角の場合
                    If my_temp Like "[a-z]" Or my_temp Like "[A-Z]" Then
                        han_temp = han_temp & my_temp
                        str_temp = add_comma(str_temp)
                        kana_temp = add_comma(kana_temp)
                        kigo_temp = add_comma(kigo_temp)
                        num_temp = add_comma(num_temp)
                    
                    ElseIf my_temp Like "[。-゚]" Then        '半角カナの場合
                        kana_temp = kana_temp & my_temp
                        han_temp = add_comma(han_temp)
                        kigo_temp = add_comma(kigo_temp)
                        num_temp = add_comma(num_temp)
                    
                    ElseIf my_temp Like "*#*" Then          '数字の場合
                        han_temp = add_comma(han_temp)
                        kana_temp = add_comma(kana_temp)
                        kigo_temp = add_comma(kigo_temp)
                        num_temp = num_temp & my_temp
                        
                    Else        'アルファベット以外
                        kigo_temp = kigo_temp & my_temp
                        han_temp = add_comma(han_temp)
                        kana_temp = add_comma(kana_temp)
                        num_temp = add_comma(num_temp)
                    End If
                                                                                       
                    str_temp = add_comma(str_temp)
                    hira_temp = add_comma(hira_temp)
                    zenkana_temp = add_comma(zenkana_temp)
                End If
            Next
            
            If set_text Like "* *" Or set_text Like "* *" Then
                Cells(i, space_cell).Value = "○"
            End If
            
            Cells(i, zenkaku_cell).Value = str_temp
            Cells(i, zenhira_cell).Value = hira_temp
            Cells(i, zenkana_cell).Value = zenkana_temp
            Cells(i, hankaku_cell).Value = han_temp
            Cells(i, hankana_cell).Value = kana_temp
            Cells(i, kigo_cell).Value = kigo_temp
            Cells(i, int_cell).Value = num_temp
            result = "混在"
            
        End If
        
        Cells(i, hantei_cell) = result

Continue:
    Next

End Sub

'分類分けで、続いている文字でなければカンマを挟むためのモジュール
Function add_comma(temp As Variant) As Variant
    
    If temp <> "" And Right(temp, 1) <> "," Then
        temp = temp & ","
    End If

    add_comma = temp
End Function

Tags: , , ,

kid

はじめまして、セールスフォースグループの kid です。

厳密には 2 度目の記事ですが、以前記事を書いた際にはクライゼルチームでしたし、時間もかなり空いていましたので、簡単に自己紹介させていただきます。

私の所属するセールスフォースグループの業務ではクラウド上のCRMサービスとなる「セールスフォース」と連携したメール配信アプリケーション「Autobahn for Appexchange」の開発、運用しています。サービス基盤となるセールスフォースとデータ連携を取りながら、セールスフォース単独では難しいような大量のメール配信を実現しています。

[セールスフォースのメール配信機能を拡張「Autobahn for AppExchange」]
https://appexchangejp.salesforce.com/listingDetail?listingId=a0N300000016cKNEAY

はじめに

さて、まずセールスフォースをご利用されたことがない方も多いかと思いますので、APIを含む開発に至る概要を説明させていただきます。セールスフォースではお客様のデータや契約情報、商談状況などを管理・レポートする充実した SFA (Sales Force Automation) の機能を実装しています。最近では、マーケティングやコールセンターのための機能を実装するなど多種多様にわたったサービスとなっており、多くの企業にて導入されています。

セールスフォースの特徴の一つとして、利用者の環境に合わせて簡単にカスタマイズ可能な点が挙げられます。カスタマイズは HTML や CSS、JavaScript といったコード知識がなくとも管理画面上からマウス操作のみで行えます。ボタンを配置したり、条件ロジックを作成して承認機能を付けたりなど、プログラムを一切書くことなく開発を実施できるプラットフォームを実現しています。

一方で、セールスフォースは API も提供しており、外部サービスとの連携が容易という点も大きな特徴です。私達セールスフォースグループが提供する Autobahn for Appexchange もそういった外部連携サービスの一つで、こちらを利用することで高性能なメール配信機能を備えたセールスフォースを利用可能になります。

この他にも既存のシステムから完全移行が困難だったり、セキュリティポリシー面で個人情報となるデータをクラウド上におくことができないといった場合であっても、外部サービスとの連携が容易であるため、既存システムとセールスフォースを組み合わせて利用するといったことも可能です。実際にそのような形で利用されているお客さんも多くいます。

私からはセールスフォースにあるデータとの連携をテーマに、業務の中で培ったノウハウをご紹介していければと思います。

まず今回は、自分たちのサーバからセールスフォース上のデータに対して、PHP を利用してどのようにデータの参照・操作を実現していくのかをご紹介します。

今回の実例では以下の基本的な知識は持っている前提とします。

  • PHP
  • SOQL – セールスフォース上で利用可能な SQL 文となります。

それではまずセールスフォース上にあるデータ参照するための方法を手順を追って実装していきましょう。初回ですので、検証環境の用意から説明させていただきます。

1. API を利用できるセールスフォースの Edition について

外部からのAPIを利用する場合、セールスフォースにて API オプションが利用可能である必要があります。セールスフォースは Edition といったサービスランクで利用できる機能が区分されており、API を利用する際は Enterprise Edtion と同等以上の Edition である必要がありますのでご注意ください (参考: API アクセスのあるエディション – Salesforce.com Help Portal)。

また、API にはコール数の制限もありますので、実運用されている環境で直接実装するのではなく、検証用の Sandbox や開発者向けの Developer Edition にてご検証いただくようお願いします (参考: API の使用制限 – Help – Salesforce.com)。

以降の API によるデータの参照・操作は Developer Edition で行ってみたいと思います。

2. 検証環境を用意します

2.1. セールスフォースの組織を用意します

セールスフォースでは本番環境も期間限定の無料トライアルで利用することができますが、利用期間が限られているため、開発者用に用意された Developer Edtion を利用していきましょう。以下のサイトより無償で取得できます。

[developerforce]
http://jp.force.com/

2.2. PHPを実装できる環境を用意します

今回はPHPを利用してAPIの実装を検証しますが、PHPが動作する環境を用意するものは結構手間なものになりますので、今回はクラウド上の IDE (Integrated Development Environment) ツール Cloud9 IDE といったサービスを利用していきます。Standard 版であれば無償で利用可能ですので、画面上からサインアップしてアカウントを取得してみてください。

[Clout9 IDE]
https://c9.io

2.3. アカウントを取得したらログイン、PHPの開発環境を作成します

画面左上に、「CREATE NEW WORKSPACE」とありますので、クリックして新規作成できます。

3.2.menu

2.4. PHPを選択して、開発環境を作成します

他のプログラム言語での利用できますので、ご興味のある方は後ほど試してみてください。

5.4.PHP_Project

開発はブラウザ上から行うことができ、コンソールでの動作確認もできます。

sf-api-cloud9-ui01

 

2.5. APIを実装するためのライブラリファイルを用意します

APIの実装をゼロから実現するのはかなりハードルが高いため、一般的なサービスでは API をより活用してもらえるようにプログラム言語に合わせてライブラリを公開していることがよくあります。セールスフォースでも同様に PHP 用の Toolkit が提供されていますのでそちらを利用します。API はいくつかありますが今回は Soap API を用います。合わせて開発マニュアルについてもご紹介しておきます。

5.1.php_toolkit

[Force.com Toolkit for PHP]
http://wiki.developerforce.com/page/Force.com_Toolkit_for_PHP

[開発ドキュメント]
http://wiki.developerforce.com/page/JP:Documentation

Download の PHP toolkit をダウンロードすると GitHub の画面に移動しますので、右下の [Download ZIP] から Zip ファイルをダウンロードしましょう。ダウンロードし、手元で展開すると Force.com-Toolkit-for-PHP-master というフォルダが作成されるはずです。

続いて Cloud9 IDE 画面の左上 (WORKSPACE FILES) 部分で右クリックし、[New Folder] から soapclient フォルダを作成します。作成後、そのフォルダ上で右クリックして [Upload to this folder] を選択するとドラッグ&ドロップ用のウィンドウが表示されます。ここに、先ほど展開した Force.com-Toolkit-for-PHP-master 内の soapclient フォルダ内のファイル全てをドラッグ&ドロップしましょう。

最終的に、以下のような配置となります。以降のプログラムはこの配置前提で実行していくこととなります。

5.5.command_run

2.6. WSDL ファイルをダウンロードし、Cloud9 IDE へアップロードします

Salesfoce に API でアクセスするため、WSDL (Web Service Description Language) ファイルをセールスフォースの管理画面からダウンロードし、Cloud9 IDE にアップロードします。

Salesfoce にログインし、画面の右上の [設定] → 左下の [開発] → そのサブメニューの [API] → 右ペインに出てくる [パートナー WSDL の生成] を右クリックしてファイルを保存します。ファイル名は partner.wsdl.xml としておきましょう。

続いて Cloud9 IDE で configs というフォルダを作成し、その中に partner.wsdl.xml をアップロードします。やり方はライブラリをアップロードした方法と同様です。最終的に以下のような構成となります。

sf-api-libs-structure01.png

2.7. セキュリティトークンを発行します

準備の最後に、セールスフォースの管理画面からセキュリティトークンの発行を行います。手順は以下の通りです。

Salesfoce にログイン → 画面の右上の名前の横にある下向き矢印をクリック → 名前の下にあるメニューで [私の設定] を選択 → 画面左側の [個人用] をクリック → サブメニューの [私のセキュリティトークンのリセット] を選択

これでセールスフォースに登録されたメールアドレスにセキュリティトークンが送信されます。

3. プログラムを書きます

ではプログラムを書いていきましょう。PHP のバージョンは 5.3.3 を前提にしています。

3.1. 必要なライブラリを読込みます

// Toolkitを読込みます。
require_once("./soapclient/SforcePartnerClient.php");

3.2. 設定ファイルを用意します

// 事前に必要な情報を宣言します。 

// 今回は特定のセールスフォース組織に依存しないような場合に利用する
// Partner WSDL ファイルを利用します。
define("PARTNER_WSDL_FILE", "./configs/partner.wsdl.xml"); 

// セールスフォースへAPI接続する場合、接続元のIPアドレス許可が必要となりますが、
// 代替手段として、今回はセキュリティトークンを発行してIPアドレス許可の設定はスキップします。
define("SECURITY_TOKEN", "*************************"); 

// API でログインするセールスフォースのアカウントです。
define("LOGIN_ID", "salesforce_api@example.com");

// パスワードの後ろにセキュリティトークンを付けます。
define("LOGIN_PASS", "*********" . SECURITY_TOKEN); 

3.3. 実際にログイン処理を書いていきます

// 接続用クラスを生成します。
$sforce_connection = new SforcePartnerClient();
$soap_client = $sforce_connection->createConnection(PARTNER_WSDL_FILE, null);

try {
	// セールスフォースへログインを実行します。
    $login = $sforce_connection->login(LOGIN_ID, LOGIN_PASS);
    var_dump($login);
} catch (Exception $e) {
    var_dump($e);
}

4. 実際に動作させてみます

4.1. 実行結果を確認してみます

$ php api.php 
object(stdClass)#4 (7) {
  ["metadataServerUrl"]=>
  string(62) "https://ap.salesforce.com/services/Soap/m/29.0/00D10000000ZSLk"
  ["passwordExpired"]=>
  bool(false)
  ["sandbox"]=>
  bool(false)
  ["serverUrl"]=>
  string(62) "https://ap.salesforce.com/services/Soap/u/29.0/00D10000000ZSLk"
  ["sessionId"]=>
  string(112) "00D10000000ZSLk!AR4AQMzhSrUA8V.CInjnDYM7tJlXqPW9FjKB96MBbL8YmXlCfCTk2O.HROS68IGBFRtvb1aodrYsVYhzPHQsYxfbaCzz.Pi0"
  ["userId"]=>
  string(18) "0051000000281V1AAI"
  ["userInfo"]=>
  object(stdClass)#5 (22) {
    ["accessibilityMode"]=>
    bool(false)
    ["currencySymbol"]=>
    string(3) "¥"
    ["orgAttachmentFileSizeLimit"]=>
    int(5242880)
    ["orgDefaultCurrencyIsoCode"]=>
    string(3) "JPY"
    ["orgDisallowHtmlAttachments"]=>
    bool(false)
    ["orgHasPersonAccounts"]=>
    bool(false)
    ...
    ...
  }
}

上記のようなフォーマットで値が返ってきます。
結果は QueryResult といったクラスに引き渡してもいいですし、必要な値だけ取得するようにパースするかは開発者によりまちまちですので、ここでは割愛します。
参考までに Session Id だけを取得してみましょう。

try {
    $login = $sforce_connection->login(LOGIN_ID, LOGIN_PASS);
    
    $login_info = get_object_vars($login);
    $session_id = $login_info['sessionId'];
    echo "Session Id : " . $session_id . "\n";
} catch (Exception $e) {
    var_dump($e);
}
$ php api.php 
Session Id : 00D10000000ZSLk!AR4AQMzhSrUA8V.CInjnDYM7tJlXqPW9FjKB96MBbL8YmXlCfCTk2O.HROS68IGBFRtvb1aodrYsVYhzPHQsYxfbaCzz.Pi0

4.2. データを参照します

セールスフォース上にあるデータに SOQL を発行してデータを取得します。

// 通常の SQL と大きく変わりませんが、少し独特な個所がありますので徐々に慣れていきましょう。
try {
    $query = "SELECT OrganizationType FROM Organization ";
    $result = $sforce_connection->query($query);
    $parse_result = get_object_vars($result);
    var_dump($parse_result);

} catch (Exception $e) {
    var_dump($e);
}
$ php api.php 
array(5) {
  ["queryLocator"]=>
  NULL
  ["done"]=>
  bool(true)
  ["records"]=>
  array(1) {
    [0]=>
    object(stdClass)#8 (3) {
      ["type"]=>
      string(12) "Organization"
      ["Id"]=>
      NULL
      ["any"]=>
      string(60) "Developer Edition"
    }
  }
  ["size"]=>
  int(1)
  ["pointer"]=>
  int(0)
}

4.3. データを作成してみます

セールスフォースにリードとなるデータの 1 つ追加してみます。

// 追加するデータは stdClass を利用して作成します。
$insert_data = new stdClass();
$insert_data->type = "Lead";
$insert_data->fields = array(
    "Company"  => "Tricorn", 
    "LastName" => "Koido",   
    );
try {
    $result = $sforce_connection->create(array($insert_data));
    var_dump($result);
	
} catch (Exception $e) {
    var_dump($e);
}
$ php api.php 
array(1) {
  [0]=>
  object(stdClass)#11 (2) {
    ["id"]=>
    string(18) "00Q1000000CIMDiEAP" // 作成されたレコードのId値が返ってきます。
    ["success"]=>
    bool(true)
  }
}

追加したデータはセールスフォースの画面でも確認できます。上記のプログラムでデータを追加した場合は、[リード] を選択して [私の未読リード] を表示し、[作成日] でソートすると一番新しいものとして表示されます。

sf-api-add-new-lead01.png

4.4. データを削除してみます

作成したデータを削除してみます。

// 削除した Id を配列で渡すことで削除されます。
try {
    $result = $sforce_connection->delete(array("00Q1000000CIMDiEAP"));
    var_dump($result);

} catch (Exception $e) {
    var_dump($e);
}
$ php api.php 
array(1) {
  [0]=>
  object(stdClass)#10 (2) {
    ["id"]=>
    string(18) "00Q1000000CIMDiEAP" // 正常に削除されたレコードIdが返ってきます。
    ["success"]=>
    bool(true)
  }
}

今回はここまで

いかがでしたでしょうか。初回ということであまり細かいことを気にせず、まずは API を利用してデータを操作する工程を紹介しました。次回以降も今回の内容をもとに、PHP をベースにしたセールスフォース上での開発ノウハウをご紹介していければと思います。

なお、次回は今回紹介した API を実装する上で回避することのできない API の制限や、ガバナ制限についても触れていきたいと思います。

最後に

本記事の内容は 2013年11月21日現在に検証した内容をもとに記載しています。セールスフォースのバージョンアップやライブラリの仕様変更などに伴い、動作をしなくなるといった可能性がありますので、詳細な点は公式マニュアルと合わせてご参照いただければ幸いです。

Tags: , , , ,

yamada

PHPでDKIM署名を作成する

Posted Date: 2013-10-25 13:00
Author: / No Comments

はじめまして。入社 2 年目クライゼルグループの yamada です。
初投稿となります。

最近、電子メールの認証技術として主流になりつつあるDKIM署名ですが、ネットで調べてもDKIM署名を作成するプログラムについてはこれといった情報がなかったので、今回はライブラリを使って DKIM 署名 (DKIM-Signatureヘッダ) を作成するプログラムを紹介します。

ライブラリのダウンロード

ネットで検索すると PHP の DKIM ライブラリはいくつかヒットしますが、今回は下記URLのライブラリを使います。

zipに含まれる「dkim.class.php」がライブラリの本体です。それ以外はテスト用のサンプルソースなので今回は使いません。

DKIM-Signatureヘッダの仕様決め

次にDKIM-Signatureヘッダの仕様を決めます。今回は下記の通りとします。

  • a タグ (署名作成のアルゴリズム) : rsa-sha1
  • q タグ (公開鍵取得方法) : dns/txt
  • c タグ (メールの正規化方式) : relaxed/simple (ヘッダ/本文)
  • h タグ (署名対象のヘッダ) : From、To、Subject
  • i、l、x、zタグは省略

※c タグのヘッダ正規化方式、a タグ、q タグについては、このライブラリでは固定値となります (変更する場合はライブラリの一部を書き換える必要があります)。

また、以下の値についても事前に決めておく必要があります。ここに設定する値はサンプルですので適宜変更して下さい。

  • d タグ (ドメイン) : example.com
  • s タグ (セレクタ) : dkim.sl.20131025

これらを踏まえて、作成する DKIM-Signature ヘッダは下記となります。t、bh、b タグについてはライブラリによって自動生成されることになります。

DKIM-Signature: v=1;
        a=rsa-sha1;
        q=dns/txt;
        s=dkim.sl.20131025;
        t=<送信日時>;
        c=relaxed/simple;
        h=from:to:subject;
        d=example.com;
        bh=<本文のハッシュ値>;
        b=<電子署名>

鍵の生成と DNS サーバへのレコード登録

DKIM 署名を行う秘密鍵と公開鍵のセットを作成します。CentOS 6 系であれば、opendkim パッケージの opendkim-genkey コマンドで簡単に作成できます。

# yum install opendkim
$ opendkim-genkey -d example.com -s dkim.sl.20131025

-d オプションにドメイン、-s オプションにセレクタを指定しています。上記コマンドを実行すると、以下のファイルが生成されます。

  • dkim.sl.20131025.private : 秘密鍵 (パスフレーズなし)
  • -----BEGIN RSA PRIVATE KEY-----
    MIICXAIBAAKBgQCo58N8KhMa1kblbGlhGINznh7kmQmK85kt2XSiaBmincAPeeLK
                          (省略)
    h0rlopDKO9J91moEQpkCQBeJDjKIzj3N1m2qqs4G6pWbMabHOE4StHJGAuIEFB4X
    ACOnSiiod6TaFl4QE6VI/6xH7s7gc/osRkoaFexIhcc=
    -----END RSA PRIVATE KEY-----
  • dkim.sl.20131025.txt : 公開鍵 (DNS レコード形式)
  • ; ----- DKIM key dkim.sl.20131025 for example.com
    dkim.sl.20131025._domainkey IN TXT "v=DKIM1; k=rsa; p=MIGfM (省略) DAQAB" 

この公開鍵 (DNS レコード形式) をドメインを管理する DNS サーバのレコードに事前に登録しておきます。ただし、未登録でも DKIM 署名のメール自体は送れるので、とりあえずライブラリの使い方を知りたいという方はスキップで OK です (もちろんその場合、署名されたメールは意図したとおりには検証されません)。

プログラム作成と実行

これで準備ができたのでプログラムを作っていきます。

なお、OpenSSL関数とマルチバイト文字列関数を使うため、事前にライブラリのインストールが必要です。CentOS 6 であれば以下で OK です。

# yum install openssl php-mbstring

プログラムは以下の通りです。例として宛先を to@example.com、送信者を from@example.com としていますので、ご利用の際は適宜書き換えて下さい。なお、以下のプログラムは PHP 5.3 で動作確認しています。

[send-dkim-mail.php]

<?php

include_once("dkim.class.php");

$to          = "to@example.com";
$subject     = "DKIM test";
$body        = "This is DKIM test mail.";
$headers     = "From: from@example.com";

$domain      = "example.com";
$selector    = "dkim.sl.20131025";

$private_key = "-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCo58N8KhMa1kblbGlhGINznh7kmQmK85kt2XSiaBmincAPeeLK
                      (省略)
h0rlopDKO9J91moEQpkCQBeJDjKIzj3N1m2qqs4G6pWbMabHOE4StHJGAuIEFB4X
ACOnSiiod6TaFl4QE6VI/6xH7s7gc/osRkoaFexIhcc=
-----END RSA PRIVATE KEY-----";

$pass_phrase = "";      // パスフレーズなし

$options = array(
    "dkim_body_canonicalization" => "simple",   // 本文の正規化方式
    "signature_headers" => array(               // 署名対象のヘッダ
        "From",
        "To",
        "Subject"
    )
);

$signature = new mail_signature(
    $private_key,
    $pass_phrase,
    $domain,
    $selector,
    $options
);

// DKIM-Signature ヘッダを作成する
$signed_headers = $signature->get_signed_headers($to, $subject, $body, $headers);

// 通常のメールヘッダの前に、作成した DKIM-Signature ヘッダを付け加える
$headers = $signed_headers . $headers;

// DKIM 署名 (DKIM-Signatureヘッダ) 付きのメールを送信する
mb_send_mail($to, $subject, $body, $headers);

このプログラムを以下のように実行すれば DKIM 署名付きのメールを送信することができます。

$ php send-dkim-mail.php

署名が正しく作成されたかどうかを確認するには、メールを Gmail などの DKIM に対応した Web メールに送るのが手っ取り早いでしょう。

Gmail 側で受信したメールのソースを開いて、Authentication-Results ヘッダに「dkim=pass」と記録されていれば、署名が正しく作成されたことになります。

今回紹介したライブラリは簡単で使いやすいですが、パラメータが固定されていたりと不便な点もあるので、ぜひその辺を改修したバージョンがリリースされることを期待したいですね。

Tags: ,

toda

システムの規模の肥大化に伴うクラス数増大により、あるクラスにメソッド追加が集中することがあります。基盤となるクラスのメソッドが増大していくものです。
クラスを追加するときに、その関連メソッドを基盤クラス側のメソッドとして追加していくとそのような事態に陥りがちです。いわゆる、Blob(肥満児)アンチパターンですね。

あまり上手くない例えですが、ある会員情報を保存している MemberTable クラスに対して、その会員情報を操作するフォームを表す Form クラスが追加されていることがある (1:N) とします。

その MemberTable に所属している Form の一覧を取得する getForms() メソッドを MemberTable クラスに追加したいわけですが、静的にメソッドを追加した場合、Form クラスが使われない場合は無駄なメソッドが追加されることになります。

Form クラスだけならまだしも、このようなクラスが増えていくと、常に使用されるとは限らないメソッドが MemberTable クラスに追加されていくことになり、MemberTableのコードが無駄に肥大化することになります。また、 Form クラスの修正の影響が MemberTable クラスに及ぶことがありえるため、修正すべき箇所が複数のコードに分散してしまい保守性の観点からも望ましくはありません。

この問題の一つの解決手段として、メソッドを(必要なときに)動的に追加することで、静的に定義されるメソッド数を減らすという方法があります。
Form クラスが読み込まれた時に、 Form クラスのコード側で MemberTable クラスに getForms() メソッドを追加することができれば、MemberTable クラスのソースに手を加える必要はないので、MemberTable の肥大化を防ぐことができますし、 Form クラスの修正を Form クラスのコードに抑えることができます。

具体的な実装例としては、動的に追加するメソッドを無名関数として登録しておき、基盤クラスの __call() マジックメソッドを経由して呼び出すという方法があります。

class MemberTable {
  // 動的に追加されたメソッドの配列
  static private $_methods = array();

  // 動的にメソッドを追加
  static public function addMethod($name, $func) {
    self::$_methods[$name] = $func;
  }

  // 動的に追加されたメソッドの呼び出し
  public function __call($name, $args)
  {
    $func = self::$_methods[$name];

    // 無名関数に $this を bind してから実行
    call_user_func_array($func->bindTo($this, 'MemberTable'), $args);
  }
}

Form クラス側のコードでは、読み込み時に MemberTable::addMethod() でメソッドを追加します。

class Form {
  // 詳細略
}

// MemberTable クラスに getForms() メソッドを追加
MemberTable::addMethod("getForms", function(){
  // 詳細略
  return $forms;
});

Form クラスが読み込まれるときに、MemberTable::addMethod() でメソッドを追加されているので、何も気にせずに getForms() を呼び出すことができます。

$forms = (new MemberTable())->getForms();

ポイントはメソッドの呼び出し時に、bindTo() で無名関数の $this を bind しておくことです。これを忘れると、無名関数内で $this を参照できません。
あと、bindTo() するときにスコープ(第2引数)にクラス名を指定しておく必要があることをお忘れなく。

しかし、bindTo() が使えるのは PHP 5.4 以降であって、公式パッケージが未だに PHP 5.3 である RedHat/CentOS では、無名関数を $this に bind することはできません。

そこで無名関数の呼び出し時に、引数に $this を暗黙的に追加して、無名関数側の引数として受け取るという方法があります。

class MemberTable {
  public function __call($name, $args)
  {
    // 引数の先頭に $this を追加
    array_unshift($args, $this);

    return call_user_func_array(self::$this->_methods[$name], $args);
  }
}

無名関数を登録するときは、最初の引数で $this を受け取るようにします。
クラスのメソッドは $self 経由で呼び出します。

class::addMethod('method', funciton($self, $arg1, $arg2..){
  // 詳細略
  return $forms;

  // メソッドを呼び出すときは $self から呼び出す
  $self->someMethod();
});

これである程度同様のことが実現できます。
もちろん制約もあって、$self は自身の private, protected メソッドを呼び出すことはできません。

早く RedHat/CentOS の公式パッケージが PHP 5.4 ベースになってくれることを期待したいところです。

Tags:

s-24

9月も早いものですね!2013年も終わりそうで寂しいs-24です。
7年ぶりに草野球を欠席してしまいました。皆さんも体調管理に気を付けて下さい。

前回記事 では PHPMD の使い方を簡単にご紹介しましたが、今回は続編ということで PHPMD と Jenkins を組み合わせて typo などによる未定義変数の検出を自動化しちゃいます。設定方法と、普段どう使ってるかを紹介しますね!

目次

  1. プラグインのインストール
  2. プロジェクトの作成と設定
  3. 普段の運用

プラグインのインストール

以下では Jenkins のセットアップ自体は完了している前提で話を進めます。

まず、プラグインマネージャーから以下の 2 つのプラグインをインストールします。「Jenkinsの管理」→「プラグインの管理」をクリックすると、プラグイン管理画面に移りますので、そちらからインストールできます。(アップデートがあれば、アップデートありますって表示が出てくれますよ!)

  • Phing Plugin
  • PMD Plugin

01

 

プロジェクトの作成と設定

さて、それでは新規ジョブを作成します。「新規ジョブ作成」を選択し、ジョブ名の入力とプロジェクトの種類の選択を行い、「OK」をクリックします。今回はジョブ名を Product-PHPMD-UnUsedCodeCheck としてみます。

01

次に設定画面が出てくるので、各項目設定していきます。

  1. 「説明」にプロジェクトの概要を記載します。
  2. 01

  3. 「ソースコード管理システム」でソースコードのリポジトリ置き場を指定します。
  4. 01

  5. 日次で実行する場合は、「ビルド・トリガ」の項目で「定期的に実行」をクリックして、スケジュールに設定を記載します。ここでは早朝 4 時 10 分にセットしています。書式に関しては右側のクエスチョンマークをクリックするとヘルプが出ますのでそちらを参照ください。
  6. 01

  7. 「ビルド」の「ビルド手順の追加▼」をクリックして追加する処理の一覧を表示し、その中から「Phing の呼び出し」を選択します。「使用するPhing」、「ターゲット」については空欄でOKです。
  8. 01

    01

  9. 最後に、ビルド後の処理に PHPMD を追加します。「ビルド後の処理」の「ビルド後の処理の追加▼」をクリックして追加する処理の一覧を表示し、その中から「PMD警告の集計」を選択します。「集計するファイル」については空欄でOKです。
  10. 01

    01

ここまで設定が完了したら「保存」をクリックして設定を保存します。

さて、次はビルドファイルの作成と設置ですが、置き場であるワークスペースを作成する必要があるので、「ビルド実行」をクリックして一度ビルドを実行します。

01

この時点でのビルドは失敗しますが OK です。

01

次はビルドファイルの作成と設置です。こちらは Jenkins を実行しているサーバにログインして作業を行います。

Jenkins のプロジェクト関連ファイルの置き場は /var/lib/jenkins/jobs/ とします。

上記のビルドで /var/lib/jenkins/jobs/<プロジェクト名>/workspace/ というディレクトリが作成されているはずなので、そのディレクトリに以下の内容のファイルを build.xml として設置して下さい。なお、①「project name=」に記載するプロジェクト名、②「fileset dir=」に設定するディレクトリパス、③「include name=」、「exclude name=」に記載するチェック対象プログラム、検査から除外するプログラムについては適宜変更して下さい。

ちなみに今回のルールセットは unusedcode のみにしています。他にも codesize、design、naming なども指定できますが (詳しくは 前回記事 参照) 、今回の目的では unusedcode で十分なので敢えてそれだけにしています。

<?xml version="1.0" encoding="utf-8" ?>
<project name= "Product-PHPMD-UnUsedCodeCheck" basedir= "." default= "main">

    <target name="main" depends="phpmd"></target>

    <!-- PHPMD -->
    <target name="phpmd" >
         <phpmd rulesets= "unusedcode" >
             <fileset dir= "/var/lib/jenkins/jobs/Product-PHPMD-UnUsedCodeCheck/workspace/include/product/" >
                 <include name= "**/*.php" />
                 <exclude name= "**/*Test.php" />
             </fileset>
             <formatter type= "xml" outfile= "reports/pmd.xml"/>
         </phpmd>
    </target>
</project>

上記の build.xml ではレポートファイルの置き場所を reports ディレクトリとしているため、そのディレクトリを作成しておく必要があります。

$ mkdir /var/lib/jenkins/jobs/Product-PHPMD-UnUsedCodeCheck/workspace/reports/

設定は以上です。では、改めてビルドを実行してみましょう。

01

ビルド履歴にビルドの進捗がゲージで表示されます。このゲージをクリックするとビルド中なら進捗状況が、ビルド後 (失敗) であればどこで止まったかを参照できます。

01

実行後、ビルド履歴のビルド日時をクリックすると以下のように表示されます。これで未使用コードの一覧を参照することができます。リンクを手繰るとどのファイルにどの程度の未使用コードがあるか、実際にコードのどの部分かを追っていくことができます (その画面は後述の「普段の運用」の部分でお見せしますね)。

01

このように 1 度目のビルドでは未使用のコードについて網羅的に検出を行ってくれます。その後のビルドでは、前回ビルドとの差 (新しく出てきたもの、修正されたもの) についての情報も表示してくれます。

01

 

普段の運用

私の場合、1 度目の結果をざっと確認した後の 2 度目以降の確認では新しい警告についてのみ確認を行い、未定義変数などが無いかをチェックしています。今の運用では未使用コードをゼロにすることが目的ではなく、変更が加わった部分に未定義変数など問題があるコードがないかを早期に発見することが目的なので、毎日のチェックの流れは以下のようにしています。

  1. 早朝に Jenkins 側で自動ビルドとレポートの作成 (上記設定で自動化)
  2. 出社後にビルド結果のレポートを確認 (主に新しい警告の有無)
  3. 01

  4. 新しい警告が出ている場合はコードをチェック
    • 上記画面の「XXX 個の新しい警告」をクリックすると、警告が生じたプログラムの一覧が表示されます。
    • 01

    • 更にそのプログラムをクリックすると、実際に未使用コードが検出された場所をハイライト表示してくれます。
    • 01

  5. 未定義変数など問題があるコードを見つけたら開発チームに共有

こんな感じで日々のコードチェックを効率化しています。だいぶいい感じでチェックできてますが、もっと効率が良い方法あれば是非教えて下さい!

参考サイトやオススメ本

参考サイト: PHPMD(PHP Mess Detector)をjenkinsで利用してみる

オススメ本: 「Jenkins実践入門 ~ビルド・テスト・デプロイを自動化する技術」

Tags: , , ,

momoken

はじめまして。インフラグループのmomokenと申します。
前回のturuと同様、初登場です。

先日、VMware上の仮想マシンで使用しているNICのアダプタ vmxnet3のバージョンアップで嵌ったのでその話を。同じように嵌ってしまった人の助けとなれば幸いです。

なお、仮想マシンの構成は以下の通り。

  • OS:CentOS release 6.4 (Final)
  • kernel:2.6.32-358.14.1.el6.i686
  • vmware-tools:9.0.5.21789 (build-1065307)

仮想マシンが載っている VMware ESXi の構成は以下の通り。

  • VMware ESXi 5.1.0 Update 1 (build-1117900)

目次

  1. 問題の発生
  2. 原因
  3. 対処

1.問題の発生

先日仮想マシンのカーネルアップデートに伴いvmware-toolsを再設定した際に
以下のメッセージを確認しました。

The module vmxnet3 has already been installed on this system by another
installer or package and will not be modified by this installer. Use the flag
--clobber-kernel-modules=vmxnet3 to override.

既にvmxnet3とかインストールされているからこのインストーラでは変更しないよ!
上書きするなら「--clobber-kernel-modules=vmxnet3」を使えとのこと。

vmxnet3の確認。

# modinfo vmxnet3
filename:       /lib/modules/2.6.32-358.14.1.el6.i686/kernel/drivers/net/vmxnet3/vmxnet3.ko
version:        1.1.29.0-k
license:        GPL v2
description:    VMware vmxnet3 virtual NIC driver
author:         VMware, Inc.
srcversion:     300574F157A2E481CA33E17
alias:          pci:v000015ADd000007B0sv*sd*bc*sc*i*
depends:
vermagic:       2.6.32-358.14.1.el6.i686 SMP mod_unload modversions 686

/etc/vmware-tools/manifest.txtを覗いてみると

vmxnet3.version = "1.1.34.0"
vmxnet3.installed = "FALSE"

という記述。
今入っているバージョンより新しいのでインストールしてみます。

# /usr/bin/vmware-config-tools.pl --clobber-kernel-modules=vmxnet3
Initializing...
・・・・・
The module vmxnet3 has already been installed on this system by another package
but has been marked for clobbering and will be overridden.

Found a compatible pre-built module for vmxnet3.  Installing it...
・・・・・

インストールしてくれた様子。
確認してみる。

# modinfo vmxnet3
filename:       /lib/modules/2.6.32-358.14.1.el6.i686/updates/vmware/vmxnet3.ko
supported:      external
version:        1.1.34.0
license:        GPL v2
description:    VMware vmxnet3 virtual NIC driver
author:         VMware, Inc.
alias:          pci:v000015ADd000007B0sv*sd*bc*sc*i*
depends:
vermagic:       2.6.32-279.el6.i686 SMP mod_unload modversions 686
parm:           enable_shm:Shared memory enable (array of int)
・・・・・

ちゃんと1.1.34になった。
さて再起動しましょ!

起動失敗加工

ふむ。起動に失敗☆

2.原因

NICのアダプタタイプをE1000に変えて再度起動。
※アダプタタイプは変更が出来なかったので一度NICを削除して追加しなおしました。

動してみると正常に起動した。

で、原因としてはmodinfoコマンドでちゃんと表示されています。赤字部分に注目です。

# modinfo vmxnet3
filename:       /lib/modules/2.6.32-358.14.1.el6.i686/updates/vmware/vmxnet3.ko
supported:      external
version:        1.1.34.0
license:        GPL v2
description:    VMware vmxnet3 virtual NIC driver
author:         VMware, Inc.
alias:          pci:v000015ADd000007B0sv*sd*bc*sc*i*
depends:
vermagic:       2.6.32-279.el6.i686 SMP mod_unload modversions 686
parm:           enable_shm:Shared memory enable (array of int)
・・・・・

ビルドした環境のカーネルバージョン が、「2.6.32-279.el6.i686」で、
現在のカーネルは、「2.6.32-358.14.1.el6.i686 」。
これだとモジュールのロードの際にエラーとなってしまいます。

3.対処

現在動いているカーネルでビルドしなおしてインストールします。
まず、vmware-tools が持っているvmxnet3のオブジェクトファイルを削除します。

# cd /usr/lib/vmware-tools/modules/binary/bld-2.6.32-279-i686-RHEL6.3/objects/
# ll
total 360
-rw-r--r-- 1 root root 23864 Jul 30 17:46 pvscsi.o
-rw-r--r-- 1 root root 16832 Jul 30 17:46 vmblock.o
-rw-r--r-- 1 root root 81804 Jul 30 17:46 vmci.o
-rw-r--r-- 1 root root  1559 Jul 30 17:46 vmci.symvers
-rw-r--r-- 1 root root 60172 Jul 30 17:46 vmhgfs.o
-rw-r--r-- 1 root root 12564 Jul 30 17:46 vmmemctl.o
-rw-r--r-- 1 root root  6776 Jul 30 17:46 vmsync.o
-rw-r--r-- 1 root root 71940 Jul 30 17:46 vmxnet3.o ←コレ
-rw-r--r-- 1 root root 25088 Jul 30 17:46 vmxnet.o
-rw-r--r-- 1 root root 49096 Jul 30 17:46 vsock.o
# rm vmxnet3.o

ビルドに必要なパッケージをインストールします。

# yum install gcc make kernel-devel

ここまで準備して vmware-config-tools.plを実行すればビルドしてインストールしてくれます。

# /usr/bin/vmware-config-tools.pl
Initializing...
・・・・・
Before you can compile modules, you need to have the following installed...

make
gcc
kernel headers of the running kernel

Searching for GCC...
Detected GCC binary at "/usr/bin/gcc".
The path "/usr/bin/gcc" appears to be a valid path to the gcc binary.
Would you like to change it? [no]

Searching for a valid kernel header path...
Detected the kernel headers at
"/lib/modules/2.6.32-358.14.1.el6.i686/build/include".
The path "/lib/modules/2.6.32-358.14.1.el6.i686/build/include" appears to be a
valid path to the 2.6.32-358.14.1.el6.i686 kernel headers.
Would you like to change it? [no]

Using 2.6.x kernel build system.
make: Entering directory `/tmp/modconfig-yeFbTE/vmxnet3-only'
/usr/bin/make -C /lib/modules/2.6.32-358.14.1.el6.i686/build/include/.. SUBDIRS=$PWD SRCROOT=$PWD/. \
MODULEBUILDDIR= modules
make[1]: Entering directory `/usr/src/kernels/2.6.32-358.14.1.el6.i686'
CC [M]  /tmp/modconfig-yeFbTE/vmxnet3-only/vmxnet3_drv.o
CC [M]  /tmp/modconfig-yeFbTE/vmxnet3-only/vmxnet3_ethtool.o
CC [M]  /tmp/modconfig-yeFbTE/vmxnet3-only/vmxnet3_shm.o
LD [M]  /tmp/modconfig-yeFbTE/vmxnet3-only/vmxnet3.o
Building modules, stage 2.
MODPOST 1 modules
CC      /tmp/modconfig-yeFbTE/vmxnet3-only/vmxnet3.mod.o
LD [M]  /tmp/modconfig-yeFbTE/vmxnet3-only/vmxnet3.ko.unsigned
NO SIGN [M] /tmp/modconfig-yeFbTE/vmxnet3-only/vmxnet3.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.32-358.14.1.el6.i686'
/usr/bin/make -C $PWD SRCROOT=$PWD/. \
MODULEBUILDDIR= postbuild
make[1]: Entering directory `/tmp/modconfig-yeFbTE/vmxnet3-only'
make[1]: `postbuild' is up to date.
make[1]: Leaving directory `/tmp/modconfig-yeFbTE/vmxnet3-only'
cp -f vmxnet3.ko ./../vmxnet3.o
make: Leaving directory `/tmp/modconfig-yeFbTE/vmxnet3-only'

確認しまっしょ!

# modinfo vmxnet3
filename:       /lib/modules/2.6.32-358.14.1.el6.i686/updates/vmware/vmxnet3.ko
supported:      external
version:        1.1.34.0
license:        GPL v2
description:    VMware vmxnet3 virtual NIC driver
author:         VMware, Inc.
srcversion:     09E5FA27CEBE474CA7F44A1
alias:          pci:v000015ADd000007B0sv*sd*bc*sc*i*
depends:
vermagic:       2.6.32-358.14.1.el6.i686 SMP mod_unload modversions 686
parm:           enable_shm:Shared memory enable (array of int)
・・・・・

ちゃんとビルドしたカーネルが変わっています。
シャットダウンしてNICのアダプタをvmxnet3に変えて起動してみましょう。
無事に起動できるはずです。

なんか無理やり感が拭えないですがこの方法で復旧できました。

どうも vmware-tools が自前で保持している以外のkernelのバージョンにインストールしようとすると無理やり近い(?)バージョンでビルド済のオブジェクトファイルをインストールするようです。また、オブジェクトファイル削除後は、「--clobber-kernel-modules=vmxnet3」の引数を与えなくても勝手にビルドしてインストールしてくれちゃいます。色々なケースを試したわけではないので一概には言えないのですが、vmware-config-tools.pl があまりうまく動いてくれていないようです。

この辺の動きを探ろうと vmware-config-tools.pl の中身を覗こうとしましたが 14967 行あったのでまだ見ていないです。※決して面倒くさいとかでは無いです。時間がないだけです。時間がないだけなのです。(;^ω^)メンドクセ

もしもっとスマートな方法をご存知の方がいらっしゃればぜひ教えてください!

参考サイト

Linuxデバイスドライバ開発入門
ESXi5.1 の RHEL6.4 に vmxnet3 ドライバ入れたらコケた

Tags: , , , ,

turu

こんにちは。毎日暑いですね。
ビールのことで頭がいっぱいな品質管理グループの turu です。
Tricorn Labs では初登場です。よろしくお願いします。

今回のエントリでは2点、ご紹介しようと思います。

  1. トライコーン式Redmineステータスについて
  2. Redmineのステータス設定方法について

 

1.トライコーン式Redmineステータスについて

トライコーンではバグ管理システムとして Redmine と trac を利用しています。
そのうちのRedmineのステータスについてご紹介します。

まず、弊社プロジェクトで Redmine を使うにあたって
デフォルトステータス (新規/進行中/解決等) ではいくつかの問題がありました。

  • チケットが終了したときの状況が不明
  • 不具合分析がしづらい
  • 開発、品質管理のどちらにボールがあるか不鮮明
  • チケット一覧からプロジェクトの状況が把握しづらい

ということで、上記問題を解決するために

Redmineをトライコーン式にカスタマイズすることにしました。

Redmineのステータスが変更できること、ご存知でしたか? 私は知りませんでした。

ということで(2回目)、
トライコーンのプロジェクトに適したステータスとワークフローを考えてみました。
私がいままで利用してきたバグ管理システムを参考に (Bugzilla、JIRA、影舞、trac、など)。

そして、できたのが以下です。
ほとんど Bugzilla からいただきました。先にワークフローからご紹介。

 

■ワークフロー

Redmineステータス

※左下のlater等は、どのステータス、どの担当者からでも変更できるようにRedmineで設定しています。

また、ステータスの中で、later だけ特殊です。
次のバージョンで生きるように、チケットをlaterに変更する際は、対応予定のバージョンへ変更します。

Redmine更新画面

 

■ステータス一覧

開発ステータス 詳細
new 新規チケット
assigned アサイン済み
working 開発担当者が対応中
reopend 不具合発生等、再度対応が必要
品管ステータス 詳細
testing テストが可能な状態
終了ステータス 詳細
fixed 不具合発生→修正→確認
end テストしたが、不具合が発見されずに終了

※最初から不具合として起案されたチケットに関しては、fixedステータスにする

later 次以降のリリース時に対応する
invalid 仕様または無効
wontfix 不具合だが、対応しない(修正コストに見合わない等)
worksforme 現象の再現ができない
duplcate 重複したチケット
closed テストの必要がない(またはテスト不可)チケットとして終了

開発ステータス…開発の対応が必要なステータス
品管ステータス…品管の対応が必要なステータス
終了ステータス…今回のプロジェクト(バージョン)では対応終了のステータス

 

プロジェクトが終了した際、チケットは終了ステータスのいずれかに割り振られていることになります。

今後、さらに詳細な不具合分析をしたいので fixed の中でさらに不具合を分類したいと思っています。
分類例..仕様バグ、機能バグ、データミス、ユーザビリティ低下、など。

いまのプロジェクトを完遂するだけが目的になるのではなく、次へどう活かすかを意識しながらプロジェクトを進められればいいなと思っています。

 

2.Redmineのステータス設定方法について

Redmineトップ

 

■ 管理>チケットのステータス

ステータスを新規作成/編集/削除ができます。
また、「終了」扱いされるステータスも設定できます。
先ほどの■ステータス一覧の「終了ステータス」はここで終了フラグが立つように設定します。

Redmineステータス設定

 

■ 管理>ワークフロー

ユーザ(ロール)がどのステータスを変更できるか、ステータス遷移の設定ができます。

Redmineワークフロー設定

 

これはトラッカーごとに設定しなければならないのでロール×トラッカーで一通り設定後、
コピーしてしまうと楽です。
※ユーザ毎に設定を替えたい場合は、別途編集が必要
Redmineコピー

 

今回のようにRedmineの管理者権限で色々さわるのは初めてだったので
もしみなさんが知ってる小技等あれば教えていただきたいです!
フィルタを固定で設定する方法など(カスタムクエリを作成するのがめんどうで、、)

見直してみると思っていたより長くなってしまいました。
ブログを書くのはたいへんですね。
今後もなにか良いネタがあれば投稿します。
最後まで読んでいただき、ありがとうございます。

 

参考URL

バグのライフサイクル – Mozilla Developer Network
チケットのステータスの意味 — Redmine.JP

Tags: , ,

2 / 1712345...10...最後 »