Ajax通信をする小さなアプリの作成[JavaScript Primer]

JavaScript Primer - 迷わないための入門書 #jsprimerAjaxで通信 · JavaScript Primer #jsprimerを実際にやって見た際のメモ.

作るもの

  • APIを呼び出して,GitHubからユーザー情報を取得する
  • 取得した情報をページに表示する

環境

  • macOS catalina
  • Node.js v13.13.0

真っ白のページを作る

まず,サーバーを立てて,HTMLにアクセスできる状態にします.

プロジェクトディレクトリの作成

今回はAjax-sampleというディレクトリを作成し,そのディレクトリ以下で作業をします.

HTMLファイルの作成

エントリーポイントとなるhtmlファイルを作成します.index.jsを読み込むだけの真っ白なページです.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Ajax Sample</title>
</head>
<body>
<script src="index.js"></script>
</body>
</html>

JavaScriptファイルの作成

読み込めているかを確認するだけなので,適当な文字列を表示するコードを書きます.

console.log("OK");

サーバーを立てる

Node.jsを使ってサーバーを立てます.Express.jsを使ってもいいですが,今回はhttp.createServerを使って素朴に実装します.

一部のIDEは特別な設定をしなくてもhtmlをローカルサーバー上で見られるので,この部分は省略できます.

実装

Server.js

const http = require('http');
const fs = require('fs');

const contentType = new Map([
    ["html", "text/html"],
    ["js", "text/javascript"],
    ["css", "text/css"],
    ["png", "image/png"],
    ["ico", "image/vnd.microsoft.icon"],
]);


http.createServer(function (req, res) {
    const ext = req.url.split('.').pop();

    try {
        const data = fs.readFileSync('.' + req.url);
        res.writeHead(200, {'Content-Type': contentType.get(ext)});
        res.write(data);
    } catch (e) {
        res.writeHead(404, {'Content-Type': 'text/plain'});
        res.write(e.message);
    }
    res.end();
}).listen(8080);

http.createServer

req.urlで指定されたファイルを返します.指定されたファイルが存在しない時,エラーメッセージを返します.

http.createServer(function (req, res) {
    const ext = req.url.split('.').pop();

    try {
        const data = fs.readFileSync('.' + req.url);
        res.writeHead(200, {'Content-Type': contentType.get(ext)});
        res.write(data);
    } catch (e) {
        res.writeHead(404, {'Content-Type': 'text/plain'});
        res.write(e.message);
    }
    res.end();
}).listen(8080);

参考:Node.js http.createServer() Method

contentType

[拡張子, contentType]Map.リクエストされたファイルの拡張子によって,適切なContentTypeを返します.

const contentType = new Map([
    ["html", "text/html"],
    ["js", "text/javascript"],
    ["css", "text/css"],
    ["png", "image/png"],
    ["ico", "image/vnd.microsoft.icon"],
]);

参考:よくある MIME タイプ - HTTP | MDN

HTMLの表示

 Ajax-sample
 ├── index.html
 ├── index.js
 └── server.js

Ajax-sample下でnode server.jsを実行し,http://localhost:8080/index.htmlにアクセスすると,以下のようなページが表示されます.コンソールには"OK"が表示されます.ファビコンは用意していないので404が返ります.

f:id:noy72:20200513151056p:plain:w600

今回の実装ではhttp://localhost:8080以下の文字列をファイル名として処理するので,http://localhost:8080http://localhost:8080/indexではファイルが読み込めません.

HTTP通信

次に,APIUsers | GitHub Developer Guide)を呼び出して,レスポンスをコンソールに表示します.

HTTPリクエストを送る関数の作成

fetchメソッドでHTTPリクエストを作成,送信ができます.このメソッドはPromiseを返すため,thenで成功時と失敗時に呼ばれる関数を登録します.また,レスポンスのデータをjsonに変換するメソッドもPromiseを返すので,jsonをコンソールに表示する関数を渡しておきます.

特定の文字がURIに含まれていると正しく動作しないため,encodeURIComponent()エスケープしておきます.

