Slackのポストをリリース作業のチェックリストとして使ってみる

こんにちわ、サボっていて久しぶりにブログを投稿するURAです。
現在、maker town というデザインシミュレーター付クラウド型オンデマンドEC(長い!)なるものを開発中です。
maker town自体についてはまたの機会にご紹介するとして、今回はmaker townのリリース作業で煩雑に感じていた部分を、最近ちょっと改良してみたお話をしたいと思います。

前提

  • GitHub Flowを採用している
  • 機能追加・バグ修正をある程度まとめてリリースすることが多いため、事前にリリースする対象のリストを作っている。
  • リリース対象のリストはSlackで共有していた(リリースが終わったら流れてもOK)

問題点

  • プルリクエスト(PR)を出す人とリリース作業をする人が同じとは限らないため、すべてのPRのレビューが済んだのかが、リリース作業をする人から見て分かりづらい。
  • GitHubでリリース対象すべてのPRがCloseしたかチェックする作業が無駄。
  • リリースする人以外から見ても、リリース作業の進捗が分かりづらい。

解決策

Slackで共有していたリリース対象のリストを共同編集できるようにして、PRのレビューが済んだらチェックを付けて進捗を見える化する。

Slackのポスト機能をリストの共同編集に利用する!

「知らんけど、Slackになにかしら共同編集するための機能あるでしょ?」と言ってみたところ
たにすぎ池田がSlackのポスト機能を探してきてくれました。グッジョブ。

Slackのポストの使い方

1. 左のショートカットボタンから「ポスト」を検索して選択する

2. リリース対象のチェックリストを作成

3. 右上の「共有」を開いて「他のメンバーの編集を許可する」にチェックして共有

まとめ

改良したばかりでまだリリース作業がラクになったかは分かりませんが、運用しやすくなるよう開発フローは継続して改善していきたいです。

ものすごく基本的な話ですが…

はじめに

こんにちは、イメージ・マジックのもあいです。
bashで簡単なツールを作っているときに、つまらない内容ではまってしまったので、備忘録としておいておきます。

bashで関数

あまり、bashで関数は作らないのですが、スクリプトの長さがそこそこあるのとパラメータによる分岐があったので関数かしてソースコードを整理していた時に、スクリプトが正常に動作しなくなってしまいました。そのソースコードの主な抜粋は下記のようになります。
#!/bin/bash
FIRST=${1}

function add ()
{
  echo add function
}

function remove ()
{
  echo remove function;
}

if [ "${FIRST}" = "add" ]; then
  add
elif [ "${FIRST}" = "remove" ]; then
  remove
else
  echo ${0} add|remove 以下使い方
fi
そしてこのスクリプトを実行したときの結果は下記の通り
remove function
想定としては「add|remove 以下使い方」が表示されることでしたが、remove関数実行されてしまいました。

結論

結論はechoに|removeと書いてしまっているからでした。あまりBashで深くスクリプトを作成していないので、この行に行き着くのにものすごく時間がかかりました。このechoの後ろの文字を”で囲めば想定した動作になりました。 使い方として記載したechoで関数が呼び出されることになるのは考えていなかったのでびっくりしました。

どっちなの問題

こんにちは、イメージマジック三浦です。
弊社本社がある小石川一帯は最近大きいビルの建設が終わり、テナントも入り始めてきました。工事はまだまだ継続中ですが、工事が終わったらどんな街になっているのか気になります。
最近解釈が分かれやすい表現は極力避けたいと感じた例を見かけたので、人によって解釈が分かれる、または分かれやすい例を「どっちなの問題」と名付けて書いてみます。

こんな数式を見かけました

いきなりですが、四則演算の問題です。以下の式を計算するといくつになるでしょうか。
6÷2(1+2)
回答は2通りに分かれるそうです。
説1:6÷2(1+2)=3×3=9
説2:6÷2(1+2)=6÷6=1

真実が実は2つ?

回答が2通りに分かれるポイントは、カッコの中を計算した後の掛け算と割り算をどういう順番で扱っているかです。「2(1+2)」は「2×(1+2)」の掛け算記号が省略されているのですが、省略される掛け算を扱う優先順位が異なることで答えが2説出てきます。
  • 説1:カッコの中を計算した後、左から順番に割り算と掛け算を実行する。
  • 説2:カッコの中を計算した後、省略されている掛け算を優先して実行し、その後で割り算を実行する。

