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

test

最終回 不正アクセスを通知する

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

 ログの情報を分析して、管理者に警告メッセージを送信するのは、サーバーの運用管理ではよく使う手法です。インターネット側からアクセスできるようにしたサーバーの場合は、特にセキュリティ関連の通知ができると便利です。

 そこで、暗号化通信の「SSH」(Secure SHell)による、Linuxサーバーへのログイン認証のログを監視して、リモートログインや、不正(らしき)アクセスを検知したときにSNSに通知するシェルスクリプトを作成します(図1)。

図1 ログを解析して通知をSNSに送信するシェルスクリプト

漢のUNIX(Vol.64掲載)

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

著者:後藤 大地

今回も引き続き、プログラミング言語の「Rust」について解説する。前回も取り上げたが、Rustの学習は「The Rust Programming Language」(https://doc.rust-lang.org/book/)に沿って進めるのがよいと思う。The Rust Programming Languageではまず、「数当てゲーム」(Guessing Game、英訳としては「推測ゲーム」)のプログラムを開発する。これによって、Rustのプログラミングを一通り学べる。

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

図3 自動生成されるRustプログラムのソースコードファイル「src/main.rs」

fn main() {
    println!("Hello, world!");
}

図4 コンパイルに必要な構成情報や依存関係を記したファイル「Cargo.toml」

[package]
name = "guessing_game"
version = "0.1.0"
authors = ["Daichi GOTO <daichigoto@example.com>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

図6 ユーザーに対して入力を求め、その入力を受け付けて「あなたの予測値:」として入力値を表示するプログラム(main.rs)

use std::io;

fn main() {
    println!("数当てゲーム!");

    println!("数を入力してください");

    let mut guess = String::new();

    io::stdin().read_line(&mut guess)
        .expect("読み込みに失敗しました");

    println!("あなたの予測値: {}", guess);
}

図8 変数に値を2回代入するサンプルコード(src/main.rs)

fn main() {
    let v;

    v = 1;

    v = 2;
}

図10 mutを指定して値を変更できる変数と宣言したサンプルコード(src/main.rs)

fn main() {
    let mut v;

    v = 1;

    v = 2;
}

図12 read_line()関数の戻り値を表示させるコード

use std::io;

fn main() {
    let mut s = String::new();

    println!("戻り値: {}", 
        io::stdin().read_line(&mut s).expect("エラー"));
}

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

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

著者:しょっさん

ソフトウエアを正しく作るために、エンジニアたちはどんなことを知らなければならないでしょうか。実際のコードを使って、より良くしていくためのステップを考えてみましょう。第4回は、3回目のイテレーションを実施し、システムに必要な機能を実装していきます。

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

図3 マイグレーションファイルのテンプレート

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    /*
      Add altering commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.createTable('users', { id: Sequelize.INTEGER });
    */
  },

  down: (queryInterface, Sequelize) => {
    /*
      Add reverting commands here.
      Return a promise to correctly handle asynchronicity.

      Example:
      return queryInterface.dropTable('users');
    */
  }
};

図4 経費清算のマイグレーションファイルにuser_idのカラムを追加するように修正した

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.addColumn('expenses',
      'user_id', {
      type: Sequelize.UUID,
      foreignKey: true,
      references: {
        model: 'users',
        key: 'id',
      },
      onUpdate: 'RESTRICT',
      onDelete: 'RESTRICT',
    }
    );
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.removeCulumn('expenses',
      user_id);
  }
};

図5 config/database.tsファイル

import { Sequelize } from 'sequelize';

export default function (): Sequelize {
  const env: string = process.env.NODE_ENV || 'development';
  const config: any = require('./config.json')[env];

  if (config.use_env_variable) {
    const config_url: any = process.env[config.use_env_variable];
    return new Sequelize(config_url, config);
  } else {
    return new Sequelize(config.database, config.username, config.password, config);
  }
}

図6 権限テーブルのモデルファイル

import { Sequelize, Model, DataTypes } from 'sequelize';
import * as config from '../config/database';

const sequelize: Sequelize = config.default();

class Role extends Model {
  public id!: number;
  public user_id!: string;
  public name!: string;
  public readonly careated_at!: Date;
  public readonly updated_at!: Date;
}

Role.init({
  id: {
    type: DataTypes.INTEGER,
    autoIncrement: true,
    allowNull: false,
    primaryKey: true,
  },
  user_id: {
    type: DataTypes.UUID,
    allowNull: false,
    validate: {
      isUUID: 4
    }
  },
  name: {
    type: DataTypes.STRING(128),
    allowNull: false,
    defaultValue: ''
  }
}, {
  tableName: 'roles',
  underscored: true,
  sequelize: sequelize
});