function fetchUserInfo(userId) {
    fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`).then(
        (response) => {
            if (response.ok) {
                return response.json().then(userInfo => console.log(userInfo));
            } else {
                console.log("Error :", response);
            }
        },
        (error) => {
            console.log(error);
        }
    );
}

参考:encodeURIComponent() - JavaScript | MDN

HTTPリクエストを送るためのボタンを作成

HTMLにボタンを作成し,そのボタンを押すとfetchUserInfoを呼び出すようにします.ここでは,userIdを埋め込んでいます.

...
<body>
<button onclick="fetchUserInfo('noy72');">Get user info</button>
<script src="index.js"></script>
</body>
...

実際に送ってみる

http://localhost:8080/index.htmlにアクセスし,表示されているボタンを押すと,HTTPリクエストが送られます.結果は以下のようにコンソールに表示されます.

f:id:noy72:20200513150935p:plain:w600

データをページに表示する

得られたデータをページに表示するようにします.

今回は,

  • アバター
  • ユーザー名
  • ユーザーID
  • フォロー数
  • フォロワー数
  • レポジトリ数

を表示表示します.

HTMLの組み立て

HTMLは以下のような構造にします.

.
├── ユーザー名,ユーザーID
├── アバター
└── .
    ├── フォロー数
    ├── フォロワー数
    └──レポジトリ数

Element#innerHTMLプロパティに作成したHTML文字列をセットする方法がありますが,HTMLのエスケープ処理をしたくないので,今回はElementオブジェクトを生成してツリーを構築します.

結果を表示する部分の作成

button要素とscript要素の間に空のdiv要素を入れておきます.

...
<button onclick="fetchUserInfo('noy72');">Get user info</button>
<div></div> <!-- ここにユーザー情報を表示する -->
<script src="index.js"></script>
...

HTMLの組み立てとDOMへの要素の追加

HTML要素はdocument.createElementで,単なる文字列はdocument.createTextNodeで作成します.Element#appendChildで子要素を追加してHTMLを組み立てます.

組み立てたHTMLは,用意しておいたdiv要素に挿入します.今回はquerySelectordiv要素を受け取り,そこに組み立てたHTMLを挿入します.

function buildHTML(info) {
    const user_name = document.createElement("h4");
    user_name.appendChild(
        document.createTextNode(`${info.name} (@${info.login})`)
    );

    const avatar = document.createElement("img");
    avatar.src = info.avatar_url;
    avatar.alt = info.login;
    avatar.height = 100;

    const list = document.createElement("ul");
    const following = document.createElement("li");
    following.appendChild(
        document.createTextNode(`Following: ${info.following}`)
    );
    const followers = document.createElement("li");
    followers.appendChild(
        document.createTextNode(`Followers: ${info.followers}`)
    );
    const repos = document.createElement("li");
    repos.appendChild(
        document.createTextNode(`Repos: ${info.public_repos}`)
    );
    list.appendChild(following);
    list.appendChild(followers);
    list.appendChild(repos);

    const result = document.querySelector('body > div');
    result.appendChild(user_name);
    result.appendChild(avatar);
    result.appendChild(list);
}

上記のコードは以下のようなHTMLを生成します(${}の部分は展開されます).

<div>
  <h4>${info.name} (@${info.login})</h4>
  <img src="${info.avater_url}" alt="${info.login}" height="100">
  <ul>
    <li>Following: ${info.following}</li>
    <li>Followers: ${info.followers}</li>
    <li>Repos: ${info.public_repos}</li>
  </ul>
</div>

buildHTMLを呼ぶ

fetchして得られたデータをbuildHTMLの引数にし,関数を呼びます.これで,ボタンを押すとデータが表示できるようになります.

if (response.ok) {
  response.json().then(userInfo => {
    buildHTML(userInfo);
  });
} else {
...

f:id:noy72:20200513151540p:plain:w600

Asyncを使う

現在はfetchメソッドのコールバックでbuildHTMLメソッドを呼び,DOMに変更を加えています.

これを,Asyncを使って同期処理のように書き換えてみます.

main関数の追加

直接fetchUserInfoを呼ばずに,main関数を通して呼ぶようにします.

function main(){
    fetchUserInfo("noy72")
}
...
<body>
<button onclick="main();">Get user info</button>
<div></div>
...

Promiseオブジェクトを返すようにする

fetchUserInfoでDOMを書き換えるのではなく,Promiseオブジェクトを返すようにします.成功した場合はResponse#jsonの戻り値をそのまま返し,失敗した場合はPromise.rejectでエラーを返します.

function fetchUserInfo(userId) {
    return fetch(`https://api.github.com/users/${encodeURIComponent(userId)}`).then(
        (response) => {
            if (response.ok) {
                return response.json();
            } else {
                return Promise.reject(
                    new Error(`${response.status} ${response.statusText}`)
                );
            }
        }
    );
}

mainでPromiseを処理

fetchUserInfoPromiseを返すようになったので,mainで処理します.成功した場合は(userInfo) => buildHTML(userInfo)が実行され,DOMを書き換えます.エラーが発生した場合は,(error) => console.log(error)が実行されます.

function main() {
    fetchUserInfo("noy72")
        .then((userInfo) => buildHTML(userInfo))
        .catch((error) => console.log(error))
}

これでPromiseを使った処理ができるようになりました.コードは変わりましたが意味的にはPromiseを使っていないのと同じなので,前回実行した時と同様の動作をします.

Asyncを使う

Promiseを返すfetchUserInfoawaitすることができます.

async function main() {
    try {
        const userInfo = await fetchUserInfo("noy72");
        buildHTML(userInfo);
    } catch (error) {
        console.log(error);
    }
}

fetchのコールバックで処理を行う実装から,手続的に処理を行う実装になりました.

作成したアプリのリポジトリ

JavaScript-Sample-Apps/Ajax-sample at master · noy72/JavaScript-Sample-Apps · GitHub

まとめ

