せっかくだから俺は各政党のホームページを解析するぜ

目的

ネット選挙が解禁されて早一年、公職選挙法に反逆するような使い方がちらほらみられる、今日この頃いかがおすごしでしょうか。
さて、今回は、各政党のホームページを解析して、どんな単語が使われているかしらべ、似たような特徴のある政党を抽出してみようと思います。

手順

  1. 各政党のホームページをダウンロードしてくる。
  2. ダウンロードしたページをMecabで形態素解析する。
  3. tf-idfで各単語のスコアを求める
  4. コサイン類似度による文章の距離の測定を行い、政党間の距離を求める。
  5. GraphVizで、その関連を表示する。

結果

2014年の各政党のホームページ解析
http://needtec.sakura.ne.jp/analyze_election/page/analyzehp/2014

ソースコード
https://github.com/mima3/analyze_election/tree/master/script_comp_manifesto

ソースコードをダウンロードしてきて以下のスクリプトを実行する。

# ホームページをダウンロードしてDBに格納
python create_parties_db.py parties_hp_2014.sqlite party_hp_json_2014.json

# 形態素解析して単語の数の集計
python create_parties_tokens.py parties_hp_2014.sqlite

# tf-idfとコサイン類似度の計算をして結果をJSONとPNGに記録する。
python create_tf_idf_report.py parties_hp_2014.sqlite party_hp_result_2014.json  party_hp_result_2014.png "ms ui gothic"

実行には下記のライブラリをインストールする必要があります。
・nltk
・lxml
・MeCab
・urllib2
・pydot

解説

tf-idfによる文章の解析

文章yにおいて単語xのtf-idfの値は以下のようになる。

tf = 文章y中に単語xが登場した数 / 文章中の単語数
idf = 1.0 + log( 文章の総数 / 単語xが登場する文章の数 )
tf-idf = tf × idf

多くのドキュメントに登場する単語は重要度が下がりスコアが低くなり、特定のドキュメントにしか登場しない単語は重要度があがりスコアが大きくなる

コサイン類似度による文章の距離の測定

文章1が単語(A,B,C)を持っており、その単語のTF-IDFの値が(0.1,0.2,0.3)とする。
文章2が単語(C,D,E)をもっており、その単語のTF-IDFの値が(0,4,0.5,0.6)とする。

文章に存在しない単語のTF-IDFを0とするとして、全ての単語についてのTF-IDFを作成する。

文章1の(A,B,C,D,E)は(0.1,0.2,0.3,0,0)となる。
文章2の(A,B,C,D,E)は(0,0,0.4,0.5,0.6)となる

この文章1と文章2のベクトルの角度のコサインは両者の類似度を表す。
まったく同じ文章の場合は文章1と文章2の角度は0度になる。

Pythonの場合は、nltk.cluster.util.cosine_distanceで計算してくれる。

せっかくだから俺は衆院選の比例代表の議席数を計算するぜ

ネット選挙が解禁されて早一年、どこぞの政党が勝つと日本が終わるというノストラダムスがネット上に大量発生して、キバヤシさんの突っ込みが追い付かない今日この頃いかがおすごしでしょうか?

さて、今回は比例代表選挙におけるドント式の計算をやってみようと思います。

結果

http://needtec.sakura.ne.jp/analyze_election/page/dondt/shuin_47

投票数を入力して、「計算」ボタンを押すと、投票数に応じた議席数が取得できます。
政党支持率をそのまま突っ込むと、自民党が140議席取得して、維新の会すら全滅するという結果になります。

なお、2013年の参院選挙の時は、直近の東京都議会選挙の党別の得票率を入力すると、試算結果と実際の結果の差が1議席といい感じになりました。

ソースコード

https://github.com/mima3/analyze_election

実際のドント式の計算ファイル

dondt_util.py

# coding=utf-8
import os
import re
from collections import defaultdict
import math
import json
import copy
import operator

class political_party_info:
    """
    政党の情報を格納するクラス
    name : 政党名
    votes:得票数
    max    :立候補者数(いくら得票してもこれをこえた議席は取得できない)
    seats:取得議席数
    """
    def __init__(self, name, votes, max):
        self.name = name
        self.votes = votes
        self.max = max
        self.seats = 0

def select_political_party(votes):
    """
    政党名をキー、値をpolitical_party_infoとしたディクショナリ中でもっとも得票している党名を取得する
    """
    max = -1
    ret = None
    for k, v in votes.items():
        # 同数の場合、本来くじで決めるが今回は登録順とする
        if max < v.votes:
            ret = k
            max = v.votes
    return ret

def dondt(votes_data, max):
    """
    ドント方式による
    votes_data: 政党名をキー、値をpolitical_party_infoとしたディクショナリ
    max:総議席数
    votes_data[x].seatsに議席数が格納されます。
    """
    tmp_votes = copy.deepcopy(votes_data)
    for i in range(1, max+1):
        s = select_political_party(tmp_votes)
        if s is None:
            return None
        votes_data[s].seats += 1
        tmp_votes[s].votes = math.floor(votes_data[s].votes / (votes_data[s].seats + 1))
        if tmp_votes[s].max == votes_data[s].seats:
            #立候補した数超えたので以降この政党への投票は無効
            tmp_votes[s].votes = 0
    return votes_data

商を求める際の端数は切り捨てています。
もし同一の得票数になった場合は、本来はくじで決めるが、ここでは文字コード順にしています(ex みどりの党は自民党より優先されている)

ドント式の計算方法については下記を参照。
http://www.pref.tochigi.lg.jp/senkyo/sangisenkyo/qanda/qanda-9.html

比例区の情報

比例区のブロックの議席数や、各党の候補者数は本来であれば、総務省のページから作成するのが筋です。しかしながら、総務省はテキスト処理できないPDFでのみでしか公開しておりません。

比例ブロックの議席数はともかく、候補者のデータ入力はしんどいので、朝日新聞のホームページをWebスクレイピングして、CSVファイルを作成するようにしました。
以下のURLを対象としています。
http://www.asahi.com/senkyo/sousenkyo47/kouho/B01.html

http://www.asahi.com/senkyo/sousenkyo47/kouho/B11.html

2012年と同じようなHTMLの構成だったので、たぶん次の解散でもつかえるでしょう。

script/analyze_asahi_hirei.py

# !/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import urllib2
import lxml.html
import re
import os.path
import urlparse

def print_area(url):
    r = urllib2.urlopen(url, timeout=30)
    html = r.read()
    dom = lxml.html.fromstring(html)
    parties = dom.xpath('//div[@class="snkH2Box"]/h2')
    tables = dom.xpath('//table[@class="snkTbl01"]')
    block = dom.xpath('//div[@class="BreadCrumb"]/h1')[0].text_content().encode('utf-8')
    block = block[block.find(':')+len(':'):]
    for i in range(0, len(parties)):
        h2 = parties[i].text_content().encode('utf-8')
        partyName = h2.split('\n')[0]
        members = tables[i].xpath('tbody/tr')
        for m in members:
            name = m.xpath('td[@class="namae"]')[0].text_content().encode('utf-8')
            lstNum = m.xpath('td[@class="lstNum"]')[0].text_content().encode('utf-8')
            age = m.xpath('td[@class="age"]')[0].text_content().encode('utf-8')
            status = m.xpath('td[@class="status"]')[0].text_content().encode('utf-8')
            net = m.xpath('td[@class="net"]/ul')[0]
            twitterEl = net.xpath('li[@id="twitter"]/a')
            facebookEl = net.xpath('li[@id="facebook"]/a')
            hpEl = net.xpath('li[@id="HomePage1"]/a')
            areaEl = m.xpath('td[@class="w"]/a')
            area = ''
            twitter = ''
            facebook = ''
            hp = ''
            if twitterEl:
                twitter = twitterEl[0].attrib['href'].encode('utf-8')
            if facebookEl:
                facebook = facebookEl[0].attrib['href'].encode('utf-8')
            if hpEl:
                hp = hpEl[0].attrib['href'].encode('utf-8')
            if areaEl:
                area =areaEl[0].text_content().encode('utf-8')
            print ('%s,%s,%s,%s,%s,%s,%s,%s,%s,%s' % (block, partyName,lstNum, name, age, status, area, twitter, facebook, hp))

def main(argvs, argc):
    """
    このスクリプトでは、朝日新聞の情報から小選挙区の立候補者を取得します
    """
    for i in range(1, 12):
        url = ('http://www.asahi.com/senkyo/sousenkyo47/kouho/B%s.html' % str(i).zfill(2))
        print_area(url)

if __name__ == '__main__':
    argvs = sys.argv
    argc = len(argvs)
    sys.exit(main(argvs, argc))

朝日新聞からデータを取得したのち、全角を半角にしたり、「県」を付与した比例区の立候補者のデータ
https://github.com/mima3/analyze_election/blob/master/script/candidate_shuin_47_hirei.csv

朝日新聞のデータをみつつ作成した比例ブロックの情報CSV
https://github.com/mima3/analyze_election/blob/master/script/block_shuin_47_hirei.csv

【2019年11月版】ブラウザ側でデータを保存するため各ブラウザにおけるローカルストレージの挙動の調査

概要

このドキュメントではローカルストレージの説明と、各ブラウザでの動作を検証する。
ローカルストレージとは、データをブラウザ側に蓄積する仕組みである。

保存時に、サイトごとにKeyと文字の組み合わせでデータを格納する。

サイトごとの領域は、それぞれ独立しており、別のサイトで記録したデータを操作することはできない。
また、それぞれのサイト毎に保存できる上限は制限されている。その具体的サイズはブラウザ毎にことなる。

詳細な仕様は下記を参照のこと。
https://html.spec.whatwg.org/multipage/webstorage.html#the-localstorage-attribute

実装例

下記にローカルストレージの実装例を紹介する。

localStorage APIの使用例

localStorageの操作を行うAPIの使用例を紹介する。
このAPIは、処理が完了するまで制御の戻らない同期処理になっている。

指定のキーにデータを格納する

localStorage.setItem("KeyName","Data");

格納するデータは、文字列に過ぎない。
もし、オブジェクトを格納したい場合は、JSONを使用して変換をおこなうか、後述のstore.jsライブラリを使用すること。

サイト毎の容量を超えた場合は例外が発生する。
この時発生する例外は過去はブラウザによって異なっていたが、2019年11月時点で主要なブラウザではQuotaExceededErrorが発生する。

ブラウザ名 例外の名前
FireFox 69.0.3 QuotaExceededError
Safari12.1.1 QuotaExceededError
Chrome 78 QuotaExceededError
IE11 QuotaExceededError
Microsoft Edge 44.18362.449.0 QuotaExceededError

指定のキーの取得

console.log(localStorage.getItem("KeyName"))

存在しないキーを指定した場合はnullが返ってくる。

指定のキーの削除

localStorage.removeItem("keyName")

存在しないキーを指定してもエラーとならない。

すべてのキーの削除

localStorage.clear()

サイトに登録されているキーをすべて削除する。

登録されているキーの列挙

    var keys = [];
    for (var i = 0; i < localStorage.length; ++i) {
      keys.push(localStorage.key(i));
    }
    window.alert(keys.join("\n")); 

keyメソッドを使用すれば、キーの名称を取得できる。

store.js ローカルストレージ用のライブラリ

store.js
https://github.com/marcuswestin/store.js

store.jsはローカルストレージの操作を行うMITライセンスのライブラリである。
ローカルストレージが使用できる場合は、それを利用し、IE6,IE7などではuserData を用いて同等の処理を行う。
このライブラリにオブジェクトを渡すとJSONに変換して格納する。

storageイベント

setItem(), removeItem(), clear() メソッドが実行された時に、storageイベントが同じサイトを見ている、別のウィンドウで発行される。

以下にそのイベントを取得する例を示す。

  window.addEventListener("storage", function (event) {
    var data = 'key:' + event.key +
               ' oldValue:' + event.oldValue +
               ' newValue:' + event.newValue +
               ' url:' + event.url +
               ' storageArea:' + event.storageArea;
    console.log(event);
  });

ブラウザを2つ開いて、片方でSetItemなどを行うと、もう片方でイベントが発行される。

ブラウザ間の挙動の差

ここでは検証用のコードを使用して各ブラウザのローカルストレージの動作にどのような差があるか検証する。

  • FireFox 69.0.3 + Windows10
  • Safari12.1.1 + macOS 10.14.5
  • Chrome 78 + Windows10
  • IE11 + Windows10
  • Microsoft Edge 44.18362.449.0 + Windows10

検証用コード

下記のサイトにアクセスする。
http://needtec.sakura.ne.jp/release/storageTest.html

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
  <title>Storage Sample</title>
</head>
<body>
<p>
  KEY:
  <input id="storeKey" type="text" value="testData"></input>
</p>
<p>
  DATA:<BR>
  <textarea id="storeMsg" col="40" row="10"></textarea>
