シェルスクリプトマガジン

test

香川大学SLPからお届け!(Vol.67掲載)

投稿日:2020.07.25 | カテゴリー: コード

著者:山内 真仁

以前、ゲームエンジン「Unity」を使ったレースゲームをSLPのチーム活動で
作成しました。その経験を生かして今回は、Unityとプログラミング言語「C#」を使って、簡単なレースゲームを作成する方法を紹介します。Unityの物理エンジンを使用することで、複雑なコードを書かなくてもリアルな挙動のゲームを作成できます。

シェルスクリプトマガジン Vol.67は以下のリンク先でご購入できます。

図12 「CarController.cs」ファイルに記述する内容

using UnityEngine;

public class CarController : MonoBehaviour
{
  public GameObject[] wheelMeshes = new GameObject[4];  // ホイールオブジェクトを格納する配列
  public WheelCollider[] wheelColliders = new WheelCollider[4];  // Wheel Colliderを格納する配列
  public float maxMotorTorque = 300f;     // 車輪に加える最大トルク
  public float brakeTorque = 500f;        // ブレーキのトルク
  public float maxSteerAngle = 25f;       // ステアリングの最大舵角
  float accel, steer;                     // アクセルとステアリングの入力値
  bool brake;                             // ブレーキをかけているかどうか
  // 画面を描画するたびに実行されるメソッド(実行間隔はフレームレートに依存)
  void Update()
  {
    steer = Input.GetAxis("Horizontal");    // ←→で旋回
    accel = Input.GetAxis("Vertical");      // ↑↓でアクセル
    brake = Input.GetKey(KeyCode.Space);    // スペースでブレーキ
    // Wheel Colliderの動きを見た目に反映する
    for (int i = 0; i < 4; i++) {
      wheelColliders[i].GetWorldPose(out Vector3 position, out Quaternion rotation);
      wheelMeshes[i].transform.position = position;
      wheelMeshes[i].transform.rotation = rotation;
    }
  }
  // フレームレートに依存せず、定期的に実行されるメソッド(0.02秒に1回)
  void FixedUpdate()
  {
    // Wheel Colliderに各パラメータを代入
    for(int i = 0; i < 4; i++) {
      if (i < 2) wheelColliders[i].steerAngle = steer * maxSteerAngle;  // ステアリング(前輪)
      wheelColliders[i].motorTorque = accel * maxMotorTorque;           // アクセル
      wheelColliders[i].brakeTorque = brake ? brakeTorque : 0f;         // ブレーキ
    }
  }
}