GithubAPIを呼び出し,取得したデータを表示するアプリを作成しました.

  • サーバーを立てて,HTMLの表示とJavaScriptの実行をした
  • fetchを使ってHTTPリクエストを送った
  • DOMを書き換えて,APIから取得したデータを表示した
  • Async functionに置き換えた.

現在の実装ではユーザー名を埋め込んでいますが,Promiseを活用する · JavaScript Primer #jsprimerではユーザー名を変更できるように実装しています.

JavaScript勉強メモ [JavaScript Primer]

JavaScript Primerの第一章を読んで,知らなかった部分や重要な部分のメモ.

はじめに

JavaScript って何

JavaScript はウェブページ上に複雑なものを実装することを可能にするプログラミング言語です。

(引用:JavaScript - ウェブ開発を学ぶ | MDN)

   

JavaScriptは主にウェブブラウザの中で動くプログラミング言語です。

(引用:JavaScriptとは · JavaScript Primer #jsprimer)

Node.js って何

Node.js はスケーラブルなネットワークアプリケーションを構築するために設計された非同期型のイベント駆動の JavaScript 環境です。

(引用:Node.js とは | Node.js)

ECMAScript って何

JavaScriptの文法を定める仕様.ECMAScriptはどの実行環境でも共通な動作が定義されている.

ES2015は2015年にアップデートされたECMASCriptのこと.

strict mode って何

コードの先頭に "use strict";を書くと,実行モードがこれになる.

ある公文や機能が禁止される.

基本文法

変数

const

再代入不可.ただし,オブジェクトのプロパティなどは変更できるので,定数ではない.

const x = 5;
x = 6; // TypeError: Assignment to constant variable.

const obj = { value: 5 };
obj.value = 6;

let

再代入可.

var

あまり使わない方が良い.

  • 再定義が可能
  • 変数の巻き上げが起こる
var x = 6;
var x = 7; // エラーは起きない

console.log(hoge); // エラーは起きない.hoge = undefined
var hoge = 5;

命名

キャメルケース

データ型

プリミティブ型

真偽値や数値などの基本的な値.イミュータブル.

  • Boolean

  • Number

  • String

  • undefined

  • null

  • Symbol

    一意で不変な値のデータ型

オブジェクト

プリミティブ型の値やオブジェクトからなる集合.ミュータブル.

関数

引数

呼び出し時の引数が少ないと,undefinedが代入される.引数が多いと,その引数は無視される.

function add(a, b) {
    console.log(a, b);
    return a + b
}

console.log(add(2, 4));
// 2 4
// 6
console.log(add(2, 4, 7));
// 2 4
// 6
console.log(add(2));
// 2 undefined
// NaN

Spread構文

配列の前に...をつけると,配列の値を展開したものが返される.

function add(a, b) {
    return a + b
}

const a = [2,4];
console.log(...a);          // 2 4
console.log(add(...a)); // 6

arguments

関数に渡された引数の値が全て入ったArray-likeなオブジェクト

function add(a, b) {
    console.log(arguments); // [Arguments] { '0': 2, '1': 4, '2': 7, '3': 'foo' }
    return a + b
}

add(2, 4, 7, "foo");

オーバーロードはない

後から宣言した関数のみが有効.

function f(){
    console.log(5);
}

function f(x){
    console.log(x);
}

f(); // undefined
f(6); // 6

分割代入

ブレースでオブジェクトのプロパティを取り出せる.引数だけでなく,変数宣言時にも使える.

function print({value}) {
    console.log(value);
}

const obj = {
    value: 555
};
print(obj); // 555

const { value } = obj;
console.log(value); // 555

即時実行関数

匿名関数の宣言と実行をまとめて行うことで,グローバルスコープの汚染を避けられる.

(function() {
    console.log("foo");
})();

クロージャ

参照されている変数のデータが保持されるため,変数xcounter()実行後も参照し続けられる.

const counter = () => {
    let x = 0;
    return function inc(){
        return ++x;
    }
};

const inc = counter();
console.log(inc()); // 1
console.log(inc()); // 2
console.log(inc()); // 3

オブジェクト

プロパティの存在確認

const obj = {
    value: 1,
    str: "foo"
};
console.log("value" in obj);   // true
console.log("str" in obj);     // true
console.log("obj" in obj);     // false

in演算子hasOwnPropertyメソッドは同じ動作をするが,厳密には異なる.

const obj = {};
console.log("toString" in obj);    // true
console.log(obj.hasOwnProperty("toString")); // false

inobjには定義されていないが,プロトタイプオブジェクトには存在するため真

hasOwnPropertyobj自体に定義されていないため偽

配列

TypedArray

固定長,かつ型付きの配列.

const typedArray = new Int8Array(2);
typedArray[0] = 127;
typedArray[1] = 128;
console.log(...typedArray); // 127 -128

配列の検索

findIndexはインデックスを返し,findは要素を返す.

const array = ["a", "bb", "ccc"];
console.log(array.findIndex(obj => obj === "bb")); // 1
console.log(array.find(obj => obj === "bb")); // bb

配列の操作

push_front : unshift

pop_front : shift

const array = ['foo'];
array.push('push');
array.unshift('unshift');
console.log(array); // [ 'unshift', 'foo', 'push' ]

console.log(array.pop()); // push
console.log(array); // [ 'unshift', 'foo' ]

console.log(array.shift()); // unshift
console.log(array); // [ 'foo' ]

多次元配列を一次元配列にする

Array#flatを使う.パラメータで展開する深さを指定できる.全て展開する場合は,Infinityを用いる.

const array = [[[1], [2]], [3], 4];
console.log(array.flat(1)); // [ [ 1 ], [ 2 ], 3, 4 ]
console.log(array.flat(Infinity)); // [ 1, 2, 3, 4 ]

配列のコピー

Array#concatArray#sliceで配列をコピーすることができる.

function f_1(array) {
    const a = array.concat();
    a[0] = "new";
    return a;
}

function f_2(array) {
    const a = array.slice();
    a[0] = "new";
    return a;
}

function f_3(array) {
    const a = array;
    a[0] = "new";
    return a;
}

{
    const array = [1, 2];
    console.log(f_1(array));    // [ 'new', 2 ]
    console.log(array);         // [ 1, 2 ]
}
{
    const array = [1, 2];
    console.log(f_2(array));    // [ 'new', 2 ]
    console.log(array);         // [ 1, 2 ]
}
{
    const array = [1, 2];
    console.log(f_3(array));    // [ 'new', 2]
    console.log(array);         // [ 'new', 2]
}

文字列

タグ付きテンプレート関数

関数 テンプレート文字列と記述すると,${}で区切られた文字列と${}の評価結果を引数にして関数を呼ぶ.

function f(strings, ...values) {
    console.log(strings);
    console.log(values);
}

f(["aa", "bb", "cc"], 1, 2); // 普通の関数呼び出し
// [ 'aa', 'bb', 'cc' ]
// [ 1, 2 ]
f`aa${1}bb${2}cc`; // テンプレート文字列を使った関数呼び出し
// [ 'aa', 'bb', 'cc' ]
// [ 1, 2 ]

クラス

コンストラクタはクラス名ではなくconstructorで定義する.必須.

class MyClass {
    constructor() {
        console.log("constructor")
    }
}

const myClass = new MyClass(); // constructor

また,クラスを値として定義できる.

const c = class MyClass {
    constructor() {
        console.log("constructor")
    }
}

メソッドの定義

functionと書かずに関数名(パラメータ)と書く.このメソッドはプロトタイプメソッドと呼び,インスタンス間で共有される.

class MyClass {
    constructor() {
        this.value = 0;
    }

    inc() {
        this.value++;
    }
}

const myClass = new MyClass();
console.log(myClass.value); // 0
myClass.inc();
console.log(myClass.value); // 1

以下のようにthisに対してメソッドを定義すると,インスタンス間で共有されない.

this.inc = () => {
    this.value++;
}

アクセッサプロパティ

メソッドの頭にgetsetをつけると,プロパティへの参照や代入時に呼び出されるメソッドが定義できる.

アクセッサプロパティを用いると,プロパティに対する参照,代入をしたときに何らかの処理を行える.

class MyClass {
    constructor() {
        this.value = 0;
    }

    get a(){
        return this.value;
    }

    set b(value){
        this.value = value;
    }
}

const myClass = new MyClass();
console.log(myClass.a); // 0
console.log(myClass.b = 3); // 3
console.log(myClass.a); // 3

非同期処理

どのスレッドで実行されるか

class Timer {
    constructor() {
        this.start = Date.now();
    }

    // Timerインスタンスを生成してから経過した時間を表示する.
    log(message) {
        console.log(
            String(Date.now() - this.start) + " ms : " + message
        );
    }
}

// tミリ秒コードの実行を止める.
function block(t) {
    const start = Date.now();
    while (true) {
        const diff = Date.now() - start;
        if (diff > t) {
            return;
        }
    }
}

const timer = new Timer();
timer.log("Begin");
setTimeout(() => {
        timer.log("Begin_setTimeout");
        block(1000);
        timer.log("End_setTimeout");
    }, 3000
);
timer.log("End");

このコードの実行結果は以下のようになる.

0 ms : Start
6 ms : End
3012 ms : Start_setTimeout
4013 ms : End_setTimeout

メインスレッドのtimer.log("Start")timer.log("End")が順に実行され,その約3秒後にsetTimeout内の関数が呼ばれる.この動作から,setTimeout内の関数とメインスレッドは独立して動いているように見える.

次に,以下のようにblock()を増やしてみる.

const timer = new Timer();
timer.log("Start");
setTimeout(() => {
        timer.log("Start_setTimeout");
        block(1000);
        timer.log("End_setTimeout");
    }, 3000
);
block(5000);
timer.log("End");

このコードの実行結果は以下のようになる.

0 ms : Start
5007 ms : End
5008 ms : Start_setTimeout
6009 ms : End_setTimeout
  1. 実行
  2. 実行してから3秒後にsetTimeout内の関数実行
  3. 実行してから5秒後にtimer.log("End")を実行

とはならない.

  1. 実行
  2. 実行してから3秒後にsetTimeout内の関数を実行しようとするが,block(5000)の実行が終わっていないので実行されない.
  3. 実行してから5秒後にtimer.log("End")を実行
  4. setTImeout内の関数を実行

となる.setTimeoutでの関数実行はメインスレッドで行われるため,メインスレッドをblock()で止めると,その分実行が遅れる.

Promise

非同期処理の結果を表現するオブジェクト.

const promise = new Promise(関数 foo);
promise.then(fooの実行に成功したときに呼ぶ関数,fooの実行に失敗したときに呼ぶ関数)

resolverejectの2つの引数を取る関数をPromiseに与える.与えた関数の実行が成功ならresolveを呼び,失敗ならrejectを呼ぶ.

function foo(resolve, reject) {
    resolve(123);
}

function success(value) {
    console.log("resolve", value);
}

function failure(value) {
    console.log("reject", value * 2);
}

const promise = new Promise(foo);
promise.then(success, failure); // resolve 123


// 匿名関数を使った場合
const promise = new Promise((resolve, reject) => {
        resolve(123);
    }
).then(
    (value) => {
        console.log("resolve", value);
    },
    (value) => {
        console.log("reject", value * 2);
    }
); // resolve 123

例外が起こった場合はrejectを呼ぶ.

const promise = new Promise((resolve, reject) => {
        throw new Error("hogehoge");
    }
).then(
    (value) => {
        console.log("resolve", value);
    },
    (value) => {
        console.log("reject!!", value);
    }
);
// reject!! Error: hogehoge
// (省略)

Promiseチェーン

thenPromiseオブジェクトを返すので,.then().then()...と繋げることができる.

const timer = new Timer();
const promise = new Promise((resolve, reject) => {
        timer.log("1. 何らかの処理を実行");
        block(1000);
        resolve();
    }
).then(
    () => {
        timer.log("2. 処理が成功")
    }
).then(
    () => {
        timer.log("3. 別の処理を実行");
        block(2000);
        timer.log("4. 処理が終了")
    }
);
1 ms : 1. 何らかの処理を実行
1007 ms : 2. 処理が成功
1007 ms : 3. 別の処理を実行
3008 ms : 4. 処理が終了

複数のPromiseをまとめる

Promise.all(Promiseのリスト)と書く.

const timer = new Timer();

function getPromise(ms) {
    return new Promise((resolve) => {
        timer.log(`${ms}ミリ秒待機`);
        setTimeout(() => {
            resolve(ms);
        }, ms);
    })
}

const p1 = getPromise(1000);
const p2 = getPromise(2000);
const p3 = getPromise(3000);

Promise.all([p1, p2, p3]).then((values) => {
        timer.log("Promise.all");
        console.log(values);
    }
);
1 ms : 1000ミリ秒待機
9 ms : 2000ミリ秒待機
9 ms : 3000ミリ秒待機
3014 ms : Promise.all
[ 1000, 2000, 3000 ]

Promiseが1つでも失敗すると,Promise.allは失敗時の処理を呼び出す.

Promiseが1つでも成功したときにコールバック関数を呼ぶときはPromise.raceを使う.

Async Function

非同期処理を同期処理として書きたくなったり,thenで無限にチェーンが繋がっていくのが辛くなったときに使える構文.

async functionと記述すると,その関数はPromiseを返す.

async function f() {
    return 123456;
}

f().then(value => {
    console.log(value); // 123456
});

この場合だと,f()Promise.resolve(123456);を返す.

async functionawaitを使って実行が終わるまで待つことができる.

const timer = new Timer();

async function f() {
    timer.log("Begin async function f");
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve(123456), 1000)
    });
}

