「パワートゥザエッジ」を読んだ

「なぜ組織がアジャイルに向かうのか」についての洞察を得るために「パワートゥザエッジ」を読みました。

個人的に興味を持ったところをまとめています。

パワートゥザエッジ―ネットワークコミュニケーション技術による戦略的組織論

パワートゥザエッジ―ネットワークコミュニケーション技術による戦略的組織論

指揮統制(C2)

指揮統制(Command and Control:C2)は6つの型に分類することが出来る。

  • 周期型
    • 定期的なスケジュールに基づいて中央から詳細な命令を発する
  • 割り込み型
    • 不定期に中央から詳細な命令を発する
    • 周期型より強力な通信能力が求められる
  • 問題解決型
    • 作戦レベルの司令部が軍の各構成要素に対して、目的を規定することに注力する
    • 上級の司令官の付与した制約の範囲内で「革新性と柔軟性」を発揮することができる
  • 問題提示型
    • 命令は問題解決型同様に目的を中心に組み立てられるが、より少ない指標と制約
    • 任務は部下に対して問題として与えられ、それをどうやって解決すべきかについては極力詳細を提示しない
  • 選択的統制型
    • 非常に有能な部隊を提供して、大まかな任務を与える
    • 基本的には部下が十分に能力を発揮できるようにサポート役に徹する
    • 状況をモニタリングし必要に応じて介入する
  • 無統制型
    • 戦域司令官の主要な役割は軍の支援をすること、すなわち任務達成の可能性を最大化し、勝利のために軍が必要とする情報と軍備を提供する

NCWの理念の採用により、自己同期化された部隊や行動の実現が可能になるが、混乱を避けるためには以下の前提条件を備える必要がある。

  • 明確かつ一貫した指揮意図の理解
  • 高品質の情報および共通の状況認識
  • 部隊の全レベルでの能力
  • 情報、部下、上官、同胞および機器への信頼

工業化時代の組織

工業化時代の組織の特徴としては以下があげられるが、これらの特徴は工業化時代のコミュニケーションの制約に依存していた。

  • 特徴
    • 分業
    • 専門化
    • 階層化組織
    • 最適化
    • 調停
    • 統合計画
    • 相互干渉の回避

コミュニケーション

  • プッシュ型アプローチ
    • 誰がどんな情報を求めているかを配信する側が知っておく必要がある
  • プル型アプローチ
    • どこで情報が得られるかさえ明確であれば、情報を求めている側が自律的に取得しに来る

工業化時代から情報化時代へ

役割の変化

  • 広い分野に存在する潜在的脅威への対応
  • 間組織や非政府組織(NGO)との連携

    求められる資質

  • 相互運用性:協業がしやすい仕組み(誰が参加するかということに対して柔軟)
    • 非軍事的パートナーとの協業
  • 俊敏性:効率よい学びにより不確実性に柔軟に対応する
    • 状況を理解する能力
    • 状況に対応するための適切な手段の所有
    • 適時的に応答する手段を統合運用する能力

      それを支える技術

  • 通信技術の向上
    • 組織の末端(エッジ)が力を持つことが可能となってきた

パワートゥザエッジ(PTE)

PTEは「エッジの主体に強いパワーを持たせることと、独自の権限を持った組織を増やすことにより、組織やシステムのパワーを増大させることができる」とする考え方。

PTEの考え方を指揮統制およびその情報基盤に適用したとき、軍事組織は工業化時代の欠点を克服し、成功に必要な相互運用性および俊敏性を獲得することができる。

PTEが実現された組織の例として、「エッジ型組織」ではすべての構成要素に情報が与えられ、当然の行動を取る自由も与えられる。

階層型組織とエッジ型組織

階層構造が中心にパワーを保持しようとするのに対し、エッジ型組織はエッジにパワーを移動する。

階層型組織は既知の状況と任務行為においては非常に優れているが、未知の場合には非常に制限されることになる。