図14 CarController.csファイルに追加する記述

  GameObject brakeLight, headLight;  // ランプ類のオブジェクトを格納する変数
  // ゲーム開始時に1回のみ実行されるメソッド
  void Start()
  {
    // ランプ類のオブジェクトを探して取得
    brakeLight = GameObject.Find("SkyCarBrakeLightsGlow");
    headLight = GameObject.Find("SkyCarHeadLightsGlow");
  }
  void Update()
  {
    // ランプ類の点灯・消灯
    brakeLight.SetActive(brake);
    if (Input.GetKeyDown(KeyCode.H)) {
      headLight.SetActive(!headLight.activeSelf);
    }

図16 「Timer.cs」ファイルに記述する内容

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class Timer : MonoBehaviour {
  public Text timeText;     // タイム表示用のテキスト
  float startTime;          // 計測開始の時刻
  bool start, check, goal;  // 各地点の通過フラグ
  void Start() {
    // オブジェクトのコンポーネントを取得
    timeText = GameObject.Find("TimeText").GetComponent<Text>();
    timeText.text = "TIME  00.000";  // テキストの初期化
  }
  void Update() {
    // スタートしてからゴールするまでタイムを表示
    if (!goal && start)
      timeText.text = "TIME  " + (Time.time - startTime).ToString("00.000");
    // ゴール後に[R]キーを押すとリスタート
    if (goal && Input.GetKey(KeyCode.R))
      SceneManager.LoadScene(SceneManager.GetActiveScene().name);
  }
  // トリガーオブジェクトに侵入した時に呼び出されるメソッド
  void OnTriggerEnter(Collider other) {
    if (other.gameObject.name == "StartPoint") {
      if (check) {
        goal = true;  // チェックポイント通過済みならゴール
        timeText.color = Color.red;
      } else if (!start && !check) {
        start = true; // チェックポイントを通過していない場合、タイム計測開始
        startTime = Time.time;
      }
    } else if (start && other.gameObject.name == "CheckPoint")
      check = true;   // チェックポイントを通過
  }
}

Webアプリケーションの正しい作り方(Vol.67記載)

投稿日:2020.07.25 | カテゴリー: コード

著者:しょっさん

ソフトウエアを正しく作るために、エンジニアたちはどんなことを知らなければならないでしょうか。実際のコードを使って、より良くしていくためのステップを考えてみましょう。第7回は、JavaScriptフレームワーク「Vue.js」でアプリを作成し、テストとリリースの方法を紹介します。

シェルスクリプトマガジン Vol.67は以下のリンク先でご購入できます。

図2 Vue.js および各ライブラリを利用するための定義(simple.htmlから抜粋)

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.11"></script>
<script src="https://unpkg.com/vue-router@3.3.2/dist/vue-router.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@0.19.2/dist/axios.min.js"></script>
<script src="./jwt-decode.min.js"></script>

図3 Vue RouterのHTMLファイル側の定義

<div id="app">
  <h1>経費精算アプリケーション(Vue.js)</h1>
  <router-link to="/expense">経費登録</router-link>
  <router-link to="/payment">経費精算</router-link>
  <router-link to="/login">ログイン</router-link>
  <router-link to="/logout">ログアウト</router-link>
  <router-view />
</div>

図4 Vue RouterのJavaScript側の定義

const router = new VueRouter({
  routes: [
    // 精算処理
    {
      path: "/expense",
    },
    // 支払処理
    {
      path: "/payment",
    },
    // 認証 - ログイン
    {
      path: "/login",
    },
    // 認証 - ログアウト
    {
      path: "/logout",
    },
    // どのURLにも該当しなかった場合
    {
      path: "*",
      redirect: "/expense",
    },
  ],
});

図5 認証部分のVueコンポーネントとVue Router定義

const Login = {
  template: "#login",
  data: function () {
    return {
      user: localStorage.user || "",
      password: localStorage.password || "",
      remember: localStorage.remember || false,
      error: false,
    };
  },
  methods: {
    login: function () {
      axios
        .post(${baseUrl}/api/auth, {
          user: this.user,
          password: this.password,
        })
        .then((response) => {
          if (response.status === 200) {
            // ログインが成功した場合は、ローカルストレージにトークンを保管する(ログインが成功した状態とする)
            this.error = false;
            localStorage.token = response.data.token;
            // "remember me" チェックボックスが付いていたら、各々の入力項目を保管する
            if (this.remember) {
              localStorage.user = this.user;
              localStorage.password = this.password;
              localStorage.remember = true;
            } else {
              // 逆にオフであれば入力項目の内容を破棄する
              delete localStorage.user;
              delete localStorage.password;
              delete localStorage.remember;
            }
            // ログイン成功したら /expense へ切り替える
            this.$router.replace("/expense");
          } else {
            this.error = true;
          }
        })
        .catch((response) => {
          this.error = true;
          this.remember = false;
          console.log(response);
        });
    },
  },
};

const router = new VueRouter({
  routes: [
(略)
    // ログイン画面
    {
      path: "/login",
      component: Login,
    },
    // ログアウト処理
    {
      path: "/logout",
      beforeEnter: (to, from, next) => {
        delete localStorage.token;
        next("/login");
      },
    },
  ],
});

図6 認証部分のHTMLテンプレート

<script type="text/x-template" id="login">
  <form @submit.prevent="login">
    <input v-model="user" type="email" placeholder="Your Email" autofocus="" />
    <input v-model="password" type="password" placeholder="Your Password" />
    <button type="submit">ログイン</button>
  </form>
  <div v-show="error">
    <p>ログイン失敗</p>
  </div>
</script>

図7 請求処理ののVueコンポーネントとVue Router定義

const Expense = {
  template: "#expense",
  data: function () {
    let decoded = {};
    if (localStorage.token) {
      // トークン内のユーザー情報を基に変数へ配置
      decoded = jwt_decode(localStorage.token);
    }
    return {
      user: decoded.email || "",
      id: decoded.id || "",
      user_name: decoded.user_name || "",
      date: "",
      type: "",
      amount: "",
      description: "",
      error: false,
    };
  },
  // 経費を登録するメソッド
  methods: {
    expense: function () {
      axios
        .post(
          ${baseUrl}/api/expense,
          {
            user: this.user,
            user_id: this.id,
            user_name: this.user_name,
            date: this.date,
            type: this.type,
            amount: this.amount,
            description: this.description,
          },
          {
            headers: {
              Authorization: Bearer ${localStorage.token},
            },
          }
        )
        .then((response) => {
          if (response.status === 200) {
            // 正常に登録できた場合は、変更が伴うフィールドをクリアーして、再度入力可能な状態にする
            this.error = false;
            console.log(response);
            this.date = "";
            this.type = "";
            this.amount = "";
            this.description = "";
          }
        })
        .catch((response) => {
          this.error = true;
          console.log(response);
        });
    },
  },
};

const router = new VueRouter({
  routes: [
    // 経費登録
    {
      path: "/expense",
      component: Expense,
      beforeEnter: (to, from, next) => {
        // 認証前の場合は /login ページへ遷移する
        if (!localStorage.token) {
          next({
            path: "/login",
            query: { redirect: to.fullPath },
          });
        } else {
          next();
        }
      },
    },
(略)
});

図8 請求処理のHTMLテンプレート

<script type="text/x-template" id="expense">
  <div>
    <form @submit.prevent="expense">
      <input v-model="user_name" type="text" placeholder="Your Name" />
      <input v-model="user" type="email" placeholder="Your Email" />
      <input v-model="id" type="hidden" placeholder="Your User ID" />
      <input v-model="date" type="datetime-local" placeholder="経費利用日" autofocus="" />
      <input v-model="type" type="text" placeholder="費目" />
      <input v-model="amount" type="number" placeholder="金額" />
      <input v-model="description" type="text" placeholder="費用詳細" />
      <button type="submit">経費申請</button>
    </form>
    <p v-if="error" class="error">経費登録失敗</p>
  </div>
</script>

図9 支払処理のVueコンポーネントとVue Router定義

// 経費リストを axios を利用して取得する関数
const getPayments = function (callback) {
  axios
    .get(${baseUrl}/api/payment, {
      // JWTの認可ヘッダー
      headers: {
        Authorization: Bearer ${localStorage.token},
      },
    })
    .then((response) => {
      if (response.status === 200) {
        // "response.data" 配下に経費リストが含まれる
        callback(null, response.data);
      } else {
        callback(true, response);
      }
    })
    .catch((response) => {
      callback(true, response);
    });
};

// 経費リスト用の Vueコンポーネント
const Payment = {
  template: "#payment",
  data: function () {
    return {
      loading: false,
      error: false,
      payments: function () {
        return [];
      },
    };
  },
  // 初期化されたときにデータを取得する
  created: function () {
    this.fetchData();
  },
  // ルーティングが変更されてきたときに再度データを取得する
  watch: {
    $route: "fetchData",
  },
  // 経費データを取得するメソッドのメイン部分
  methods: {
    fetchData: function () {
      this.loading = true;
      getPayments(
        function (err, payments) {
          this.loading = false;
          if (!err) this.payments = payments;
          else this.error = true;
        }.bind(this)
      );
    },
  },
};

const router = new VueRouter({
  routes: [
(略)
    {
      path: "/payment",
      component: Payment,
      beforeEnter: (to, from, next) => {
        // 認証前の場合は /login ページへ遷移する
        if (!localStorage.token) {
          next({
            path: "/login",
            query: { redirect: to.fullPath },
          });
        } else {
          next();
        }
      },
    },
(略)
  ],
});

図10 支払処理のHTMLテンプレート

<script type="text/x-template" id="payment">
  <div>
    <table>
      <tr>
        <th>ユーザー名</th>
        <th>発生日</th>
        <th>費目</th>
        <th>経費</th>
        <th>詳細</th>
      </tr>
      <tr v-for="payment in payments">
        <td>{{payment.user_name}}</td>
        <td>{{payment.date}}</td>
        <td>{{payment.type}}</td>
        <td>{{payment.amount}}</td>
        <td>{{payment.description}}</td>
      </tr>
    </table>
    <div class="loading" v-if="loading">アクセス中...</div>
    <p v-if="error" class="error">経費取得失敗</p>
  </div>
</script>

図14 controllers/auth/authentication.tsファイルの追加・改修部分

import jwt from "jsonwebtoken";

export class Authentication {
(略)
  static verifyLocal(username: string, password: string, done: any) {
    User.findOne({
      where: {
        email: username,
        deleted_at: null,
      },
    })
      .then((user) => {
        if (user && bcrypt.compareSync(password, user.hash)) {
          const opts = {
            issuer: process.env.ISSUER,
            audience: process.env.AUDIENCE,
            expiresIn: process.env.EXPIRES,
          };
          const secret: string = process.env.SECRET || "secret";
          const token: string = jwt.sign(
            {
              email: user.email,
              id: user.id,
              user_name: user.last_name + " " + user.first_name,
            },
            secret,
            opts
          );
          return done(null, token);
        }
        return done(true, "authentication error");
      })
      .catch((err) => done(true, err));
  }
(略)
}

図15 controllers/auth/authorization.tsファイル

import { Request, Response, NextFunction } from "express";
import { User } from "../../models/user";
import passport from "passport";
import { Strategy as JWTStrategy, ExtractJwt } from "passport-jwt";

export class Authorization {
  // JWTトークンで該当するユーザーの有無をチェック
  static verifyJWT(req: Request, jwt_payload: any, done: any) {
    User.findOne({
      where: {
        email: jwt_payload.email,
        deleted_at: null,
      },
    }).then((user) => {
      if (!user) return done(null, false);

      return done(null, user.get());
    });
  }

  // JWT Strategyの定義
  static setJWTStrategy() {
    const field = {
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      issuer: process.env.ISSUER,
      audience: process.env.AUDIENCE,
      secretOrKey: process.env.SECRET || "secret",
      passReqToCallback: true,
    };
    passport.use(new JWTStrategy(field, this.verifyJWT));
  }

  // 認可チェック
  static isAuthorized(req: Request, res: Response, next: NextFunction) {
    passport.authenticate("jwt", { session: false }, (err, user) => {
      if (err) {
        res.status(401).json({ status: "10001" });
      }
      if (!user) {
        res.status(401).json({ status: "10002" });
      } else {
        return next();
      }
    })(req, res, next);
  }
}

図16 API用のモジュールとルーティング定義

// API用ルーティング先のモジュール
import auth from "./api/auth";
import payment from "./api/payment";
import expense from "./api/expense";

// APIルーティング
app.use("/api/auth", auth);
app.use("/api/expense", Authorization.isAuthorized, expense);
app.use("/api/payment", Authorization.isAuthorized, payment);

図18 請求処理(/api/expense.ts)

import { Request, Response, NextFunction } from "express";
import Express from "express";
import { Expense } from "../models/expense";
const router = Express.Router();

// POST 経費の入力
router.post("/", (req: Request, res: Response, next: NextFunction) => {
  Expense.create(req.body)
    .then((result) => {
      res.status(200).json(result);
    })
    .catch((err) => {
      console.log(err);
      res.status(400).json({ id: 20002, message: err });
    });
});

export default router;

図19 支払処理(/api/payment.ts)

import { Request, Response, NextFunction } from "express";
import Express from "express";
import { Expense } from "../models/expense";
const router = Express.Router();

// POST / ユーザーの認証処理
router.get("/", (req: Request, res: Response, next: NextFunction) => {
  Expense.findAll()
    .then((results) => {
      res.status(200).json(results);
    })
    .catch((err) => {
      res.status(400).json({ id: 20011, message: err });
    });
});

export default router;

図35 修正した/src/index.tsファイル

import { Request, Response, NextFunction } from "express";
import Express from "express";
const app = Express();
import logger from "morgan";
import { Authentication } from "./controllers/auth/authentication";
import { Authorization } from "./controllers/auth/authorization";

app.use(logger("dev"));
app.use(Express.json());
app.use(Express.urlencoded({ extended: true }));

Authentication.initialize(app);
Authentication.setLocalStrategy();
Authorization.setJWTStrategy();

app.use(Express.static("htdocs"));

// API用ルーティング
import auth from "./api/auth";
import payment from "./api/payment";
import expense from "./api/expense";

// API
app.use("/api/auth", auth);
app.use("/api/expense", Authorization.isAuthorized, expense);
app.use("/api/payment", Authorization.isAuthorized, payment);

app.use((req: Request, res: Response, next: NextFunction) => {
  var err: any = new Error("Not Found");
  err.status = 404;
  next(err);
});

// error handler
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "development" ? err : {};

  res.status(err.status || 500);
  res.json(err);
});

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(http://localhost:${port});
});