プログラム言語での扱い方

コンピューターではこの数式をどう処理するのか調べてみましたが、そもそもプログラム言語では掛け算記号の省略表記ができませんでした。参考として、手元にあるpython3.7で省略表記しようとした時のTraceback・説1の結果を出す式表記・説2の結果を出す式表記を書いた結果を掲載します。
(結果が少数表記になる点は、ここでは考慮しないことにします。)
Python 3.7.1 (v3.7.1:260ec2c36a, Oct 20 2018, 14:57:15) [MSC v.1915 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>
>>> 6/2(1+2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'int' object is not callable
>>>
>>> 6/2*(1+2)
9.0
>>>
>>> (6/2)*(1+2)
9.0
>>>
>>> 6/(2*(1+2))
1.0
>>>
カッコをつけることで、掛け算・割り算の優先順位が明示されています。

正しい答えはどっち?

正しい答えは説2の方でした。実際に関数電卓で計算してみると、説2の答えが出てきます。
これは解釈が分かれやすい「どっちなの問題」です。

その他の「どっちなの問題」

今回のテーマから派生して、解釈が分かれる「どっちなの問題」の例を挙げます。

0の0乗はいくつ?

「xの0乗」は、0でない実数(-1のルートを考えなくてよい世界)では常に1ですが、0の0乗はどうなるのかは数学の中でも統一されていません。1と決めると都合がいい分野と「定義しない」と決めると都合がいい分野があり、各分野が都合のよい方を定義に採用して理論構築しています。

旧暦2033年問題

今使われている「グレゴリオ暦」のひとつ前「天保暦」の問題です。天保暦では2月・5月・8月・11月を設定し、設定後に他の月を設定します。しかし、100年に一回程、設定できる月が1つに定められないという事態が発生するケースがあり、直近では2033年~2034年に起こるというものです。
俳句で使う季語は旧暦の春夏秋冬で区分けされる等、決められないことによる影響は小さくないようです。こちらは一定のルールを決めて解決しようとしているようですが、解決方法が複数あって検討中のようです。

詰将棋【最後の審判】

詰将棋は、限られた自軍の駒を動かしながら相手の王将を詰ますパズルで、本将棋のルールに加えて詰将棋独自のルールがあります。本将棋のルールでは「禁じ手」が規定されていますが、「最後の審判」は詰将棋のルール上「禁じ手」を指すしかない状況が双方で生じた時に勝敗を判断できない、という状態が発生する作品です。
便宜上、詰将棋の1種として記載していますが、実は詰将棋として成立するかも結論が出ていません。 こちらについて興味のある方は、【詰将棋 最後の審判】で検索してみてください。

注:単に【最後の審判】で検索すると、ミケランジェロの「最後の審判」がヒットします。これも 「最後の審判」と言われて、絵なのか詰将棋なのかで解釈が分かれる「どっちなの問題」の一例です。

Doctrine SQL Filterでマルチテナントを実現する

こんにちは、岡野です。

最近マルチテナントのシステムを開発することがあり、共通のDBをテナントごとに管理する方法を調べました。

結論

Doctrineの以下機能を使用するとCRUDの各SQLを改変でき、テナントを透過的に制御可能(すなわち毎回WHERE句にテナントの条件を書かなくて良い)。 但し、@OneToOneは期待通りに動作しないため要注意。

環境

Doctrine 2.7.4
Symfony 4.4.16

SELECTの調査結果

以下の様なSQL Filterを登録。
class TenantFilter extends SQLFilter
{
    public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
    {
        // 100は実際には可変(後述)
        return $targetTableAlias.'.tenant_id = 100';
    }
}

単純なSELECT

$em->find(Parent::class, 1);
→ SELECT ... FROM parent WHERE id = 1 AND tenant_id = 100;

@OneToMany

$em->find(Parent::class, 1)->getChilds();
→ SELECT ... FROM parent WHERE id = 1 AND tenant_id = 100;
→ SELECT ... FROM child WHERE parent_id = 1 AND tenant_id = 100;

@ManyToOne

@OneToMany同様。

@ManyToMany

@OneToMany同様。

@OneToOne

$em->find(Example1::class, 1)->getExample2();
→ SELECT ... FROM example1 e1 LEFT JOIN example2 e2 ON e2.example1_id = e1.id WHERE e1.id = 1 AND e1.tenant_id = 100;
⇒ example2テーブルへのtenant_id = 100が存在しない

join()

$em->createQueryBuilder('c')->join('c.parent')->where('c.id = 1');
→ SELECT ... FROM child c JOIN parent p ON c.parent_id = p.id AND p.tenant_id = 100 WHERE c.id = 1 AND c.tenant_id = 100;

leftJoin()

join()同様。



INSERT/UPDATE/DELETEの調査結果

以下の様なDoctrine Event Listenerを登録。
class TenantListener
{
    public function prePersist(LifecycleEventArgs $args): void
    {
        assert($args->getEntity()->getTenant() === null);
        // 100は実際には可変
        $args->getEntity()->setTenant($this-em->find(Tenant::class, 100));
    }
    public function preUpdate(LifecycleEventArgs $args): void
    {
        // 安全のため他テナントを操作していないかチェックする
        if ($args->getEntity()->getTenant()->getId() !== 100) {
            throw new \LogicException();
        }
    }
    public function preRemove(LifecycleEventArgs $args): void
    {
        // preUpdate()同様
    }
}

INSERT

$em->persist(new Example());
→ INSERT INTO example (..., tenant_id) VALUES (..., 100);


その他

上記のようにDoctrine FilterのaddFilterConstraint()内でtenant_idを直接設定するとDoctrine Query CacheによりSQLが固定化されてしまい期待通りに動作しない。そのため以下の通りparameterを経由する必要があった。

// 外部から呼ぶ(RequestListenerなどから)
public function setTenantId(int $tenantId): void
{
    $this->setParameter('tenant_id', $tenantId, Types::INTEGER);
}

public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
    $tenantId = (int)$this->unquoteParameterValue($this->getParameter('tenant_id'));
    return $targetTableAlias.'.tenant_id = '.$tenantId;
}

private function unquoteParameterValue(string $value): string
{
    $len = strlen($value);
    if ($value[0] !== "'" || $value[$len - 1] !== "'") {
        throw new \LogicException($value);
    }
    return str_replace(['\\"', '\\\\'], ['"', '\\'], substr($value, 1, $len - 2));
}

Vue.jsでそれっぽい何かを作る2(改造)

株式会社イメージ・マジックの技術ブログ、今回の担当のsoenoです。 実務でいろいろ触ってはいるのですが。投稿しやすいコンテンツとなるとどうにも…。 というわけで以前の投稿に乗っかろうと思います。


何をするか

以前の投稿で作成したVueの機能を流用し、改造、改善していきます。

何を変えるか考える

前の機能の印象から変更点を考える

  1. webなのに白黒ってどうなのだろう。
    • 色を増やす
    • 色を増やせるように色の選択機能を追加 
  2. 見本と入力エリアが同じ大きさってどうなのだろう。(比較はしやすい?)
    • サイズを変える
  3. 升目少ない。
    • 増やす。
  4. どこ操作していいかわからない。
    • 入力エリアに枠を付けて目立たせる
  5. そろったかどうかどう確認するかわかりにくい(今は自動で勝手にチェックする)
    • チェックボタンを押すまでは比較しない
  6. 大人よりは子供向けのゲームになるだろうな。
    • ひらがなにする。

結果

できました!
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>vue</title>
    <style type="text/css">
      .wrapStyle{
          margin: 0 50px;
          display: block;
          position: relative;
          float: left;
      }
      .celAreaWrap{
        border: solid 2px #414141;
      }
      .celWrap{
          display: inline-block;
          box-sizing: content-box;
          margin:0;
          display: inline-block;
      }
      .cel{
          border:solid thin #eeeeee; 
          box-sizing: border-box;
          display: inline-block;
          margin:0;
          padding:0;
      }
      .colorCel{
        border: solid 2px #666666;
      }
      .btn{
        margin-top: 50px;
        border: solid 2px #666666;
        padding: 15px;
        min-width: 121px;
        text-align: center;
        background: #ffa500;
      }
      .cel_clr0{
          background-color:#ffffff;
      }
      .cel_clr1{
          background-color:#111111;
      }
      .cel_clr2{
          background-color:#ff0000;
      }
      .cel_clr3{
          background-color:#fc5000;
      }
      .cel_clr4{
          background-color:#f8ab04;
      }
      .cel_clr5{
          background-color:#48ff00;
      }
      .cel_clr6{
          background-color:#09f7ff;
      }
      .cel_clr7{
          background-color:#002fff;
      }
      .cel_clr8{
          background-color:#a304ff;
      }
    </style>
</head>
<body>
<div id="app">
  <div class="wrapStyle" v-bind:style="mihonBoxStyle">
    <p>みほん</p>
    <div v-for="(items, yoko) in resultList" v-bind:style="mihonCelWrap">
      <div v-for="(item, tate) in items" 
            v-bind:style="mihonCelStyle" class="cel" 
            v-bind:class="'cel_clr' + resultList[yoko][tate]">
      </div> 
    </div>
  </div>
  <div class="wrapStyle" v-bind:style="boxStyle">
    <p>みほんをまねしてね</p>
    <div class="celAreaWrap">
      <div v-for="(items, yoko) in userList" v-bind:style="celWrap">
        <div v-for="(item, tate) in items"
            v-on:click="changeCel(userList[yoko][tate])" :data-yoko="yoko" :data-tate="tate"
            v-bind:style="celStyle" class="cel"
            v-bind:class="'cel_clr' + userList[yoko][tate]">
        </div>
      </div>
    </div>
    <div>
      <p>いろをえらぼう</p>
      <div v-bind:style="celWrap">
        <div v-for="id in colorIdList" v-on:click="changeColor(id)" v-bind:style="celStyle" class="cel colorCel" v-bind:class="'cel_clr' + id">
        </div>
      </div>
    </div>
  </div>
  <div class="wrapStyle">
      <div v-on:click="check()" class="btn">そろったかな?</div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script language="javascript">
new Vue({
  el: '#app',
  data: {
    cellSize:60,
    nowColor:1,
    resultList: [
      [0,0,1,1,1,1,1,0,0],
      [0,1,4,4,4,4,4,1,0],
      [1,4,4,1,4,1,4,4,1],
      [1,4,4,4,4,4,4,4,1],
      [1,4,1,4,4,4,1,4,1],
      [1,4,1,4,4,4,1,4,1],
      [1,4,4,1,1,1,4,4,1],
      [0,1,4,4,4,4,4,1,0],
      [0,0,1,1,1,1,1,0,0],
    ],
    userList: [
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
      [0,0,0,0,0,0,0,0,0],
    ],
    
    colorIdList:[0,1,2,3,4,5,6,7,8],
    
    mihonBoxStyle:{
      width:0,
    },
    mihonCelWrap:{
      height:0,
    },
    mihonCelStyle:{
      width:0,
      height:0,
    },

    boxStyle:{
      width:0,
    },
    celWrap:{
      height:0,
    },
    celStyle:{
      width:0,
      height:0,
    },
  },
  created() {
    this.mihonCelStyle.width = this.cellSize/3+ 'px';
    this.mihonCelStyle.height = this.cellSize/3+ 'px';
    this.mihonCelWrap.height = this.cellSize/3+ 'px';
    this.mihonBoxStyle.width = this.mihonCelWrap * this.resultList[0].length + 'px';

    this.celStyle.width = this.cellSize+ 'px';
    this.celStyle.height = this.cellSize+ 'px';
    this.celWrap.height = this.cellSize+ 'px';
    this.boxStyle.width = this.cellSize * this.resultList[0].length + 4 + 'px';//4はボーダー
  },
  
  methods: {
    changeColor(id) {
      this.nowColor = id;
    },
    changeCel() {
      const yoko = parseInt(event.currentTarget.dataset.yoko);
      const tate = parseInt(event.currentTarget.dataset.tate);
      Vue.set(this.userList[yoko], tate, this.nowColor);
    },
    //確認ボタンが押されたら揃ったか確認する。
    check() {
      for(let i=0,len = this.resultList.length; i<len; i++){
        for(let ii=0,len2 = this.resultList[i].length; ii<len2; ii++){
          if(this.userList[i][ii] !== this.resultList[i][ii]){
            //不一致があったら処理を抜ける
            alert("まだそろってません。");
            return;
          }
        }
      }
      setTimeout(function () {
        alert("そろいました。おめでとうございます!");
      }, 100);
    },
  },
})
</script>
</body>
</html>

puppeteerでスクレイピングしてブログの投稿を検知する

こんにちは、イメージ・マジックの池田です。
今回はタイトルにもある通り、puppeteerのスクレイピング(とついでにブログの投稿検知)についてやっていきます。なぜなら最近プライベートで触ったからという、とても個人的な理由です。

puppeteerとは?

puppeteerというのは、Node.jsを用いてスクレイピングをするライブラリです。
はい、死ぬほど雑な説明をしました。違うわ!とまではいかないまでもお叱りを受けそうですね。詳しくは公式サイトをご覧ください。公式の英語サイトに投げる暴挙。とにかくNode.jsでスクレイピングのできるライブラリです。しかもChromeを作っているGoogle製。使わない理由はありませんね。

どうやって使うの?

  1. Node.jsの最新版をインストールします
  2. 好きな場所にフォルダを作り、npm init -yと打ち、npm i puppeteerします
用意はこれだけです。試しに公式サイトに載っているコードを実行してみましょう。同じフォルダにexample.jsを作成し、以下のコードを打ち込み、PowerShellなどからnode example.jsで実行してみてください。
const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  await page.screenshot({ path: 'example.png' });

  await browser.close();
})();
同じフォルダにexample.pngというファイルが作成されたと思います。ファイルには見知らぬサイトのスクリーンショットが。puppeteerがブラウザを画面に出したりせずに撮ってきてくれたスクリーンショットです。実際にこのページにアクセスしてみると、画像そのままのサイトが表示されたと思います。
これを応用することで、ブログの投稿を検知することが可能となります。RSSがあればこういうことをしなくても大丈夫だったりしますが、そもそもRSSが下火だったり、RSSを提供するようなサイトじゃなかったりもしますからね。