階層型組織 エッジ型組織
指揮 指示する 状況を整える
リーダーシップ 地位による 能力による
統制 指示による 属性に現れる
意思決定 組織が行う 全員が行う
情報 蓄積する 共有する
主な情報の流れ 垂直、指揮に伴う 水平、指揮からは独立
情報管理 プッシュ 発信して、必要なものを選ぶ
情報源 少人数に集中 取捨選択、市場に適合的
組織プロセス 指示される、線形 動的、並行的
エッジの個人 強制される 権限を与えられる

Windows7環境でDockerを利用して検証環境(Solr、Redis)を手早く準備する方法

新しいミドルウェアをとりあえず触ってみたい場合に、個別にインストールするのは、面倒ですし、環境が汚れてしまう恐れがあります。

そんな時は、Dockerを利用すると便利です。

環境

Windows7

Dockerの導入

Windows7環境ではDockerToolbox一択なので、以下の記事を参考にDockerToolboxをインストールします。

Docker Toolboxのインストール:Windows編

VirtualBox上にdefaultという名前で仮想マシンが作成されます。

付属のターミナルは利用しにくいですが、defaultの仮想マシンが実行中であれば、お好きなターミナルから接続可能です(デフォルトのID/PWはdocker/tcuserです)。

実際に使ってみる

事前にIPを確認する

仮想ホストのIPを確認しておく。

Docker-machine ls

Solr

Docker Hubで公開されている手順を参考に起動してみます。

  • コアを格納するディレクトリを作成する。
    • D:\var\solr\cores
  • Solrの公式Imageを取得し、起動する。
docker pull solr:5.5.5
docker run --name solr -d -p 8983:8983 -v /d/var/solr/cores:/opt/solr/server/solr/mycores -t solr:5.5.5

Redis

Docker Hubで公開されている手順を参考に起動してみます。

  • コアを格納するディレクトリを作成する。
    • D:\var\redis\data
  • Solrの公式Imageを取得し、起動する。
# Redisを起動する
docker pull redis:4.0.11
docker run --name redis -d -p 6379:6379 -v /d/var/redis/data:/data redis:4.0.11 redis-server --appendonly yes
  • 動作確認
# 動作確認
# コンテナに接続する
docker exec -it 【CONTAINER ID】 /bin/bash
# Redisに接続
redis-cli
# コマンドラインでset/get/delなどを試すことができる
set test hoge
get test
del test
# Redisから切断
exit
# コンテナから切断
exit

参考

SolrMeterを使ってみた

Solrの負荷テストツールとしてgithubに公開されているSolrMeterを使ってみました。

準備

buildする

cd C:\Users\XXXXX\Desktop\tmp

git clone https://github.com/lafourchette/solrmeter.git

cd solrmeter

mvn package

起動する

java -jar solrmeter/target/solrmeter-0.3.1-SNAPSHOT-jar-with-dependencies.jar

負荷テスト用のQueryを準備する

SolrのログファイルをSolrMeterのTools>Extract Queriesより解析し、負荷テスト用のQueryを作成することができます。

実行

設定変更

Edit>Settingsより設定を以下のように変更し、Apply→OKで設定を反映します。

※Choose the query modeで「external」を選択しない場合、デフォルトのコアにしかリクエストを送れないようです。

実行・監視

Query Consoleの実行ボタンより実行すると、下部パネルに結果が出力されます。

なお、Solrサーバのリソース状況はSolrMeterでは参照できないので、vmstatなどで確認すると良いでしょう。

使ってみて

SolrのautoCommitやキャッシュの設定のチューニング時に有用なツールだと思いました。

ConcurrentUpdateSolrClientでautoCommitが効かない場合の対処方法

Solrに対する頻繁なCommitは、Commitやインデクシング処理の速度悪化の要因となります。

回避方法としてはautoCommitの利用が挙げられますが、

SolrJのConcurrentUpdateSolrClientをtry-with-resources文で利用している場合に、なぜかインデクシングされない問題がありました。

今回は、その問題を回避するためにやったことを紹介します。

autoCommitの効かなかったコード