export { Role };

図7 ユーザーマスターテーブルのモデルファイル

import { Sequelize, Model, DataTypes } from 'sequelize';
import { Expense } from './expense';
import { Role } from './role';
import * as config from '../config/database';

const sequelize: Sequelize = config.default();

class User extends Model {
  public id!: string;
  public boss_id?: string;
  public first_name?: string;
  public last_name!: string;
  public email!: string;
  public hash!: string;
  public deleted_at?: Date;
  public readonly careated_at!: Date;
  public readonly updated_at!: Date;
}

User.init({
  id: {
    type: DataTypes.UUID,
    allowNull: false,
    defaultValue: DataTypes.UUIDV4,
    primaryKey: true,
    validate: {
      isUUID: 4
    }
  },
  boss_id: {
    type: DataTypes.UUID
  },
  first_name: {
    type: DataTypes.STRING(32)
  },
  last_name: {
    type: DataTypes.STRING(32),
    allowNull: false
  },
  email: {
    allowNull: false,
    unique: true,
    type: DataTypes.STRING,
    validate: {
      isEmail: true
    }
  },
  hash: {
    allowNull: false,
    type: DataTypes.STRING(256)
  },
  deleted_at: {
    type: DataTypes.DATE,
    defaultValue: null
  },
}, {
  tableName: 'users',
  underscored: true,
  sequelize: sequelize
});

User.hasMany(Role, {
  sourceKey: 'id',
  foreignKey: 'user_id',
  as: 'roles'
})

User.hasMany(Expense, {
  sourceKey: 'id',
  foreignKey: 'user_id',
  as: 'expenses'
});

User.hasOne(User, {
  sourceKey: 'id',
  foreignKey: 'boss_id',
  as: 'users'
});

export { User };

図8 passportライブラリを使って、パスワード認証を行う部分(index.tsへの追加)

import bcrypt from 'bcrypt';
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';
import { User } from './models/user';

// passport 初期化
app.use(passport.initialize());
app.use(passport.session());

// passport の認証定義
passport.use(new LocalStrategy({
  usernameField: 'user',
  passwordField: 'password'
}, (username, password, done) => {
  User.findOne({ where: { email: username, deleted_at: null } }).then(user => {
    if (!user || !bcrypt.compareSync(password, user.hash))
      return done(null, false);
    return done(null, user.get());
  })
}));

// passport 認証時のユーザ情報のセッションへの保存やセッションからの読み出し
passport.serializeUser((user: User, done) => {
  return done(null, user);
});
passport.deserializeUser((user: User, done) => {
  User.findByPk(user.id).then(user => {
    if (user) {
      done(null, user.get());
    } else {
      done(false, null);
    }
  })
});

図9 ログインの強制(index.ts への追加)

// ログインの強制
app.use((req, res, next) => {
  if (req.isAuthenticated())
    return next();
  if (req.url === '/' || req.url === '/login')
    return next();
  res.redirect('/login');
});

図10 src/routes/login.tsファイル(ログインスクリプト)

import Express from 'express';
const router = Express.Router();
import passport from 'passport';

// GET /login ユーザーログインフォーム
router.get('/', (req: Express.Request, res: Express.Response): void => {
  res.send('<h1>LOGIN</h1><form action="/login" method="post">ユーザーID:<input type="text" name="user" size="40"><br />パスワード<input type="password" name="password"><input type="submit" value="ログイン"><br /><a href="/login">ログイン</a><br /><a href="/expenses/submit">経費入力</a><br /><a href="/expenses/payment">支払い処理</a>');
});

// POST / ユーザーの認証処理
router.post('/',
  passport.authenticate('local', {
    successRedirect: '/',
    failureRedirect: '/login'
  })
);

export default router;

図11 src/routes/expenses/submit.tsファイルの修正

import Express from ‘express’;
const router = Express.Router();
import { Expense } from '../../models/expense’;

// GET /expenses/submit 入力フォーム
router.post('/', (req: Express.Request, res: Express.Response): void => {
  Expense.create(req.body)
    .then(result => {
      res.redirect(‘/‘);
    });
});

// POST /expenses/submit 経費の申請
router.get('/', (req: Express.Request, res: Express.Response): void => {
  const user = req!.user!.email;
  const id = req!.user!.id;
  res.send(<h2>経費入力</h2><form action="/expenses/submit" method="post"><input type="hidden" name="user_id" value="${id}">申請者名:<input type="text" name="user_name" value="${user}"><br />日付:<input type="date" name="date"><br />経費タイプ:<input type="text" name="type"><br />経費詳細:<input type="text" name="description"><br />金額:<input type="number" name="amount"><br /><input type="submit" value="経費申請"><br /><a href="/login">ログイン</a><br /><a href="/expenses/submit">経費入力</a><br /><a href="/expenses/payment">支払い処理</a>);
});