</p>
<p>
  EVENT:<BR>
  <select id="eventLog" multiple="multiple"></select>
</p>
<button id="update">update</button>
<button id="reload">reload</button>
<button id="testObject">testObject</button>
<button id="delete">delete</button>
<button id="clear">clear</button>
<button id="keyList">keyList</button>
<p>
  Big Data:<BR>
  <input id="bigSize" type="text" value="5242880"></input>
  <button id="updateBigData">大量データ書き込み</button>
</p>
<script>
  reloadStorage();
  // 指定のキーの値を更新
  document.getElementById('update').addEventListener('click', function() {
    let key = document.getElementById('storeKey').value;
    let contents = document.getElementById('storeMsg').value;
    localStorage.setItem(key, contents);
    console.log('localStorage.setItem(%s, %s)', key, contents);
  });

  // ストレージ読み込み
  function reloadStorage() {
    let key = document.getElementById('storeKey').value;
    let contents = localStorage.getItem(key);
    document.getElementById('storeMsg').value = contents;
    console.log('localStorage.getItem(%s) is %s', key, contents);
  }

  // 指定のキーの値を読み込む
  document.getElementById('reload').addEventListener('click', function(){
    reloadStorage();
  });

  // 指定のキーの値を削除
  document.getElementById('delete').addEventListener('click', function(){
    let key = document.getElementById('storeKey').value;
    localStorage.removeItem(key);
    console.log('localStorage.removeItem(%s)', key);
  });

  // 全て削除
  document.getElementById('clear').addEventListener('click', function(){
    localStorage.clear();
    console.log('localStorage.clear()');
  });

  // キーの一覧
  document.getElementById('keyList').addEventListener('click', function(){
    let keys = [];
    for (var i = 0; i < localStorage.length; ++i) {
      keys.push(localStorage.key(i));
    }
    window.alert(keys.join("\n")); 
  });

  // オブジェクトで書き込む
  document.getElementById('testObject').addEventListener('click', function(){
    let obj = {test:"123", value:123};
    localStorage.setItem('testObject', obj);
    console.log(localStorage.getItem('testObject'));
    window.alert(JSON.stringify(localStorage.getItem('testObject'))); 
    // オブジェクトは書き込んでも無意味。
    // [object Object]という文字列になるだけ
  });

  // 指定のサイズのデータを作成する
  document.getElementById('updateBigData').addEventListener('click', function(){
    try {
      let value = parseInt(document.getElementById('bigSize').value,10);
      let data = Array(value).join("x");
      let key = 'k';
      localStorage.clear();
      console.log('大量データ作成 key:%s size:%d', key, value);
      localStorage.setItem(key, data);
    } catch(e) {
      console.log(e);
      window.alert(e);
    }
  });

  // storageイベント
  window.addEventListener("storage", function (event) {
    let data = 'key:' + event.key +
               ' oldValue:' + event.oldValue +
               ' newValue:' + event.newValue +
               ' url:' + event.url +
               ' storageArea:' + JSON.stringify(event.storageArea);
    console.log('storage event : %o', event);
    let eventLog = document.getElementById('eventLog');
    let option = document.createElement("option");
    option.text = data;
    eventLog.appendChild(option);
  });

  // storagecommit イベント
  window.addEventListener("storagecommit", function (event) {
    console.log('storagecommit event : %o', event);
  });

</script>
</body>
</html>

FireFox 69.0.3 + Windows10

保存先:
C:\Users\ユーザ名\AppData\Roaming\Mozilla\Firefox\Profiles\\webappsstore.sqlite
SQLiteの形式で以下のようなSQLで取得できる。

select * from webappsstore2;

イベントの挙動:
・自分のWindowで行った操作のイベントは、自分のWindowでは発行されない。
・setItemで前回値と同じ場合はイベントが発行されない。
・removeItemで存在しないキーを指定した場合、イベントは発行されない。
・clear済みで、再度、クリアしてもイベントは発行されない。

保存の限界:
キー名を「k」とし、5242881文字をデータとして保存した場合にQuotaExceededError例外が発生する。

開発者ツールでの確認方法:
StorageタブにてLocal Storageを選択する
image.png

ユーザによるローカルストレージの削除方法:
Optionsを選択
image.png

Privacy & Security タブにて、「Clear Data」ボタンを押下
image.png

image.png

即時消える模様

Private Windowsの時
通常のモードで記録した内容は読み込めない
書き込み時にはエラーにはならない。
同じPrivate Windowにおいては書き込んだ内容を別のタブから読みだせる。※
ブラウザを終了して再度、Private Windowsで同じ開いた場合、前回記録した内容は読み込めない。

※新しいタブで開いた場合の挙動は以下の通り
①Private Window Aでローカルストレージを書き込む
②Private Window Bを新しいタブで開く
→この時点では①の内容は取得できない。
③Private Window Aで①と同じキーでローカルストレージを書き込む
④Private Window Bにイベントが発生。以降③で記録した内容が取得できる

Safari12.1.1 + macOS 10.14.5

保存先:
~/Library/Safari/LocalStorage/

アクセスできない場合は以下参照。
http://osxdaily.com/2018/10/09/fix-operation-not-permitted-terminal-error-macos/

フォルダを確認すると以下のようにURL事にSQLite3のファイルが存在する。
image.png

ItemTableを確認するとキーとデータが格納されている。
image.png

イベントの挙動:
・自分のWindowで行った操作のイベントは、自分のWindowでは発行されない。
・setItemで前回値と同じ場合はイベントが発行されない。
・removeItemで存在しないキーを指定した場合、イベントは発行されない。
・clear済みで、再度、クリアしてもイベントは発行されない。

保存の限界:
キー名を「k」とし、2621441文字をデータとして保存した場合にQuotaExceededError例外が発生する。

開発者ツールでの確認方法:
ストレージタブを開いてローカルストレージで確認可能
image.png

ユーザによるローカルストレージの削除方法:
環境設定を開く
image.png

プライバシータブのWebサイトデータを管理を押下
image.png

すべてを削除ボタンを押下
image.png

プライベートウィンドウの時
通常のモードで記録した内容は読み込めない
書き込み時にはエラーにはならない。
同じプライベートウィンドウの別タブで記述した内容は読みだせない
同じプライベートウィンドウの同じタブで記述した内容は別のページに遷移後、戻っても呼び出せる。
ブラウザを終了して再度、プライベートウィンドウを開いた場合、前回記録した内容は読み込めない。

Chrome 78 + Windows10

保存先:
C:\Users\ユーザ名\AppData\Local\Google\Chrome\User Data\Default\Local Storage\leveldb

LevelDBで記録している。

頑張れば中身見れそうだが、正直よくわからない。
https://scrapbox.io/mima3/leveldb

イベントの挙動:
・自分のWindowで行った操作のイベントは、自分のWindowでは発行されない。
・setItemで前回値と同じ場合はイベントが発行されない。
・removeItemで存在しないキーを指定した場合、イベントは発行されない。
・clear済みで、再度、クリアしてもイベントは発行されない。

保存の限界:
キー名を「k」とし、5242881文字をデータとして保存した場合にQuotaExceededError例外が発生する。

開発者ツールでの確認方法:
ApplicationタブにてStorage>Local Storageを選択する。
image.png

ユーザによるローカルストレージの削除方法:
設定⇒詳細設定の表示⇒履歴データの削除より、「Cookie と他のサイトやプラグインのデータ」を削除する。

シークレットモード時
通常モードで記録した内容を読みだせない
書き込み時にはエラーにはならない。
同じシークレットモードの別タブでは書き込んだ内容を読みだせる。
ブラウザを終了して再度、シークレットモードで同じ開いた場合、前回記録した内容は読み込めない。

Internet Explore 11

保存先:
C:\Users\ユーザ名\AppData\LocalLow\Microsoft\Internet Explorer\DOMStore\0FOJP942
XML形式

イベントの挙動:
・自分のWindowで行った操作のイベントでも、自分のWindowで発行される。
・setItemで前回値と同じ場合でもイベントが発行される。
・removeItemで存在しないキーを指定した場合でも、イベントは発行されない。
・clear済みで、再度、クリアした時に、イベントは発行される
・XMLにデータが書き込まれた時に、操作を行ったウィンドウだけにstoragecommitイベントが発行される。このイベントの取得例は下記の通り

  window.addEventListener("storagecommit", function (event) {
    console.log("storagecommit");
    console.log(event);
  });

・大量のデータをsetItemした場合の挙動が不安定
例:
 30000文字をsetItemする。⇒storageイベントが発生する。
 もう一度、30000文字をsetItemする。⇒storageイベントが発生しない
 一旦、キーを削除する。⇒storageイベントが発生する
 30000文字をsetItemする。⇒storageイベントが発生する。

保存の限界:
キー名を「k」とし、5242881文字をデータとして保存した場合にQuotaExceededError例外が発生する。

開発者ツールでの確認方法:
なし

ユーザによるローカルストレージの削除方法:
オプション⇒「全般」タブ⇒閲覧の履歴の削除にて、「クッキーとWebサイトデータ」を選択して削除後、IEを再起動する。
http://msdn.microsoft.com/ja-jp/library/ie/bg142799%28v=vs.85%29.aspx

Microsoft Edge 44.18362.449.0 + Windows10

保存先:
C:\Users\ユーザー名\AppData\Local\Packages\Microsoft.MicrosoftEdge_8wekyb3d8bbwe\AC#!001\MicrosoftEdge\User\Default\DOMStore\E2WN2O9U
XMLファイルに格納されている。

イベントの挙動:
・自分のWindowで行った操作のイベントは、自分のWindowでは発行されない。
・setItemで前回値と同じ場合はイベントが発行されない。
・removeItemで存在しないキーを指定した場合、イベントは発行されない。
clear済みで、再度、クリアした場合、イベントが発行される。

保存の限界:
キー名を「k」とし、5242881文字をデータとして保存した場合にQuotaExceededError例外が発生する。

開発者ツールでの確認方法:
ストレージタブを選択する。
image.png

ユーザによるローカルストレージの削除方法:
履歴データの消去から削除する
image.png
※対象のページが開いている間は削除したはずのローカルストレージが使えるようなので、対象ページを閉じる。

InPrivateブラウズの場合
通常モードで記録した内容を読みだせない
書き込み時にはエラーにはならない。
同じInPrivateブラウズの別タブでは書き込んだ内容も読みだせない。
別ページに遷移すると書き込んだ内容が読めなくなる。
ブラウザを終了して再度、InPrivateブラウズで同じ開いた場合、前回記録した内容は読み込めない。

まとめ

ローカルストレージを使用することで容易にクライアントサイドに情報を保持できる。
最近の主要なブラウザでは、その挙動は近くなっている。(マイクロソフト系を除く)

store.jsを用いれば、それはさらに容易になる

大きなデータの扱うのには適さない。この理由は二つある。
1つは同期処理のため、大きなデータを扱うと、そこで処理が止まる。
もう一つはサイト毎にストレージの上限が決められており、それは精々数MBであるからだ。

ストレージの上限は「わからない」ものとして処理した方が良い。

ユーザーがブラウザーの機能で、ローカルストレージをバックアップする手段はないので、消えたら困るデータは格納すべきでない。もし、格納する場合は、エクスポートの機能は必要だろう。

IE11を使用する場合、ストレージのイベントの使用は避けた方が良い。

IE11やEdgeでsinon.jsなどでlocalStorage.setItemなどのMockやFakeを作成できない。localStorage.setItemにたいして関数の上書きができないためである。

また、以下の方法でLocalStorageを無効にされている可能性があるので、必ず使える前提は問題がある。
主要ブラウザでCookie / JavaScript / Web Storageを無効にする方法まとめ
https://qiita.com/Udomomo/items/32b1c84807e562b8ce79

以上。

ペアプログラミングに関する調査報告

はじめに

本文はペアプログラミングについての資料を調査した結果を報告するとともに、その有効性について考察するものです.多くの内容はLaurie William の執筆した「ペアプログラミング エンジニアとしての指南書」と彼女の論文が元になっています.

ペアプログラミング

概要

ペアプログラミングとはプログラミングスタイルの一つで,「二人」のプログラマが共同で実装を行うことです.一人が実際のコードを記述し、もう一人はそれをチェックしながらナビゲートをします。この役割を随時交代しながら作業を進めます.

pair.png

効果

ペアプログラミングを行うことで様々な効果が期待できます.ここではそのいくつかを紹介します.

レビュー効果

ナビゲーターが常時,実装のチェックを行うため,コードレビューを常時行っているのと同等の効果が発生します.このことにより,高品質な製品の作成を期待できます.

デバッグ時の効果

問題を他人に説明するだけで,その問題を解決したという経験を誰もがした事があると思います.ペアを組むことにより,その説明を行う機会が多くなり,早期に問題を解決できる効果があります.