export default app;

バーティカルバーの極意(Vol.67掲載)

投稿日:2020.07.25 | カテゴリー: コード

著者:飯尾 淳

前回に引き続き、筆者らの研究グループが開発したTwitterのトレンド分析システムについて解説します。前回は、そもそもTwitterのトレンドとは何かから始まり、開発者登録をしてトレンドAPIを叩き、そのリストを収集する方法まで説明しました。今回は、トレンドをキーにしてツイートを取得する方法と、ツイート群を対象として共起ネットワークグラフを作る方法を紹介します。

シェルスクリプトマガジン Vol.67は以下のリンク先でご購入できます。

図4 トレンドを収集して分析するプログラムのコード(その1)

#!/usr/bin/env python
###########################################################
# ヘルパーファンクション
###########################################################
import re
# 特殊文字をエスケープする
def escape(s, quoted=u'\'"\\', escape=u'\\'):
  return re.sub(u'[%s]' % re.escape(quoted),
                lambda mo: escape + mo.group(),
                s)
# ノードのプリティプリンタ
def pp_node(ary_of_nodes):
  for node in ary_of_nodes:
    keyword = escape(node[0])
    frequency = node[1]
    print("node_hash['{0}'] = 
      trend.nodes.create(word: '{1}', freq: {2:6.2f})"
        .format(keyword, keyword, frequency))