export default router;

図12 用意した型定義ファイル

interface UserModel {
  id: string;
  boss_id?: string;
  first_name: string;
  last_name: string;
  email: string;
}

declare namespace Express {
  export interface User extends UserModel { }
}

図13 認証関係をつかさどるAuthenticationクラスを記述した「src/controllers/auth/index.ts」ファイル

import bcrypt from 'bcrypt';
import { User } from "../../models/user";
import passport from 'passport';
import { Strategy as LocalStrategy } from 'passport-local';

export class Authentication {
  static initialize(app: any) {
    // passport 初期化
    app.use(passport.initialize());
    app.use(passport.session());

    passport.serializeUser(this.serializeUser);
    passport.deserializeUser(this.deserializeUser);
  }
  static serializeUser(user: any, done: any) {
    return done(null, user);
  }
  static deserializeUser(user: any, done: any) {
    User.findByPk(user.id)
      .then(user => {
        return done(null, user);
      })
      .catch(() => {
        return done(null, false);
      });
  }

  static verify(username: string, password: string, done: any) {
    User.findOne(
      {
        where: {
          email: username
        }
      }
    ).then(user => {
      if (!user || !bcrypt.compareSync(password, user.hash))
        return done(null, false);
      return done(null, user.get());
    });
  }

  static setStrategy() {
    // passport の認証定義
    const field = {
      usernameField: 'user',
      passwordField: 'password'
    };
    passport.use(new LocalStrategy(field, this.verify));
  }
}

図14 認証関係のテストケースを記述した「authentication.test.ts」ファイル

import { Authentication } from '../src/controllers/auth/index';
import { User } from '../src/models/user';

describe('authentication', () => {
  it('serialize', done => {
    const callback = (arg: any, user: User) => {
      expect(user.id).toBe('811FCB5D-7128-4AA6-BFEE-F1A8D3302CDA');
      expect(user.email).toBe('test@example.com');
      expect(arg).toBeNull();
      done();
    }
    const user_sample = {
      id: '811FCB5D-7128-4AA6-BFEE-F1A8D3302CDA',
      last_name: 'test',
      email: 'test@example.com',
    }

    Authentication.serializeUser(user_sample, callback);
  });

  it('deserialize - positive', done => {
    const callback = (arg: any, user: any) => {
      expect(arg).toBeNull();
      expect(user.id).toBe('811FCB5D-7128-4AA6-BFEE-F1A8D3302CDA');
      done();
    }
    const user_sample = {
      id: '811FCB5D-7128-4AA6-BFEE-F1A8D3302CDA',
      last_name: 'test',
      email: 'test@example.com',
    }

    Authentication.deserializeUser(user_sample, callback);
  });
  it('deserialize - negative', done => {
    const callback = (arg: any, user: any) => {
      expect(arg).toBeNull();
      expect(user).toBe(false);
      done();
    }
    const user_sample = {
      id: '',
      last_name: 'test',
      email: 'test@example.com',
    }

    Authentication.deserializeUser(user_sample, callback);
  });

  it('verify - positive', done => {
    const callback = (arg: any, user: any) => {
      expect(arg).toBeNull();
      expect(user.id).toBe('811FCB5D-7128-4AA6-BFEE-F1A8D3302CDA');
      expect(user.email).toBe('test@example.com');
      expect(user.last_name).toBe('test');
      done();
    }

    Authentication.verify('test@example.com', 'password', callback);
  })

  it('verify - negative', done => {
    const callback = (arg: any, user: any) => {
      expect(arg).toBeNull();
      expect(user).toBe(false);
      done();
    }

    Authentication.verify('test@example.com', 'incorrect', callback);
  })

  it('verify - deleted', done => {
    const callback = (arg: any, user: any) => {
      expect(arg).toBeNull();
      expect(user).toBe(false);
      done();
    }

    Authentication.verify('deleted@example.com', 'password', callback);
  })
})