ペア間の学習

ツール使用のコツ,プログラミング言語,設計方法など様々な知識,ノウハウがパートナー間で共有できます.特に文字や言葉では伝えられない技術の継承ができるベストに近い方法です.このことは新入社員の教育や新しくプロジェクトに入った人間のトレーニングとしての効果が発揮できます.

人材喪失のリスク削減

ペアプログラミングによってどのタスクも最低,二人以上は経験者が存在することになります.
これにより、バックアップとフォロー体性を整えることができ、人的資源の消耗を抑えることができます. 
また,最悪の場合でも,トラックナンバーを増やすことができます.トラックナンバーとは「何人のプログラマがトラックに轢かれたら(=退職)プロジェクトが立ち行かなくなるか」という数値です.もちろん最悪は1人です.ペアプログラミングを行うことにより,(最悪でも)トラックナンバーが2以上になります.

ペアプレッシャー

ペアプログラマは互いに建設的なプレッシャーを掛け合います.プログラマは熱心かつ懸命にプログラムに取り組むようになります.それはパートナーを失望させたくないからです.

ペアプログラミングの歴史と事例

ペアプログラミングの概念は決して新しいものではありません.ここでは過去に報告された事例について紹介します

Fred Brookの報告

「人月の神話」の著者Fred Brooksはペアプログラミングの研究を行っているLaurie Williams に次のようなメールを送っています.
「同級生のBill Wright と私は大学院の時(1953-1956)に初めてペアプログラミングをためしました.私たちは1500行の欠陥なしのコードを作成しました.それは一回で正常に動作しました」

Dick Gabrielの事例

Common Lisp (CL) の考案者,Dick Gabrielは1972-1973までMIT人工知能研究所にいました.そのころ,ペアプログラミングが一般的に実践されていたと述べています.その後,彼がイリノイ大学とスタンフォード大学に在籍中にJonl WhiteとペアプログラミングをおこないMacLisp の移植作業をおこなっています.

Larry Constantineの報告

「ソフトウェア開発のカオス」の著者Larry Constantine はWhitesmith社での「ダイナミックデュオ」と呼ばれる開発方法を観察して,より早く,欠陥なしでコードが生産されたことを報告しました.
彼は二人のプログラマは余剰ではなく,効率と品質を増すための近道になると,1995年に結論付けています.

Jim Coplien(James O Coplien) の報告

「C++プログラミングの筋と定石」の著者Jim Coplienは“Developing in Pairs”というパターンを1995年に発表しました.「ペア開発者は個々の開発者より盲点が少ない傾向があり,より効率的なプロセスになる」と述べています.

ヒル空軍基地の事例

 二人一組になる手法をおこなった,ヒル空軍基地でのプロジェクトは次のような結果を残しています.

このプロジェクトでの総生産量は人月あたりの生産行数(llpm)は175lppmでした.一方,一般的な生産量は77llpmに過ぎませんでした.またシステム統合時のエラー率は組織基準よりも3桁低い数字でした.

C3の事例

 Chrysler Comprehensive Compensation project 略称 C3,クライスラーで実施された給与計算プロジェクトです.COBOLベースの既存給与計算システムをリプレスする作業でしたが、安定した状態を得る事ができませんでした.
 その後,1996年にKent Beckがプロジェクトを立て直すため投入されました.そのとき実践した項目のなかにペアプログラミングが含まれています.Kent Beckは次のように述べています.
「ここ半年の間で発生した唯一の問題は,ペアを組まずに書いたコードが引き起こしたものだけです」

ペアプログラミングの実験

ペアプログラミングとソロでのプログラミングにどのような差があるか実験した結果を紹介します.
この実験は1999年にユタ大学で行われたものです.この実験ではソロプログラムとペアプログラムが直接対決しています.

実験の概要

上級ソフトウェアエンジニア課程に在籍する学生41人を対象に実験を行いました.学生たちは全員同じクラスに出席し,同じ指導を受けて,ペアプログラミングについて賛否両論の講義理論に参加しました.
その後,学生を2グループに分けました.グループは成績が優,良,可の学生が同じ割合になるように構成しています.
13人の学生には学生全員が同じ課題に対して個々に作業を行う対照群を形成しました.
28人の学生は全員で2人チームを組み,作業を行う実験群を形成しました.実験群と対照群は同じ課題を仕上げます.(共同作業を行うペアには2グループ間で同じ作業負荷になるように追加の課題を割り当てました.)
そして,品質,生産性,士気について情報を収集しました.

実験の結果

品質

各課題を自動化された開発後テストを実施した結果以下のようになりました.
pair.png

ペアで作成した場合,15%少ない欠陥率になりました.また,この欠陥率の結果はペアの場合,安定していましたが,個人の場合,平均値から変動が大きくなっていました.さらに,個人は断続的にプログラムを提出しなかったり,遅れたりすることがありましたが,ペアは予定通り提出しました.これはペア間のプレッシャーによるものだと考えられます.
また,ペアが作成したプログラムは常に個人より20%少ない行数で同じ機能を作成できました。

pair.png

生産性

各作業にどれだけの時間がかかったか記録をしました.結果,最初ペアは一人の場合より,60%多い人時をついやしましたが,平均的には15%の多くの時間しか使っていませんでした.また,これには統計的な有意性はありませんでした.それは13組中2組で平均値が吊り上げられていたためです.一人および,ペアの時間コストの中央値は本質的に一致していました.

pair.png

士気

80%以上の学生が,作業を楽しみ,かつ自分のおこなった作業に自信が持てたとアンケートに答えました.
pair.png

pair.png

実験のまとめ

Williamsはこの実験結果を以下のようにまとめています.
・ペアは高品質のコードを作成します.
・ペアでおこなった場合,平均的には15%の余計な時間しか使用しません.
・ペアは自分の仕事に楽しみと自信を与えます.

コードの開発時間が15%増加しますが,品質が高くなるので,将来的なリソースが減少します.そのため,投資する価値は十分にあると,Williamsは述べています.

実験方法の批判的見解

上記のユタ大学の実験は,ペアプログラミングを導入する際の強い資料になり得ると思いました.
しかし,この実験方法には問題がありました.この実験に批判的な見解を紹介します.

実験群の選出方法の問題

実験群の28人はランダムで選ばれたわけではありません.共同作業を行いたい志願者35人の中から28人を選択したことになります.つまり,6人は志望とことなる対照群に入ることになります.これらのことは人員の構成に偏りを生じることになります.

・実験群の全員は自分が望むグループに所属しました.対照群は半数が望まないグループに所属しています.これはその後の彼らの性能に影響を与える可能性はあります.
・実験群の全員は熱意あるボランティアであり,より大きな能力で作業を進めるでしょう.この特性は対照群の7人が持っていないものです.
・実験群に与えられた追加課題は彼らの能力や士気にいい影響を与えた可能性があります.
・14組中13人は自分でパートナーを選択しました.したがって実験は一般的なペアプログラミングではなく,関係者が自分でパートナーを選べる特別な実験になっています

そして,この人員の偏りはアンケートに対して肯定的な答えを返すことになります.なぜなら彼らは良い条件でパートナーを選択しています.また,自分で志願して,ペアプログラミングを行った人間ばかりなので肯定的な答えが返るようになります.

ホーソン効果

ホーソン効果とは自分が観察の対象になっているという意識が生産性を向上させる効果のことです.
この実験に参加した人間はWilliamsが共同ソフト開発の権威であることを知っています.また,Williamsは彼らにとってはボスです.
彼らは意識的かそうでないかはともかく,Williamsにとって好ましい結果を作成するように振う動機付けがされます.

プラシーボ効果

Williamsの実験の重大の弱点は独立して行わなかった事です.この実験の前にペアプログラミングについての指導を行っています.そのため,被験者が効果があると思い込んでしまう可能性は高いです

実験結果ついて

実はこの研究では4回実験を行っています.4回の実験のうち,最後の実験は無効とされており,最初の実験は変則的であるとして無視されており,所要時間の情報は統計的に重要でないとされています.
これはWilliamsにとって必要な結果に偏っているといえるでしょう.

批判的な見解のまとめ

これらのことをまとめると,この実験の信頼性は低く,この実験をペアプログラミングの擁護の手段に引き出すのは問題があります.

考察

 ペアプログラミングについての個人的な考察を本項では述べます.

 残念ながら,常時,ペアプログラムを採用することのは費用対効果を考えた場合,現状では慎重にならざるを得ません.少なくともコストと品質の計算ができるデータが揃うまでは実践することは難しいでしょう.

 経験と直感的にペアプログラミングを考えた場合,品質は確実に上がると思います.生産量も一人でやるよりもあがる可能性はある場合が多いと考えられます.ただ,人的資源を二倍使用するコストに対する効果を得たと証明するのは難しいでしょう.また,品質を追求するなら,テスト駆動型の開発,あるいは継続的なレビューなどで代替可能です.開発者からも導入の抵抗が強いペアプログラミングをあえて採用するにはメリットが少ないと思われます.

その上で現状,ペアプログラミングという手段を取るとしたら以下の状況が考えられます.

(1) 教育/訓練
 新入社員の教育や,新しくプロジェクトに投入された人材の訓練を行う際に利用します.ペア間でのノウハウの共有が通常より速く進むことを期待します.

(2) プロジェクトの立ち上げ時
 ペア間でノウハウの共有を進める効果と共にチームワークを育む効果を期待します.

(3) ボトルネックの作業
 並列で作業を行えない状況の時に,ボトルネックをペアプログラミングで可能な限り早く,正確に完了させます.

 最後に,ペアプログラミングは大きな可能性と魅力を感じる開発手法です.少なくとも,言葉や文章で伝えられない暗黙知の共有を行うには最善の手段の一つだと考えています.これは長期的なスパンで考えた場合,ペアプログラミングの最大の効果だと考えています. 常時行うのは資源や環境的に厳しいにせよ,状況によっては採用を検討するに値する手法だと思います.

参考文献

[1]Laurie Williams, Robert Kessler 「ペアプログラミング エンジニアとしての指南書」,テクノロジックアート,2003
[2]ペアプログラミングのチュートリアル
[3]Radium Software Development
[4]Experimenting with Industry’s “Pair-Programming” Model in the Computer Science Classroom
[5]ペアプロにメタファ適用
[6]Joel on Software射撃しつつ前進
[7]山崎敏 「火事場プロジェクトの法則」,技術評論社,2006

ソフトウェア技術者を襲うバーンアウトとボーアウト

バーンアウト(burnout,燃え尽き症候群)とは、今まで普通に仕事をしていた人が、急に、あたかも「燃え尽きたように」意欲を失い、働かなくケースのことです。
barn.png

以下で自分にバーンアウトの兆候があるか、どうか診断できます。
http://needtec.sakura.ne.jp/mentaltest/burnout.html

逆に、ボーアウト(Boreout)とは仕事に退屈しきっている状態を指します。能力以下の仕事しか与えられず、仕事に関心を失い、職に就いていながら、いわばニートのような状況にいることであり「社内ニート症候群」ともいえます。

boar.png

以下で自分にボーアウトの兆候があるか、どうか診断できます。
http://needtec.sakura.ne.jp/mentaltest/boreout.html

バーンアウトとボーアウトは対極の関係にあります。
boar.png
仕事量が一定の範囲を超えた場合、バーンアウトの発生リスクが生じ、仕事量が一定の範囲を下回った場合にボーアウトの発生リスクが生じると考えられます。

バーンアウトとボーアウトは対局の関係でいながら密接な関係にあるともいえます。
どんな職場でも、みなで力を合わせてある決まった量の仕事を仕上げなければならないです。しかし、全員が同じように働くわけではないのです。
熱心さ、優秀さ、上昇志向の強さなどの原因は色々であるにせよ、一部の人間が多く働く傾向にあり、その結果、彼らは同僚の仕事を奪うことになります。彼らはしだいにバーンアウトに苦しみ始め、残りのメンバーは仕事が少なくなる問題を抱えます。
能力以下の仕事しか与えられない人達は退屈し始め、仕事に関心を失います。その結果できた自由時間は、他の事に使われます。ネットサーフィンやゲーム、読書などです。
やがて、裏ワザを使って忙しそうに見せかけるようになり、あまり仕事がまわってこないようにする。すると、働きすぎの同僚が彼らがやらない分の仕事を片付け、その結果として、両者ともストレスにさらされる。こうして悪循環が発生して、まわり続けます。

この結果は、バーンアウトにせよ、ボーアウトにせよ離職、転職等の人的リソースの損失になります。

バーンアウト