# リンクのプリティプリンタ
def pp_link(ary_of_links):
  for link in ary_of_links:
    (kw1,kw2) = link[0].split(',')
    print("link = Link.create(corr: {0:6.2f})".format(link[1]))
    print("node_hash['{0}'].links << link".format(escape(kw1)))
    print("node_hash['{0}'].links << link".format(escape(kw2)))
# 値を正規化する。p_fragは百分率とするときTrue
def normalize(hash_obj, p_flag):
  maxval = max(hash_obj, key=lambda x: x[1])[1]
  factor = 100 if p_flag else 1
  return [(x[0], x[1] / maxval * factor) for x in hash_obj]

図5 トレンドを収集して分析するプログラムのコード(その2)

###########################################################
# ワードグラフの作成
###########################################################
import MeCab
# 「PATH_TO_MECAB_NEOLOGD」の部分は環境に合わせて修正
m = MeCab.Tagger("-d PATH_TO_MECAB_NEOLOGD")
stopwords = {'*', 'HTTPS', 'HTTP', 'WWW', 'の', 'ん', 'ン', 'ω', '???'}
def create_graph(keyword, collected, tweets):
  ary_of_ary = []
  for tweet in tweets:
    ary = []
    lines = m.parse(tweet).split('\n')
    for line in lines:
      if line.count('\t') == 0: continue
      (kw, prop) = line.split('\t')
      props = prop.split(',')
      if len({props[-3]} & stopwords) > 0: continue
      if props[1] == '固有名詞': ary.append(props[-3])
    ary_of_ary.append(ary)
  KW_THRESHOLD = 20
  kw_dict = {}
  counter = 0
  for ary in ary_of_ary:
    for kw in ary:
      if kw in kw_dict: kw_dict[kw] = kw_dict[kw] + 1.0
      else: kw_dict[kw] = 1.0
      counter = counter + 1
  for kw,val in kw_dict.items(): kw_dict[kw] = val / counter * 100
  if len(kw_dict) > 0:
    kw_dict = sorted(kw_dict.items(), 
                     key = lambda x: x[1], 
                     reverse = True)[0:KW_THRESHOLD-1]
    kw_dict = normalize(kw_dict, True)
    print("node_hash = {}")
    print("trend = Trend.create(label: '{0}', collected:'{1}')"
            .format(escape(keyword), collected))
    pp_node(kw_dict)
  corr_dict = {}
  for src in kw_dict:
    for dst in kw_dict:
      if src == dst: continue
      src_w = src[0]
      dst_w = dst[0]
      sd_pair = src_w + "," + dst_w
      if sd_pair in corr_dict: continue
      prob = 0.0
      for ary in ary_of_ary:
        if ((src_w in ary) & (dst_w in ary)): prob = prob + 1.0
      prob = 100 * prob / len(ary_of_ary)
      if prob > 0: corr_dict[dst_w + "," + src_w] = prob
  if len(corr_dict) > 0:
    lk_dict = sorted(corr_dict.items(), 
                     key = lambda x: x[1], reverse = True)
    # 後半3/4をカットして短くする
    lk_dict = lk_dict[0:int(len(lk_dict)*1/4)]
    if len(lk_dict) > 0:
      lk_dict = normalize(lk_dict, True)
      pp_link(lk_dict)

図6 トレンドを収集して分析するプログラムのコード(その3)

###########################################################
# すでに登録済みか否かのチェック
###########################################################
import sqlite3
from contextlib import closing
def has_been_registered(keyword, collected):
  # 「PATH_TO_THE_DATABASE_FILE」の部分は環境に合わせて修正
  dbfile = "PATH_TO_THE_DATABASE_FILE(development.sqlite3)"
  with closing(sqlite3.connect(dbfile)) as conn:
    c = conn.cursor()
    sql = 'select label, collected from trends \
           where label=? and collected=?'
    res = (keyword, collected)
    c.execute(sql, res)
    return (len(c.fetchall()) > 0)
###########################################################
# メイン関数
###########################################################
from twitter import *
from datetime import date
def main():
  # WOEID を希望の場所に合わせて指定。「23424856」は日本
  woeid = 23424856
  # アプリ登録時に取得した情報を下記に設定
  CK = 'ADD_HERE_YOUR_CONSUMER_KEY'
  CS = 'ADD_HERE_YOUR_CONSUMER_SECRET'
  AT = 'ADD_HERE_YOUR_ACCESS_TOKEN'
  AS = 'ADD_HERE_YOUR_ACCESS_TOKEN_SECRET'
  twitter = Twitter(auth = OAuth(AT,AS,CK,CS))
  results = twitter.trends.place(_id = woeid, exclude="hashtags")
  d = date.today()
  collected = d.strftime('%Y-%m-%d')
  for location in results:
    for trend in location["trends"]:
      keyword = trend["name"]
      if has_been_registered(keyword, collected): continue
      query_kw = keyword + " exclude:retweets exclude:nativeretweets"
      tw_rslts = twitter.search.tweets(q=query_kw, 
                                       lang='ja', locale='ja', count=100)
      tw_ary = []
      for tweet in tw_rslts["statuses"]: tw_ary.append(tweet["text"])
      create_graph(keyword, collected, tw_ary)
if __name__ == "__main__": main()

図7 ワードグラフ作成部で出力されるRubyスクリプトの例

node_hash = {}
trend = Trend.create(label: 'ホシガリス', collected:'2020-06-07')
node_hash['アニポケ'] = trend.nodes.create(word: 'アニポケ', freq: 100.00)
node_hash['ポケモン'] = trend.nodes.create(word: 'ポケモン', freq:  56.76)
node_hash['ゴルーグ'] = trend.nodes.create(word: 'ゴルーグ', freq:  29.73)
node_hash['思'] = trend.nodes.create(word: '思', freq:  18.92)
(略)
node_hash['ピカチュウ'] = trend.nodes.create(word: 'ピカチュウ', freq:  5.41)
node_hash['パチリス'] = trend.nodes.create(word: 'パチリス', freq:  5.41)
node_hash['スピアー'] = trend.nodes.create(word: 'スピアー', freq:  5.41)
link = Link.create(corr: 100.00)
node_hash['ポケモン'].links << link
node_hash['アニポケ'].links << link
link = Link.create(corr:  75.00)
node_hash['ゴルーグ'].links << link
node_hash['アニポケ'].links << link
(略)
link = Link.create(corr:  25.00)
node_hash['マユルド'].links << link
node_hash['ドクケイル'].links << link
node_hash = {}
trend = Trend.create(label: '孤独のグルメ', collected:'2020-06-07')
(略)