プログラムの流れ

前提条件:昨日や一週間前に取ってきたタイトル一覧を手元に持っている(持っていなければ、下記手順の1~2、5だけを行います)
  1. ブログにアクセスする
  2. タイトル一覧を取得する
  3. 手元で持っているタイトル一覧と照合する
  4. 手元にないタイトルがあれば、そのタイトルを通知する
  5. 最新のタイトル一覧を手元に保存する
  6. おわり
簡単ですね。これをTypeScriptで書いたコードが下記です。私はVisual Studio Codeと組み合わせて使うTypeScriptが大好きなので、本来ならばこれの宣伝もしたいのですが、TypeScriptの素晴らしさは恐らく全ての生物が既に知っているので、ここでは取り扱いません。
import puppeteer, { Page } from "puppeteer";
import { readFileSync, writeFileSync } from "fs";
import csvParse = require("csv-parse/lib/sync")
import csvStringify = require("csv-stringify/lib/sync")
import { difference } from "./setUtils";

const CURRENT_FILE_NAME = 'current.csv';
const TITLE_SELECTOR = '.entry-title';

const readCurrentCsv = (): Set<string> | null => {
  try {
    const file = readFileSync(CURRENT_FILE_NAME);
    return new Set((csvParse(file) as Array<Array<string>>).flat());
  } catch (error) {
    // Error NO ENTryらしいです
    if (error.code === 'ENOENT') {
      return null;
    } else {
      throw new Error("知らんエラー。君誰?");
    }
  }
}