この項ではバーンアウトについて掘り下げてみましょう。
バーンアウトという概念を, 初めて学術論文でとりあげたのはフロイデンバーガー(Freudenberger,1974)です。
彼は 保健施設に勤務していた間,数多くの同僚が精神的, 身体的異常を訴えるのを目撃しました。同僚たちは一年余りの間に徐々にあたかもエネルギーが枯渇していくように、仕事に対する意欲や関心を失ってくのです。彼は、同僚が陥った状態を表現するのに「ドラッグ常用者の状態」を意味するスラングであったバーンアウトを用いました。

彼はバーンアウトを次のように定義しています。
「辞書的な意味で言えば、バーンアウトという言葉は、エネルギー、力、あるいは資源を使い果たした結果、衰え、疲れ果て消耗してしまったことを意味する。(中略)実際のところ、バーンアウトは、人によりその症状も程度も異なる」

その後、数多くの研究者の関心をひき、バーンアウトの概念は急速に広がっていきました。
そのなかで、バーンアウトという状態をいかに測定するかという取組が始まります。
バーンアウトという状態を測定するための物差しをつくる、いわゆる尺度化にとりくんだのが、マスラックを中心としたグループで彼らのマスラック・バーンアウト・インベントリー(MBI:Maslach Burnout Inventory)でした。 MBI は, バーンアウトを以下の 3つの症状から定義しています。

バーンアウトの3つの症状

情緒的消耗感

「仕事を通じて情緒的に力を出し尽くして消耗してしまった状態」と定義します.
この情緒的消耗感がバーンアウトの主症状で、残りの2つはこの副産物です。

サービスのやり取りをする関係のなかで、相手の気持ちを思いやり、勝手放題にふるまう相手をうけいれ、問題を解決していくことを求められることが少なくありません。つまり情緒的なエネルギーを必要とされており、それがバーンアウトへのリスクを高めています。 自らの役割に誠実な人ほど, 日々接するクライアントと, このような感情のやりとりを繰り返していく中で, 疲弊し,消耗していきます。 これが, バーンアウトと呼ばれている過程です。

脱人格化

「サービスの受け手に対する無情で非人間的な対応」と定義します。

バーンアウトとは情緒的資源を使い尽くしてしまった状態です。そのような状態に陥ってしまった人が、さらなる消耗を防ぐために情緒的資源の節約を行います。クライアントや同僚との間に距離を置き、かれらとの関係を仕事上の関係として割り切り、サービスのやり取りを客観視することで自らを守ろうとします。

個人的達成感

「ヒューマン・サービスの職務にかかわる有能感、達成感」と定義されています
バーンアウトにいたる人は、それ以前までに高いレベルのサービスを提供し続けた人だけに、前後の落差はおおきく、とりわけ本人にとって質の低下は明白です。成果の急激な落ち込みと、自己評価の低下は個人的達成感の低下と名付けられています。

バーンアウトの発生の要因

バーンアウトの発生の要因となるものはなんでしょうか?
個人要因と環境要因と分けて考えることができます。

どのようなタイプの人間がバーンアウトしやすいのでしょうか?
プライスとマーフィーによればバーンアウトは「理想に燃え使命感にあふれれた人を襲う病」と述べています。
ひたむきで自我関与が高く、そのような人は自らの性格ゆえにバーンアウトしやすいと述べています。ひたむきに働く人は、あまりに多くの仕事をなしとげようとし、できない場合に、できないことを深く悩みがちです。
際限なく繰り返される相手からの要求と慢性的な人材不足、そのような環境のなかで、ひたむきで自我関与の高い人間が極度の消耗を経て、バーンアウトに陥ることは想像に難くありません。

環境的要因にはどのようなものがあるでしょうか?
次のようなものが考えられます。
・過重負担
・自律性
・人間関係

過重負担を考える場合、一般的に長時間勤務や肉体にたいする重い負担の作業を思い浮かびます。もちろんこれらが、過重な労働負担であることは間違いありません。ただ、勤務時間、作業量といった量的な意味でなく質的な意味での負担も考えなければいけません。多くの人間と接する必要がある場合や、作業的に神経を使うものは質的な意味で負担が大きいものと考えられます。

自律性とは自らの意思で仕事のスケジュールや方法を決定できる程度を示します。一般に医師や研究職では自律性が高く、事務職などの定型的な業務をこなす職では自律性が低いものとされます。
バーンアウトが頻発している職種は社会的な位置づけが不明瞭で、他の職種からの干渉も多く、専門職としての認知度が希薄である職種に多いことが知られています。
看護師にはバーンアウトが多く、専門職として自立性が高い医師ではバーンアウトが問題にされないのは自律性がバーンアウトと密接にかかわっていると考えられるからです。
ソフトウェア技術者は一見、自律性が高いように見えますが、その作業において多くの制約が課せられます。たとえば、派遣のプログラマなどには自律性があるとはいえないでしょう。

人間関係もまたバーンアウトに関係する要因です。上司や同僚との人間関係が良好なほど個人的達成感を得やすく、脱人格化を起こしにくくなります。逆に、関係がうまくいかないほど、情緒的消耗を経験しやすいことになります。

これらの要因をみていると、バーンアウトの予防についていくつか考えることができます。
これについては後で述べます。

ボーアウト

この項ではボーアウトについて掘り下げてみましょう。
ボーアウトは次の3つから成り立っています。

・能力以下の仕事しかさせてもらえない―「自分はもっと有能だ」という不満
・無関心―仕事との一体感の欠如
・退屈―やる気のなさと無力感

これらは互いに関連していて、相互作用があります。絶えず能力以下の仕事しかさせられてなければ、当然関心を失います。

ボーアウトになると働いているふりをします。
書類でカムフラージュしたり、本来より多くの作業時間を申告して、自由時間を謳歌したりします。

ボーアウトには、なりにくい職業があります。たとえば、農業や工場ではボーアウトは生じにくいです。
気が乗らないという理由で稲を植えなかったり、畑を耕さないということは聞いたことがありません。もちろん、怠け者はいます。しかし、、仕事を済ませたフリはできません。
工場でも同じです。機械が常にうごいて、車が組み立てられていく。仕事をしているふりなんてできやしません。つまり、目にみえる、測量できる具体的な結果が必要なのです。働いているふりなんてしたらすぐにばれて首になります。
そういう意味ではソフトウェア技術者は、働いているフリはしやすいといえるでしょう。仕様書を読んでいるのと、Yahooニュースを読んでいる違いに気づける管理職はいません。

さて、ボーアウトの歴史的なりたちを考えてみましょう。
かってマルクスは「疎外された労働」という表現をしました。マルクスによると工場労働者は製品から疎外されているといいます。彼らは製造過程の全体を見渡すことも、その製品に係ることもできないからです。
ボーアウトを観察すると、疎外は重要な意味を持ちます。
分業化、細分化がすすんでくると、自分の仕事と最終的にできあがったものとの間に、関連性を見出すことができなくなります。このことにより自身の仕事に対する関心が薄れても已む得ないといえます。

バーンアウト・ボーアウトを予防するためのチーム運営

バーンアウト、ボーアウトを予防するためのチーム運営について考えてみましょう。
バーンアウト、ボーアウトは仕事の量が適切に割り当てられていない場合に、発生すると考えられます。

作業量の調整をどのように行うべきでしょうか?
「朝会」は有効な手段の一つだと思います。
各人が毎朝、「昨日行ったこと、今日行うこと、問題点」を報告するのです。予定外に増えた作業や、負荷はここで調整することが可能です。逆に、予定外に早く終わった場合も同じことがいえるでしょう。

この朝会は、「達成感」を満たす場にも使用できます。
たとえば、作業者が仕事を終えたと報告した場合、賞賛し、感謝の意をつたえればいいのです。自己評価が低くなるのを防ぐことが可能であると思います。
強制的に毎朝コミュニケーションをさせることになるので、燃え尽き症候群の症状の「脱人格化」による同僚に対する関心の薄れを予防できます。

疎外された労働を防ぐにはどうしたらいいでしょうか?
これは、チームリーダが語るしかありません。
わが社の経営理念はXXXで、そのために製品Xを作成し、今回のリリースではどういう機能が必要で、そのために貴方には~をしてもらいたいと筋道をつけて語るのです。
多くのチームリーダーが行ってしまう、「言われたことだけやればいい」的な言動は、作業者と作業に距離を作るものです。
あとあまりおおっぴらには言えませんが、作業をやらせる場合に、この技術を覚えておくと転職に有利だという言説も作業に対する関心を持たせる上で有効です。
なんにせよ、自身の行う作業が自分自身に強く関連づくように仕向ける必要があります。

自律性というのは難しい問題です。
自身の判断がチーム運営に反映されるチャンスがなければいけません。
定期的に作業者にヒアリングをしたり、「ふりかえり」とよばれるチーム運営に関する会議を定期的に開くといいでしょう。
「ふりかえり」とはチーム運営において、続けておいた方が良い活動、問題点、挑戦したい事を話し合いで決め実施していくことです。
以下にその方法が詳しく記述してあります。
http://agile.esm.co.jp/scrum/retrospective.html

これにより、自身の判断が全体に反映されるチャンスが出てきて、自律性が高まることが期待できます。

チームを運営するときは、実際のプロダクトに関係する「必須」な作業以外にも、「できたら行う」という作業を用意しておくといいでしょう。
たとえば、それは、画面のテストの自動化だったり、プロジェクトで使用するツールだったりします。
これらの作業をRedmineのチケットなり、Tracのチケットとして記録しておき、余力が出た人間が行うようにしておくのです。
自分が選択して作業を行うなら、自律性もありますし、調整作業としても使用できます。

このように、バーンアウトとボーアウトを防ぐチーム運営方法を考えてみました。
ただし、重要なのは、ここであげた個別具体例ではありません。
これは、実際の自分の組織の中で、どのように行うか考えるヒントにすぎないのです。

まとめ

ソフトウェア技術者が陥りやすい症状としての、バーンアウトとボーアウトについて説明をしました。
バーンアウトとボーアウトは逆の位置にいながら、たがいに強い関連があります。

バーンアウト、ボーアウトは最終的に人的リソースを失う厄介な問題ではあります。
しかし、その性質を把握することにより、完全に防ぐのは無理でも抑制することは可能です。

ここではそのきっかけを紹介しました。

参考

バーンアウトの心理学
http://www.amazon.co.jp/dp/4781910696

ボーアウト 社内ニート症候群
http://www.amazon.co.jp/dp/4062149222

僕にそのコピペを直せというのか

ソースコードをコピーアンドペーストで作成したコードクローンの方が信頼性がいいという言説もないことはないですが、多くの場合、コードクローンの存在は我々を土壇場で苦しめることが多いです。

担当者が逃げた、ソースコードを、夜遅くまで修正してリリースしたあとに、実は「コピペで作っているから別の所も全部なおしてテストしてね」とか言われて、作業をやり直すのは、とてもとても悲しいものです。

殆ど同じソースコードを間違い探しのように微妙に変更して、修正していくのは屈辱の極みです。

土壇場において、このような罰ゲームを避けるために、コピペの頻度というものは常時監視しておき、異常な頻度の場合はただちに是正すべきです。

ここでは、コピペの検出を行うツールについて説明します。

PMD-CPD

PMDはJavaで実装されたJavaのソースコードの潜在的問題を検出するツールです。
http://pmd.sourceforge.net/snapshot/

この機能の一部に重複したコードを検出するCPDコマンドが存在しており、次のプログラミング言語の重複を検出可能です。
・Java
・JSP
・C++
・Ruby
・Fortran
・PHP
・C#
・PLSQL
・Ecmascript

インストール方法

下記のいずれかのファイルをダウンロード、解凍して任意のフォルダに展開してください。
http://sourceforge.net/projects/pmd/files/pmd/

実行例

Windowsでの実行例

cpd --minimum-tokens 50 --language ecmascript --format text --encoding utf8 --files C:\tool\clonedigger\test\ > result.txt

Linuxでの実行例

bin/run.sh cpd --minimum-tokens 35 --format xml --language ruby --files /var/lib/redmine/app/ > result.xml

パラメータ

パラメータはwindowsとlinuxで共有です。

パラメータ 説明
--minimum-tokens 重複を検知するトークン数を指定します。
--format text,xml,csvが選択できます。xmlで出力した場合はjenkinsで使用できます。
--language プログラミング言語の種類を指定します.
--files 検査対象のソースコードのディレクトリを指定します。これは再帰的に検出します。
--encoding 検査対象のソースコードのエンコードを指定します

GUI

bin/cpdgui.batを実行することでGUIで動作することもできます。
clone.png

Clonedigger

ClonediggerはPythonとJavaで実装されたコピペ検出ツールです。
http://clonedigger.sourceforge.net/

検出可能なプログラミング言語は次の通りです。

・python
・java
・lua
・javascript

python以外のプログラミング言語の検出には、後述するコツがあります。

インストール方法

easy_install clonedigger

実行例

Pythonの検出例

ファイルを指定した場合

clonedigger -l python -o ./test.html C:\tool\clonedigger\test\test_utf8.py