レッドハットのプロダクト(Vol.67記載)

投稿日:2020.07.25 | カテゴリー: コード

著者:杉本 拓

「Red Hat Integration」はアプリやデータの連携を実現するための、インテグレーションパターン、API 連携、API管理とセキュリティ、データ変換、リアルタイムメッセージング、データストリーミングなどを提供するオープンソース製品です。同製品には多くの機能が含まれていますが、本連載ではその概要と一部の機能を紹介します。

シェルスクリプトマガジン Vol.67は以下のリンク先でご購入できます。

図18 二つのdependencyを追加する

   <dependency>
     <groupId>org.jboss.resteasy</groupId>
     <artifactId>resteasy-jackson2-provider</artifactId>
   </dependency>
   <dependency>
     <groupId>io.apicurio</groupId>
     <artifactId>apicurio-registry-utils-serde</artifactId>
     <version>1.2.1.Final</version>
   </dependency>

図19 「AvroRegistryExample.java」ファイルの内容

package com.redhat.kafka.registry;
 
import java.io.File;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeUnit;
 
import javax.enterprise.context.ApplicationScoped;
 
import org.apache.avro.Schema;
import org.apache.avro.generic.GenericData;
import org.apache.avro.generic.GenericData.Record;
import org.eclipse.microprofile.reactive.messaging.Outgoing;
 
import io.reactivex.Flowable;
import io.smallrye.reactive.messaging.kafka.KafkaRecord;
 
@ApplicationScoped
public class AvroRegistryExample {
 
   private Random random = new Random();
   private String[] symbols = new String[] { "RHT", "IBM", "MSFT", "AMZN" };
 
   @Outgoing("price-out")
   public Flowable<KafkaRecord<String, Record>> generate() throws IOException {
       Schema schema = new Schema.Parser().parse(
           new File(getClass().getClassLoader().getResource("price-schema.avsc").getFile())
       );
       return Flowable.interval(1000, TimeUnit.MILLISECONDS)
           .onBackpressureDrop()
           .map(tick -> {
               Record record = new GenericData.Record(schema);
               record.put("symbol", symbols[random.nextInt(4)]);
               record.put("price", String.format("%.2f", random.nextDouble() * 100));
               return KafkaRecord.of(record.get("symbol").toString(), record);
           });
   }
}

図20 「price-schema.avsc」ファイルの内容

{
   "type": "record",
   "name": "price",
   "namespace": "com.redhat",
   "fields": [
       {
           "name": "symbol",
           "type": "string"
       },
       {
           "name": "price",
           "type": "string"
       }
   ]
}

図21 登録されたAvroのスキーマ

{
  "createdOn": 1575919739708,
  "modifiedOn": 1575919739708,
  "id": "prices-value",
  "version": 1,
  "type": "AVRO",
  "globalId": 4
}

図22 プロパティファイル

# Configuration file
kafka.bootstrap.servers=localhost:9092
 
mp.messaging.outgoing.price-out.connector=smallrye-kafka
mp.messaging.outgoing.price-out.client.id=price-producer
mp.messaging.outgoing.price-out.topic=prices
mp.messaging.outgoing.price-out.key.serializer=org.apache.kafka.common.serialization.StringSerializer
mp.messaging.outgoing.price-out.value.serializer=io.apicurio.registry.utils.serde.AvroKafkaSerializer
 
mp.messaging.outgoing.price-out.apicurio.registry.url=http://localhost:8081/api
mp.messaging.outgoing.price-out.apicurio.registry.artifact-id=io.apicurio.registry.utils.serde.strategy.TopicIdStrategy

ラズパイセンサーボードで学ぶ電子回路の制御(Vol.67掲載)

投稿日:2020.07.25 | カテゴリー: コード

著者:米田 聡

シェルスクリプトマガジンでは、小型コンピュータボード「Raspberry Pi」(ラズパイ)向けのセンサー搭載拡張ボード「ラズパイセンサーボード」を制作しました。最終回は、前回作成したスクリプトによって記録された温度と湿度をグラフ化して、Webブラウザで閲覧可能にします。

シェルスクリプトマガジン Vol.67は以下のリンク先でご購入できます。