const fetchTitles = async (page: Page): Promise<Set<string>> => {
  const elements = await page.$$(TITLE_SELECTOR);
  return new Set(await Promise.all(elements.map(async ele => (await ele.getProperty('textContent'))!.jsonValue())) as Array<string>);
}

const writeCsv = (newTitles: Set<string>) => {
  writeFileSync(CURRENT_FILE_NAME, csvStringify([...newTitles].map(title => [title])));
}

const notify = (diffTitles: Set<string>) => {
  console.info(`
「${[...diffTitles].join('、')}」が開発者ブログに投稿されました。見てみましょう!
https://techblog.imagemagic.jp/
`.trim());
  // みたいのを通知したりしたいよね
}

const main = async (page: Page) => {
  const currentTitles = readCurrentCsv();
  const newTitles = await fetchTitles(page);

  if (currentTitles === null) {
    writeCsv(newTitles);
    return;
  }

  const diffTitles = difference(newTitles, currentTitles);
  if (!diffTitles.size) {
    return;
  }

  notify(diffTitles);
  writeCsv(newTitles);
}

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://techblog.imagemagic.jp/');

  try {
    await main(page);
  } finally {
    await browser.close();
  }
})();
main関数の中身を見れば、先程のプログラムの流れと大体同じなことがわかると思います。このプログラムをたとえばAWSのLambdaに乗せて、CloudWatchで定期的に叩くなどすれば、差分検知通知システムの完成です。
細かいコードの解説は省きます。皆さんも興味のあるサイト(たとえば本テックブログ!)をスクレイピングしたりして、実際に触ってみてください。