フォルダを指定した場合

clonedigger -l python -o ./test.html C:\tool\clonedigger\test\test_utf8.py

フォルダを指定した場合は、サブフォルダも検出します。
-oで指定したファイルに以下のようなHTMLを作成します。

clone.png

もし、次のように--cpd-outputオプションを使用するとXML形式で出力されます。この出力形式はPMD/CPDと同じものになります。

clonedigger -l python --cpd-output -o test.xml C:\tool\clonedigger\test\python\

Javaの検出例

Python以外のソースコードを検出する場合、カレントディレクトリにjava_antlrが存在しないと動作しません。
Windows、Python2.7の場合は次のような操作が必要になります。

cd C:\Python27\Lib\site-packages\clonedigger-1.1.0-py2.7.egg\clonedigger
clonedigger -l java --cpd-output -o test.xml C:\tool\clonedigger\test\test.java

JavaScriptの検出例

JavaScriptを検出するときには、カレントディレクトリにjs_antlrが存在しないと動作しません。
Windows、Python2.7の場合は次のような操作が必要になります。

cd C:\Python27\Lib\site-packages\clonedigger-1.1.0-py2.7.egg\clonedigger
clonedigger -l js --cpd-output -o test.xml C:\tool\clonedigger\test\test.js

ブラウザで動作させるJavaScriptは次のようないい加減な実装でも動作します。

  // 最後のコンマが余分
  var questions = [
    {message: "ああああ", category: Category.emotionalExhaustion} ,
  ];

しかし、clonediggerではこのような不正な記述があると、解析が中断されエラーとなります。この時次のようなエラーメッセージが表示されるので、修正のヒントになるでしょう

line 14:2 rule arrayItem failed predicate: { input.LA(1) == COMMA }?

AIST CCFinderX

コメントで教えてもらった、AIST CCFinderXは下記のページからダウンロードできます。
http://www.ccfinder.net/ccfinderxos-j.html

動作させるに32ビットのJavaとPython2.6(上でも下でもダメ)が必要になります。
Windowsのバイナリをダウンロードした場合、32bitで動作させないと動かないので、gemx.batは以下のように修正する必要があります。

set PATH=C:\Windows\SysWOW64;C:\TracLight\python;C:\TracLight\python\python\Scripts\;%~dp0\scripts
set CCFINDERX_PYTHON_INTERPRETER_PATH=C:\TracLight\python\python.exe

以下の画像はgemx.batで検知した画面の例になります。
無題.png

その他、散布図なども表示できます。これらのGUIは魅力的といえるでしょう。

コマンドラインから実行する場合は次のようになります。

ccfx p java -d c:\dev\java\

この時出力される、a.ccfxdはバイナリファイルでGemXで開くことが可能です。以下のコマンドでテキスト形式に変換できます。

>ccfx p a.ccfxd > test.txt

VisualBasicの対応

この手のツールとしてはめずらしくVisualBasicを対応しています。
VB6やVBAで作ったコードも解析しているようです。しかし、clsファイルは認識しないのでccfx_prep_scripts.iniを以下のように修正してください。

visualbasic=.vb;.bas;.frm;.cls

その他

・マルチバイトを扱っているソースに対する処理が失敗していることがあります。
・Jenkinsとの連携は調べた限り、簡単にはできそうもないです。
・更新履歴のページがデッドリンクになっていることから、現在はもうメンテナンスされていないようです。
 ソースコードがあるので、必要に応じて自分で治す必要があるでしょう。

Jenkinsによる監視

JenkinsのViolationsプラグインを用いることでコードクローンの遷移を監視できます。
https://wiki.jenkins-ci.org/display/JENKINS/Violations

  1. Jenkinsのスクリプト上にてワークスペース上にXMLを生成します。

  2. プロジェクトの設定のビルド後の処理でReportViolationsのcpdに1.のXMLを指定します。
    clone.png

  3. 各ビルドを行うことで以下のようなレポートが作成されます。
    clone.png

VisualBasic6.0のシステムのリスクと.NET への移行方法についての考察

ここでは現在も稼働しているVisualBasic6.0のシステムのリスクと、どのように.NETへ移行したらよいかを考察します。

このドキュメントをPowerPointで表現したものは下記にあります。
http://www.nicovideo.jp/watch/sm22232272
http://needtec.sakura.ne.jp/doc/VisualBasic6system.pdf

VisualBasic6.0を使用し続ける場合のリスク

VisualBasic6.0を使用したリスクには大きくわけて以下のリスクが存在します。

・サポートに関するリスク
・開発要員に関するリスク

サポートに関するリスク

VB6ランタイムはWindows8まで保障されていますが、Grid32.dll,graph32.ocxなどの一部のランタイムは非サポートになりました。

IDE(統合開発環境)はWindows7以降で非サポートです。実験結果として動作するという報告はありますが、あくまで非サポートです。

WindowsVista,WindowsServer2008,Windows7,Windows8に関するVisualBasicのサポート状況は下記を参考にしてください。
http://msdn.microsoft.com/ja-jp/vstudio/ms788708.aspx

開発環境が非サポートになったということは、サードパーティが提供するOCX,DLLのサポート状況が不透明になります。
マイクロソフトがVB6の開発環境を非サポートにしている状況で、サードパーティが、OCXやDLLのサポートを続けるとは考えづらいです。

また、64ビットプロセスのサポートも行いません。
64ビットマシンであっても32ビットプロセスとして動作することを義務付けられます。このことはプロセスのメモリの上限が最大4Gになることを意味しており、どんなにメモリを積んだとしてもそれ以上のメモリを1プロセスで使用することができなくなります。

開発要員のリスク

今後はVisualBasic6.0の開発要員を確保しづらくなることが予想されます。
以下の図は2012/4/27にIPA独立法人情報処理推進機構が発表した、ソフトウェア産業の実態把握に関する調査の結果です。

vb6.png

これは新規で人手で作成したプログラミング言語の比率をあらわします。
VB6はCOBOLの0.4%以下のその他に分類されています。

このことより、新規案件でVisualBasic6.0が採用されていないことを表します。
このことにより、新規に業界に入った人間は、VisualBasic6.0を扱うことが少なくなることになります。つまり、VisualBasic6.0を扱える人材の年齢が上がっていくことになり、これは単価の上昇を意味します。
このことより、将来、安価な労働力の確保ができなくなることが想像できます。

対応策

VisualBasic6.0でシステムを開発し続けることにはいくつかのリスクがあることを説明しました。
ここでは、今後どのようにすべきかを考察します。

考えるられる方法として3つの方法が存在します。

・リスクを把握した上で使い続ける
・.NETに移行する
・新規機能のみ.NETに置き換える

リスクを把握した上で使い続ける

OSのバージョンアップやWindows Updateを行わない前提であれば可能な選択肢です。

たとえば、工場の機械を制御するシステムのように、インターネットにつながず、他のOfficeなどのアプリケーションを使用しない閉じた環境であれば、OSのバージョンアップやWindowsUpdateなどのセキュリティを考慮しないですむので、VB6を使い続けることができるかもしれません。

ただし、更新しないつもりでも、マシンが壊れて新しいマシンを入れる場合に古いOSを入手できないというリスクや、和暦をつかっている場合は元号が変わった場合にOSに対してなんらかのパッチを当てなければならないリスクが存在します。

.NETに移行する

これはMicrosoftが推奨している方法で技術的には正しい選択肢でしょう。
しかし、コストの問題が発生します。
VB6のソースコードから.NETのソースコードに変換する、コンバートツールは存在しますが、その精度は低く、最終的には人間が確認する必要があります。
仮にうまくいっても全テストを実施しなければなりません。

潤沢な予算がなければ選択するのは難しいでしょう。

新規機能のみ.NETにおきかえる

上記2案の折衷案です。
現状のコードをなるべくいじらずにいかしつつ、問題が発生した箇所や、新規機能を.NETに置き換えていきます。
これにより、段階的な.NETへの移行作業を行えます。

基本的にはCOMで.NETの機能をラップしてVBから使用します。
.NETでCOMを作成する方法については以下と同様の手順で作成することができます。

VBAまたはVBSからCOM経由で使用できる.NETのライブラリの作成方法
http://qiita.com/mima_ita/items/efcd1a6ea86f09047984

もし、フォームやコントロールを作成する必要がある場合、Microsoftより提供されているInterop Forms Toolkit を利用すれば、.NETで作成したコントロールやフォームをCOM経由でVBから使用することが簡単になるでしょう。

vb6.png

Interop Forms Toolkit 2.0 Tutorial
http://www.codeproject.com/Articles/18954/Interop-Forms-Toolkit-2-0-Tutorial

http://www.microsoft.com/en-us/download/details.aspx?id=3264

Windows7 + VB2008.NETでのInterop Forms Toolkitの使用方法

COMの作成方法

以下でInterop Forms Toolkitを使用してVB2008.NETのCOMを作成してみます。

1.Interop Forms Toolkitをインストールして、管理者権限としてVisualStudioを起動します。これはCOMの登録に管理者権限が必要だからです。

2.新規プロジェクトで[VB6 Interop UserControl]のテンプレートを選択します。
vb6.png

3.テンプレートを選択してプロジェクトを作成すると、VBB.NETでCOMのコントロールやインターフェイスを実装できるようになります。

vb6.png

4.VB6から使用したい機能をCOM参照可能なクラスのCOM参照可能なパブリックプロシージャとして実装してください。
vb6.png

vb6.png

5.実装が終わり、ビルドを行う時に、COMとして登録されるように、プロジェクトの設定で「COM相互運用機能の登録」を選択してください。
vb6.png

もし、VisualStudioのない環境でCOMを登録するにはRegasmコマンドを使用します。

Regasm AssemblyName.dll /tlb: FileName.tlb /codebase

http://support.microsoft.com/kb/817248/ja

COMの使用方法

1.VB6やVBAでCOMを使用するには「参照設定」で作成したDLLを選択します。
コントロールとして使いたい場合は、ツールボックスから、その他のコントロールで追加できます。

vb6.png

2.VB6やVBA側でオブジェクトを作成して使用することが可能になります。

vb6.png

デバッグの方法

デバッグの方法としてはCOMを実装したソリューションにテストようのプロジェクトを追加してそこで単体テストをすると楽です。
同じソリューションであれば、ブレイクやウォッチが使用できてデバッグが楽です。

vb6.png

あるいは、VisualStudioでVB6.0で作成したプロセスにアタッチすることで、.NETで作成したCOMのデバッグが行えます。
VB6.0側と.NET側のどちらにバグの原因があるかわからない場合はこの方法がよいでしょう。

vb6.png

注意点

Interop Forms Toolkit2.1を動かした際に気になった点を以下に示します。

・Interop Forms Toolkit 2.1インストール時にHelp Fileのショートカットがプログラムファイルに作成できない場合がある。(インストールは正常にできる)

VB6で32bitを超えるメモリ空間にアクセスしたい場合

1プロセスではむりです。
.NETで64ビットのアウトプロセスのCOMサーバを作成して、別プロセスとして独立させ、それをVB6から操作してください。
.NETにおける64ビットプロセスと32ビットプロセスについて
http://qiita.com/mima_ita/items/57d7c1101543e214b1d6

まとめ

このようにCOMを利用することで一部の機能の.NET化を行うことができるので、移行の作業量を抑えながら,段階的な.NETへの移行が可能になります。

なお、COMから.NETを使用する場合、.NET フレームワークをCOMを使用する際に読み込むのでVB6.0単体の時より起動時間が掛かる可能性があるので注意してください。

参考

Windows Vista、Windows Server 2008、Windows 7、および Windows 8 に対するVisual Basic 6.0 のサポートに関する声明
http://msdn.microsoft.com/ja-jp/vstudio/ms788708.aspx

Visual Basic 6.0 から Visual Basic .NET または Visual Basic 2005 アセンブリを呼び出す方法
http://support.microsoft.com/kb/817248/ja

COM 相互運用 (Visual Basic)
http://msdn.microsoft.com/ja-jp/library/6bw51z5z.aspx

Yuya Yamaki’s blog Microsoft InteropForms Toolkit 1.0
http://d.hatena.ne.jp/Yamaki/20061101

Interop Forms Toolkit 2.0 Tutorial
http://www.codeproject.com/Articles/18954/Interop-Forms-Toolkit-2-0-Tutorial

Erlangで実装したHTTP経由で使用できるメッセージキューのLMQ(Lightweight Message Queue)の紹介

LMQ(Lightweight Message Queue)はErlangで実装したHTTP経由で使用できるメッセージキューです。
以下のようにLMQが動作している端末に別の端末からHTTP POSTによりメッセージを追加、別の端末でHTTP-GETによりキューのデータを取得することができます。
lmq.png

また、次のように、2つ以上のLMQをしようして冗長化が可能です。
lmq.png