async function g(){
    const a = await f();
    timer.log("End async function f");
    const b = await f();
    timer.log("End async function f");
    console.log(a, b);
}

g();

await f()で約1秒間,止まっているのがわかる.

0 ms : Begin async function f
1009 ms : End async function f
1010 ms : Begin async function f
2015 ms : End async function f
123456 123456

awaitを削除するとどうなるか.

async function g(){
    const a = f();
    timer.log("End async function f");
    const b = f();
    timer.log("End async function f");
    console.log(a, b);
}
0 ms : Begin async function f
5 ms : End async function f
5 ms : Begin async function f
5 ms : End async function f
Promise { <pending> } Promise { <pending> }

1秒もかからずにメインスレッドが終了する.async functionであるf()Promiseを返すだけである.

感想

JavaScript PrimerはJS初心者にかなりおすすめ.説明が丁寧でわかりやすく,関連した情報も併せて書いてあるので,単に文法を調べる以上の情報が得られる.また,サンプルコードをサイト上で実行できるので,コードをいじることも簡単にできる.

非同期処理が全くわかっていなかったので,https://jsprimer.net/basic/async/:tilte は特に勉強になった.よく見るコールバックを用いたコードから,Promise,Async Functionを段階を踏んで説明してくれるので,とてもわかりやすかった.