solrconfig.xmlにautoCommitが設定されていたため、autoCommitされる事を期待して、明示的なcommitをリクエストしなかったところ、Solrにインデクシングされなかった。

   public void batchIndex(String url, Collection<SolrInputDocument> docs) throws IOException, SolrServerException {
        try (SolrClient client = new ConcurrentUpdateSolrClient.Builder(url).build()) {
            client.add(docs);
            // client.commit(true, true); 明示的なcommitをリクエストしない
        }
    }

やったこと

調査

  • Solrサーバへリクエストが飛んでいるかどうかを確認。
    • なぜかリクエストが飛んでいなかった。
  • ConcurrentUpdateSolrClientの実装を確認。
    • リクエストを行う処理は複数スレッドで並列で行う処理となっているが、close()メソッドがコールされると、処理を中断するようになっていた。

対応後のコード

ConcurrentUpdateSolrClient#blockUntilFinished()をコールし、処理完了まで待機するようにした。

   public void fixedBatchIndex(String url, Collection<SolrInputDocument> docs) throws IOException, SolrServerException {
        try (ConcurrentUpdateSolrClient client = new ConcurrentUpdateSolrClient.Builder(url).build()) {
            client.add(docs);
            // 処理完了まで待機する
            client.blockUntilFinished();
        }
    }

ToodledoのログをGASを使ってGoogleスプレッドシートに保存する方法

TODOの管理にはToodledoを利用していまして、実績をIFTTTに連携してEvernoteで、1日1ノートになるように記録していました。 そうする中で2点困っていることがありました。

  • Evernoteに連携できる項目が少なく、特に予実管理するために必要な項目(Length:予定作業時間、Timer:実績作業時間)が無いため、Evernote側の記録だけを見ても、各タスクの作業時間がわからなかった。
  • 週に1〜2回の頻度で1日複数ノートに別れて記録されてしまい、手でメンテナンスする必要があった。

そこで、ToodledoのAPIを使って、実績を、Googleスプレッドシートに記録することにしました。

実際に行なった事

Toodledo

ToodledoのデータにアクセスするためにAPIを利用します。

APIを利用するためにDeveloper's API Documentation : Register & Stats ✓ Toodledo APIよりアプリケーションを登録し、Client ID、Secretを発行します。

Redirect URLは以下の形式で指定する。 「https://script.google.com/macros/d/{PROJECT KEY}/usercallback」

{PROJECT KEY}はhttps://script.google.com/から確認が出来ます。

Googleスプレッドシート

  1. ツール->スクリプトエディタよりスクリプトエディタを開く
  2. OAuth2 ライブラリをインポートする
    1. エディタのリソース>ライブラリよりライブラリを追加する
    2. プロジェクトキー:MswhXl8fVhTFUH_Q3UOJbXvxhMjh3Sh48
  3. コードを書く
var CLIENT_ID = 'XXXXX', // 発行したClient IDを指定
    CLIENT_SECRET = 'XXXXX'; // 発行したSecretを指定

function onOpen() {
  SpreadsheetApp.getUi()
    .createMenu('Toodledo')
    .addItem('Authorize', 'openAuthDialog')
    .addToUi();
}

function openAuthDialog() {
  var toodledoService = getToodledoService();
  if (toodledoService.hasAccess()) {
    Browser.msgBox('Already autherized');
  } else {
    var authorizationUrl = toodledoService.getAuthorizationUrl();
    var template = HtmlService.createTemplate(
        '<a href="<?= authorizationUrl ?>" target="_blank">Authorize</a>');
    template.authorizationUrl = authorizationUrl;
    var page = template.evaluate();

    SpreadsheetApp.getUi()
      .showModalDialog(page, 'Authorize');
  }
}

//////////////////////////////////////////////////////////////

function getToodledoService() {
  // Create a new service with the given name. The name will be used when
  // persisting the authorized token, so ensure it is unique within the
  // scope of the property store.
  return OAuth2.createService('Toodledo')
      // Set the endpoint URLs, which are the same for all Toodledo services.
      .setAuthorizationBaseUrl('https://api.toodledo.com/3/account/authorize.php')
      .setTokenUrl('https://api.toodledo.com/3/account/token.php')

      // Set the client ID and secret, from Toodledo
      //   https://api.toodledo.com/3/account/doc_register.php
      .setClientId(CLIENT_ID)
      .setClientSecret(CLIENT_SECRET)
      .setScope('basic tasks')

      // Set the name of the callback function in the script referenced
      // above that should be invoked to complete the OAuth flow.
      .setCallbackFunction('authCallback')

      // Set the property store where authorized tokens should be persisted.
      .setPropertyStore(PropertiesService.getUserProperties())
}