※スクレイピングは少し油断するとアクセス負荷を相手のサーバーにかけてしまったり、そもそもスクレイピングを許可していないサイトもあります。使い方を間違えれば違法行為です。この辺りのサイトを見つつ、用法用量を守って正しくお使いください。

Markdownについて

はじめに

Suzukiです。
今回はmarkdownについて書きます。

Markdown記法とは

Markdown記法とは軽量マークアップ言語の1つです。
記法については以下の公式ドキュメントを参考にしてください。
参考:mermaid公式ドキュメント -> https://mermaid-js.github.io/mermaid/#/

図表を描くならmermaid

簡単な図を表すのに便利です。
試しに書いてみると、   日本の地方別人口の円グラフ
```mermaid
pie title 日本の地域別人口(万人)(平成 22 年)
    "関東" : 4175
    "近畿" : 2250
    "中部" : 2157
    "九州" : 1467
    "東北" : 942
    "中国" : 758
    "北海道" : 552
    "四国" : 404
```
というような円グラフが描けます。
ライブエディタは以下にあります。
参考:Mermaid Live Editor -> https://mermaid-js.github.io/mermaid-live-editor/#/edit/eyJjb2RlIjoiZ3JhcGggVERcbiAgICBBW0NocmlzdG1hc10gLS0-fEdldCBtb25leXwgQihHbyBzaG9wcGluZylcbiAgICBCIC0tPiBDe0xldCBtZSB0aGlua31cbiAgICBDIC0tPnxPbmV8IERbTGFwdG9wXVxuICAgIEMgLS0-fFR3b3wgRVtpUGhvbmVdXG4gICAgQyAtLT58VGhyZWV8IEZbZmE6ZmEtY2FyIENhcl0iLCJtZXJtYWlkIjp7fSwidXBkYXRlRWRpdG9yIjpmYWxzZX0
 