簡単なプログラムだけど,非同期処理の関数をPromiseで包んで,awaitを使って同期処理っぽく書くことができるようになって満足.

次はアプリ開発第二部: 応用編(ユースケース) · JavaScript Primer #jsprimer

subprocess.runで環境変数を展開する[Python]

subprocessで展開する

> echo $HOGE
hogehoge

のような環境変数が定義されているとする.

以下のソースコードを実行すると,環境変数が展開されずに$HOGEが出力される.

import subprocess

subprocess.run(['echo', '$HOGE']) # 出力:$HOGE

subprocessでは,環境変数の展開に限らず,ワイルドカードチルダによるホームディレクトリの展開ができない. 上記の機能を使いたい場合は,shellオプションを有効にする.

import subprocess

subprocess.run(['echo $HOGE'], shell=True) # 出力:hogehoge

これで環境変数が展開できる.

Pythonドキュメントに書いてあること

shell が True なら、指定されたコマンドはシェルによって実行されます。あなたが Python を主として (ほとんどのシステムシェル以上の) 強化された制御フローのために使用していて、さらにシェルパイプ、ファイル名ワイルドカード環境変数展開、~ のユーザーホームディレクトリへの展開のような他のシェル機能への簡単なアクセスを望むなら、これは有用かもしれません。しかしながら、Python 自身が多くのシェル的な機能の実装を提供していることに注意してください (特に glob, fnmatch, os.walk(), os.path.expandvars(), os.path.expanduser(), shutil)。