function authCallback(request) {
  var toodledoService = getToodledoService();
  var isAuthorized = toodledoService.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput('Success! You can close this tab.');
  } else {
    return HtmlService.createHtmlOutput('Denied. You can close this tab');
  }
}

function clearService() {
  OAuth2.createService('Toodledo')
  .setPropertyStore(PropertiesService.getUserProperties())
  .reset();
}

// Reusable function to generate a callback URL, assuming the script has been published as a
// web app (necessary to obtain the URL programmatically). If the script has not been published
// as a web app, set `var url` in the first line to the URL of your script project (which
// cannot be obtained programmatically).
function getCallbackURL(callbackFunction) {
  var url = ScriptApp.getService().getUrl();      // Ends in /exec (for a web app)
  url = url.slice(0, -4) + 'usercallback?state='; // Change /exec to /usercallback
  var stateToken = ScriptApp.newStateToken()
    .withMethod(callbackFunction)
    .withTimeout(120)
    .createToken();
  Logger.log(url + stateToken);
}

////////////////////////////////////////////////////

function getCompletedTasks() {
  var folders = getFolders();
  var contexts = getContexts();
  
  var toodledoService = getToodledoService();
  var response = UrlFetchApp.fetch('http://api.toodledo.com/3/tasks/get.php?access_token=' + toodledoService.getAccessToken() + '&comp=1&after=' + getAfter() + '&fields=folder,context,goal,location,tag,startdate,duedate,duedatemod,starttime,duetime,remind,repeat,status,star,priority,length,timer,added,note,parent,children,order,meta,previous,attachment,shared,addedby,via,attachments');
  var tasks = JSON.parse(response);

  var values = [];
  if (tasks.length > 1) {
    for (var i = 1; i < tasks.length; i++) {
      var task = tasks[i];
      var value = [];
      value.push(task.id);
      value.push(task.title);
      value.push(typeof folders[task.folder] === "undefined" ? "" : folders[task.folder]);
      value.push(task.tag);
      value.push(typeof contexts[task.context] === "undefined" ? "" : contexts[task.context]);
      var startdate = (task.starttime === 0 ? task.startdate : task.starttime);
      value.push(startdate === 0 ? "" : new Date((startdate - 60 * 60 * 9) * 1000)); // GMT UNIXタイムスタンプ->日付に変換
      var duedate = (task.duetime === 0 ? task.duedate : task.duetime);
      value.push(duedate === 0 ? "" : new Date((duedate - 60 * 60 * 9) * 1000)); // GMT UNIXタイムスタンプ->日付に変換    
      value.push(new Date((task.completed - 60 * 60 * 9) * 1000)); // GMT UNIXタイムスタンプ->日付に変換
      value.push(task.length);
      value.push(task.timer / 60); // 秒->分に変換
      value.push(task.note);
      values.push(value);
    }
    
    // 最終行に追記
    var sheet = SpreadsheetApp.getActiveSheet();
    var nextRow = sheet.getLastRow() + 1;
    sheet.getRange(nextRow, 1, values.length, values[0].length).setValues(values);
  }
}

function getFolders() {
  var toodledoService = getToodledoService();
  var response = UrlFetchApp.fetch('http://api.toodledo.com/3/folders/get.php?access_token=' + toodledoService.getAccessToken());
  var folders = JSON.parse(response);

  var map = new Object();
  for (var i = 0; i < folders.length; i++) {
    var folder = folders[i];
    map[folder.id] = folder.name;
  }
  
  return map;
}

