Ginqでカッコ良く配列を操作する
出向先にいる優秀なフリーランスのプログラマに教えてもらったGinqという面白いライブラリのご紹介です。
GinqはLINQにインスパイアされたPHPのライブラリです。
LINQを知らないという人は次をどうぞ。
Ginqの勉強方法
基本的な使い方はGitHub - akanehara/ginq: `LINQ to Object` inspired DSL for PHPに記載されています。
ただそれだけだと不十分なので、個人的にオススメなのはtest/GinqTest.phpのテストケースに目を通すことです。
GinqTest.phpには短いコードでGinqメソッドの使用した例とその結果が数多く記述されています。
Ginqの使用例①
<?php // この配列から、、、 $list = array( array('id' => 1, 'type' => 1, 'score' => 1952), array('id' => 2, 'type' => 1, 'score' => 23523), array('id' => 3, 'type' => 1, 'score' => 9832), array('id' => 4, 'type' => 2, 'score' => 85322), array('id' => 5, 'type' => 2, 'score' => 9149), array('id' => 6, 'type' => 2, 'score' => 33), array('id' => 7, 'type' => 3, 'score' => 185), array('id' => 8, 'type' => 3, 'score' => 25981), array('id' => 9, 'type' => 3, 'score' => 456), array('id' => 10, 'type' => 3, 'score' => 78881), ); // 次のようにtype=2を除くtypeの中で最大のsocreを持つ要素を抽出したい。 $maxScoreList = array( array('id' => 2, 'type' => 1, 'score' => 23523), array('id' => 10, 'type' => 3, 'score' => 78881), );
「グループ単位で、あるルールに従って最大のレコードを抽出する」ためのサンプルを紹介します。
まずはPHPのみで書いてみました。
<?php $maxScoreList = array(); foreach ($list as $item) { $type = $item['type']; // type=2の要素を除外する。 if ($type == 2) { continue; } // $typeが一度登場している場合はscoreを比較する。 if (isset($maxScoreList[$type])) { // scoreが最大の要素で置換する。 if ($maxScoreList[$type]['score'] < $item['score']) { $maxScoreList[$type] = $item; } // $typeが初登場の場合は戻り値の配列に追加する。 } else { $maxScoreList[$type] = $item; } } // インデックスを採番し直す。 $maxScoreList = array_values($maxScoreList);
もう少しコンパクトに書くこともできますが、可読性を考えたらこれぐらいに留めておいた方がいいかな?
最後のarray_valuesは歯抜けになった配列のインデックスを0からの連番にするためによくやる手法です。
次はGinqを使った例です。
<?php $maxScoreList = Ginq::from($list) // type=2の要素を除外する。 ->where(function ($v) { return $v['type'] != 2; }) // typeが同じ要素をグルーピングする。 ->groupBy(function ($v) { return $v['type']; }) // $grにはグルーピングした要素の塊が渡ってくる(例えばtype=1の要素だけとか) ->select(function ($gr) { // socreが最大の要素を取得する。 return $gr->maxWith(function ($v1, $v2) { return $v1['score'] - $v2['score']; }); }) // インデックスを採番し直す。 ->renum() ->toArray();
ループを使っていないのがカッコイイと思うのは私だけかな(;;´(ェ)`)
Unixのパイプのように、データの形がメソッドチェーンで実行する度に変わる様子がイメージできるので、自分はこっちの書き方の方が好きです。
あとメソッドがSQLのキーワードと同じ名前になっていることが多いので、SQLを知っているならメソッドがやっていることを直感的に理解しやすいです。
Ginqの使用例②
<?php // curlでYahoo!のレスポンスヘッダの情報を取得する。 $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, 'http://www.yahoo.co.jp'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPGET, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_NOBODY, true); $result = curl_exec($ch); curl_close($ch); // $resultにレスポンスヘッダの文字列が入っているのでそれを配列に変換する。 // 具体的には次のようなレスポンスヘッダの文字列を、、、 $result = "HTTP/1.1 200 OK Server: nginx Date: Thu, 30 Jun 2016 03:21:03 GMT Content-Type: text/html; charset=UTF-8 ・・・ "; // array(ヘッダ名 => ヘッダ値)の配列に変換したい。 $headerList = array( 'Server' => 'nginx', 'Date' => 'Thu, 30 Jun 2016 03:21:03 GMT', 'Content-Type' => 'text/html; charset=UTF-8', );
次は前の例よりも少し実践的な内容で、Webページのレスポンスヘッダの文字列をパースして連想配列に変換するサンプルを紹介します。
まずはPHPのみの場合です。
<?php // 改行文字列を配列に変換する。 $resultItemList = explode("\n", trim($result)); $headerList = array(); foreach ($resultItemList as $resultItem) { // 「ヘッダ名: ヘッダ値」の形の文字列を、ヘッダ名とヘッダ値に分割する。 list($name, $value) = explode(':', trim($resultItem), 2); // ヘッダ値が存在したら戻り値配列に追加する。 if (isset($value)) { $headerList[$name] = $value; } }
explode関数の第三引数の2は、分割数を最大で2にするという意味です。
コロンが複数あった場合でも最初に見つかったコロンで文字列が2分割されます。
次はGinqを使用した例です。
<?php $headerList = Ginq::from(explode("\n", $result)) // 「ヘッダ名: ヘッダ値」の形の文字列を、array(ヘッダ名、ヘッダ値)に変換する。 ->select(function($v) { return explode(':', trim($v), 2); }) // :をデリミタとして2分割できていないものは除外する。 ->where(function($v) { return count($v) == 2; }) // array(ヘッダ名、ヘッダ値)をarray(ヘッダ名 => ヘッダ値)に変換する。 ->select(function($v) { return trim($v[1]); }, function($v) { return $v[0]; }) ->toArray();
(´(ェ)`)・・・
もしかしたら、これはPHPだけのほうがわかりやすいかも(汗
ただし、もう少し複雑な加工処理が入るとGinqに軍配があがるかもしれません。
パフォーマンスについて
今の現場では、数百万以上のユーザがいるソシャゲのAPIでGinqを使用しています。
Ginqはよく遅いと言われていますが、体感的に遅いと思ったことは一度もありません。
(扱う配列のサイズがそれほど大きくないというのもありますが、、、)
PHP7で高速化も見込めるのでパフォーマンスはそれほど気にする必要がないのかなぁという印象です。
Sass/CompassをEclipseから利用する
外部ツールなど使わずに普段利用しているEclipse上でSass/Compassの作業を完結させたかったので、導入からEclipseでCompassのコンパイルを行うまでの手順をメモしました。
Rubyのダウンロード
SassもCompassもRubyで書かれているためRubyのインストールが必須になります。
ダウンロードサイト
http://rubyinstaller.org/downloads/
RubyInstallersにあるリンクから環境にあわせて最新のRubyをダウンロードします。
※ 2016.01.18時点ではバージョン2.2.3が最新
Sass、Compassのインストール
コマンドプロンプトを起動して次のコマンドを実行します。
> gem update --system > gem install sass > gem install compass # バージョンが表示されることを確認する > compass -v
Eclipseの設定
EclipseからCompassを実行するためにAntを利用します。
私の使用しているEclipseは「Pleiades All in One 4.4.2」の「PHP 64bit Standard Edition」なのですが、build.xmlを作成してもAntビルドの項目が出てこなかったのでソフトウェア(Java EE開発ツール)を追加でインストールしました。
もしEclipseにJavaEEの開発環境が既に組み込まれている場合は次の設定は不要です。
Java EE開発ツールのインストール
- Eclipse
- 「ヘルプ」メニュー → 「新規ソフトウェアのインストール...」
- 使用可能なソフトウェア画面
- 「作業対象」に「Luna - http://download.eclipse.org/releases/luna」を入力する。
- フィルター入力に「Java EE」を入力する。
- 名前に「Eclipse Java EE 開発ツール」が表示されるのでチェックを入れる。
- 「次へ」ボタンを押下する。
- インストール詳細画面
- 「次へ」ボタンを押下する。
- ライセンスのレビュー画面
- 「使用条件の条項に同意します」を選択する。
- 「完了」ボタンを押下する。
- 「ソフトウェア更新」ダイアログが表示されたら「はい」ボタンを押下する。
動作確認
次のようなファイル群を作成してstyle.scssからstyle.cssが作成できることを確認します。
cssフォルダ以下はCompassのコンパイル時に自動生成されるので存在しなくても問題ありません。
test ├── build.xml ├── config.rb ├── css #コンパイル時に生成される │ └── style.css └── scss └── style.scss
新規プロジェクトの作成
Eclipseから「test」という名前で新規プロジェクトを作成します。
プロジェクトの種類はなんでも構いません。
config.rbの作成
config.rbはCompassの設定ファイルです。
testフォルダの下に次の内容でファイルを作成します。
http_path = "/" sass_dir = "scss" css_dir = "css" cache = false
build.xmlの作成
build.xmlにCompassでコンパイルを行う設定を記述します。
testフォルダの下に次の内容でファイルを作成します。
<?xml version="1.0" encoding="utf-8" ?> <project name="test" default="all"> <!-- Rubyのインストールフォルダ(binフォルダ)を指定する --> <property name="ruby.bin" value="C:\Ruby22-x64\bin" /> <target name="compass"> <exec executable="${ruby.bin}\ruby" failonerror="true"> <arg value="${ruby.bin}\compass" /> <arg value="compile" /> <arg value="-s" /> <arg value="expanded" /> </exec> </target> <target name="all" depends="compass"> </target> </project>
Sassファイルの作成
テスト用にstyle.scssを作成します。
test/scssフォルダの下に次の内容でファイルを作成します。
@import "compass"; #hoge { @include border-radius(10px); }
高負荷時に「java.net.NoRouteToHostException: 要求アドレスに割り当てられません」が発生した場合の対応方法
Javaで作成したWebAPIの負荷試験を行っている最中に次のようなExceptionが発生しました。
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server. at sun.reflect.GeneratedConstructorAccessor1475.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:422) at com.mysql.jdbc.Util.handleNewInstance(Util.java:408) at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:1137) at com.mysql.jdbc.MysqlIO.<init>(MysqlIO.java:356) at com.mysql.jdbc.ConnectionImpl.coreConnect(ConnectionImpl.java:2504) at com.mysql.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:2541) at com.mysql.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:2323) at com.mysql.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:832) at com.mysql.jdbc.JDBC4Connection.<init>(JDBC4Connection.java:46) at sun.reflect.GeneratedConstructorAccessor1288.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:422) at com.mysql.jdbc.Util.handleNewInstance(Util.java:408) at com.mysql.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:417) at com.mysql.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:344) at java.sql.DriverManager.getConnection(DriverManager.java:664) at java.sql.DriverManager.getConnection(DriverManager.java:247) at core.dts.mysql.MysqlConnection.Open(MysqlConnection.java:120) at core.dts.mysql.MysqlConnection.Query(MysqlConnection.java:203) at core.dts.mysql.MysqlShard.Query(MysqlShard.java:174) at core.dts.mysql.MysqlShard.Query(MysqlShard.java:190) at core.mdl.record.Record.RecordRead(Record.java:186) at core.mdl.record.RecordManager.GetRecord(RecordManager.java:135) at app.mdl.record.usr.UserAccount.GetInstance(UserAccount.java:292) at app.ctrl.BaseAction.InitUserData(BaseAction.java:210) at app.ctrl.BaseAction._Execute(BaseAction.java:106) at core.ctrl.Action._OnExecute(Action.java:475) at core.ctrl.Action.Execute(Action.java:660) at core.Application.Execute(Application.java:370) at core.ctrl.Let.OnRequest(Let.java:66) at core.ctrl.Let.doPost(Let.java:32) at javax.servlet.http.HttpServlet.service(HttpServlet.java:648) at javax.servlet.http.HttpServlet.service(HttpServlet.java:729) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:291) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:239) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:219) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:106) at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:142) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:79) at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:610) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:88) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:518) at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1091) at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:668) at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.process(Http11NioProtocol.java:223) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1517) at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1474) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) at java.lang.Thread.run(Thread.java:745) Caused by: java.net.NoRouteToHostException: 要求アドレスに割り当てられません at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:345) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) at java.net.Socket.connect(Socket.java:589) at java.net.Socket.connect(Socket.java:538) at java.net.Socket.<init>(Socket.java:434) at java.net.Socket.<init>(Socket.java:244) at com.mysql.jdbc.StandardSocketFactory.connect(StandardSocketFactory.java:258) at com.mysql.jdbc.MysqlIO.<init>(MysqlIO.java:306) ... 51 more
The last packet sent successfully to the server was 0 milliseconds ago.
java.net.NoRouteToHostException: 要求アドレスに割り当てられません
(´(ェ)`)・・・
(´(ェ)`)?
普通にアクセスする分には問題ないのですが、JMeterによる高負荷をかけると1分経過したぐらいからエラーが発生して、それ以降はDBアクセスができない状態になります。
しかし暫くするとまたDBアクセスできるようになる謎の現象に見舞われました。
MySQLにエラーログが出てないことから当初はTomcatやJDBC周りを疑ったのですが、実はOS(CentOS 6.6)の問題でした。
どうやらローカルポートを食いつぶしているのが原因だったようです。
■ 参考サイト
ローカルポートを食いつぶしていた話
上記サイトを参考にしてOSの設定を弄ってみました。
/etc/sysctl.confの末尾に次の3行を追加します。
net.ipv4.ip_local_port_range = 32768 65000 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 10
sysctl.conf修正後は次のコマンドで設定を反映します。
sysctl -p
設定後はエラーが出なくなりました( ・´ー・`)b
Skype7.3の広告領域を削除する方法など
新しいスカイプのUIが馴染めない(´;ω;`)ので広告領域の削除を含めて
個人的に使いやすくなるような設定をまとめてみました。
フォント変更する(チャット部分)
- フォントの設定画面を開いて編集します。
- 「ツール」メニュー → 設定 → チャット&SMS → チャット表示スタイル → フォントの変更
- 私はフォント名『Verdana』、スタイル『標準』、サイズ『9』で設定しています^^
広告領域を削除する
サイドメニューをシンプルにする
- 「表示」メニュー → 「コンパクトサイドビュー」を選択してチェックを付ける。
設定後の見た目は次のような感じになります。
■ 2015.04.24追記
Skypeをアップデートすると設定が元に戻るという話を聞いたので自動更新はオフにしておいた方が良いかもしれません。
phpredisを使用してランキング集計を行う
Redisでリアルタイムランキング集計するっていうのが周りで流行っているので自分なりにまとめてみました。
phpredisを使用して自分の前後のランキングと同率順位のランキングを集計してみます。
とりあえずphpredisのクラス・メソッドを使用してみる
- Redisのランキング機能を利用するためにはソート済みセットというデータ型を利用します。
- ソート済みセットを操作するコマンド(メソッド)は名前が z から始まります。
<?php // Redis接続 $redis = new Redis(); $redis->connect("localhost", 6379); // 指定したキーを削除する。 $redis->delete('ranking'); // メンバーを追加する。 // $redis->zAdd(key, score, member); $redis->zAdd('ranking', 1, 'user1'); $redis->zAdd('ranking', 5, 'user5'); $redis->zAdd('ranking', 10, 'user10_1'); $redis->zAdd('ranking', 10, 'user10_2'); $redis->zAdd('ranking', 10, 'user10_3'); $redis->zAdd('ranking', 50, 'user50'); $redis->zAdd('ranking', 100, 'user100'); $redis->zAdd('ranking', 500, 'user500'); $redis->zAdd('ranking', 1000, 'user1000'); // メンバーを削除する。 // $redis->zRem(key, member); $redis->zRem('ranking', 'user10_3'); // 昇順スコアランキングのランクを取得する。 // ランクは0から始まる。 // $redis->zRank(key, member) $result = $redis->zRank('ranking', 'user10_2'); var_dump($result); /* int(3) */ // 降順スコアランキングのランクを取得する。 // ランクは0から始まる。 // $redis->zRevRank(key, member) $result = $redis->zRevRank('ranking', 'user10_2'); var_dump($result); /* int(4) */ // 昇順スコアランキングの指定したランクのメンバーを取得する。 // startとendは0から始まるランクを指定する。 // WITHSCORESにtrueを指定した場合はスコアも取得する。 // $redis->zRange(key, start, end, [WITHSCORES]) $result = $redis->zRange('ranking', 0, 4); var_dump($result); /* array(5) { [0]=> string(5) "user1" [1]=> string(5) "user5" [2]=> string(8) "user10_1" [3]=> string(8) "user10_2" [4]=> string(6) "user50" } */ // 降順スコアランキングの指定したランクのメンバーを取得する。 // startとendは0から始まるランクを指定する。 // WITHSCORESにtrueを指定した場合はスコアも取得する。 // $redis->zRevRange(key, start, end, [WITHSCORES]) $result = $redis->zRevRange('ranking', 0, 4); var_dump($result); /* array(5) { [0]=> string(8) "user1000" [1]=> string(7) "user500" [2]=> string(7) "user100" [3]=> string(6) "user50" [4]=> string(8) "user10_2" } */ // 指定したスコア内にいるメンバーの数を取得する。 // $redis->zCount(key, min, max) $result = $redis->zCount('ranking', 1, 10); var_dump($result); /* int(4) */
自分の前後のランキング集計
<?php // Redis接続 $redis = new Redis(); $redis->connect("localhost", 6379); // 指定したキーを削除する。 $redis->delete('ranking'); // メンバーを追加する。 for($i = 1; $i <= 100; $i++) { $redis->zAdd('ranking', $i, 'user' . $i); } // メンバーuser30を中心とする。 $self = "user30"; // メンバーuser30のランクを取得する。 $selfRank = $redis->zRevRank('ranking', $self); // メンバーuser30を中心として前後5人のメンバーとそのスコアを取得する。 $start = ($selfRank < 5) ? 0 : $selfRank - 5; // $endはソート済みセット内の要素数を超えていてもエラーにならない。 $end = $selfRank + 5; $result = $redis->zRevRange('ranking', $start, $end, true); // ランキング結果を画面に表示する。 foreach ($result as $member => $score) { echo "member=" . $member . ", score=" . $score . "<br />"; } /* member=user35, score=35 member=user34, score=34 member=user33, score=33 member=user32, score=32 member=user31, score=31 member=user30, score=30 member=user29, score=29 member=user28, score=28 member=user27, score=27 member=user26, score=26 member=user25, score=25 */
同率順位のランキング集計
- zRangeやzRevRangeだとスコアが同じでもランクが異なる(0から始まる連番になる)ので、ロジックで同率順位づけする必要があります。
<?php // Redis接続 $redis = new Redis(); $redis->connect("localhost", 6379); // 指定したキーを削除する。 $redis->delete('ranking'); // メンバーを追加する。 $redis->zAdd('ranking', 1, 'user1'); $redis->zAdd('ranking', 5, 'user5'); $redis->zAdd('ranking', 10, 'user10_1'); $redis->zAdd('ranking', 10, 'user10_2'); $redis->zAdd('ranking', 10, 'user10_3'); $redis->zAdd('ranking', 50, 'user50_1'); $redis->zAdd('ranking', 50, 'user50_2'); $redis->zAdd('ranking', 100, 'user100'); // 全メンバーのランクを取得する。 // zRevRangeの第3引数(end)に-1を指定するとソート済みセットの末尾の要素まで取り出す。 $result = $redis->zRevRange('ranking', 0, -1, true); // ランキング結果を画面に表示する。 foreach ($result as $member => $score) { // 自分よりスコアが大きいメンバーの数を数える。 // zCountの第3引数(max)の+infは無限大のこと(-infで無限小になる) $rank = $redis->zCount("ranking", $score + 1, "+inf"); // $rankは0から始まってしまうので1加算して表示する。 echo "member=" . $member . ", score=" . $score . ", rank=" . ($rank + 1) . "<br />"; } /* member=user100, score=100, rank=1 member=user50_2, score=50, rank=2 member=user50_1, score=50, rank=2 member=user10_3, score=10, rank=4 member=user10_2, score=10, rank=4 member=user10_1, score=10, rank=4 member=user5, score=5, rank=7 member=user1, score=1, rank=8 */