引用元:subprocess --- サブプロセス管理 — Python 3.8.1 ドキュメント

例えば,環境変数を得るなら,expandvarsを使えば良い.

import os

print(os.path.expandvars("$HOGE")) # 出力:hogehoge

所感

subprocessまみれにしてPythonでシェルスリプトを書くのはやめたほうがいいと思う.

Junit4のテストをコンソールで実行する

用意したもの

  • junit-4.13.jar
  • hamcrest-core-1.3.jar
  • sampleプロジェクト
ディレクトリ構成
root/
  ┗ sample
       ┣ Add.java
       ┗ AddTest.java
Add.java
package sample;

public class Add {
    public int add(int a, int b){
        return a + b;
    }
}
AddTest.java
package sample;

import static org.junit.Assert.*;

public class AddTest {
    @org.junit.Test
    public void Test01(){
        Add add = new Add();
        assertEquals(5, add.add(2,3));
    }
}

テスト実行

コンパイル (カレントディレクトリ:root/sample)

クラスパスにjunitへのパスを指定しないとコンパイルできない.

javac -cp /path/to/junit-4.13.jar Add.java AddTest.java

テスト実行 (カレントディレクトリ:root/)

クラスパスにカレントディレクトリ,junit, hamcrestへのパスを指定しないと実行できない.

java -cp .:/path/to/junit-4.13.jar:/path/to/hamcrest-core-1.3.jar org.junit.runner.JUnitCore sample.AddTest

出力

JUnit version 4.13
.
Time: 0.01

OK (1 test)

エラーが出るとき

何かがおかしいと,以下のようなエラーが出力される.

java.lang.IllegalArgumentException: Could not find class [AddTest]
Exception in thread "main" java.lang.NoClassDefFoundError: sample/AddTest (wrong name: AddTest)

確認する部分は以下.

  • クラスパスに,カレントディレクトリ,humcrest,junitへのパスを指定している.
  • テスト実行をパッケージの親ディレクトリで行なっている(今回の例でいうとroot/).
  • テストクラス名にパッケージ名をつけている(今回の例でいうと,AddTestではなく,sample.AddTest).
  • パッケージ名とディレクトリ名が正しい.

Visual Studio で .Net Core のコンソールアプリ開発をしていたら System.Drawing.Image が使えなかったので NuGet で System.Drawing.Common をインストールした際のメモ

「.Net Core の」っておかしくないか?

環境・状態

  • Windows10
  • Visual Studio2019
  • .Net Core のコンソールアプリを開発中
  • System.Drawing.Image にエラーが出る.using もできない.

System.Drawing.Image の名前参照が解決できない

アセンブリ参照を追加しなければならないとかなんとかのエラーが出る. そこで,参照の追加を試みた.

参照マネージャーから追加する

ソリューションエクスプローラーを右クリックして参照マネージャーを開く. しかし,「アセンブリ」の項目がない.

  • アセンブリ ← これがない
  • プロジェクト
  • 共有プロジェクト
  • ...

参照マネージャーからアセンブリ参照を追加するのはできなかった.

System.Drawing.Common をインストールする

System.Drawing が使えない解決策として,System.Drawing.Common を使えば良いらしい.

参考:.NET CoreでSystem.Drawingを使う - 備忘録