このLMQにより、HTTPプロトコルを用いたプロセス間通信や、端末間の通信を容易に分散化させることが可能になります。つまり、設計しだいで、スケーリングが可能なシステムを構築することが容易になると期待できます。

このソフトウェアのライセンスはThe MIT License (MIT)となっています。
詳細は下記を参照してください。
https://github.com/iij/lmq

Debianでのインストール

1.前提条件としてGitをインストールしておいてください。
これはlmqのmakeに使用します。

2. Erlangをインストールします。
 ソースコードを以下からダウンロードします。今回は17.1を利用しました。
http://www.erlang.org/download.html

3.解凍したフォルダで次の作業を行います。

./configure
make install

4.erlコマンドが使えることを確認します。

root@debian:/share/otp_src_17.1# erl
Erlang/OTP 17 [erts-6.1] [source] [64-bit] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V6.1  (abort with ^G)
1> 1+1.
2

5.LMQを取得し、makeします。

git clone https://github.com/iij/lmq.git
cd lmq
make rel

この時以下のようなエラーがでるかもしれません。

Cannot update the ref 'HEAD' というエラー

ERROR: cmd /q /c git checkout -q origin/master failed with error: 128 and output:
error: Couldn't set HEAD
fatal: Cannot update the ref 'HEAD'.

make: *** [deps] Error 1

この場合は、lmq/deps中のいづれかが、git clone後のcheckoutに失敗しています。
手動で、check outしましょう。

※再現しないので、おそらく、実験環境の問題だと思います。

ERROR: OTP release 17 does not match required regex R15|R16 というエラー
msgpack_rpc/rebar.configがR16までしか認めていないせいです。

以下のように修正してmakeしなおせばビルドができるでしょう。

 R15|R16|17

※現状では対応済みのようです。

6.LMQをmakeするとrel/lmq/bin/lmqが作成されています。

次のコマンドを実行してlmqを起動します

rel/lmq/bin/lmq start

7.メッセージキューの登録は次の通り

root@debian:~/lmq# curl -i -XPOST localhost:9980/messages/myqueue -H 'content-type: text/plain' -d 'ゆっくりしていってね'
HTTP/1.1 200 OK
connection: keep-alive
server: Cowboy
date: Sun, 03 Aug 2014 20:25:35 GMT
content-length: 14
content-type: application/json

{"accum":"no"}

8.メッセージキューの取得は次の通り

root@curl -i localhost:9980/messages/myqueue
HTTP/1.1 200 OK
connection: keep-alive
server: Cowboy
date: Sun, 03 Aug 2014 20:25:53 GMT
content-length: 30
content-type: text/plain
x-lmq-queue-name: myqueue
x-lmq-message-id: f26f95a1-7b6b-48c3-b18d-9184fb6796f2
x-lmq-message-type: normal

ゆっくりしていってね

メッセージキューを取得した場合、x-lmq-message-idが取得できます。
メッセージに応じた処理を行い、その成否を返します。

curl -i -XPOST 'localhost:9980/messages/myqueue/f26f95a1-7b6b-48c3-b18d-9184fb6796f2?reply=ack'

replyは以下のいづれかを指定してください。
ack: 処理が正常に終了 -> メッセージをキューから削除
nack: 処理が継続できなくなった -> メッセージをキューに戻す
ext: 処理に時間がかかっている -> メッセージの処理可能時間を延長

Windowsでインストール(失敗)

結論から言います。
現時点ではLMQをインストールすることは不可能そうです。
lmqが依存するjsonxのmakeに失敗しました。
このライブラリのビルドで失敗しているケースが他にもあるようなので、現状動作しないとみていいでしょう。
https://github.com/5HT/n2o/issues/13

インストールに失敗した方法

環境:
Winsows7 64bit Home Premium.

1.以下からWindowsのインストーラを取得する。
http://www.erlang.org/download.html

2.インストール後、システム環境設定で「C:\Program Files (x86)\erl6.1\bin」にパスが通るようにする。

3.gitとmakeが使えないとlmqがインストールできないので、msysGitをインストールする。
http://msysgit.github.io/

このページの下の方のDownloadを選択すること。(上の方だとGit for WindowsでGit以外のmakeなどができない)

lmq.png

もし、インストール中にmsgfmtでエラーが出たら、「LOCALE=C」を実行後、再度、「make install」を行う
https://groups.google.com/forum/#!msg/msysgit/noypkk5XzAI/ol1o2oK1wqsJ

4.あとは、debianと同様にlmqをインストールする。
ただし、現状は以下のようなエラーが発生する。