図15 テストデータ作成用のシードファイル(src/seeders/*-demo-user.js)

'use strict';

module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('users', [
      {
        id: '811FCB5D-7128-4AA6-BFEE-F1A8D3302CDA',
        email: 'test@example.com',
        last_name: 'test',
        hash: '$2b$10$IPwsYH8cAD9IarEGhj1/Vua2Lz4y/FD7GubAB.dNgfxgqx6i5heyy',
        created_at: new Date(),
        updated_at: new Date()
      },
      {
        id: '326260F7-2516-4C17-B8D1-DE50EF42C440',
        email: 'deleted@example.com',
        last_name: 'deleted',
        hash: '$2b$10$IPwsYH8cAD9IarEGhj1/Vua2Lz4y/FD7GubAB.dNgfxgqx6i5heyy',
        created_at: new Date(),
        updated_at: new Date(),
        deleted_at: new Date()
      }
    ]);
  },

  down: (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('users', {
      id: [
        '811FCB5D-7128-4AA6-BFEE-F1A8D3302CDA',
        '326260F7-2516-4C17-B8D1-DE50EF42C440'
      ]
    });
  }
};

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

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

著者:清水赳

前回に引き続き、OSSのシステム監視ツール「Prometheus」を「Itamae」というプロビジョニングツールを使って、サーバー監視システムを構築する方法を紹介します。今回は、Prometheusでノード情報を取得・計算する方法や、外形監視、データの可視化方法について解説します。

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

図5 「./cookbooks/blackbox_exporter/default.yml」ファイルに記述する内容

url_head = "https://github.com/prometheus/blackbox_exporter/releases/download"
url_ver  = "v0.16.0"
origin_dir = "blackbox_exporter-0.16.0.linux-amd64"
install_dir = "/usr/local/bin"
#==== Blackbox Exporterのインストール
execute "download blackbox_exporter" do
  cwd "/tmp"
  command "wget #{File.join(url_head, url_ver, origin_dir)}.tar.gz"
end
execute "extract blackbox_exporter" do
  cwd "/tmp"
  command "tar xvfz #{origin_dir}.tar.gz"
end
execute "install blackbox_exporter" do
  cwd "/tmp"
  command "mv #{File.join(origin_dir, "blackbox_exporter")} #{install_dir}"
end
#==== Blackbox Exporterをサービスとして登録する
remote_file "/etc/systemd/system/blackbox_exporter.service" do
  owner "root"
  group "root"
  source "files/etc/systemd/system/blackbox_exporter.service"
end
remote_directory "/etc/blackbox_exporter" do
  owner "root"
  group "root"
  source "files/etc/blackbox_exporter"
end
service "blackbox_exporter" do
  action :restart
end

図6 「./cookbooks/blackbox_exporter/files/etc/systemd/system/blackbox_exporter.service」ファイルに記述する内容

[Unit]
Description=BlackboxExporter

[Service]
ExecStart=/usr/local/bin/blackbox_exporter --config.file /etc/blackbox_exporter/blackbox.yml

[Install]
WantedBy=multi-user.target

図7 「./roles/client.rb」ファイルの編集内容

include_recipe "../cookbooks/node_exporter" # 前回追加
include_recipe "../cookbooks/blackbox_exporter" # 今回追加

図8 「./cookbooks/blackbox_exporter/files/etc/blackbox_exporter/blackbox.yml」ファイルに記述する内容

modules:
  http_2xx:
    prober: http
    http:
  http_post_2xx:
    prober: http
    http:
      method: POST
  icmp:
    prober: icmp

図9 「./cookbooks/prometheus/files/etc/prometheus/prometheus.yml」ファイルに追加する内容

  - job_name: 'blackbox'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
        - localhost
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: localhost:9115

特集1 はじめてのRust(Vol.64記載)

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

著者 :河野 達也

最近、Rustというプログラミング言語の名前をよく見かけるようになりました。米Amazon Web Services社、米Dropbox社、米Facebook社、米Mozilla財団などは、Rustを使ってミッションクリティカルなソフトウエアを開発しています。Rust とはどんな言語でしょうか。シンプルなプログラムの開発を通してRustの世界に飛び込みましょう。

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

Part1

図1 Rust のプログラムの例

// src/main.rsファイルの内容
//
// reqwestクレートを使うために、Cargo.tomlファイルのdependencies
// セクションに reqwest = "0.9" と書く

// main関数。プログラムが起動すると最初に呼ばれる
// この関数は引数を取らず、Result型の値を返す
fn main() -> Result<(), Box<dyn std::error::Error>> {
  // WebサービスのURI文字列をservice_uri変数にセットする
  let service_uri =
    "http://weather.livedoor.com/forecast/webservice/json/v1?city=130010";
  // 指定したURIに対してGETリクエストを送信し、レスポンスボディを取得する
  let body = reqwest::get(service_uri)?.text()?;
  // レスポンスボディを表示する
  println!("body = {:?}", body);
  // Okを返してmain関数から戻る
  // return文は不要だがこの行だけ行末にセミコロンがないことに注意
  Ok(())
}

図12 エラーになるRustのプログラムの例

// Circle型を定義する
#[derive(Debug)]
struct Circle {
  r: f64, // 円の半径、f64型
}

fn main() {
  // Circleの値をつくる
  let a = Circle{ r: 5.8 };
  // take_circle()を呼ぶとCircleの所有権が
  // 変数aから関数の引数bにムーブ
  take_circle(a);
  // aの内容を表示
  //(所有権がないのでコンパイルエラーになる)
  println!("{:?}", a);
}

fn take_circle(b: Circle) {
  // 何らかの処理

} // ここで引数bがスコープを抜けるのでCircleは削除

図21 sqrt()関数の定義コード

/// この関数はニュートン法で平方根を求めます。
fn sqrt(a: f64) -> f64 {
    // 未実装を表す。実行するとエラーになりプログラムが終了する
  unimplemented!()
}

図23 let文とif式を追加したsqrt()関数の定義コード

fn sqrt(a: f64) -> f64 {
  // 変数x0を導入し、探索の初期値に設定する
  let x0 = if a > 1.0 {
    a
  } else {
    1.0
  };
}

図24 loop式を追加したsqrt()関数の定義コード

fn sqrt(a: f64) -> f64 {
  let x0 = if a > 1.0 {
    a
  } else {
    1.0
  };
  // loopで囲まれたブロックは
  // break式が呼ばれるまで繰り返し実行される
  loop {
    // √aのニュートン法による漸化式で次項を求める
    let x1 = (x0 + a / x0) / 2.0;
    if x1 >= x0 {
      break;   // 値が減少しなくなったらloopから抜ける
    }
    x0 = x1;
  }
}

図25 戻り値の記述を追加したsqrt()関数の定義コード

fn sqrt(a: f64) -> f64 {
  let x0 = if a > 1.0 {
    a
  } else {
    1.0
  };
  loop {
    let x1 = (x0 + a / x0) / 2.0;
      if x1 >= x0 {
        break;
      }
      x0 = x1;
  }
  x0
}

図26 mut修飾子を追加したsqrt()関数の定義コード

fn sqrt(a: f64) -> f64 {
  let mut x0 = if a > 1.0 {
    a
  } else {
    1.0
  };
  loop {
    let x1 = (x0 + a / x0) / 2.0;
    if x1 >= x0 {
      break;
    }
    x0 = x1;
  }
  x0
}

図27 main()関数のコード

fn main() {
  let a = 2.0;
  // aの値とニュートン法で求めた平方根を表示
  println!("sqrt({}) = {}", a, sqrt(a));
}

Part2

図2 Cargo.tomlファイルに追加する設定

[dependencies]
chrono = "0.4"
clap = "2.33"
csv = "1.1"
hdrhistogram = "6.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

図3 コマンドライン引数を処理するプログラム

// clapで定義されているclap::Appとclap::Argの
// 二つの型をスコープに入れる
use clap::{App, Arg};
fn main() {
  // clap::Appでコマンド名やバージョンなどを設定
  let arg_matches = App::new("trip-analyzer")
    .version("1.0")
    .about("Analyze yellow cab trip records")
    // INFILEという名前のコマンドライン引数を登録
    .arg(Arg::with_name("INFILE")
      .help("Sets the input CSV file")
      .index(1)  // 最初の引数
    )
    // get_matches()メソッドを呼ぶとユーザーが与えた
    // コマンドライン引数がパースされる
    .get_matches();
  // INFILEの文字列を表示。"{:?}"はデバッグ用文字列を表示
  println!("INFILE: {:?}", arg_matches.value_of("INFILE"));
}

図5 Option型の定義(抜粋)

enum Option<T> {
  None,    // Noneバリアント
  Some(T), // Someバリアント。T型の値を持つ
}

図6 Result型の定義(抜粋)

Result<T, E> {
  Ok(T),   // 処理成功を示すバリアント。T型の成功時の値を持つ
  Err(E),  // 処理失敗を示すバリアント。E型のエラーを示す値を持つ
}

図7 match式とif let式の使用例

■match式の使用例

match arg_matches.value_of("INFILE") {
  // 値がパターンSome(..)にマッチするなら、
  // 包んでいる&strをinfile変数にセットし、=>以降の節を実行
  Some(infile) => println!("INFILE is {}", infile),
  // 値がパターンNoneにマッチするなら、=>以降の節を実行
  None => eprintln!("Please specify INFILE"),
}

■if let式の使用例

// 値がパターンSome(..)にマッチするなら、
// 包んでいる&strをinfile変数にセットし、true節を実行
if let Some(infile) = arg_matches.value_of("INFILE") {
  println!("INFILE is {}", infile);
} else {
  // そうでなければelse節を実行
  eprintln!("Please specify INFILE");
}

図8 コマンドライン引数を必須にする変更

(略)
    .arg(Arg::with_name("INFILE")
      .help("Sets the input CSV file")
      .index(1)
      .required(true) // この行を追加
    )
    // コマンドライン引数がない場合は
    // ここでエラーメッセージを表示してプログラム終了
    .get_matches();
  // 次の行を追加
  let infile = arg_matches.value_of("INFILE").unwrap();
  // 次の行を変更
  println!("INFILE: {}", infile);
}

図9 std::fmtモジュールのDebugトレイトの定義(抜粋)

trait Debug {
  fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error>;
}

図10 analyze()関数の定義コードを追加

use clap::{App, Arg};
// Errorトレイトをスコープに入れる
use std::error::Error;
// CSVファイルのパスを引数に取り、データを分析する
fn analyze(infile: &str) -> Result<String, Box<dyn Error>> {
  // CSVリーダーを作る。失敗したときは「?」後置演算子の働きにより、
  // analyze()関数からすぐにリターンし、処理の失敗を表すResult::Errを返す
  let mut reader = csv::Reader::from_path(infile)?;
  // 処理に成功したので(とりあえず空の文字列を包んだ)Result::Okを返す
  Ok(String::default())
}
fn main() {
(略)

図11 main()関数の定義コードを変更

fn main() {
(略)
  let infile = arg_matches.value_of("INFILE").unwrap();
  match analyze(infile) {
    Ok(json) => println!("{}", json),
    Err(e) => {
      eprintln!("Error: {}", e);
      std::process::exit(1);
    }
  }
}

図12 analyze()関数の定義コードを変更

fn analyze(infile: &str) -> Result<String, Box<dyn Error>> {
  let mut reader = csv::Reader::from_path(infile)?;
  // レコード数をカウントする
  let mut rec_counts = 0;
  // records()メソッドでCSVのレコードを一つずつ取り出す
  for result in reader.records() {
    // resultはResult<StringRecord, Error>型なので?演算子で
    // StringRecordを取り出す
    let trip = result?;
    rec_counts += 1;
    // 最初の10行だけ表示する
    if rec_counts <= 10 {
      println!("{:?}", trip);
    }
  }
  // 読み込んだレコード数を表示する
  println!("Total {} records read.", rec_counts);
  Ok(String::default())
}

図15 構造体Tripの定義コードを追加

type LocId = u16;
#[derive(Debug)]  // Debugトレイトの実装を自動導出する
struct Trip {
  pickup_datetime: String,  // 乗車日時
  dropoff_datetime: String, // 降車日時
  pickup_loc: LocId,        // 乗車地ID
  dropoff_loc: LocId,       // 降車地ID
}

図16 Tripをデシリアライズするための書き換え(その1)

// SerdeのDeserializeトレイトをスコープに入れる
use serde::Deserialize;
type LocId = u16;
// serde::Deserializeを自動導出する
#[derive(Debug, Deserialize)]
struct Trip {
  // renameアトリビュートでフィールド名と
  // CSVのカラム名を結びつける
  #[serde(rename = "tpep_pickup_datetime")]
  pickup_datetime: String,
  #[serde(rename = "tpep_dropoff_datetime")]
  dropoff_datetime: String,
  #[serde(rename = "PULocationID")]
  pickup_loc: LocId,
  #[serde(rename = "DOLocationID")]
  dropoff_loc: LocId,
}

図17 analyze()関数の定義コードを変更

fn analyze(infile: &str) -> Result<String, Box<dyn Error>> {
(略)
  let mut rec_counts = 0;
  // records()メソッドをdeserialize()メソッドに変更する
  for result in reader.deserialize() {
    // どの型にデシリアライズするかをdeserialize()メソッドに
    // 教えるために、trip変数に型アノテーションを付ける
    let trip: Trip = result?;
    rec_counts += 1;
(略)
}

図19 RecordCounts構造体の定義を追加

use serde::{Deserialize, Serialize};  // Serializeを追加
// serde_jsonでJSON文字列を生成するためにSerializeを自動導出する
#[derive(Debug, Serialize)]
struct RecordCounts {
  read: u32,    // CSVファイルから読み込んだ総レコード数
  matched: u32, // 乗車地や降車地などの条件を満たしたレコードの数
  skipped: u32, // 条件は満たしたが異常値により除外したレコードの数
}

図20 RecordCountsのデフォルト値をつくる関数を定義

impl Default for RecordCounts {
  fn default() -> Self {
    Self {
      read: 0, // read: u32::default(), としてもよい
      matched: 0,
      skipped: 0,
    }
  }
}

図21 analyze()関数の定義部分のrec_counts変数が使われている行を変更

fn analyze(infile: &str) -> Result<String, Box<dyn Error>> {
(略)
  let mut rec_counts = RecordCounts::default();
(略)
  for result in reader.deserialize() {
(略)
    rec_counts.read += 1;
    if rec_counts.read <= 10 {
(略)
  }
  println!("{:?}", rec_counts); // フォーマット文字列を変更
(略)
}

図22 日時を変換するparse_datetime()関数の定義コード

// chronoの利用にほぼ必須となる型やトレイトを一括してスコープに入れる
use chrono::prelude::*;
// NaiveDateTimeは長いのでDTという別名を定義
// chrono::NaiveDateTimeはタイムゾーンなしの日時型
type DT = NaiveDateTime;  
// ついでにResult型の別名を定義する
type AppResult<T> = Result<T, Box<dyn Error>>;
// 日時を表す文字列をDT型に変換する
fn parse_datetime(s: &str) -> AppResult<DT> {
  DT::parse_from_str(s, "%Y-%m-%d %H:%M:%S").map_err(|e| e.into())
}

図23 分析レコードを絞り込むための関数定義コードを追加(その1)

// LocIdがミッドタウン内ならtrueを返す
fn is_in_midtown(loc: LocId) -> bool {
  // LocIdの配列を作る
  let locations = [90, 100, 161, 162, 163, 164, 186, 230, 234];
  // 配列に対してバイナリーサーチする。
  // locと同じ値があればOk(値のインデックス)が返る
  locations.binary_search(&loc).is_ok()
}
// ロケーションIDがJFK国際空港ならtrueを返す
fn is_jfk_airport(loc: LocId) -> bool {
  loc == 132
}

図24 分析レコードを絞り込むための関数定義コードを追加(その2)

// 月曜から金曜ならtrueを返す
fn is_weekday(datetime: DT) -> bool {
  // 月:1, 火:2, .. 金:5, 土:6, 日:7
  datetime.weekday().number_from_monday() <= 5
}

図25 分析レコードを絞り込むためにanalyze()関数の定義コードを変更

// 戻り値型をAppResultに変更
fn analyze(infile: &str) -> AppResult<String> {
(略)
  for result in reader.deserialize() {
(略)
    if is_jfk_airport(trip.dropoff_loc) && is_in_midtown(trip.pickup_loc) {
      let pickup = parse_datetime(&trip.pickup_datetime)?;
      if is_weekday(pickup) {
        rec_counts.matched += 1;
      }
    }
  }
(略)
}

図27 統計的な処理をするためのコード

use hdrhistogram::Histogram;
// DurationHistogramsをタプル構造体として定義する
// この構造体はHistogramを24個持つことで、1時間刻みの時間帯ごとに
// 所要時間のヒストグラムデータを追跡する。
// Vec<T>型は配列の一種
struct DurationHistograms(Vec<Histogram<u64>>);
// 関連関数やメソッドを実装するためにimplブロックを作る
impl DurationHistograms {
  // Histogramsを初期化する関連関数。記録する上限値を引数に取る
  fn new() -> AppResult<Self> {
    let lower_bound = 1; // 記録する下限値。1秒
    let upper_bound = 3 * 60 * 60; // 記録する上限値。3時間
    let hist = Histogram::new_with_bounds(lower_bound, upper_bound, 3)
      .map_err(|e| format!("{:?}", e))?;
    // histの値を24回複製してVec<T>配列に収集する
    let histograms = std::iter::repeat(hist).take(24).collect();
    Ok(Self(histograms))
  }
}

図28 所要時間を登録するためのメソッドを追加(その1)

impl DurationHistograms {
  fn new() -> AppResult<Self> {
(略)
  }
  fn record_duration(&mut self, pickup: DT, dropoff: DT) -> AppResult<()> {
    // 所要時間を秒で求める。結果はi64型になるがas u64でu64型に変換
    let duration = (dropoff - pickup).num_seconds() as u64;
(略)

図29 所要時間を登録するためのメソッドを追加(その2)

impl DurationHistograms {
(略)
    let duration = (dropoff - pickup).num_seconds() as u64;
    // 20分未満はエラーにする
    if duration < 20 * 60 {
      Err(format!("duration secs {} is too short.", duration).into())
    } else {
      let hour = pickup.hour() as usize;
      // タプル構造体の最初のフィールドの名前は0になるので、
      // self.0でVec<Histogram>にアクセスできる。さらに個々の
      // Histogramにアクセスするには [インデックス] で
      // その要素のインデックスを指定する
      self.0[hour]
        // Histogramのrecord()メソッドで所要時間を記録する
        .record(duration)
        // このメソッドはHistogramの作成時に設定した上限(upper_bound)
        // を超えているとErr(RecordError)を返すので、map_err()で
        // Err(String)に変換する
        .map_err(|e| {
          format!("duration secs {} is too long. {:?}", duration, e).into()
        })
    }
  }
}

図30 統計処理をするためにanalyze()関数の定義コードを変更

fn analyze(infile: &str) -> AppResult<String> {
(略)
  let mut rec_counts = RecordCounts::default();
  let mut hist = DurationHistograms::new()?;
  // for式を変更
  for (i, result) in reader.deserialize().enumerate() {
(略)
    if is_jfk_airport((略)) {
(略)
      if is_weekday(pickup) {
        rec_counts.matched += 1;
        let dropoff = parse_datetime(&trip.dropoff_datetime)?;
        hist.record_duration(pickup, dropoff)
          .unwrap_or_else(|e| {
            eprintln!("WARN: {} - {}. Skipped: {:?}", i + 2, e, trip);
            rec_counts.skipped += 1;
          });
      }
    }       
(略)
  }
(略)
}

図32 DisplayStats構造体の定義コード

#[derive(Serialize)]
struct DisplayStats {
  record_counts: RecordCounts,
  stats: Vec<StatsEntry>,
}

図33 DisplayStats構造体の定義コード

#[derive(Serialize)]
struct StatsEntry {
  hour_of_day: u8, // 0から23。時(hour)を表す
  minimum: f64,    // 最短の所要時間
  median: f64,     // 所要時間の中央値
  #[serde(rename = "95th percentile")]
  p95: f64,        // 所要時間の95パーセンタイル値
}

図34 DisplayStats型にnew()関連関数を定義するコード

impl DisplayStats {
  fn new(record_counts: RecordCounts, histograms: DurationHistograms) -> Self {
    let stats = histograms.0.iter().enumerate()
      // mapメソッドでhdrhistogram::Histogram値からStatsEntry値を作る
      .map(|(i, hist)| StatsEntry {
        hour_of_day: i as u8,
        minimum: hist.min() as f64 / 60.0,
        median: hist.value_at_quantile(0.5) as f64 / 60.0,
        p95: hist.value_at_quantile(0.95) as f64 / 60.0,
      })
      .collect();
      Self {
        record_counts,
        stats,
      }
  }
}

図35 analyze()関数の定義コードを書き換える

let display_stats = DisplayStats::new(rec_counts, hist);
let json = serde_json::to_string_pretty(&display_stats)?;
Ok(json)

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

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

004 レポート 専修大学3年次の最終発表会
005 NEWS FLASH
008 特集1 はじめてのRust/河野達也 コード掲載
031 姐のNOGYO
032 特集2 Viscuitで学ぶコンピュータサイエンス/渡辺勇士
042 ラズパイセンサーボードで学ぶ 電子回路の制御/米田聡
045 香川大学SLPからお届け!/清水赳 コード掲載
052 錆(さび)/桑原滝弥、イケヤシロウ
054 MySQL Shellを使おう/梶山隆輔
060 法林浩之のFIGHTING TALKS/法林浩之
062 バーティカルバーの極意/飯尾淳
066 Webアプリの正しい作り方/しょっさん コード掲載
076 円滑コミュニケーションが世界を救う!/濱口誠一
078 漢のUNIX/後藤大地 コード掲載
084 中小企業手作りIT化奮戦記/菅雄一
090 ユニケージ新コードレビュー/坂東勝也
096 Techパズル/gori.sh
098 コラム「新しい風が吹いてくる」/シェル魔人

Vol.64

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

 「C」や「C++」のような高性能や高効率なソフトウエア開発に適しており、なおかつ、安全性を重視した次世代のプログラミング言語「Rust」が最近注目されています。特集1では、Rustの特徴、使いどころ、開発環境構築方法、プログラミングのやり方などを、初心者にも分かりやすいように、紹介しています。また、CSV 形式のデータを分析するという実用的なプログラムを扱っています。
 特集2では、ビジュアル開発ツール「Viscuit」(ビスケット)を使ったプログラミングを紹介しています。「メガネ」というツールだけで、驚くようなプログラムが、子供でも作成できます。Viscuitでコンピュータサイエンスを体験してみましょう。
 このほか、連載「ユニケージ新コードレビュー」ではプログラミング言語「AWK」の使いどころを、連載「センサーボードで学ぶ電子回路の制御」ではリアルタイムクロック(RTC)を実装する方法を紹介しています。
 今回も読み応え十分のシェルスクリプトマガジン Vol.64。お見逃しなく!

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

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

Vol.64 補足情報

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

姐のNOGYO

p.31にあるUSPファームのサイトの「http://www.uspeace.jp/」は「https://farm.usp-lab.com/」の誤りです。お詫びして訂正いたします。

情報は随時更新致します。

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

  • -->