上記サイトではNugetでインストールしている. Nugetは.Netのパッケージマネージャーで,Visual Studioインストーラからインストールすることもできる. 自分の環境では,デフォルトでインストールする設定になっていた.

docs.microsoft.com

上記に書いてあると通り,

ツール>NuGetパッケージマネージャー>パッケージマネージャーコンソール

で,コンソールを開く.

以下のコマンドでインストールする.

Install-Package System.Drawing.Common

結果

System.Drawing.Image の名前参照が解決できるようになった.

『思考・論理・分析 「正しく考え,正しくわかること」の理論と実践』を読んだ

公式:産業能率大学出版部 思考・論理・分析

www.hanmoto.com

かなり前から積んでいた日常用法的論理の本.普段なんとなく使っている「論理的」や「分析」の意味を定義していて,「なるほどね〜」という気持ちになった.「前提はなんだろう」「どのような論理展開をしたんだろう」という視点を持てるので,そのような考えに慣れてない人には特に良さそう.

分かることは分けること

第一章「思考」の部分で分かることは分けることであるという内容がある.

物事を識別するためには色々なものと比べることで差異を際立たせ,その物事特有の性質を理解しなければならない.比べる際には,どのような切り口で比較するかが重要となる.この切り口を列挙することが「分けること」で,複数ある切り口を用いて差異を際立たせることで「分かる」のだという.

そして,「分ける」ために以下の3つが必要であると述べている.

  • ディメンジョンの統一

    分ける,あるいは比べる対象の抽象水準を同一にする

    「りんごか野菜どちらか好きか」は抽象水準が異なるため不適切で,「りんごかトマト」「果物か野菜」の方がより適切.(親が同じでないと比較できないという感じ?)

  • クライテリアの設定

    分類基準,分類の切り口のこと.食べ物を「素材」で分けると「野菜,果物」などに分類できて,「国籍」で分けると「和食,中華」などに分類できる.

  • MECE (Mutually Exclusive, Colloectively Exhaustive)

    相互背反,集合網羅.モレがなく,かつダブりがない.現実で厳密なMECEは無理なので,MECE的な分類が必要になる.

言われてみればそれはそうだけど,あまり意識しない部分.

因果補足の難しさ

相関には単純相関と因果があって,これを見極めるのが難しい.

本書では以下の3つの留意点を挙げている.

  • 因果の強さ
    原因が結果に与える影響力はそれぞれ異なる.
  • 第三ファクター
    X -> A, X ->B のような因果関係があり,A, B に相関関係を生じさせてしまうような X.A -> B に因果関係があるように見える.
    例)X: 家族数,A: 茶碗の数,B: 米の消費量
  • 直接的連動関係
    ある結果を直接的に引き起こしている関係.

直接的連動関係についての例示は面白い.

「スピードの出し過ぎが事故の原因である」は一見正しそうに見えるが,スピードの出し過ぎが直接事故を引き起こしている訳ではない.

スピードの出し過ぎ→飛び出しに気づくのが遅れた→ブレーキを踏むのが遅れた→事故

のように,原因→結果の鎖がいくつも繋がっている.因果関係に働きかけて結果を変えるためには,複数ある原因を正しく認識することが必要である.

「人間がいなくなれば犯罪がなくなる」のような意見は,働きかける原因を間違えた例.

分析について

分析は,思考と同じように,情報を整理してなんらかの意味合いを得ることだと述べている.

分析の要件として以下の3つを挙げている.

  • 意味合いがアウトプット
    分析対象をまとめるだけでは意味がない.分析した結果,何がわかったのかというアウトプットが大事.

  • 情報収拾の必要性
    情報がなければ分析がそもそもできない.

  • 目的の存在
    原因の解決や手段の発見など,分析の目的が存在する.分けて分かることが目的でない.

目的があり,その上で必要な情報を集め,その情報から目的に沿った意味合い(結論)を見つけるのが分析であるということかな?.

最近聞いた,「データマイニングした結果を整理するだけだとアクショナブルじゃない」というのはつまり,「情報をまとめただけで意味合いが得られていない」ということか.

まとめ

  • 『思考・論理・分析 「正しく考え,正しくわかること」の理論と実践』を読んだ
  • 今までなんとなく使ってきた「論理的」の意味が少しわかった
  • こういうメタ知識は大切
  • 第三章の「分析」は知らないことが多かった

関連書籍

www.utp.or.jp

こちらは記号をいじる方の論理学.ルールに従って式を変形させるパズル的なやつ. 「トマトが野菜であるなら、トマトサラダは野菜サラダである」が証明できるようになる.

演繹法帰納法はこっちにも載っている.

はてなサマーインターン2019に参加しました

まとめ

はてなサマーインターンに参加しました

id:noy72 です.はてなサマーインターン2019の参加記です.

インターンとの出会い

1, 2年前に研究室の繋がりではてなに遊びに行ったことがありました.私が所属する研究室のOBがはてなにおり,そのOBの方から研究室の先輩,そして私に話が流れてきました.