メリット

HTMLで書くより簡単に記述できます。
またHTMLのタグも使用できるので便利です。
PDFに変換することで配布しやすくなります。

実際の使用法

markdownで文章を書いてmermaidで図を描きこみ、PDFに変換して配りやすくして使っています。
最近では、業務の作業手順書をmarkdownで書きPDFに変換して関係部署に配布しました。

余談

実は今書いているのもmarkdownエディタを使って書いています(笑)
右にプレビューを出しながら書くと、見栄えを確認しながら書けるので便利ですね。

キーマクロを作ってみた話

こんにちは。イメージマジック三浦です。昼食のために外に出たある日、風から感じる香りが変わったなと感じます。もうすぐ春ですね。
今回はnginxのログを解析するために、サクラエディタのキーマクロを作った話です。

キーマクロを作った経緯

システムのパフォーマンスが悪い時期があり、ボトルネック調査の一環でnginxのログを解析していました。 解析時はExcelやGoogleスプレッドシートでフィルタリングできるように、テキスト置換を繰り返してきましたが、次第にパターンが決まってきて、置換自体を手動リプレイできる状態になってきました。そしてはたと気づきました。
 

ここまでパターン化できているならキーマクロで自動化しよう!

キーマクロの記録方法

ここで、キーマクロの記録について触れておきます。
キーマクロは「ツール」>「キーマクロの記録開始」とクリックすることで、記録開始できます。タイトルバーに「キーマクロの記録中」と出てきます。
上の状態で「ツール」>「キーマクロの記録終了」とクリックすると、キーマクロ記録を終了できます。