function getContexts() {
  var toodledoService = getToodledoService();
  var response = UrlFetchApp.fetch('http://api.toodledo.com/3/contexts/get.php?access_token=' + toodledoService.getAccessToken());
  var contexts = JSON.parse(response);

  var map = new Object();
  for (var i = 0; i < contexts.length; i++) {
    var context = contexts[i];
    map[context.id] = context.name;
  }
  
  return map;
}

// 前日のUNIXタイムスタンプを取得する(翌日深夜にトリガーを起動させることを考慮して)
function getAfter() {
  var now = new Date();
  var yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
  return Math.round( yesterday.getTime() / 1000 );
}

設定

OAuthの認可

スプレッドシートを開くとToodledoというメニューが表示されるので、Toodledo->Authorizeから認可を行う(初回のみ)。

トリガーの設定

プログラムで前日分を取得するようにしているので、トリガーも合わせて設定します。

私の場合は、時間主導型で日次で深夜に1回実行させるようにしました。

参考

Google Apps ScriptでOAuth2を使う(GitHubのissueを出力) - dackdive's blog

Developer's API Documentation : Version 3.0 ✓ Toodledo API

Scrapyを使ってクローリング、スクレイピングをやってみた話

はじめに

普段はJavaを主に利用しているのですが、ちょっとした作業で使うのにはちょっと面倒臭いものです。

今回はあるサイトからデータを取得したいとの相談を受けまして、Pythonにより実装されたクローリング、スクレイピングフレームワークであるScrapyを利用してやってみました。

なお、Spiderクラスのみの最小の構成で動作させているため、Itemクラスは作成していません。

インストール

pip3 install scrapy

プロジェクトの作成

scrapy startproject first_scrapy
New Scrapy project 'first_scrapy', using template directory '/usr/local/lib/python3.6/site-packages/scrapy/templates/project', created in:
    /Users/XXXXX/first_scrapy

You can start your first spider with:
    cd first_scrapy
    scrapy genspider example example.com

cd first_scrapy
tree .
.
├── first_scrapy
│   ├── __init__.py
│   ├── __pycache__
│   ├── items.py
│   ├── middlewares.py
│   ├── pipelines.py
│   ├── settings.py
│   └── spiders
│       ├── __init__.py
│       └── __pycache__
└── scrapy.cfg

4 directories, 7 files

設定の変更

settings.pyにはScrapyの様々なオプションを指定できます。

指定可能なオプションは公式から確認できます。

今回は以下のオプションのみ変更しています。

  • DOWNLOAD_DELAY:1つのページをダウンロードしてから、次のページをダウンロードすするまでの間隔(単位:秒)
  • ROBOTSTXT_OBEY:robots.txtがある場合は、それに従うかどうか
  • FEED_EXPORT_ENCODING:出力ファイルの文字コード
DOWNLOAD_DELAY = 3
ROBOTSTXT_OBEY = True
FEED_EXPORT_ENCODING = 'utf-8'

Spiderの作成

ガイドメッセージに従いSpiderを作成していきます。

Spiderは「scrapy.Spider」のサブクラスで、最初にアクセスするURLと、どのようにHTMLからデータを抽出するかを定義します。

scrapy genspider モジュール名 クローリング対象のドメイン

scrapy genspider example example.com
Created spider 'example' using template 'basic' in module:
  first_scrapy.spiders.example

以下のようなコードが作成されます

# -*- coding: utf-8 -*-
import scrapy


class ExampleSpider(scrapy.Spider):
    name = 'example'
    allowed_domains = ['example.com']
    start_urls = ['http://example.com/']

    def parse(self, response):
        pass

start_urlsには最初にアクセスするURLを指定します。

この処理を実行するとstart_urlsにアクセスした結果を引数としてparseメソッドが実行されます。

responseに対して、XPathCSS形式でセレクタを指定することで欲しい情報を取得することが可能となっています。

具体的には以下のイメージです。

    def parse(self, response):
        ids = response.xpath('//div[@class="example"]/@id').extract()
        for id in ids:
            yield {
                "ID" : id
            }
        pass

scrapy runspiderを実行することでSpiderを実行することができます。 以下の例の場合、結果がexample.csvに出力されます。