その時にバイトやインターンの話を聞き,機会があれば行ってみようと思いました.

インターン選考

選考は Web申し込み→簡単なプログラミング課題→面接 の順に行われます.

Web申し込み

書類を書かなくて良いのはいいですね.

プログラミング課題

プログラミング課題はROT13でした.アルゴリズム部分での差別化はあまりできないと思ったので,基礎的な部分をきちんと抑えることを意識して ROT N (-1e9 \leq N \leq 1e9 ぐらい) といくつかのテストを書きました.

面接

面接はがっつり時間をとって行われ,十分すぎるほど話せます.インターンにおいて初めの学びがこの面接で,こちらの意見に対し,面接官の方が「意見の要約+それについて良いと思う理由」を返すのが非常に印象的でした(勘違いだったら忘れて下さい).とても話しやすかったです.要約することで相手の意見をきちんと理解していることを伝え,その上で感想を言う…… 見習おう,と思いました.

もし私がインターンの面接をまた受けるなら,今まで何をやってきたのか→なぜこのインターンに応募したのか→どのチームに配属されたいか・何をしたいか という流れを抑えると思います.今回の面接では失敗したので.

インターン

はてなサマーインターンでは講義パートの前半部分とチーム開発の後半部分に分けられます.そこで感じたことをつらつら書きます.

インターンの参加メンバー

インターンの参加者は自分を含め 8 人です.自分以外の7人,みんな強そうだと思いました(小並感).

前半2週間

わからなかったら人に聞く

メンターの方がちょくちょく声をかけてくれるので質問がしやすかったです.また,インターンに割いている人数が多いため,質問待ちをすることがありませんでした.

密度が高い

課題には業務時間ほぼまるごとかかりました.業務時間中に暇になることなく,残業続きになることなく,ちょうどよい分量だと思います.用意されているオブション課題を全部終わらせるなら残業は必要そうです.自分は業務後,社員さんに混じって呑気にMTGのドラフトをしていました.

学ぶ範囲が広くて面白い

前半二週間で様々な講義が行われます.Web開発に必要な知識に加え,コミュニティについてや企画についての講義もありました.

成果発表について

想定していた全ての機能を実装することはできませんでした(MTGのせいではないはず).経験のないフロントエンドでかなり詰まったので,全く知らない事に関してはどこかでちょっと触っておくといいかなと思います.

後半2週間

丁寧に教えてくれる

質問があれば丁寧に答えてくれるし,何もわからない状態であればペアプロをしてくれる.多くの時間を使ってもらいました.

開発に参加できた

何もわからない状態から始まりましたが,メンターやチームの方のサポートがあり何とか手を動かすことができました.

TDDとかテストとか

最初からTDDのようにテストを書いてから実装を進めればよかったんじゃないかと思っています.また,テストを読めばクラスや関数がどう使われているかがわかってコードが読みやすいんじゃないでしょうか.

誰か教えて下さい.

優しい

平和に過ごせました.ありがとうございます.

後半二週間で実際にやったこと

配属先はtomato3713さんと同じくmackerelチームで,ダッシュボードのウィジェットの開発をしました.私の担当はバックエンドで,scalaAPIを生やしました.もっと手際よく開発したかった……

開発した機能の詳細はtomato3713さんの記事に任せます.

f:id:noy72:20190913120513j:plain:w500
こんなのです

2019/09/30:追記

アップデート情報にアラートステータスウィジェットが載りました. ヘルプも増えてる〜(当たり前)

mackerel.io

mackerel.io

思ったこと

時間は大切

学びたいことがあるなら時間のある時にたくさん手を出しておいた方が良い.

悩みすぎ禁物

全くわからないときは全くわからないことを伝える.よくわかっていない状態で何かするのは無駄.

日頃の行い

そこまで時間をかけてはいないけど,技術書を読むだとか,写経でWebアプリ作るだとか,Pythonスクレイピングするだとか,Chrome拡張機能を作るだとかで得た知識があったおかけで,インターンでは致命傷で済みました.

学ぶぞ! という気持ちで新しいことに挑むのもいいですが,自分が必要に思う機能を小さくても良いから作ってみるということを普段から行うのも無理なく知識を増やせていいと思いました.

あんまり会社っぽくない……?

想像の会社とちょっと雰囲気が違いました.一部の社員の方から「サークルっぽい」「研究室っぽい」という声を聞いたので,やはり特殊なようです.ただ,東京オフィスは京都オフィスより会社っぽいとの話も聞いたので,はてなというより京都オフィスの特徴のようです.

おわりに

インターンに参加しようと思っている人へ:はてなインターンおすすめです.

インターンで会った人へ:どこかで会ったらボドケで遊んでください.

他のインターン生の記事

furutsuki.hatenablog.com

utgwkk.hateblo.jp

blondenamazu.hatenablog.com

ergofriend.hatenablog.com

10-1.hatenablog.com

tomato3713.hatenablog.com

lunastera.hatenablog.com