簡単なキーマクロを作ってみる

まずは簡単なキーマクロを作ってみます。

(1,2,3,4,5)
を正規表現置換で
(1)
(2)
(3)
(4)
(5)
のようにする操作をキーマクロで保存してみます。 正規表現は以下の通りです。
「ツール」>「キーマクロの読込」とすると、以下のファイルが出てきます。
このパス内にある「RecKey.mac」が、先ほど記録したキーマクロの実体ファイルです。このファイルをデスクトップなどにコピーします(元の場所におくと次回のキーマクロ記録で消えてしまうため)。

キーマクロ機能拡張

先のキーマクロの中身はこのようになっています。
S_ReplaceAll(',', '\\)\\r\\n\\(', 1068);  // すべて置換
S_ReDraw(0);    // 再描画
Excelできれいに貼り付けられるようにしたいという目的を踏まえ、キーマクロを拡張していきます。今回は以下のような方針で拡張しました。
  • S_ReplaceAll のパターンを増やして、異なる方法での置換を実行する
  • S_ReDrow(0)を最後に呼び出すことで、画面再描画は最小限にする
  • 1行をタブで区切られた時の要素数が均一かつ、Excelに貼り付けた時に情報がきれいに出る。
拡張したキーマクロの一部を紹介します。
// HTTPメソッドの分割
S_ReplaceAll('\"(GET|POST|HEAD) ', '\t$1\t', 1068);
// HTTP Status の分割
S_ReplaceAll(' ([2-5][0-9][0-9]) ', '\t$1\t', 1068);
// 再描画
S_ReDraw(0);
拡張したキーマクロは、共通設定の「マクロ」タブで登録し、メニューから実行できるようにします。そこから先は置換結果やExcelに貼りつけた結果を確認しながら、正規表現を調整していきます。

仕上げ

Excelへの貼り付けを繰り返すうちに、以下の点も入れたいという気持ちが出てきたので、拡張中のキーマクロに組み込みます。
  • 先頭に項目行を入れたい
  • 置換後は保存しておいて開いたらすぐに貼り付けられるようにしたい