==> Entering directory `c:/Users/username/git/lmq/deps/jsonx'
==> jsonx (compile)
Compiling c_src/jsonx.c
c_src/jsonx.c:1: warning: -fPIC ignored for target (all code is position independent)
In file included from c_src/jsonx.c:4:
c_src/jsonx.h:3:21: error: erl_nif.h: No such file or directory
In file included from c_src/jsonx.c:4:
c_src/jsonx.h:10: error: expected specifier-qualifier-list before 'ERL_NIF_TERM'
c_src/jsonx.h:31: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'decode_nif'
c_src/jsonx.h:32: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'encode_nif'
In file included from c_src/jsonx.c:4:
c_src/jsonx.h:66: error: expected specifier-qualifier-list before 'ERL_NIF_TERM'
c_src/jsonx.h:72: error: expected specifier-qualifier-list before 'ErlNifBinary'
c_src/jsonx.h: In function 'enc_fields_base':
c_src/jsonx.h:86: error: 'EncEntry' has no member named 'records_cnt'
c_src/jsonx.h: At top level:
c_src/jsonx.h:138: error: expected specifier-qualifier-list before 'ERL_NIF_TERM'
c_src/jsonx.h:149: error: expected '=', ',', ';', 'asm' or '__attribute__' before '*' token
c_src/jsonx.h: In function 'ukeys_size':
c_src/jsonx.h:156: error: 'ERL_NIF_TERM' undeclared (first use in this function)
c_src/jsonx.h:156: error: (Each undeclared identifier is reported only once
c_src/jsonx.h:156: error: for each function it appears in.)
c_src/jsonx.h: In function 'keys_base':
c_src/jsonx.h:161: warning: implicit declaration of function 'ukeys_base'
c_src/jsonx.c: At top level:
c_src/jsonx.c:7: error: expected ')' before '*' token
c_src/jsonx.c:19: error: expected ')' before '*' token
c_src/jsonx.c:59: error: expected ')' before '*' token
c_src/jsonx.c:64: error: expected ')' before '*' token
c_src/jsonx.c:69: error: expected ')' before '*' token
c_src/jsonx.c:75: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'make_encoder_resource_nif'
c_src/jsonx.c:152: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'make_decoder_resource_nif'
c_src/jsonx.c:223: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'nif_funcs'
c_src/jsonx.c:232: warning: data definition has no type or storage class
c_src/jsonx.c:232: warning: type defaults to 'int' in declaration of 'ERL_NIF_INIT'
c_src/jsonx.c:232: warning: parameter names (without types) in function declaration
cc: /DWIN32: No such file or directory
cc: /D_WINDOWS: No such file or directory
cc: /D_WIN32: No such file or directory
cc: /DWINDOWS: No such file or directory
cc: Files: No such file or directory
cc: (x86)/erl6.1/lib/erl_interface-3.7.17/include: No such file or directory
cc: Files: No such file or directory
cc: (x86)/erl6.1/erts-6.1/include: No such file or directory
ERROR: cmd /q /c cc -c /Wall /DWIN32 /D_WINDOWS /D_WIN32 /DWINDOWS -g -Wall -fPIC  -Ic:/Program Files (x86)/erl6.1/lib/erl_interface-3.7.17/include -I
c_src/jsonx.c:1: warning: -fPIC ignored for target (all code is position independent)
In file included from c_src/jsonx.c:4:
c_src/jsonx.h:3:21: error: erl_nif.h: No such file or directory
In file included from c_src/jsonx.c:4:
c_src/jsonx.h:10: error: expected specifier-qualifier-list before 'ERL_NIF_TERM'
c_src/jsonx.h:31: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'decode_nif'
c_src/jsonx.h:32: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'encode_nif'
In file included from c_src/jsonx.c:4:
c_src/jsonx.h:66: error: expected specifier-qualifier-list before 'ERL_NIF_TERM'
c_src/jsonx.h:72: error: expected specifier-qualifier-list before 'ErlNifBinary'
c_src/jsonx.h: In function 'enc_fields_base':
c_src/jsonx.h:86: error: 'EncEntry' has no member named 'records_cnt'
c_src/jsonx.h: At top level:
c_src/jsonx.h:138: error: expected specifier-qualifier-list before 'ERL_NIF_TERM'
c_src/jsonx.h:149: error: expected '=', ',', ';', 'asm' or '__attribute__' before '*' token
c_src/jsonx.h: In function 'ukeys_size':
c_src/jsonx.h:156: error: 'ERL_NIF_TERM' undeclared (first use in this function)
c_src/jsonx.h:156: error: (Each undeclared identifier is reported only once
c_src/jsonx.h:156: error: for each function it appears in.)
c_src/jsonx.h: In function 'keys_base':
c_src/jsonx.h:161: warning: implicit declaration of function 'ukeys_base'
c_src/jsonx.c: At top level:
c_src/jsonx.c:7: error: expected ')' before '*' token
c_src/jsonx.c:19: error: expected ')' before '*' token
c_src/jsonx.c:59: error: expected ')' before '*' token
c_src/jsonx.c:64: error: expected ')' before '*' token
c_src/jsonx.c:69: error: expected ')' before '*' token
c_src/jsonx.c:75: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'make_encoder_resource_nif'
c_src/jsonx.c:152: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'make_decoder_resource_nif'
c_src/jsonx.c:223: error: expected '=', ',', ';', 'asm' or '__attribute__' before 'nif_funcs'
c_src/jsonx.c:232: warning: data definition has no type or storage class
c_src/jsonx.c:232: warning: type defaults to 'int' in declaration of 'ERL_NIF_INIT'
c_src/jsonx.c:232: warning: parameter names (without types) in function declaration
cc: /DWIN32: No such file or directory
cc: /D_WINDOWS: No such file or directory
cc: /D_WIN32: No such file or directory
cc: /DWINDOWS: No such file or directory
cc: Files: No such file or directory
cc: (x86)/erl6.1/lib/erl_interface-3.7.17/include: No such file or directory
cc: Files: No such file or directory
cc: (x86)/erl6.1/erts-6.1/include: No such file or directory

make: *** [compile] Error 1

まとめ

DebianやMacなどでは、LMQの導入は難しくはないでしょう。

しかし、WindowsではLMQを動作させるのは無理、もしくは、困難です。もしかしたらCygwinでは行えるかもしれませんが、仮にできてもライセンス的に配布にリスクがあるので当方では無理です。

Windows以外のサーバーでメッセージキューを配置するという前提ならば、十分活用のチャンスはあると思います。

JavaScriptのテンプレートJsRenderとJsViewsの紹介

かって、JavaScriptのテンプレートとしてjquery.tmpl.jsが存在した。
jQuery公式サイトにものっていたが、現在はメンテナンスされておらず、公式から削除された。
https://github.com/BorisMoore/jquery-tmpl

しかしながら、同じ作者のJsRender、JsViewsといわれるライブラリが後継として存在している。
このドキュメントでは JsRender, JsViewsについての解説を行う。
http://www.jsviews.com/

JsRender

JsRenderはDOMやjQueryの依存関係なしで、軽量でありながら強力で拡張可能なテンプレートエンジンである。

JsRenderは以下からダウンロードすることが可能だ。
http://www.jsviews.com/#download

実装のサンプルとJsRenderの機能

JsRenderを利用した実装のサンプルを用いてJsRenderの機能を紹介する。

単純なオブジェクトの出力例

ここでは単純なオブジェクトをテンプレートを使用して出力するサンプルをしめす。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"  type="text/javascript"></script>
<script src="./jsrender.min.js" type="text/javascript"></script>

<script id="theTmpl" type="text/x-jsrender">
  <p> Name: {{:name}} </p>
</script>

<script type="text/javascript">
$(function () {
  var data = {
    name: 'Jack'
  };
  var html = $('#theTmpl').render(data);
  $('#result').append(html);
}); 
</script>
</head>
<body>
  <div id="result"></div>
</body>
</html>

出力:

Name: Jack 

まず、text/x-jsrenderというtypeのscript中にテンプレートの記述を行う。

<script id="theTmpl" type="text/x-jsrender">
  <p> Name: {{:name}} </p>
</script>

これはrender functionによりデータとひもづけられてHTMLが作成される。

  var html = $('#theTmpl').render(data);

この時、作成されるHTMLは次のようになる。

  <p> Name: Jack </p>

配列の出力例

先の例ではテンプレートに渡すデータがオブジェクトだった。もし、配列を渡した場合はどうなるであろうか?
先のサンプルを配列に置き換えてみる。

  var data = [
    {name: 'Jack'},
    {name: 'Sara'},
    {name: 'Dick'}
  ];

ここで作成されるHTMLは次のようになる。

  <p> Name: Jack </p>
  <p> Name: Sara </p>
  <p> Name: Dick </p>

データとして配列を渡した場合は、複数のデータが作成されることになる。

もし、渡すデータとしてはオブジェクトであり、そのプロパティが配列だった場合は、テンプレートの中でループをすることで表示することが可能だ。

以下のようなデータがあったとしよう。

  var data = {
    title: 'タイトル',
    names:[
      {name:'Jack'},
      {name:'Sara'},
      {name:'Dick'}
    ]
  };

この時はテンプレートにループを加えるとよい。

<script id="theTmpl" type="text/x-jsrender">
  <h1>Title {{:title}}</h1>
  {{for names}}
    <p>Name: {{:name}}</p>
  {{/for}}
</script>

この時、生成されるHTMLは次の通りになる。

  <h1>Title タイトル</h1>
    <p>Name: Jack</p>
    <p>Name: Sara</p>
    <p>Name: Dick</p>

もし、データの配列中にさらに配列があったらどうであろうか?

  var data = {
    title: 'タイトル',
    names:[
      {name: 'test1', sub:['a','b','c']},
      {name: 'test2', sub:['e','f','g']}
    ]
  };

以下のように、テンプレート中のループの中にさらにループを記述するだけでよい。

<script id="theTmpl" type="text/x-jsrender">
  <h1>Title {{:title}}</h1>
  {{for names}}
    <p>Name: {{:name}}</p>
    {{for sub}}
      <p>subitem: {{:}}</p>
    {{/for}}
  {{/for}}
</script>

この場合の出力結果は以下のようになる。

  <h1>Title タイトル</h1>
    <p>Name: test1</p>
      <p>subitem: a</p>
      <p>subitem: b</p>
      <p>subitem: c</p>
    <p>Name: test2</p>
      <p>subitem: e</p>
      <p>subitem: f</p>
      <p>subitem: g</p>

オブジェクトのプロパティの列挙

オブジェクトのプロパティ名とその値を列挙するサンプルを示す。
以下のようなオブジェクトが存在しており、addressのプロパティ名と値を列挙したいものとする。

  var data = [
    {
      "name": "Pete",
      "address": {
        "street": "12 Pike Place",
        "city": "Seattle",
        "ZIP": "98101"
      }
    },
    {
      "name": "Heidi",
      "address": {
        "street": "5000 Broadway",
        "city": "Sidney",
        "country": "Australia"
      }
    }
  ];

この場合はテンプレートにforではなくpropsを用いる。
すると、>keyでプロパティ名、>propでプロパティの値が取得できる。

<script id="theTmpl" type="text/x-jsrender">
  <h1>Name {{:name}}</h1>
  {{props address}}
    <b>{{>key}}:</b> {{>prop}}<br/>
  {{/props}}
</script>

この時のHTMLの出力は次のようになる。

  <h1>Name Pete</h1>
    <b>street:</b> 12 Pike Place<br/>
    <b>city:</b> Seattle<br/>
    <b>ZIP:</b> 98101<br/>
  <h1>Name Heidi</h1>
    <b>street:</b> 5000 Broadway<br/>
    <b>city:</b> Sidney<br/>
    <b>country:</b> Australia<br/>

文字列をテンプレートとして使用する場合

今までは、テンプレートの記述はscriptタグで行ったが、これを直接文字列で記述することも可能である。

  var data = {
    name: 'Jack'
  };
  $.templates('testTmpl', '<label>Name:</label>{{:name}}');

  var html = $.render.testTmpl(data);
  $('#result').append(html);

templates関数にテンプレート名とテンプレートを指定することでテンプレートを登録できる。登録したテンプレートは、テンプレート名を指定することで利用することができる。

<p>Name:Jack</p>

テンプレートの遅延読み込み

文字をテンプレートとして、利用できることを利用してテンプレートの遅延読み込みができることを意味する。

たとえば次のような、テキストファイルが存在する。

test.txt

<label> Name:</label> {{:name}}

これを以下のような、コードでtext.txtをテンプレートとして遅延読み込みができる。

<script type="text/javascript">
$(function () {
  var data = {
    name: 'Jack'
  };
  $.get('./test.txt', function(value) {
    console.log(value);
    var testTmp = $.templates(value);
    var html = testTmp.render(data);
    $('#result').append(html);
  });
}); 
</script>

テンプレート内で別のテンプレートを使用する場合

テンプレート内でincludeタグを利用する事で別のテンプレートを使用する事が可能である。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"  type="text/javascript"></script>
<script src="./jsrender.min.js" type="text/javascript"></script>

<script id="peopleTemplate" type="text/x-jsrender">
  <div>
    {{:name}} lives in {{include tmpl="#addressTemplate"/}}
  </div>
</script>

<script id="addressTemplate" type="text/x-jsrender">
    <b>{{>address.city}}</b>
</script>

<script type="text/javascript">
$(function () {
  var data = [
    {
      "name": "Pete",
      "address": {
        "city": "Seattle"
      }
    },
    {
      "name": "Heidi",
      "address": {
        "city": "Sidney"
      }
    }
  ];
  var html = $('#peopleTemplate').render(data);
  console.log(html);
  $('#result').append(html);
}); 
</script>
</head>
<body>
  <div id="result"></div>
</body>
</html>

この時のHTMLの出力は次のようになる

 <div>
    Pete lives in 
    <b>Seattle</b>

  </div>

  <div>
    Heidi lives in 
    <b>Sidney</b>

  </div>

includeタグはtmpl属性で指定したテンプレートの内容に置き換わっていることが確認できる。
このincludeを利用する事で、テンプレートの共通化を行える。

分岐の例

テンプレート中で{{if}}}タグを利用することでテンプレートを分岐することができる。
これは必要に応じて{{else}}タグも利用できる。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"  type="text/javascript"></script>
<script src="./jsrender.min.js" type="text/javascript"></script>

<script id="theTmpl" type="text/x-jsrender">
  {{if displayType == 0}}
    <p>Name: {{:name}} </p>
  {{else displayType == 1}}
    <p>Name: {{:azana}} </p>
  {{else}}
    <p>Name: {{:nickName}} </p>
  {{/if}}
</script>

<script type="text/javascript">
$(function () {
  var data = [
    {
      "name": "Kannu",
      "azana": "Unchou",
      "nickName": "Hige",
      "displayType": 0
    },{
      "name": "Chouhi",
      "azana": "Ekitoku",
      "nickName": "Haruhi",
      "displayType": 1
    },
    {
      "name": "Ryubiu",
      "azana": "Gentoku",
      "nickName": "Kotei",
      "displayType": 2
    },
    {
      "name": "ShokatuRyou",
      "azana": "Koumei",
      "nickName": "awawa",
      "displayType": 3
    }
  ];
  var html = $('#theTmpl').render(data);
  console.log(html);
  $('#result').append(html);
}); 
</script>
</head>
<body>
  <div id="result"></div>
</body>
</html>

ここで作成されるHTMLの結果は以下のようになる。data.displayTypeにより表示される名前が切り替わっていることが確認できる。

    <p>Name: Kannu </p>
    <p>Name: Ekitoku </p>
    <p>Name: Kotei </p>
    <p>Name: awawa </p>

テンプレートに渡すデータのエスケープ処理

テンプレートに渡すデータ中の文字にHTMLのタグが存在する場合がある。このデータ中のタグをエスケープ処理を行うには、{{>データ名}}を用いる。

以下のサンプルで、エスケープ処理を行わない場合と、行った場合でどのような違いが生じるか確認する。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"  type="text/javascript"></script>
<script src="./jsrender.min.js" type="text/javascript"></script>

<script id="theTmpl" type="text/x-jsrender">
  <p>{{:description}}</p>
  <p>{{>description}}</p>
</script>

<script type="text/javascript">
$(function () {
  var data = {description: "A <b>very nice</b> appartment"};
  var html = $('#theTmpl').render(data);
  console.log(html);
  $('#result').append(html);
}); 
</script>
</head>
<body>
  <div id="result"></div>
</body>
</html>

出力結果は以下の通りとなり、{{:}}の場合は、渡された文字データがそのまま出力されているが、{{>}}でエスケープ処理が適切になされることが確認できる。

  <p>A <b>very nice</b> appartment</p>
  <p>A <b>very nice</b> appartment</p>

まとめ

JsRenderを用いることで、JavaScriptのテンプレートを記述することができ、ビューとロジックの分離を容易に行うことができる。

JsViews

JsViewsはJsRenderとJsObservableと呼ばれるライブラリを用いてテンプレートとデータのリンクを行うことができる。
これにより、データに変更があった場合に描画の自動更新をすることが可能になる。

実装のサンプルとJsViewsの機能

JsViewsを利用した実装のサンプルを用いてJsViewsの機能を紹介する
JsViewsは、JsRenderと同じページからダウンロードできる。

data-linked tagの例

ここではdata-linked タグの使用例を見てみよう。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"  type="text/javascript"></script>
<script src="./jsviews.min.js" type="text/javascript"></script>
<script id="teamTemplate" type="text/x-jsrender">

<div class="buttons">
  <button id="add">Add</button>
  <button id="update">Update</button>
</div>
<ol>
  {^{for members}}
    <li>
      {^{:name}} 
      {^{:age}}
      <img class="remove" src="http://www.jsviews.com/resources/images/close.png" />
    </li>
  {{/for}}
</ol>

</script>

<script type="text/javascript">
$(function () {
  var team = {
    members: [
      {name: "Robert", age:10},
      {name: "Sarah", age:12}
    ]
  };
  var cnt = 1;
  $.templates("#teamTemplate").link("#team", team)
    .on("click", ".remove", function() {
      var view = $.view(this);
      $.observable(team.members).remove(view.index);
    })
    .on("click", "#add", function() {
      $.observable(team.members).insert(0, {name: "new" + cnt++, age:1});
    })
    .on("click", "#update", function() {
      for(var i = 0; i < team.members.length;++i) {
        $.observable(team.members[i]).setProperty("age", team.members[i].age + 1);
      }
    });
}); 
</script>
</head>
<body>
  <div id="team"></div>
</body>
</html>

出力例:
無題.png

このプログラムを実際動作させると、Addボタンでアイテムの追加、xボタンでアイテムの削除、Updateボタンで年齢の更新が行われることがわかるだろう。つまり、ある種の操作により、表示の内容が更新されていることが確認できる。

どのように作っているかを説明する。
まずは、テンプレートの部分を見てみる。

<script id="teamTemplate" type="text/x-jsrender">

<div class="buttons">
  <button id="add">Add</button>
  <button id="update">Update</button>
</div>
<ol>
  {^{for members}}
    <li>
      {^{:name}} 
      {^{:age}}
      <img class="remove" src="http://www.jsviews.com/resources/images/close.png" />
    </li>
  {{/for}}
</ol>

</script>

JsRenderの場合、{{for members}}や{{:name}}となっていたものが、{^{for members}}や{^{:name}}になっているのがわかるだろう。「^」を使用することでデータがリンクされていることを示している。

次にデータとテンプレートを関連付けているところを見てみよう。

  $.templates("#teamTemplate").link("#team", team)
    .on("click", ".remove", function() {
      var view = $.view(this);
      $.observable(team.members).remove(view.index);
    })
    .on("click", "#add", function() {
      $.observable(team.members).insert(0, {name: "new" + cnt++, age:1});
    })
    .on("click", "#update", function() {
      for(var i = 0; i < team.members.length;++i) {
        $.observable(team.members[i]).setProperty("age", team.members[i].age + 1);
      }
    });

「#tempTemplate」で指定したテンプレートとオブジェクト「team」を関連付けて「#team」に出力している。
.onで続くものは、どのようなイベントで、このテンプレートの内容が更新されるかを記述している。
更新する場合、追加には「\$.observable.indert」,削除には「\$.observable.remove」,特定のプロパティの更新には「\$.observable.setProperty」を使用する。

その他observableの機能はJsObservableのAPIを参照するとよい。
http://www.jsviews.com/#jsoapi

data-link属性の例

ここでは data-link属性を用いた例を紹介する。
INPUTなどにdata-link属性をつけることで、INPUTで入力した内容で、data-linkで指定したデータのプロパティの変更を行うことができるようになる。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"  type="text/javascript"></script>
<script src="./jsviews.min.js" type="text/javascript"></script>
<script id="theTmpl" type="text/x-jsrender">
<input data-link="name trigger=true"/>-
<input data-link="lastName trigger=true"/>
<br>
{^{:name}}-{^{:lastName}}
</script>

<script type="text/javascript">
$(function () {
  var data = {
    "name": "Jack",
    "lastName": "Hanma"
  };

  var template = $.templates("#theTmpl");
  template.link("#result", data);

  $("#check").click(function(){
    alert(JSON.stringify(data));
  });
}); 
</script>
</head>
<body>
  <div id="result"></div>
  <button id="check">check</button>
</body>
</html>

無題.png

このサンプルを実行すると、INPUTの内容を変更すると、表示しているデータはもちろん、データとして渡したJSONの内容も変更されていることが確認できる。

まとめ

JsViewsを用いてテンプレートとデータを関連付けを行うと、画面の入力によりデータを容易に更新したり、Observerを使用してデータを更新することで画面の更新も容易に行える。

カスタムタグ

JsRenderとJsViewsの項目で{{for}}や{{else}}など様々なタグを紹介した。
JsViewsを使用することにより、開発者が任意のタグを作成することが可能だ。
この方法は下記に記述してある。
http://www.jsviews.com/#customtagsapi

Python Redmineを使用してRedmineを操作する

Python Redmine

Python RedmineはRedmineと通信を行うPythonのライブラリである。

次のような特徴を有している。

・RedmineのAPI機能を100%サポート
 (プロジェクトすら作成できる)
・Python 2.7、3.4-3.7
 ※2.6,3.3のサポートは2.2.0(2019-01-13)にて削除されました。
 https://python-redmine.com/changelog.html#id1
・ORMスタイルのPythonic APIを提供している。
 (Django ORMに影響を受けています)
・Apache2.0ライセンス

以下にその詳細が記述してある。
https://python-redmine.com/

インストール方法

pipまたはeasy_installを用いてインストールが可能である。

$ pip install python-redmine

または

$ easy_install python-redmine

Redmine側の設定として、「管理」→「設定」画面におけるAPIタブで「REST APIによるWebサービスを有効にする」にチェックをいれておくこと。

image.png

ここでAPIを有効にすると個人設定画面でAPIキーを確認できる。
image.png

サンプル

以下にサンプルを記述する。
このサンプルはWindows上のPython3.7+python redmine2.2.1+Redmine 4.0.3.stableで確認している。

認証方法

ユーザ名とパスワードを指定してredmineに接続できる。

from redminelib import Redmine
redmine = Redmine('http://localhost/redmine', username='admin', password='admin')

あるいは次のように、APIキーを用いて接続することも可能だ。

from redminelib import Redmine
redmine = Redmine('http://localhost/redmine', key='e4a413a3b7a3c238102b7393c035bbc5f5eb6409')

チケットの操作

チケットの操作については下記を参照
https://python-redmine.com/resources/issue.html

チケットの作成

redmine.issue.new()でチケットのオブジェクトを生成して、そこにプロパティを設定後、保存することでチケットの作成ができる。

import datetime
from redminelib import Redmine
redmine = Redmine('http://192.168.0.200/', key='60076966cebf71506ae3f2391da649235a2b1d46')
issue = redmine.issue.new()
issue.project_id = 'Test1'
issue.subject = 'サブジェクト'
issue.tracker_id = 1     #トラッカー
issue.description = 'チケットの内容をしめす。\n改行もできる。'
issue.status_id = 1      #ステータス
issue.priority_id = 1    #優先度
issue.assigned_to_id = 1 #担当者のID
issue.watcher_user_ids = [1] # ウォッチするユーザのID
issue.parent_issue_id = 12     # 親チケットのID
issue.start_date = datetime.date(2014, 1, 1) #開始日
issue.due_date = datetime.date(2014, 2, 1)   #期日
issue.estimated_hours = 4   # 予想工数
issue.done_ratio = 40
issue.custom_fields = [{'id': 1, 'value': 'foo'}]
issue.uploads = [{'path': 'C:\\dev\\python3\\redmine\\test.txt'}]
issue.custom_fields = [{'id': 1, 'value': 'foo'}]
issue.save()

単一チケットの取得

redmine.issue.get()でチケットIDを指定する。もし存在しない場合、
redmine.exceptions.ResourceNotFoundError例外が発生する。

import datetime
from redminelib import Redmine
from redminelib.exceptions import ResourceNotFoundError

redmine = Redmine('http://192.168.0.200/', key='60076966cebf71506ae3f2391da649235a2b1d46')
try:
    issue = redmine.issue.get(60)
    print (dir(issue))
    print ('id:%d' % issue.id)
    print ('project:%s' % issue.project.name)
    print ('project_id:%d' % issue.project.id)
    print ('subject:%s' % issue.subject)
    print ('tracker:%s' % issue.tracker.name)
    print ('tracker_id:%d' % issue.tracker.id)
    print ('description:%s' % issue.description)
    print ('status:%s' % issue.status.name)
    print ('status:%d' % issue.status.id)
    print ('author:%s' % issue.author.name)
    print ('author_id:%d' % issue.author.id)
    if hasattr(issue, 'assigned'):
        print ('assigned:%s' % issue.assigned_to.name)
        print ('assigned_id:%d' % issue.assigned_to.id)
    print ('watcher--------')
    for u in issue.watchers:
        print (' %d:%s' % (u.id, u.name))
    print ('作成日:%s' % issue.created_on)
    print ('更新日:%s' % issue.updated_on)
    if hasattr(issue, 'start_date'):
        print ('start_date:%s' % issue.start_date)
    if hasattr(issue, 'due_date'):
        print ('issue_date:%s' % issue.due_date)
    if hasattr(issue, 'issue.estimated_hours'):
        print ('estimated_hours:%d' % issue.estimated_hours)
    print ('作業時間:%d' % issue.spent_hours)
    print ('作業時間の記録----------')
    for t in issue.time_entries:
        print('  ID:%d' % t.id)
        print('  活動:%s' % t.activity)
        print('  コメント:%s' % str(t.comments))
        print('  作成日:%s' % t.created_on)
        print('  時間:%s' %t.hours)
        print('  チケットID:%s' % t.issue)
        print('  プロジェクトID:%s' % t.project)
        print('  日付:%s' % t.spent_on)
        print('  更新日:%s' % t.updated_on)
        print('  user:%d %s' % (t.user.id,t.user.name)) 
    print ('done_ratio:%d' % issue.done_ratio) 
    print ('priority:%s' % issue.priority.name)
    print ('priority_id:%d' % issue.priority.id)
    print ('custom_fields----')
    for c in issue.custom_fields:
        print ('  %d:%s = %s' % (c.id, c.name, c.value))
    print ('attachements---')
    for f in issue.attachments:
        print ('  id:%d' % (f.id))
        print ('  author:%s' % (f.author)) 
        print ('  content_url:%s' % (f.content_url))
        print ('  created_on:%s' % (f.created_on))
        print ('  description:%s' % (f.description))
        print ('  filename:%s' % (f.filename))
        print ('  filesize:%d' % (f.filesize))
        print ('  ---------------')
    print ('changeset---')
    for c in issue.changesets:
        #コミットログがディクショナリ型として格納
        print ('  %s' % c)
    if hasattr(issue, 'parent'):
        print ('parent:%s' % issue.parent)
    print ('children----------')
    for c in issue.children:
        print ('  %s:%s' % (c.id, c.subject))
    print ('relation----------')
    for r in issue.relations:
        print ('  %d:%d->%d(%s)' % (r.id, r.issue_id, r.issue_to_id, r.relation_type)) 
except (ResourceNotFoundError):
    print ('Not found')

入力が省略されている項目は、プロパティ自体が存在していない。
そのため、hasattrで存在チェックをしなければならない。

watchersや担当者、作成者のユーザ情報には必要最小限のものしか入っていない。login名などを取得したい場合は次のようにユーザIDからユーザの詳細を取得すること。

user = redmine.user.get(u.id)

すべてのチケットを取得

redmine.issue.all()で取得することができる。
省略可能なパラメータとして、以下のものがある。

sort (string) :並び順
limit (integer) :取得数の上限
offset (integer):取得開始位置

import datetime
from redminelib import Redmine

redmine = Redmine('http://192.168.0.200/', key='60076966cebf71506ae3f2391da649235a2b1d46')
issues = redmine.issue.all(sort='category:desc')
for issue in issues:
  print ('%d:%s' % (issue.id, issue.subject))

特定の条件のチケットの取得

redmine.issue.filterを用いて特定の条件のチケットを抽出できる。
以下は、担当者が自分のチケットを抽出している。

import datetime
from redminelib import Redmine

redmine = Redmine('http://192.168.0.200/', key='60076966cebf71506ae3f2391da649235a2b1d46')
issues = redmine.issue.filter(assigned_to_id='me')
for issue in issues:
  print ('%d:%s' % (issue.id, issue.subject))

query_id を用いれば、登録済みのクエリーで検索も行える。

クエリー用のオペレータ

結局はREST APIを読んでいるにすぎないのでRedmineがサポートしているオペレータが使用できる。
http://www.redmine.org/projects/redmine/wiki/Rest_Issues

上記のドキュメントでは「!*」「>」「<」「*」「~」などの例が紹介されている。

なので例えば、誰にもアサインされていないタスクを取得するには下記のような実装になる。

import datetime
from redminelib import Redmine

redmine = Redmine('http://192.168.0.200/', key='60076966cebf71506ae3f2391da649235a2b1d46')
issues = redmine.issue.filter(assigned_to_id='!*')
for issue in issues:
  print ('%d:%s' % (issue.id, issue.subject))

また、自分以外の担当者のタスクを取得したければ以下のようになる。

import datetime
from redminelib import Redmine

redmine = Redmine('http://192.168.0.200/', key='60076966cebf71506ae3f2391da649235a2b1d46')
issues = redmine.issue.filter(assigned_to_id='!me')
for issue in issues:
  print ('%d:%s' % (issue.id, issue.subject))

否定、範囲指定などのオペレータを使用することで様々な条件付けが可能になることはわかるが、問題はどのようなオペレータがあるかRESTAPIのドキュメントからはわからないことである。

そのため、実際のコードを参照する必要がある。

redmine/app/models/query.rb
https://github.com/redmine/redmine/blob/9746ab7e5be2db5e2d233ee37365cf21ba4b893a/app/models/query.rb#L254

この実装をみれば、どのようなoperators が定義されているか確認できる。
ただし、全ての項目で全てのオペレータが使用できるわけではない。
たとえばsubjectでは「*:すべて」「!*:なし」「~:含む」は使用できるが「^:~ではじまる」は使用できない。これが使用できるかどうかは下記のフィルタ設定画面でしていできる条件と同じである。
image.png

チケットの更新

redmine.issue.getで取得したチケットは変更して保存することが可能である。
以下の例はチケットのステータスを変更している例である。

# -*- coding: utf-8 -*-
import datetime
from redmine import Redmine

redmine = Redmine('http://localhost/redmine', key='e4a413a3b7a3c238102b7393c035bbc5f5eb6409')
issue = redmine.issue.get(51)
issue.status_id = 2
issue.save()

作業時間の操作

作業時間の操作については下記を参照
https://python-redmine.com/resources/time_entry.html

※Redmine >= 3.4.0あたりから作業分類を入れる必要があるようです。

作業時間の取得

以下は作業時間を列挙する例である。

import datetime
from redminelib import Redmine

redmine = Redmine('http://192.168.0.200/', key='60076966cebf71506ae3f2391da649235a2b1d46')
time_entries = redmine.time_entry.all()
for t in time_entries:
    print('  ID:%d' % t.id)
    print('  活動:%s' % t.activity)
    print('  コメント:%s' % str(t.comments))
    print('  作成日:%s' % t.created_on)
    print('  時間:%s' %t.hours)
    print('  チケットID:%s' % t.issue)
    print('  プロジェクトID:%s' % t.project)
    print('  日付:%s' % t.spent_on)
    print('  更新日:%s' % t.updated_on)
    print('  user:%d %s' % (t.user.id,t.user.name))
    print('  作業分類 %d %s' % (t.activity.id, t.activity.name))

作業時間の記録

作業時間を記録する例を以下に示す。

import datetime
from redminelib import Redmine

redmine = Redmine('http://192.168.0.200/', key='60076966cebf71506ae3f2391da649235a2b1d46')
time_entry = redmine.time_entry.new()
time_entry.issue_id = 60
time_entry.spent_on = datetime.date(2014, 1, 14)
time_entry.hours = 3
time_entry.activity_id = 4
time_entry.comments = 'hello'
time_entry.activity_id = 8
time_entry.save()

まとめ

今回は提供されているAPIの一部しか検証していないが、Web画面で行える操作は網羅できていることが確認できた。

Python Redmineを用いることでPythonでRedmineを自由に操作できることが期待できる。
そのことは、以下のことを実現できると期待できる。

・自動テスト失敗時にプログラムからチケットを発行する。
・Excelに記述された設計書をWikiに自動コンバート
・作業時間を集計して、別のシステムに通知する。

以上