社内ツールを作るときに気を付けたいこと
数年前の先輩が残した偉大なエクセルマクロが支配している職場を経験したことはありますか。 当時は偉大だったマクロも、気づけばそのマクロが動かせなくなるからシステム更新ができない そのマクロを使うためにデータ変換するマクロができたり
どうしてそうなった
理由は明快、そのマクロを作った人がど素人で、それを使う人もど素人だから
少し、話を変えて、人はなぜプログラムを組もうとは全く思わないのに、 エクセルマクロは、ちょっとやってみようと思ってしまうのでしょうか。
エクセルの功罪の最たるところは、「インターフェース」と「データ保持」が同時にできてしまうところ A3に=A1+A2と書けてしまうところ。 A1, A2はインターフェースなのにA3は計算フローが含まれており、 また計算フローを流すためのデータ保持にもA1, A2が使われます。 見た目に単純なこの仕組みが、広義な「プログラム」として見たとき 極めて複雑で難解な仕組みで成り立っていることを理解せねばなりません。
情報処理、特にプログラミングを習ううえで最も基本的なこととして習うことがあります。 Program = Data + Algorithm この構造を明確にすることが、保守性・再利用性を維持し、柔軟な処理系を産み出すことができるんです。
ある測定器から取得したデータがCSV形式で次のようなものであったとしましょう。
測定日,2018年10月4日 測定者,田中太郎 測定器,V3 サンプリング周波数,20ms
センサー1[V],センサー2[Vp-p],センサー3[A] 3.2,1.2,0.002 3.1,1.5,0.004 …
このデータを見たときに、「入力データ構造の定義」を始められる人がど素人とそうでない人の違いです。 その「データ構造の定義」をおろそかにし、処理系(Algorithmなど)から手を付けようとする人はど素人です。
まず、純粋な測定データとメタデータに分けましょう
メタデータ 測定日: 2018年10月4日 測定者: 田中太郎 測定器: V3 サンプリング周波数: 20ms
測定データ センサー1[V],センサー2[Vp-p],センサー3[A] 3.2,1.2,0.002 3.1,1.5,0.004 …
しかしこの測定データにはまだ測定データのメタデータが含まれています。 これを分離します。
測定メタデータ 1: { 名前: センサー1 単位: V } 2: { 名前: センサー2 単位: Vp-p } 3: { 名前: センサー3 単位: A }
測定データ 1: 3.2,1.2,0.002 2: 3.1,1.5,0.004 …
他にも測定データにはサンプリング時間を振ってあげたほうがいいですし、センサーのVが本当にセンシングした物理量(歪や温度など)として何かを明示したほうがいいでしょう。
こうやって定義したデータ構造を、今度はどの形式を使って記述してやるかが次の課題です。
この内容、今ならXMLかJSONで定義するのが妥当でしょう。こればっかりは昨今のトレンドを追いかける必要がありますが、そろそろ定着して来たと思います。
それなら最初のCSVでいいじゃないかと考えると人もいると思います。注目すべきは、データがコンピュータも含めて誰もがわかりやすく使いやすい構造と形式で保持されているかです。
最初のCSVでは、測定器ごとにデータフォーマット定義書が別途必要であり、そこに不足分(センサーが示す物理量など)をテキストで別途保存しなければなりません。言い換えれば、如何に暗黙知を減らしたデータとして完結できるかかもしれません。
試しにJSONで記載してみるとこんな感じでしょうか ※見易さのために一部JSON形式から逸脱しています。
{ environment: { date: '2018-10-04', person: '田中太郎', machine: 'V3', configure: { samplingFrequency: '20ms' } }, data: { header: [{ name: 'センサー1', unit: 'V', }, { name: 'センサー2', unit: 'Vp-p', }, { name: 'センサー3', unit: 'A', }], body: [ [3.2,1.2,0.002], [3.1,1.5,0.004], ... ] } }
いかがでしょうか。まだまだ手を入れたところは多いでしょうが、拡張、修正、変換するのも容易です。 測定器が新しくなったり、別の測定データと連携した分析をしたいときも、統一的なデータ構造をもとに着手できそうじゃないですか?
実際にいろんなプログラムやマクロ作るときに、内部的に同様のことをしていることがほとんどです。 しかしそれがいろんなタイミングに散らばり、メモリ上にしか存在しないため、処理系はどんどん複雑になり、リファクタリング、リニューアルしようとしてもゼロから始めることになってないでしょうか。
一度、普遍的な形に噛み砕いて、それをマスターデータとして扱っていきましょう
AtCoder Practice BをJavascriptで解いてみる
※ネタ記事です。息抜きにでもなれば幸いです。
最近はJavascriptに慣れすぎて、他の言語を使うのがおっくうになってしまった。 今回はJavascriptでAtCoderのPractice B問題を解いてみようとおもう。
問題の原文は是非公式でみて欲しいが要約するとこうだ。
- 長さ26の配列を100回の大小比較でソートせよ
- 長さ5の配列を7回の大小比較でソートせよ
幸いJavascriptには既に配列のソートが用意されている。 というわけでこうだ。
const array = [{label: 'A'}, {label:'b'}, .... ]; const compare = (a, b) => { return query(a, b) === '<' ? 1 : -1; }; const result = array.sort(compare);
とおもったらWA(Wrong Answer)の嵐。
ソートが出来ていないわけではないが、ここでソート回数制限に気が付く
たしかNode.jsのソート (Array.prototype.sort)はクイックソートで実装されているはず。 試しにテストをつくって実行してみる。
const array = new Array(26); for(let i=0; i<array.length; i++) array[i] = Math.random(); let count = 0; const compare = (a, b) => { count++; return a-b; }; array.sort(compare); console.log(count);
そうすると、だいたい120~160程度の比較が実行されている。 ものぐさせずに計算量を調べてみる。 クイックソートの計算量は最良で n×ln(n) だが最悪で n2 になる。 n = 5 なら 8 ~ 25, n=26 なら 85 ~ 676 になる。
8?! と思うも知れないが、これはあくまでも計算量のオーダーであって、実計算量とはまた異なる。 想像してみればわかるが3つのものを、クイックソートする時に、最良は比較2回でソートは完了する。 3 × ln 3 = 3.296 ... と比較してもわかるが、実際の計算量はそのオーダーから定数が足し引きされる。
というわけで、この問題は最悪計算量を優先して、ヒープソートでも実装すればいいということだ。 で、こうなりました。
Array.prototype.bucket = function (callback) { if (!callback) callback = (a, b) => { if (a < b) return 1; if (a > b) return -1; return 0; } if (typeof callback !== 'function') throw new Error('callback require type of function'); const full = []; const push = (sub, array) => { if (sub.length <= 1) full.push(array.concat(sub)); else { for (let i = 0; i < sub.length; i++) { const t = sub.filter((x, j) => j !== i); push(t, array.concat([sub[i]])); } } }; push(this.map((x, i) => i), []); // console.log(full); const pairs = []; for (let i = 0; i < this.length; i++) { for (let j = i + 1; j < this.length; j++) { pairs.push([i, j]); } } // console.log(pairs); const findIndex = (array, value) => { let i = 0; while (array[i++] !== value); return i - 1; }; const run = (pair, current) => { if (current.length <= 1) return current[0]; const result = pair.map(x => { return Math.abs(current.length / 2 - current.filter(y => y.indexOf(x[0]) - y.indexOf(x[1]) > 0).length); }); const min = Math.min.apply(null, result); const index = findIndex(result, min); const [a, b] = [pair[index][0], pair[index][1]]; const t = callback(this[a], this[b]) || -1; return run( pair.filter(x => x.indexOf(a) < 0 || x.indexOf(b) < 0), current.filter(x => (x.indexOf(a) - x.indexOf(b)) * t > 0) ); }; const index = run(pairs, full); if (!index) throw new Error('can not sort'); return index.map(x => this[x]); };
まず、与えられた配列のソート結果の全候補を列挙する。 変数 full がこれに相当し、配列の長さが3なら [[0,1,2] [0,2,1] [1,0,2] [1,2,0] [2,0,1] [2,1,0]] となる。
次に、実際に比較する「2つの数字の組み合わせ」を列挙する。 これは変数 pairs で次のようになる。 [[0, 1] [0, 2] [1, 2]]
ここまででまだ一度も比較を実行していない。 これだけそろえば、どの数字を比較すれば、最も次の候補を減らせるかを探索すればいい。
例えば [0,1] のペアで比較をすると、先の full を2つに分割されて、 0 > 1 :: [0,1,2], [0,2,1], [2,0,1] 0 < 1 :: [1,0,2], [1,2,0], [2,1,0] となる。
実際に比較した結果をもとに候補を絞っていけばいい。
具体的には配列 [100, 7, 256] が与えられたとき、各ペアに対して
[0,1] で比較すると次の候補は 1. [0,1,2], [0,2,1], [2,0,1] 2. [1,0,2], [1,2,0], [2,1,0]
[0,2] で比較すると次の候補は 1. [0,1,2], [0,2,1], [1,0,2] 2. [1,2,0], [2,0,1], [2,1,0]
[1,2] で比較すると次の候補は 1. [0,1,2], [1,0,2], [1,2,0] 2. [0,2,1], [2,0,1], [2,1,0]
となり、どのペアで比較しても、候補は二分される。 [0,1]で比較すると、実際の数字は 100 > 7 であるため、次の候補は [[1,0,2], [1,2,0], [2,1,0]]に絞られる。
2回目の比較をしたときに候補の増減を調べると
[0,1] で比較すると次の候補は 1. [] 2. [1,0,2], [1,2,0], [2,1,0]
[0,2] で比較すると次の候補は 1. [1,0,2] 2. [1,2,0], [2,1,0]
[1,2] で比較すると次の候補は 1. [1,0,2], [1,2,0] 2. [2,1,0]
となる。ここで、優先すべきは最悪計算量を減らすことであるため、比較の結果のばらつぎが なるべく少ない比較のペアを選びたい。[0, 2]と[1,2] ではどちらもばらつきは 1 であるためどちらを選んでもいい。 実際に[0,2]で比較をすれば 100 < 256 であるため、次の候補は [[1,2,0], [2,1,0]] に絞られる。
このような戦略で行けば、常に最小の比較回数で、ソートが完了する。 試しに次のコードで比較回数を調べてみる。
require('../lib/ArrayPrototype'); const n = 10; const a = new Array(n); for (let i = 0; i < n; i++) a[i] = ({ name: `N${i}`, value: Math.random() }); let count = 0; const b = a.bucket((a, b) => { count++; return a.value - b.value }); console.log(`Count: ${count}`); b.map(x => console.log(x.value));
長さ5の配列を100回ソートしてみたが、必要な比較回数は、6~7 (ave. 6.97)となり、 目論見通り7回の比較で終了することが確かめられた。 入力に制限がない配列のソート方法としては最小の計算量となるはずだ。
それでは、提出してみよう
アルゴリズムを文字だけで説明するのって難しい 難しくない?
The GAS開発 in 2016 その4
前回実際にコードを書いて、GAS向けにビルド、アップロードする
今回はGASの独自機能をシミュレートするモックを作成して、ローカルでのテストを可能にします。
前回の doGet 関数を正規の出力を出すように作成
dev/index.js
@@ -xx,yyy +xx,zzz @@ if (query.parameter.token !== global.token) { log.e('Bad token access'); - return false; + return ContentService + .createTextOutput(JSON.stringify({content: 'Bad access'})) + .setMimeType(ContentService.MimeType.JSON); } - return true; + return ContentService + .createTextOutput(JSON.stringify({content: 'Process OK'})) + .setMimeType(ContentService.MimeType.JSON);
この ContentService はGAS上だけで動くAPIのためモックの作成が必要です。 また、前回の Log.js で用いた DocumentApp も同様。
作業ディレクトリのトップに GlibMock.js を作成
const gas = require('gas-local'); const fs = require('fs'); gas.globalMockDefault.Logger.enabled = false; const defMock = gas.globalMockDefault; const customMock = { ContentService: { createTextOutput: function (text) { return { setMimeType: function (mimeType) { return { content: text, mime: mimeType, }; }, }; }, MimeType: { JSON: 'application/javascript' }, }, DocumentApp: { openById: function (fileId) { return { getBody: function () { return { appendParagraph: function (text) { console.log(text); }, }; }, }; }, }, __proto__: defMock, }; module.exports = gas.require('./src', customMock);
このモックを使って、GAS向けにビルドされたスクリプト (src/main.js) をテストするスクリプトを test/test-main.js として作成します。
const assert = require('power-assert'); const glib = require('../GLibMock.js'); describe('doGet', function () { it('should return to BAD ACCESS', function () { const query = { parameter: {}, contextPath: '', contentLength: -1, queryString: '', parameters: {}, }; assert.equal(glib.doGet(query).content, JSON.stringify({ content: 'Bad access' })); }); it('should return to PROCESS OK', function () { const query = { parameter: { token: 'TEST_TOKEN' }, contextPath: '', contentLength: -1, queryString: 'token=TEST_TOKEN', parameters: { token: ['TEST_TOKEN'] }, }; assert.equal(glib.doGet(query).content, JSON.stringify({ content: 'Process OK' })); }); });
テストを実行するために、 package.json を書き換えます。
package.json
@@ -xx,yyy +xx,zzz @@ "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "npm run build && mocha --require intelli-espower-loader", "build": "browserify dev/index.js -t babelify -p gasify -o src/main.js", "upload": "gapps upload", "watch": "watchify dev/index.js -t babelify -p gasify -p src/main.js" },
ここまで出来たらタスクの実行から npm test を選択
テスト結果が 2passing と出ればテストが通ったことになります。 また、Google ドキュメントに出力していたログがテスト中はターミナルに表示されていることも確認できます。
前回と同様に npm upload を実行すればGoogleのサーバーにアップロードされますので一緒に動作確認しましょう
最後にGithubにcommit, push して終わり
※最初にpush したときは必ずGithubのWebからAPIキーなどが公開されていないか確認しましょう
今回つかったリポジトリはこちら
The GAS開発 in 2016 その3
実際にコードを書くための準備をします。 ここからはほとんど、Visual Studio Codeで作業
まず、dev ディレクトリと secret ディレクトリを作成。
.gitignoreファイルの末尾に次を追加
.vscode/ secret/ src/ gapps.config.json
これを怠ると、アクセス用のAPIキーやファイルIDが外部に漏れて、とんどもないことが起こりえます。
初心者がAWSでミスって不正利用されて$6,000請求、泣きそうになったお話。 - Qiita
気を付けなければいけないポイントはここだけでもないので、この辺の認証の仕組みや、 Github や GAS で何が公開されるのかは、よく確認しましょう。
続いて package.json を編集
@@ -xx,yyy +xx,zzz @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" + "build": "browserify dev/main.js -t babelify -p gasify -o src/bundle.js", + "upload": "gapps upload", + "watch": "watchify dev/main.js -t babelify -p gasify -o src/bundle.js" }, + "babel": { + "presets": [ + "es2015", + "gas" + ] + },
secret ディレクトリの中に Google.js を作成。こちらにファイルIDなどを記載する。
module.exports = { token: 'TEST_TOKEN', log: '第2回でメモしたログファイルのIDを記載', };
さらに dev ディレクトリの中に Log.js を作成。
module.exports = function (tag) { const self = this; const fileId = require('../secret/Google.js').log; const file = DocumentApp.openById(fileId); const body = file.getBody(); const levels = 'newidv'; this.level = 4; const put = (level, object) => { if (self.level < levels.indexOf(level)) return; const text = typeof object === 'string' ? object : JSON.stringify(object); const datetime = JSON.stringify(new Date()).substring(1, 20); body.appendParagraph('[' + datetime + '] (' + tag + ') ' + level.toUpperCase() + ': ' + text); }; [].map.call(levels, level => { self[level] = object => put(level, object); }); };
最後に、 dev ディレクトリの中に動作確認用のスクリプト index.js を作成
const Log = require('./Log'); global.log = new Log('MAIN'); global.token = require('../secret/Google').token; global.doGet = function (query) { const log = global.log; log.v('Call doGet function'); log.v(`Query is '${JSON.stringify(query)}'`); if (query.parameter.token !== global.token) { log.e('Bad token access'); return false; } return true; };
これをGASようにビルドします。すでにビルド設定は済ましているため、VSCodeのタスクから npm build を実行
無事に終わればターミナルに次のように表示される。 また src/main.js がビルドされたスクリプトに書き換わっている。
無事にビルドされたらアップロードする。タスクの実行から npm upload を選択
ターミナルに
The latest files were successfully uploaded to your Apps Script project.
と表示されたらアップロード完了
実際にアクセスして動作確認してみる。 今回は第2回でメモしたURLをブラウザで開く。ただし、本来 doGet の返り値に必要な結果を返していないため、次のようなエラーとなる。
これが表示されず「その操作を実行するには承認が必要です。」とでる場合は、一度Google ドライブから関数を実行して、権限の承認をしなければならない。
その後URLの最後尾に `?token=TEST_TOKEN' を付加して再度アクセス。 ドライブに用意しておいたログドキュメントを確認。
次のようなアクセスログが記録されているはず。
[2018-01-19T12:53:29] (MAIN) V: Call doGet function [2018-01-19T12:53:29] (MAIN) V: Query is 'undefined' [2018-01-19T12:54:09] (MAIN) V: Call doGet function [2018-01-19T12:54:09] (MAIN) V: Query is '{"parameter":{},"contextPath":"","contentLength":-1,"queryString":"","parameters":{}}' [2018-01-19T12:54:09] (MAIN) E: Bad token access [2018-01-19T12:54:51] (MAIN) V: Call doGet function [2018-01-19T12:54:51] (MAIN) V: Query is '{"parameter":{"token":"TEST_TOKEN"},"contextPath":"","contentLength":-1,"queryString":"token=TEST_TOKEN","parameters":{"token":["TEST_TOKEN"]}}'
ここまでで、ローカルでGASを作成、Google ドライブにアップロードし、Webからアクセスして動作という開発の流れは完成
次回はローカルでのテスト環境の構築
The GAS開発 in 2016 その2
前回(ソフトウェアとライブラリの準備)
今回はGoogle側の準備、Google ドライブにGASスクリプトとログ出力用のファイル(ドキュメント)を作成し、必要なファイルIDと公開設定を行う。
まずは自分のドライブにGoogle Apps Scriptを作成
人によってはGoogle Apps Scriptが選択肢に出てこない。その場合は+アプリを追加を先に行う。
新しいGoogle Apps Scriptを作ったら(こちらが本体)、ファイル名を「コード.gs」からmain.gsに変更し そのままウェブアプリケーションとして公開。ここでは、スクリプトファイルにアクセスするための、バージョンを振るのが目的
こいつとは別にアクセス用のファイルを用意するためこいつはファイルIDだけをメモ(ScriptID)
続いて、もう一つGoogle Apps Scriptを作成 本来Google Apps Scriptはコードを編集するたびに、新しいバージョンを振り、公開手続きを行わないと編集が反映されない。
今回は、アクセス用のファイルから、実処理が書かれたファイルを読み込ませることで、バージョンの振り直しをせずとも、 リアルタイムに編集が反映されるようにする。
リソース -> ライブラリと進み、先ほどのライブラリを読み込む設定を追加。
ライブラリを読み込んだら、コードの内容を次のようにして公開
function myFunction () { return LibraryName.myFunction(); } function doGet (q) { return LibraryName.doGet(q); } function doPost (q) { return LibraryName.doPost(q); }
doGet, doPostはそれぞれ、公開したURLにアクセスしたときに実行される関数
最後にログ出力用のドキュメントファイルを作成し、ファイルIDをメモ(LogID)
一通り終わったら、node.jsからGoogleにアップロードするための準備をする。
こちらのサイトを参考に
GoogleAppsScriptローカル開発用の公式CLI(node-google-apps-script)がついに登場したので試してみる - Qiita
gapps init ScirptID まで行う。 そこまで終わればファイル構成にsrcディレクトリとgapps.config.jsonが追加されている。
第3回は、実際にスクリプトを書いてアップロード、アクセスURLから動作確認するところまで。
The GAS開発 in 2016 その1
2018年に2016年の記事を書く そのくらい最近のWebまわりは展開が早い
Visual Studio Code, NPM, Node.jsを使って、Google Apps Scriptの開発環境のセットアップについてまとめる。 コードはEMCAScript 6(注)を使い、BabelによってGAS向けにトランスパイル、アップロードを自動化する。 また、テスト用のモックを作成し、Mochaによる自動テストが行えるようにする。
おまけとして、テスト用サーバサイド(Google側)の設定と、 Githubでのバージョン管理、APIキーが格納されたファイルをGithubへアップロードしない設定なども紹介する。
注: EMCAScriptのバージョンはあまり理解してません。アロー記法が使えるやつ
いいからお前の環境さらせって方はこちらをCloneしてどうぞ
全4回程度にまとめました。
第一回は、必要なソフトウェア・ライブラリのインストール
最初にVisual Studio CodeとNode.js, Gitクライアントをインストール
通常インストールで問題なし。各オプションは好みに応じて
.gitignoreは「Node」にしておくこと (後々の設定が簡便なため)
とりあえずMITライセンスで作成
今作った空のリポジトリを clone, そのままNPMの設定と必要になるライブラリをインストール
※リポジトリURLやユーザー名は各自で読み替える
C:\Users\ixsiid>git clone https://github.com/ixsiid/TheGAS2016.git Cloning into 'TheGAS2016'... remote: Counting objects: 5, done. remote: Compressing objects: 100% (4/4), done. remote: Total 5 (delta 0), reused 0 (delta 0), pack-reused 0 Unpacking objects: 100% (5/5), done. C:\Users\ixsiid>cd TheGAS2016
NPMの設定。基本は初期設定で問題なし。
C:\Users\ixsiid\TheGAS2016>npm init This utility will walk you through creating a package.json file. It only covers the most common items, and tries to guess sensible defaults. See `npm help json` for definitive documentation on these fields and exactly what they do. Use `npm install <pkg>` afterwards to install a package and save it as a dependency in the package.json file. Press ^C at any time to quit. package name: (thegas2016) thegas2016 version: (1.0.0) 1.0.0 description: entry point: (index.js) index.js test command: git repository: (https://github.com/ixsiid/TheGAS2016.git) keywords: author: IXSIID license: (ISC) MIT About to write to C:\Users\ixsiid\TheGAS2016\package.json: { "name": "thegas2016", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", "url": "git+https://github.com/ixsiid/TheGAS2016.git" }, "author": "IXSIID", "license": "MIT", "bugs": { "url": "https://github.com/ixsiid/TheGAS2016/issues" }, "homepage": "https://github.com/ixsiid/TheGAS2016#readme" } Is this ok? (yes) yes
babel関係のインストール これがないと、最新の記法がGASで使えない
C:\Users\ixsiid\TheGAS2016>npm install --save-dev babel-core babel-loader babel-polyfill babel-preset-env babel-preset-es2015 babel-preset-gas babel-register babelify browserify webpack + babel-register@6.26.0 + babel-loader@7.1.2 + babel-core@6.26.0 + babel-preset-env@1.6.1 + babel-polyfill@6.26.0 + babelify@8.0.0 + babel-preset-gas@1.0.0 + browserify@15.2.0 + webpack@3.10.0 added 9 packages in 10.749s
続いて、GASを使うためのライブラリ
C:\Users\ixsiid\TheGAS2016>npm install --save-dev gas-local gas-webpack-plugin gasify sync-request + gasify@0.1.2 + gas-local@1.3.0 + sync-request@4.1.0 + gas-webpack-plugin@0.2.1 added 36 packages in 5.382s
自動テスト環境のためのライブラリ
C:\Users\ixsiid\TheGAS2016>npm install --save-dev intelli-espower-loader mocha power-assert watchify + power-assert@1.4.4 + mocha@5.0.0 + intelli-espower-loader@1.0.1 + watchify@3.9.0 added 4 packages in 7.448s
第一回はここまで。 この状態で、ディレクトリには .git, node_modules, .gitignore, LICENSE, package.json, package-lock.json, README.md ファイルができているはず。
次回は、Google側のファイルの準備と、アップロード、GASの実行の準備
Raspberry Pi のGPIOを起動時に初期化する
Node.js利用
/etc/init.d/gpio-initialize.js
#!/usr/local/bin/node const Gpio = require('onoff').Gpio; [4, 17, 22, 27].map(pin => new Gpio(pin, 'in', 'both'));
sudo ln -s /etc/init.d/gpio-initialize.js /etc/rc3.d/S20gpio-initialize.js reboot
ls /sys/class/gpio
して指定のピンが初期化されていることを確認する
今回の例では - /sys/class/gpio/gpio4 - /sys/class/gpio/gpio17 - /sys/class/gpio/gpio22 - /sys/class/gpio/gpio27 が存在していれば、初期化はOK
その後
cat /sys/class/gpio/gpio4/direction
したときに "in" が返ってくればOK