ログの情報を分析して、管理者に警告メッセージを送信するのは、サーバーの運用管理ではよく使う手法です。インターネット側からアクセスできるようにしたサーバーの場合は、特にセキュリティ関連の通知ができると便利です。
そこで、暗号化通信の「SSH」(Secure SHell)による、Linuxサーバーへのログイン認証のログを監視して、リモートログインや、不正(らしき)アクセスを検知したときにSNSに通知するシェルスクリプトを作成します(図1)。
test
著者:後藤 大地
今回も引き続き、プログラミング言語の「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」
1 2 3 |
fn main() { println!("Hello, world!"); } |
図4 コンパイルに必要な構成情報や依存関係を記したファイル「Cargo.toml」
1 2 3 4 5 6 7 8 9 |
[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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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)
1 2 3 4 5 6 7 |
fn main() { let v; v = 1; v = 2; } |
図10 mutを指定して値を変更できる変数と宣言したサンプルコード(src/main.rs)
1 2 3 4 5 6 7 |
fn main() { let mut v; v = 1; v = 2; } |
図12 read_line()関数の戻り値を表示させるコード
1 2 3 4 5 6 7 8 |
use std::io; fn main() { let mut s = String::new(); println!("戻り値: {}", io::stdin().read_line(&mut s).expect("エラー")); } |
著者:しょっさん
ソフトウエアを正しく作るために、エンジニアたちはどんなことを知らなければならないでしょうか。実際のコードを使って、より良くしていくためのステップを考えてみましょう。第4回は、3回目のイテレーションを実施し、システムに必要な機能を実装していきます。
シェルスクリプトマガジン Vol.64は以下のリンク先でご購入できます。
図3 マイグレーションファイルのテンプレート
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
'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のカラムを追加するように修正した
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
'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ファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 権限テーブルのモデルファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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 ユーザーマスターテーブルのモデルファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
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への追加)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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 への追加)
1 2 3 4 5 6 7 8 |
// ログインの強制 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ファイル(ログインスクリプト)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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ファイルの修正
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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 用意した型定義ファイル
1 2 3 4 5 6 7 8 9 10 11 |
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」ファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
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」ファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
'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' ] }); } }; |
著者:清水赳
前回に引き続き、OSSのシステム監視ツール「Prometheus」を「Itamae」というプロビジョニングツールを使って、サーバー監視システムを構築する方法を紹介します。今回は、Prometheusでノード情報を取得・計算する方法や、外形監視、データの可視化方法について解説します。
シェルスクリプトマガジン Vol.64は以下のリンク先でご購入できます。
図5 「./cookbooks/blackbox_exporter/default.yml」ファイルに記述する内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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」ファイルに記述する内容
1 2 3 4 5 6 7 8 |
[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」ファイルの編集内容
1 2 |
include_recipe "../cookbooks/node_exporter" # 前回追加 include_recipe "../cookbooks/blackbox_exporter" # 今回追加 |
図8 「./cookbooks/blackbox_exporter/files/etc/blackbox_exporter/blackbox.yml」ファイルに記述する内容
1 2 3 4 5 6 7 8 9 10 |
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」ファイルに追加する内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- 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 |
著者 :河野 達也
最近、Rustというプログラミング言語の名前をよく見かけるようになりました。米Amazon Web Services社、米Dropbox社、米Facebook社、米Mozilla財団などは、Rustを使ってミッションクリティカルなソフトウエアを開発しています。Rust とはどんな言語でしょうか。シンプルなプログラムの開発を通してRustの世界に飛び込みましょう。
シェルスクリプトマガジン Vol.64は以下のリンク先でご購入できます。
図1 Rust のプログラムの例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 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のプログラムの例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 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()関数の定義コード
1 2 3 4 5 |
/// この関数はニュートン法で平方根を求めます。 fn sqrt(a: f64) -> f64 { // 未実装を表す。実行するとエラーになりプログラムが終了する unimplemented!() } |
図23 let文とif式を追加したsqrt()関数の定義コード
1 2 3 4 5 6 7 8 |
fn sqrt(a: f64) -> f64 { // 変数x0を導入し、探索の初期値に設定する let x0 = if a > 1.0 { a } else { 1.0 }; } |
図24 loop式を追加したsqrt()関数の定義コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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()関数の定義コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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()関数の定義コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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()関数のコード
1 2 3 4 5 |
fn main() { let a = 2.0; // aの値とニュートン法で求めた平方根を表示 println!("sqrt({}) = {}", a, sqrt(a)); } |
図2 Cargo.tomlファイルに追加する設定
1 2 3 4 5 6 7 |
[dependencies] chrono = "0.4" clap = "2.33" csv = "1.1" hdrhistogram = "6.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" |
図3 コマンドライン引数を処理するプログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 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型の定義(抜粋)
1 2 3 4 |
enum Option<T> { None, // Noneバリアント Some(T), // Someバリアント。T型の値を持つ } |
図6 Result型の定義(抜粋)
1 2 3 4 |
Result<T, E> { Ok(T), // 処理成功を示すバリアント。T型の成功時の値を持つ Err(E), // 処理失敗を示すバリアント。E型のエラーを示す値を持つ } |
図7 match式とif let式の使用例
■match式の使用例
1 2 3 4 5 6 7 |
match arg_matches.value_of("INFILE") { // 値がパターンSome(..)にマッチするなら、 // 包んでいる&strをinfile変数にセットし、=>以降の節を実行 Some(infile) => println!("INFILE is {}", infile), // 値がパターンNoneにマッチするなら、=>以降の節を実行 None => eprintln!("Please specify INFILE"), } |
■if let式の使用例
1 2 3 4 5 6 7 8 |
// 値がパターンSome(..)にマッチするなら、 // 包んでいる&strをinfile変数にセットし、true節を実行 if let Some(infile) = arg_matches.value_of("INFILE") { println!("INFILE is {}", infile); } else { // そうでなければelse節を実行 eprintln!("Please specify INFILE"); } |
図8 コマンドライン引数を必須にする変更
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
(略) .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トレイトの定義(抜粋)
1 2 3 |
trait Debug { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error>; } |
図10 analyze()関数の定義コードを追加
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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()関数の定義コードを変更
1 2 3 4 5 6 7 8 9 10 11 |
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()関数の定義コードを変更
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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の定義コードを追加
1 2 3 4 5 6 7 8 |
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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 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()関数の定義コードを変更
1 2 3 4 5 6 7 8 9 10 11 |
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構造体の定義を追加
1 2 3 4 5 6 7 8 |
use serde::{Deserialize, Serialize}; // Serializeを追加 // serde_jsonでJSON文字列を生成するためにSerializeを自動導出する #[derive(Debug, Serialize)] struct RecordCounts { read: u32, // CSVファイルから読み込んだ総レコード数 matched: u32, // 乗車地や降車地などの条件を満たしたレコードの数 skipped: u32, // 条件は満たしたが異常値により除外したレコードの数 } |
図20 RecordCountsのデフォルト値をつくる関数を定義
1 2 3 4 5 6 7 8 9 |
impl Default for RecordCounts { fn default() -> Self { Self { read: 0, // read: u32::default(), としてもよい matched: 0, skipped: 0, } } } |
図21 analyze()関数の定義部分のrec_counts変数が使われている行を変更
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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()関数の定義コード
1 2 3 4 5 6 7 8 9 10 11 |
// 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)
1 2 3 4 5 6 7 8 9 10 11 12 |
// 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)
1 2 3 4 5 |
// 月曜から金曜ならtrueを返す fn is_weekday(datetime: DT) -> bool { // 月:1, 火:2, .. 金:5, 土:6, 日:7 datetime.weekday().number_from_monday() <= 5 } |
図25 分析レコードを絞り込むためにanalyze()関数の定義コードを変更
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 戻り値型を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 統計的な処理をするためのコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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)
1 2 3 4 5 6 7 8 |
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)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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()関数の定義コードを変更
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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構造体の定義コード
1 2 3 4 5 |
#[derive(Serialize)] struct DisplayStats { record_counts: RecordCounts, stats: Vec<StatsEntry>, } |
図33 DisplayStats構造体の定義コード
1 2 3 4 5 6 7 8 |
#[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()関連関数を定義するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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()関数の定義コードを書き換える
1 2 3 |
let display_stats = DisplayStats::new(rec_counts, hist); let json = serde_json::to_string_pretty(&display_stats)?; Ok(json) |
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 コラム「新しい風が吹いてくる」/シェル魔人
「C」や「C++」のような高性能や高効率なソフトウエア開発に適しており、なおかつ、安全性を重視した次世代のプログラミング言語「Rust」が最近注目されています。特集1では、Rustの特徴、使いどころ、開発環境構築方法、プログラミングのやり方などを、初心者にも分かりやすいように、紹介しています。また、CSV 形式のデータを分析するという実用的なプログラムを扱っています。
特集2では、ビジュアル開発ツール「Viscuit」(ビスケット)を使ったプログラミングを紹介しています。「メガネ」というツールだけで、驚くようなプログラムが、子供でも作成できます。Viscuitでコンピュータサイエンスを体験してみましょう。
このほか、連載「ユニケージ新コードレビュー」ではプログラミング言語「AWK」の使いどころを、連載「センサーボードで学ぶ電子回路の制御」ではリアルタイムクロック(RTC)を実装する方法を紹介しています。
今回も読み応え十分のシェルスクリプトマガジン Vol.64。お見逃しなく!
※読者アンケートはこちら
p.31にあるUSPファームのサイトの「http://www.uspeace.jp/」は「https://farm.usp-lab.com/」の誤りです。お詫びして訂正いたします。
情報は随時更新致します。
shell-mag ブログの 2020年1月 のアーカイブを表示しています。