コード自体はキーマクロの記録で生成されたものをコピペします。
仕上げまで実施し、最終的には以下の通りになりました。
// ファイルの先頭に移動
S_GoFileTop(0);
// 項目行を追加
S_InsText(′項目1\t項目2\…');

// HTTPメソッドの分割
S_ReplaceAll('\"(GET|POST|HEAD) ', '\t$1\t', 1068);
// HTTP Status の分割
S_ReplaceAll(' ([2-5][0-9][0-9]) ', '\t$1\t', 1068);
(他いろいろ)
// 再描画
S_ReDraw(0);

// 保存
FileSave();
これで、アクセスログを整形しExcelに貼り付ける直前まで整形する操作のキーマクロができました。作ってみると非常に便利で、心の中でドヤ顔しています。

最後に

冒頭に記載したボトルネックについては、部員の皆様の活躍により、現在は改善されています。

UWPアプリからOpenCVしてみる(その2)

矢野です。UWPアプリからOpenCVを使う話の決着をつけておきます。 Visual Studioを起動して、メインページにボタンを追加します。 
NuGetを使って、OpenCvSharp(OpenCvSharp4, OpenCvSharp4.runtime.uwp)をインストールします。 ボタンのクリックイベントハンドラ(find_Click)に、カメラからのキャプチャ、データ変換、ARマーカーの検出、結果を表示という流れでコードを書いていきます。
private async void find_Click(object sender, Windows.UI.Xaml.RoutedEventArgs e)
{
    ImageEncodingProperties encProp = ImageEncodingProperties.CreateBmp();

    var source = captureElement.Source;

    SoftwareBitmap bitmap = null;
    using (var captureStream = new InMemoryRandomAccessStream())
    {
        await source.CapturePhotoToStreamAsync(encProp, captureStream);
        await captureStream.FlushAsync();
        captureStream.Seek(0);

        var decoder = await BitmapDecoder.CreateAsync(captureStream);
        bitmap = await decoder.GetSoftwareBitmapAsync(
            BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
    }

    Mat mat = null;
    using (var stream = new InMemoryRandomAccessStream())
    {
        var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, stream);
        encoder.SetSoftwareBitmap(bitmap);
        await encoder.FlushAsync();

        mat = Mat.FromStream(stream.AsStreamForRead(), ImreadModes.Color);
    }

    var grayMat = new Mat();
    Cv2.CvtColor(mat, grayMat, ColorConversionCodes.BGR2GRAY);

    CvAruco.DetectMarkers(grayMat,
        CvAruco.GetPredefinedDictionary(PredefinedDictionaryName.Dict4X4_50),
        out Point2f[][] corners, out int[] ids, DetectorParameters.Create(), out Point2f[][] rejectedImgPoints);

    CvAruco.DrawDetectedMarkers(mat, corners, ids);

    using (var stream = new MemoryStream())
    {
        mat.WriteToStream(stream);
        using (var raStream = stream.AsRandomAccessStream())
        {
            var decoder = await BitmapDecoder.CreateAsync(raStream);
            bitmap = await decoder.GetSoftwareBitmapAsync(
                BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
        }
    }

    var imageSource = new SoftwareBitmapSource();
    await imageSource.SetBitmapAsync(bitmap);
    image.Source = imageSource;
}
OpenCvSharpは優れたライブラリで、.NETアプリケーションからOpenCVを使いやすくラップしてくれています。おかげでOpenCVでのARマーカー検出、描画は難しくありませんでした。 ところが、それ以前にUWPのSoftwareBitmapとOpenCVのMat型の相互の変換についてはなかなか消化することができず、あちこちのオンラインドキュメントやブログやフォーラムを何度も何度も巡回しつつ、試行錯誤しつつ行ったり来たりを繰り返してようやくそれらしい形にたどり着きました。 あまりそんなことばっかりやっているわけにはいきませんが、ある程度余裕を持て、藻掻いて、悩む時間も時には大事ですね。
これでもって改めて前回の記事をカメラで撮ってみる(横着)と、無事にARマーカーとそのID(3)が認識されていることが確認できました。

PHP7.4移行作業の苦労小話

はじめに

こんにちは、イメージ・マジックのもあいです。 今回は直近で作業をしていたPHP7.0からPHP7.4への移行作業について、手動での対応ではまった点についての備忘録です。

プロパティの型が指定できることによって・・・

PHP7.4の大きな違いの一つとしてはプロパティの型が明示できるようになったことがあると思いますが、これにより移行作業が思った以上に手間だったという印象がありました。自分が体験した内容を記していきます。 サンプルとして下記コードを用意しました。

<?php
declare(strict_types=1);

class Hoge {
        private $prop1;
        private string $prop2;
        public function getProp1() {
                return $this->prop1;
        }
        public function getProp2():string {
                return $this->prop2;
        }
}
$a = new Hoge();
var_dump($a->getProp1());
var_dump($a->getProp2());

サンプルとして用意したコードですが、このコードは16行目でエラーとなり実行が止まります。Hogeクラスのプロパティprop2ですが、string型を明示しているため、初期化しないでアクセスすると下記のようなエラーとなります。

PHP Fatal error:  Uncaught Error: Typed property Hoge::$prop2 must not be accessed before initialization in /home/moai/sample/test01.php:11

昔からPHPコードを書いていた人間としては、ここまでチェックされるとPHPの簡単なことが簡単なコードで実現できるという手軽さが無くなってきている印象を受けます。Javaのような言語に比べれば全然マシですけれども。それだけPHPが使われており単純なミスを抑止できるように型チェックをするようになっているとも言えます。 上記のエラーですが対応方法はいくつかあって、コンストラクタで初期化する、nullで初期化する、プロパティを参照するところで\Errorをcatchする、といったことができます。さすがに\Errorをcatchはやり過ぎな気もします。nullで初期化するのが妥当な気がします。

最後に

そのほかに、PHPが用意している文字列操作の関数引数にnullが入るとエラーになるという地味に面倒な物もありましたが、null合体演算子を入れれば比較的簡単に解決できますが、使用箇所が多いと地味に面倒でした。