図1 サンプルのHTMLテンプレートファイル(~/webapp/templates/hello.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>こんにちは世界</title>
</head>
<body>
    <div>{{ text }}</div>
</body>
</html>

図2 サンプルのWebアプリケーションスクリプト(~/webapp/hello.py)

from flask import Flask, request, render_template

app = Flask(__name__)

@app.route('/' , methods=['GET','POST'])
def index():
  message = 'Hello, World'
  return render_template("hello.html", text=message)

if __name__ == '__main__':
  app.run(host='0.0.0.0', port=5000)

図4 トップページのテンプレートファイル(~/webapp/templates/index.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>温度・湿度グラフ化ツール</title>
</head>
<body>
  <div>グラフ化する日時の範囲を指定してください。</div>
  <form action="graph" method="post">
    <div>開始日時</div>
    <input id="startdt" type="datetime-local" name="startdt"
    min="{{ mindt }}" max="{{ maxdt }}" required> 
    <div>終了日時</div>
    <input id="enddt" type="datetime-local" name="enddt"
    min="{{ mindt }}" max="{{ maxdt }}" required> 
    <div><input type="submit" value="実行"></div>
  </form>
</body>
</html>

図5 テンプレートファイルからトップページを作成するスクリプト(~/webapp/app.py)

from flask import Flask, request,render_template
import sqlite3
import datetime

DBNAME="weather.sqlite3"

app = Flask(__name__)

@app.route('/')
def index():
  conn = sqlite3.connect(DBNAME)
  cur = conn.cursor()

  sql = "SELECT min(dt) from bme"
  cur.execute(sql)
  conn.commit()
  res = cur.fetchone()
  m = datetime.datetime.fromisoformat(res[0])
  mindt = m.strftime("%Y-%m-%dT%H:%M")

  sql = "SELECT max(dt) from bme"
  cur.execute(sql)
  conn.commit()
  res = cur.fetchone()
  m = datetime.datetime.fromisoformat(res[0])
  maxdt = m.strftime("%Y-%m-%dT%H:%M")

  conn.close()
  return render_template("index.html", maxdt=maxdt, mindt=mindt)

# ここに後でグラフ表示ページの関数を追加する

if __name__ == '__main__':
  app.run(host='0.0.0.0', port=5000)

図8 グラフを表示するページのテンプレートファイル(~/webapp/templates/graph.html)

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>グラフ</title>
</head>
<body>
  <canvas id="bmeChart"></canvas>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.js"></script>
  <script>
    var elm = document.getElementById("bmeChart");
    var bmeChart = new Chart(elm, {
      type: 'line',
      data: {
        labels: [
          {% for d in data %}
             '{{ d[0] }}',
          {% endfor %}
        ],
        datasets: [
          {
             label: '気温(度)',
             data:[ 
               {% for d in data %}
               {{ d[1] }},
               {% endfor %}
             ],
             borderColor: "rgba(255,0,0,1)",
             backgroundColor: "rgba(0,0,0,0)",
           },
           {
             label: '湿度(%)',
             data: [
               {% for d in data %}
               {{ d[2] }},
               {% endfor %}
             ],
             borderColor: "rgba(0,255,0,1)",
             backgroundColor: "rgba(0,0,0,0)",                       
           },
        ],
      },
      options: {
      },
    });
  </script>
</body>

図9 app.pyに追加する関数

@app.route('/graph', methods=['POST'])
def graph():
  if request.method == 'POST':
    startdt = datetime.datetime.fromisoformat(request.form['startdt'])
    enddt = datetime.datetime.fromisoformat(request.form['enddt'])

    conn = sqlite3.connect(DBNAME)
    cur = conn.cursor()
    sql = "SELECT dt,temp,hum from bme where dt < " + "'" + enddt.strftime("%Y-%m-%d %H:%M:%S") + "' and dt > '" + startdt.strftime("%Y-%m-%d %H:%M:%S") + "'"
    cur.execute(sql)
    conn.commit()
    res = cur.fetchall()
    conn.close()
    # データ件数が200件以上なら100件台になるよう抑える
    if len(res) > 200:
      p = int(len(res) / 100)
      res = res[::p]
  return render_template("graph.html", data=res)

特集3 PythonとSeleniumを活用 自動操作とデータ分析(Vol.67記載)

投稿日:2020.07.25 | カテゴリー: コード

著者:川嶋 宏彰

最近、プログラミング言語「Python」による自動化やデータ分析が注目されています。本特集では、Pythonと、Webブラウザを自動操作するためのライブラリ「Selenium WebDriver」を用いて、インターネットから取得できるオープンデータを例に、Webブラウザの自動操作方法およびデータ分析方法を分かりやすく紹介します。

シェルスクリプトマガジン Vol.67は以下のリンク先でご購入できます。

図4 非headlessモードのサンプルコード(sample.py)

import time
from selenium import webdriver

driver = webdriver.Chrome()
driver.get('https://www.google.com/')
time.sleep(5)
search_box = driver.find_element_by_name('q')
search_box.send_keys('ChromeDriver')
search_box.submit()
time.sleep(5)
driver.quit()

図7 headlessモードのサンプルコード

from selenium import webdriver

options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--disable-gpu')
driver = webdriver.Chrome(options=options)

driver.get('https://www.google.com/')
print(driver.title)

search_box = driver.find_element_by_name('q')
search_box.send_keys('ChromeDriver')
search_box.submit()
print(driver.title)

driver.save_screenshot('search_results.png')
driver.quit()

図24 Seleniumを用いた気温データの自動取得プログラム

import time
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.select import Select

# ダウンロード先フォルダの指定
dldir_path = Path('csv')  # csv という名前のフォルダとする
dldir_path.mkdir(exist_ok=True)  # なければ作成
download_dir = str(dldir_path.resolve())  # 絶対パスを取得
print("download_dir: " + download_dir)

options = webdriver.ChromeOptions()
options.add_experimental_option('prefs', {    # Chrome のオプションに
  'download.default_directory': download_dir  # 絶対パスで指定  
})

driver = webdriver.Chrome(options=options)

wait = WebDriverWait(driver, 10)  # 明示的待機用 (Timeout 10秒)

# 自動操作開始
driver.get('https://www.data.jma.go.jp/gmd/risk/obsdl/index.php')

# 「地点を選ぶ」
xpath = '//div[@class="prefecture" and text()="東京"]'
time.sleep(2)
driver.find_element_by_xpath(xpath).click()

xpath = '//div[@class="station" and contains(@title, "地点名:東京")]'
time.sleep(2)                                # (★)
driver.find_element_by_xpath(xpath).click()  # (★)

# 「項目を選ぶ」
driver.find_element_by_id('elementButton').click()

xpath = '//span[text()="月別値"]/preceding-sibling::input'
time.sleep(2)
driver.find_element_by_xpath(xpath).click()

css = '#日最高気温の平均'
time.sleep(2)
driver.find_element_by_css_selector(css).click()

# 「期間を選ぶ」
driver.find_element_by_id('periodButton').click()

time.sleep(2)
# <select>内の<option>要素を選択
Select(driver.find_element_by_name('iniy')).select_by_value('2010')
Select(driver.find_element_by_name('inim')).select_by_value('1')
time.sleep(2)  # いったん止めてみる
Select(driver.find_element_by_name('endy')).select_by_value('2019')
Select(driver.find_element_by_name('endm')).select_by_value('12')
time.sleep(2)

# 「CSVファイルをダウンロード」
driver.find_element_by_id('csvdl').click()
time.sleep(2)

driver.quit()

図27 e-StatのAPI機能を利用した家計調査データを取得するプログラム

import sys
import urllib
import urllib.request
import json
import calendar
import matplotlib.pyplot as plt
import japanize_matplotlib  # japanize-matplotlib を使う場合
import pandas as pd
from scipy import stats 

url = 'https://api.e-stat.go.jp/rest/3.0/app/json/getStatsData?'

app_id = '<e-Statマイページで取得したアプリケーションIDを挿入>'

cat01 = '010800150' # アイスクリーム・シャーベット
# cat01 = '010800130' # チョコレート
# cat01 = '011100030' # ビール

remove_month = 0  # 特定月を除く場合は1-12のいずれかを指定

# 指定する数字の桁数は決まっているので注意
keys = {
    'appId' : app_id,
    'lang' : 'J',
    'statsDataId' : '0003343671',  # 家計調査データ
    'metaGetFlg' : 'Y',
    'cntGetFlg' : 'N',
    'cdTab' : '01',  # 金額
    'cdTimeFrom' : '2010000101',  # 2010年1月から
    'cdTimeTo' : '2019001212',  # 2019年12月まで
    'cdArea' : '00000',  # 全国
    'cdCat01' : cat01,
    'cdCat02' : '03'  # 二人以上世帯
}

params = urllib.parse.urlencode(keys)
r_obj = urllib.request.urlopen(url + params)  # データを取得
r_str = r_obj.read()
res = json.loads(r_str)  # Pythonの辞書へ
stats_data = res['GET_STATS_DATA']['STATISTICAL_DATA']

class_obj = stats_data['CLASS_INF']['CLASS_OBJ']  # メタ情報

if 'DATA_INF' not in stats_data:  # ['DATA_INF']が入らないときのチェック用
    for co in class_obj:
        if 'CLASS' not in co:
            print("ERROR: Check params @id= {}, @name= {}" \
                .format(co['@id'], co['@name']))
    sys.exit(1)

values = stats_data['DATA_INF']['VALUE']  # 統計データの数値情報を取得

# メタ情報(CLASS_INF)から取得した品目名称を図のタイトルに使う
title = [co['CLASS']['@name'] for co in class_obj if co['@id'] == 'cat01'][0]
print(title)

# 各要素が [年, 月, 支出金額] の2次元リストにする
data = [[int(v['@time'][0:4]), int(v['@time'][6:8]), int(v['$'])] for v in values]
print("n =", len(data))  # 120 = 10年 x 12カ月

# Pandasデータフレームの準備
df = pd.DataFrame(data, columns=['year', 'month', '支出(円)'])
df['days'] = [calendar.monthrange(df.loc[i, 'year'], df.loc[i, 'month'])[1] for i in df.index]  # 各月の日数
df['支出(円/日)'] = df['支出(円)'] / df['days']  # 1日あたりの支出金額
df['y/m'] = df['year'].astype(str) + '/' + df['month'].astype(str)  # 結合用

# 気象庁の気温データとマージ
df_jma = pd.read_csv('csv/data.csv', skiprows=5, header=None, usecols=[0,1], encoding='Shift_JIS')
df_jma.columns = ['y/m', '平均最高気温(℃)']
df = pd.merge(df, df_jma, on='y/m')  # データフレームの結合

if remove_month > 0:
    df = df.query('month != @remove_month')  # 特定月を除く場合

# 相関係数を計算
corr, _ = stats.pearsonr(df['平均最高気温(℃)'], df['支出(円/日)'])
corr_str = "相関係数: {:2f}".format(corr)
print(corr_str)

# 散布図をプロット
ax = df.plot(kind='scatter', x='平均最高気温(℃)', y='支出(円/日)')
ax.set_title(title + ', ' + corr_str)
plt.show()

シェルスクリプトマガジンvol.67 Web掲載記事まとめ

投稿日:2020.07.25 | カテゴリー: コード

004 レポート Microsoft社製BASICのソースコード公開
005 NEWS FLASH
008 特集1 まんがで学ぶ自宅ネットワーク入門/麻生二郎
014 特集2 Cisco Webexが実現するテレワーク環境/粕谷一範
022 特集3 PythonとSeleniumを活用 自動操作とデータ分析/川嶋宏彰 コード掲載
035 Hello Nogyo!
036 特別企画 Microsoft Power Platform(後編)/清水優吾
050 ラズパイセンサーボードで学ぶ 電子回路の制御/米田聡 コード掲載
056 レッドハットのプロダクト/杉本拓 コード掲載
064 法林浩之のFIGHTING TALKS/法林浩之
066 バーティカルバーの極意/飯尾淳 コード掲載
072 tele-/桑原滝弥、イケヤシロウ
074 中小企業手作りIT化奮戦記/菅雄一
078 Webアプリの正しい作り方/しょっさん コード掲載
092 円滑コミュニケーションが世界を救う!/濱口誠一
094 香川大学SLPからお届け!/山内真仁 コード掲載
102 シェルスクリプトの書き方入門/大津真 コード掲載
108 Techパズル/gori.sh
110 コラム「ユニケージの本領発揮」/シェル魔人

Vol.67

投稿日:2020.07.25 | カテゴリー: バックナンバー

 新型コロナウイルス感染拡大による緊急事態宣言により、在宅勤務やテレワークが普及しました。今号に掲載した二つの特集では、在宅勤務やテレワークをテーマにしています。
 特集1では、在宅勤務で重要になる「自宅ネットワーク」を扱いました。冒頭部分にまんがを使って、自宅ネットワークの仕組みを知らない人にも分かりやすく解説しています。
 特集2は、テレワーク環境での会議やイベント、共同作業を実現するためのサービスの「Cisco Webex」です。現在、一番注目されているサービスの一つと言ってよいでしょう。機能概要、オンライン会議の開催方法や会議中の操作方法、将来実装される機能を詳しく紹介しています。
 特集3では、PythonとSelenium WebDriverを利用した、Webブラウザの自動操作と、Webブラウザから取得したデータの分析を扱っています。Selenium WebDriverを用いることで、さまざまなWebサイト上のデータをスクレイピングできます。
 特別企画では、前回と同様にMicrosoft Power Platform」を解説します。今回も読み応え十分のシェルスクリプトマガジン Vol.67。お見逃しなく!

※記事掲載のコードはこちら。記事の補足情報はこちら

※読者アンケートはこちら

Vol.67 補足情報

投稿日:2020.07.25 | カテゴリー: コード

訂正・補足情報はありません。
情報は随時更新致します。

シェルスクリプトの書き方入門(Vol.67記載)

投稿日:2020.07.22 | カテゴリー: コード

著者:大津 真

本連載ではシェルスクリプトの書き方をやさしく紹介します。対象とするシェルは、多くのLinuxディストリビューションが標準シェルとして採用する「Bash」です。第3回は、シェルスクリプトにおける条件分岐の使用方法を中心に解説します。

図1 シェルスクリプト「ping.sh」の内容

#!/bin/bash
if ping -c1 192.168.0.2 > /dev/null
then
  echo " 応答あり"
fi

図2 シェルスクリプト「secret1.sh」の内容

#!/bin/bash
secret=" ひらけごま"
echo -n " 合言葉は?: "
read aikotoba
if test $secret = $aikotoba; then
  echo " 正しい合言葉です!"
fi

図3 シェルスクリプト「secret2.sh」の内容

#!/bin/bash
secret=" ひらけごま"
echo -n " 合言葉は?: "
read aikotoba
if [[ $secret = $aikotoba ]]; then
  echo " 正しい合言葉です!"
fi

図4 シェルスクリプト「secret3.sh」の内容

#!/bin/bash
secret=" ひらけごま"
echo -n " 合言葉は?: "
read aikotoba
if [[ $secret = $aikotoba ]]; then
  echo " 正しい合言葉です!"
else
  echo " 合言葉が間違っています"
fi

図5 平成年を西暦年に変換するシェルスクリプト「heiseiToSeireki.sh」

#!/bin/bash
if [[ $# -eq 0 ]]; then
  echo " 平成年を指定してください"
  exit 1
fi
if [[ "$1" -lt 1 || "$1" -gt 31 ]]; then
  echo "1〜31 までの数値を入力してください"
  exit 1
fi
echo " 平成$1 年は西暦$(($1+1988)) 年"

図6 指定したファイル中のコロンをカンマに変換するシェルスクリプト「colon_to_comma.sh」

#!/bin/bash
fname=$1
cp "$fname" "${fname}~"
tr ":" "," < "${fname}~" > "$fname"

図7 引数チェック用のコードを追加した「colon_to_comma2.sh」

#!/bin/bash
if [[ $# -eq 0 ]]; then
  echo " 引数でファイルを指定してください"
  exit 1
fi
if [[ ! -f $1 ]]; then
  echo "$1 が見つかりません"
  exit 1
fi
fname=$1
cp "$fname" "${fname}~"
tr ":" "," < "${fname}~" > "$fname"

第5回 Chromeブラウザを導入する

投稿日:2020.07.20 | カテゴリー: 記事

 インターネットのWebサイトへのアクセスや、インターネットサービスの利用が最近のパソコンの使い道です。Linuxパソコンでもそれは変わりません。しかし、WindowsやMacと違い、インターネットで提供されているサービスのほとんどがLinuxをサポートしていません。そのため、何か問題が発生した場合は自力で解決しなくてはいけません。また、Linux自体が原因の場合、問題を解決できないこともあります。できるだけそのようなことが起きないように、WindowsやMacに提供されているツールと同じものを使います。

 インターネットへのアクセスには「Webブラウザ」を使います。これはLinuxでも同じです。本連載で利用しているLubuntuにはWebブラウザとして「Mozilla Firefox」がインストールされています。ただし、バージョンが古かったり、Linux版Mozilla Firefoxだと一部の機能が使えなかったりします。WindowsとMacと同じとは言い難いです。

 そこで、Linuxで最もお薦めなWebブラウザは「Google Chrome」です(図1)。公式サイトから入手してインストールする必要がありますが、米Google社がLinux向けに無償で提供していてWindows版やMac版のGoogle Chromeと同じように動作します。

図1 Lubuntu上でGoogle Chromeを動作させたところ

 ちなみに、Lubuntuでは、Google Chrome(以下、Chrome)のオープンソース版である「Chromium」なら標準でインストールできます。ただし、ChromiumにはFlash Playerが同こんされないなど、いくつかの違いがあるのであまりお薦めはできません(Flash Playerは2020年12月31日に提供終了予定)。

第4回 日本語入力を可能にする

投稿日:2020.07.6 | カテゴリー: 記事

 前回は、パソコンにLubuntuをインストールしました。Lubuntuのインストール時に、利用地域やタイムゾーンで日本(Aisa/Tokyo)を選んでいるので、英語が一部残っているものの、メニューなどは日本語で表示されています。そのため、Linux(Lubuntu)パソコンを日本語で使う環境が整っているように見えますが、日本語の入力ができません。

 Windowsパソコンと同様に、キーボードの[半角/全角]キーを押したら、日本語入力へ切り替えられるようにします(図1)。

図1 [半角/全角]キーを押して日本語入力と英語(半角英数)入力に切り替え

日本語入力システムが必要

 Lubuntuで日本語入力を可能にするには、インプットメソッドフレームワークと、そのフレームワークに対応した日本語入力システム(インプットメソッド)が必要です。インプットメソッドとは、パソコンなどのコンピュータ上で文字を入力するためのソフトウエア、インプットメソッドフレームワークとは、インプットメソッドと他のアプリケーションを結び付けるための機能やライブラリを含んでいるソフトウエアです。

 Lubuntuでは、「Fcitx」というインプットメソッドフレームワークと、それに対応した日本語入力システムの「Mozc」を使います。Mozcは米Google社が開発した「Google日本語入力」のオープンソース版です。Google日本語入力は、WindowsやmacOS、Android向けにも用意されていて、とても評判が高いソフトウエアです。Mozcでは一部の機能が提供されていないものの、便利に使えます。

 Fcitxは、すでにLubuntuにインストールされています。Fcitx対応のMozcは、Lubuntuにインストールしてすぐに実行できる形式の「パッケージ」としてインターネット上で配布されています。パッケージについては、別の回に詳しく紹介する予定ですが、ここでは「apt」というコマンドで、パッケージのインストール(導入)やアンインストール(削除)、アップデート(更新)ができることだけを覚えておいてください。この後で、aptコマンドを使って、Fcitx対応のMozcをインストールします。

  • shell-mag ブログの 2020年7月 のアーカイブを表示しています。

  • -->