scrapy runspider --nolog first_scrapy/spiders/example.py -o example.csv

なお、scrapy.Requestをコールすることで次のurlを引き続きスクレイピングさせることが出来ます。

# 同じメソッドでパースさせる場合
yield scrapy.Request(url)

# 別のパースメソッドでパースさせる場合
yield scrapy.Request(url ,callback=self.XXXXX)

トラブルシューティング

インストール時のエラー

Windows環境でpip3にてインストールを行う際に以下のエラーが発生しました。

error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools

2018/06/22現在、エラーのurlではダウンロードできなくなっていますが、microsoftからダウンロードできます。

scrapy runspiderの実行時エラー

scrapy runspiderを実行する際に以下のエラーが発生しました。

ImportError: No module named win32api

公式にもpywin32をインストールするように記載があったので、大人しくインストールしました。

参考

Javaでタイムスタンプを生成する方法

タイムスタンプサービスを利用すればタイムスタンプを取得することは出来るのですが、費用がかかってしまいます。
テスト等で利用したい場合に都合が悪いので、タイムスタンプを自前で生成する方法を調べてみました。

わかったこと

Bouncy Castleのorg.bouncycastle.tsp.TimeStampTokenGeneratorにて生成することが出来る。

TimeStampTokenGenerator (Bouncy Castle Library 1.59 API Specification)

実際に行ったこと

準備

タイムスタンプ生成に利用する秘密鍵と証明書を用意する。
OpenSSLが既にインストールされている場合、openssl.cnfに以下の設定を追加することで、
タイムスタンプ局の秘密鍵と証明書を発行出来ます。

設定

[ v3_tsa ]
extendedKeyUsage = critical,timeStamping

発行コマンド

openssl genrsa -out tsakey.pem 2048 # 秘密鍵を作成
openssl req -new -x509 -key tsakey.pem -out tsacert.pem  -extensions v3_tsa -days 10950 # タイムスタンプ局の証明書を発行

サンプルコード

publicbyte[] getTimeStampToken(byte[] messageImprint) throws IOException {
byte[] retVal = null;
try {
digest.reset();
byte[] hash = digest.digest(messageImprint);

// 32-bit cryptographic nonce
SecureRandom random = new SecureRandom();
int nonce = random.nextInt();

// generate TSA request
TimeStampRequestGenerator tsaGenerator = new TimeStampRequestGenerator();
tsaGenerator.setCertReq(true);
ASN1ObjectIdentifier oid = getHashObjectIdentifier(digest.getAlgorithm());
TimeStampRequest request = tsaGenerator.generate(oid, hash, BigInteger.valueOf(nonce));

// read pem file
PrivateKey privateKey = PEMUtil.pemToPrivateKey("tsakey.pem");
X509Certificate certificate = PEMUtil.pemToCertificate("tsacert.pem");

// generate TimeStampToken
SignerInfoGenerator signerInfoGenerator = new JcaSimpleSignerInfoGeneratorBuilder()
.build("SHA256WithRSAEncryption", privateKey, certificate);
DigestCalculator digestCalculator = new JcaDigestCalculatorProviderBuilder()
.setProvider(new BouncyCastleProvider()).build().get(signerInfoGenerator.getDigestAlgorithm());
TimeStampTokenGenerator tstGen = new TimeStampTokenGenerator(signerInfoGenerator, digestCalculator,
new ASN1ObjectIdentifier(ANY_POLICY));    // ANY_POLICY = "2.5.29.32.0"

List<X509Certificate> certList = new ArrayList<>(); // ここでaddCRLs、addCertificatesで設定しておかないと無効なタイムスタンプになってしまう
certList.add(certificate);
Store certs = new JcaCertStore(certList);
tstGen.addCRLs(certs);
tstGen.addCertificates(certs);
retVal = tstGen.generate(request, BigInteger.ONE, new Date()).getEncoded();    // 第二引数はタイムスタンプ局としてのシーケンシャルな値であるべきだが、ここでは固定とした
} catch (Exception e) {
thrownew IOException(e);
}

return retVal;
}