無駄と文化

実用的ブログ

Slimフレームワークで整形された JSON レスポンスを返す

f:id:todays_mitsui:20160407012622j:plain


Slim フレームワークは PHP 製のマイクロフレームワークです。

Ruby 製の Sinatra というフレームワークにインスパイアされて作られたもので。リクエストのルーティングと、それに応答する処理を記述するだけで Web アプリが作れる、その名の通り最小限(マイクロ)でシンプルなフレームワークです。

どれくらいシンプルなのか、 Slim フレームワーク公式サイトのトップページ を見てもらうと感じが掴めると思います。


この Slim フレームワーク、昨年2015年の12月に Slim 3.0.0 がリリースされ、バージョン2系列から大幅にパワーアップしました。
最小構成で Web アプリが作れるお手軽さはそのままに、PSR-7 に準拠して他のライブラリとの連携がやりやすくなりました。

個人的には「パワーアップしたな」っていう感想ではなく「カッコ良くなったな」と感じています。


Slim で JSON レスポンスを返す

本題に入ります。とは言っても小ネタですが。

Slimフレームワークは普通の HTML形式(text/html) でレスポンスを返す方法の他に、JSON形式 (application/json) でレスポンスを返す方法も標準でサポートしています。

私が Slim を使うときは手軽に JSON API を構築したいときなので、レスポンスはもっぱら JSON形式ですね。


具体的には Response クラスの withJson() というメソッドを使って、

<?php

$app = new \Slim\App;

$app->get('/hello/{name}', function ($request, $response) {
    $name = $request->getAttribute('name');
    
    $data = [
        "status" => "OK",
        "name" => $name,
    ];

    return $response->withJson($data);
});

とすれば、Content-Type ヘッダーも適切に設定した上で JSON 形式のレスポンスを返してくれます。


ただ、


このやり方で JSON を返すと、結果は

{"status":"OK","name":"foobar"}

というように1行になっていて、開発中に生の JSON レスポンスを見に行ったときなど、非常に読みづらいんです。


Slim で整形された(pretty-print された) JSON レスポンスを返す

JSON レスポンスをいい感じに整形して(pretty-printして) 返してもらう方法です。

結論から言うと、

<?php

$app = new \Slim\App;

$app->get('/hello/{name}', function ($request, $response) {
    $name = $request->getAttribute('name');
    
    $data = [
        "status" => "OK",
        "name" => $name,
    ];

    return $response->withJson($data, 200, JSON_PRETTY_PRINT);
});

このようにします。


withJson() は第3引数にオプションを指定することができます。 オプションは標準関数である json_encode() と同じものを使ってねと公式に書いてあるので、JSON_PRETTY_PRINT を指定することで整形された JSON がレスポンスとして返ります。

{
    "status": "OK",
    "name": "foobar"
}

このように。


どうやら widthJson() は内部で json_encode() を使って実装されているようです。 なのでオプションも同じものでオッケィ!ということですね。

この方法の弱点は第3引数を指定するために第2引数(ステータスコード)を省略出来なくなることくらいでしょうか。


結構需要ありそうな Tips なのに公式サイトの目立つところには書かれていないので紹介させていただきました。


私からは以上です。

Google Analytics - セグメントの理解

f:id:todays_mitsui:20160313223156j:plain

はじめに

この記事はデベロッパー向けに Google Analytics Core Reporting API のセグメントについて掘り下げて解説する記事です。
Google Analytics の初心者向けに「セグメントとは?」と解説するものではありません

Reporting API で動的セグメントを最大限カスタマイズして利用する場合には必要になってくる知識かと思います。
逆に、ライトに『API でセッション数と直帰率とを取得したいだけ~』というような場合には必要性は薄いでしょう。


また、この記事はセグメントについての完全な解説を目指すものではありません。
公式のドキュメントで見落としがちな箇所(実際に私が見落とした箇所)について、図と文章を加えて説明を試みるものです。

セグメントについて何も知らない方は、まずは公式ドキュメントを読みましょう。


導入

セグメント自体についてはGoogle公式のドキュメントで

の2ページに渡って解説してあります。

その他に、セグメントの条件指定には指標(metrics)ディメンション(dimensions)について理解しなければいけないため、

も参照する必要があるでしょう。


以降ではこれらを踏まえてセグメントの全体像について解説します。


セグメント と セグメント条件(segmentCondition)

公式ドキュメントの動的セグメントの構文リファレンスの項によれば、

セグメントは、単一または複数のセグメント条件(segmentCondition)によって構成される

と書かれています。

ここで言う「単一のセグメント条件」がセグメントに設定したい条件の最小単位だと思えば理解がしやすいでしょう。

もちろん、「単一のセグメント条件」を複数個組み合わせてより高度なセグメントを設定することもできますが、解説は後ほどに回します。
まずは「単一のセグメント条件」がどんな構造で出来ているか見ていきましょう。


単純な例を挙げます、
少なくとも 1 つのセッションで Chrome ブラウザを使用したユーザー」だけをふるい分けるセグメント条件は次のように書けます。

users::condition::ga:browser==Chrome

この例を読み解くためには、セグメント条件が階層構造を持っていることを知らなければいけません。


セグメント条件(segmentCondition)の階層構造

セグメント条件は階層構造を持っています。
それぞれの階層は :: で区切られて、全部で3層構造になっています。

f:id:todays_mitsui:20160313223212p:plain

第1階層はスコープ(conditionScope)、第2階層はタイプ(conditionType)、第3階層は条件(dimensionOrMetricConditions)と呼ばれます。


他にも タイプ と 条件 の間に perUserperSession のような指標スコープ(metricScope)が入る事がありますが、これは 条件 に紐付くフラグのようなもので、階層構造とは扱いが異なるものです。

f:id:todays_mitsui:20160313223220p:plain


それぞれの階層について個別に見ていきましょう。


スコープ - ユーザースコープ(users) と セッションスコープ(sessions)

第1階層のスコープ(conditionScope)には、ユーザースコープ(users) と セッションスコープ(sessions) のいずれかを設定することができます。

これらは集計の際にユーザー毎にふるい分けるか、セッション毎にふるい分けるかを指定するものです。


タイプ - 条件(condition) と シーケンス条件(sequence)

第2階層のタイプ(conditionType)には、条件(condition) と シーケンス条件(sequence) のいずれかを設定することができます。

条件(condition) はいわるゆ基本的な条件指定だと思ってください。
それに対して、シーケンス条件(sequence) は条件の強化版のようなもので、「○○した後に□□したユーザー」といった前後関係(ステップ)を指定した条件を設定することが出来ます。

これについては後ほど、セグメント条件の合成の項で解説を加えます。


条件 - 指標条件(metricCondition) と ディメンション条件(dimensionCondition)

第3階層の条件(dimensionOrMetricConditions)には、指標条件(metricCondition) と ディメンション条件(dimensionCondition) のいずれかを設定することができます。

それぞれ指標(metrics)ディメンション(dimension)に対して条件を設定するという意味では言葉のとおりですね。

目立った違いとしては、
指標(metrics)には後述する指標レベルという概念が存在し、指標スコープ(metricScope)を加えることで指標レベルを引き上げることが挙げられます。


セグメント条件(segmentCondition)の階層構造 まとめ

上記をまとめると、「単一のセグメント条件」は下の図のように8種類に分類できることになります。

f:id:todays_mitsui:20160313223318p:plain

一番最初の例、

users::condition::ga:browser==Chrome

では、ユーザー毎に - 条件を設定して - ディメンション(ブラウザ)でふるい分けするという意味になります。


続いては、この「単一のセグメント条件」を組み合わせた、さらに複雑なセグメント条件について解説します。


セグメント条件(segmentCondition)の合成

「複数の条件を組み合わせる」と聞いてまず思い浮かべるのは ANDOR で条件を合成することでしょうか。
Google Analytics においてももちろんそのような設定は可能です。

が、この項で特筆したいのはセグメントは各階層ごとに合成できるということです。
正直、これがややこしさの原因でもありますね。


ユーザースコープとセッションスコープは AND演算子 で合成できます。
同様に条件タイプとシーケンス条件タイプも AND演算子 で合成できます。

条件タイプ下の条件は AND演算子, OR演算子 で合成でき、シーケンス条件タイプ下の条件は AND演算子, OR演算子, FOLLOWED BY演算子, IMMEDIATELY FOLLOWED BY演算子 で合成できます。

言葉だけではアレなので図を加えておきます。

f:id:todays_mitsui:20160313223331p:plain

複雑にしようと思えばここまで出来るという例ですね。


条件の合成 - AND演算子 と OR演算子

説明のため、第3階層の条件にだけ着目します。
先ほどの例でも見たように、「ブラウザが Chrome であること」はga:browser==Chromeというディメンション条件によって指定できるのでした。


では「ブラウザが Chrome で、かつ、所在地がロンドン」ということを条件に設定したい場合はどうでしょう。

このような条件指定は AND演算子(;) によって実現できます。
具体的にはga:browser==Chrome;ga:city==London とすることで、「ブラウザが Chrome で、かつ、所在地がロンドン」を表現できます。

スコープとタイプを補って完全な形のセグメント条件を書くと、

users::condition::ga:browser==Chrome;ga:city==London

これで、「ブラウザが Chrome で、かつ、所在地がロンドン のユーザー」をふるい分けることができます。


同様に OR演算子(,) を使えば、ga:browser==Chrome,ga:city==London という書き方で「ブラウザが Chrome か、または、所在地がロンドン」という条件を表現できます。

完全な形で書くと、

users::condition::ga:browser==Chrome,ga:city==London

ですね。


条件の否定 - NOT演算子

合成とは少し違うのですが、
NOT演算子(!) を使えば、「条件に一致したユーザー(またはセッション)を集計に含めない」という条件を表現できます。

書き方は、条件の先頭を ! からはじめて、

users::condition::!ga:browser==Chrome

このように。
これで「少なくとも一つのセッションで Chrome ブラウザを使っているユーザーを除いた」集計を指定することができます。


紛らわしい表現として、

users::condition::ga:browser!=Chrome

を例に挙げておきましょう。
このような書き方をした場合には、「少なくとも一つのセッションで Chrome 以外のブラウザを使っているユーザーを含めた」集計の指定です。


シーケンス条件の合成 - FOLLOWED BY演算子 と IMMEDIATELY FOLLOWED BY演算子

タイプにシーケンス条件(sequence)を指定した場合、AND演算子 と OR演算子 に加えて、FOLLOWED BY演算子(;->>) と IMMEDIATELY FOLLOWED BY演算子(;->) を使うことができます。
これらはユーザーに対して別セッションに対する条件を記述できるという意味で、条件(condition)の強化版になっています。


FOLLOWED BY演算子(;->>) を使うことで、「前のステップで○○して、その後のステップで□□した」という条件を表現できます。

例を挙げると、

users::sequence::ga:deviceCategory==desktop;->>ga:deviceCategory==mobile

これで、「デスクトップからアクセスして、その後のセッションでモバイルからアクセスしたユーザー」を指定できます。


IMMEDIATELY FOLLOWED BY演算子(;->) の場合は、「前のステップで○○して、直後のステップで□□した」という意味合いになるので、

users::sequence::ga:deviceCategory==desktop;->>ga:deviceCategory==mobile

これで、「デスクトップからアクセスした直後のセッションでモバイルからアクセスしたユーザー」を指定できます。


さらに、タイプがシーケンス条件(sequence)の場合、^演算子を使って「最初のステップで○○して、その後のステップで~」といった条件を表現できるようなのですが、あまりにも煩雑になるため解説は割愛します。

詳しくは Core Reporting API - セグメント - 2. 条件とシーケンスの使用 - シーケンス を参照してください。


タイプの合成 - AND演算子

条件やシーケンス条件は、さらに AND演算子(;) で合成することができます。
つまり、条件とシーケンス条件を併用することができるのです。

ただし、合成に使える演算子は AND演算子(;) のみで、OR演算子(,) を使うことはできません。


スコープの合成 - AND演算子

ユーザースコープのセグメント条件とセッションスコープのセグメント条件もまた、AND演算子(;) で合成して併用することができます。
この場合も合成に使える演算子は AND演算子(;) のみで、OR演算子(,) を使うことはできません。

まずユーザースコープのセグメント条件でユーザーがふるい分けられ、選ばれたユーザーのセッションの中からセッションスコープのセグメント条件に一致するセッションのデータが集計されます。


合成には冗長な表現が許される

さぁ、これこそ私がセグメントの理解に時間を要した最大の要因です。

「ブラウザが Chrome で、かつ、所在地がロンドンのユーザー」をふるい分けるセグメント条件の例。

users::condition::ga:browser==Chrome,ga:city==London

これと同じ意味のセグメント条件を次のような2通りの表現で書くことができます。

users::condition::ga:browser==Chrome,condition::ga:city==London

または

users::condition::ga:browser==Chrome,users::condition::ga:city==London


このように Google Analytics はセグメント条件を合成した際の冗長な表現をできる限り受け入れる設計になっているようです。


逆に、

  • スコープが同じセグメント条件同士が合成されている場合、後者のスコープを省略できる
  • スコープもタイプも同じセグメント条件同士が合成されている場合、後者のスコープとタイプを省略できる

というルールがある、と解釈することもできます。


指標(metrics)のレベル

この後の指標スコープ(metricScope)につながる話題として、指標レベルについて言及しておきます。

Google Analytics で利用可能な指標の中には ページ滞在時間(ga:timeOnPage) のようにヒット(単一アクセス)毎に意味をもつものもあれば、 平均ページ滞在時間(ga:avgTimeOnPage) のようにセッション毎の括りで集計しなければ意味がないものもあります。
または、新規セッション率(ga:percentNewSessions) などはユーザー毎の括りで集計してはじめて意味をもつ指標です。

このように各指標には、その指標が集計上の意味を持ち始める括りが存在し、その括りを指標(metrics)のレベルと呼びます。

上記の例で言えば、ページ滞在時間(ga:timeOnPage) のレベルはヒット(Hit)、平均ページ滞在時間(ga:avgTimeOnPage)のレベルはセッション(Session)、新規セッション率(ga:percentNewSessions)のレベルはユーザー(User) ということになります。


指標スコープによる指標レベルの引き上げ

指標が集計上の意味を持ち始める括りを指標(metrics)のレベルと呼んでいることを紹介しました。
では、各指標はその指標のレベルでのみ意味を持つのでしょうか?
前節と同じ例に当てはめるとすれば、ページ滞在時間(ga:timeOnPage) を使って指定した ga:timeOnPage>60 はヒット毎にしか意味を持たないのでしょうか?

答えは No です。
ga:timeOnPage>60 をセッション毎の括りで解釈すれば「いずれかのセッションでのページ滞在時間の合計が60秒を超えるユーザー」をふるい分ける条件と読むことができるでしょうし、同じくユーザー毎の括りで解釈すれば「全セッションを合計してページ滞在時間が60秒を超えるユーザー」をふるい分ける条件と読むことができるでしょう。

このように「どの括りで集計したときの値を条件に用いるか」という微妙な文脈を表現するために、セグメントの指標条件には指標スコープのための修飾子を付けて指標のレベルを引き上げることができます。


指標スコープのための修飾子は全部で三つ、perHit::, perSession::, perUser:: があります。

微妙な文脈の違いを理解するのはなかなかに難しい事です。実例を見てもらう方が早いでしょう。

  • users::condition::perHit::ga:timeOnPage>60
    → いずれかのヒットでページ滞在時間が60秒を超えるユーザー

  • users::condition::perSession::ga:timeOnPage>60
    → いずれかのセッションでページ滞在時間の合計が60秒を超えるユーザー

  • users::condition::perUser::ga:timeOnPage>60
    → 全てのセッションでのページ滞在時間の合計が60秒を超えるユーザー

こんなのもあります。

  • sessions::condition::perHit::ga:timeOnPage>60
    → いずれかのヒットでページ滞在時間が60秒を超えるセッション

  • sessions::condition::perSession::ga:timeOnPage>60
    → 1セッションを通したページ滞在時間の合計が60秒を超えるセッション

スコープ と 指標スコープ の組み合わせによって条件の意味が微妙に違ってくるわけですね。


では指標スコープを省略した場合には?
その際にはスコープのレベルがデフォルトの指標スコープとして適用される、と公式ドキュメントに書いてあります。

例えば、

  • users::condition::ga:timeOnPage>60
    users::condition::perUser::ga:timeOnPage>60 と同じ

  • sessions::condition::ga:timeOnPage>60
    sessions::condition::perSession::ga:timeOnPage>60 と同じ

といった感じになります。


引き上げの条件

「スコープ と 指標スコープ の組み合わせによって条件の意味が微妙に違ってくる」と書きましたが、では全ての組み合わせが別の意味を持って許されるかというと、そういう訳ではありません。

条件の意味としてあり得ないものは無効(invalid) とされます。

例えば、sessions::condition::perUser::ga:timeOnPage>60 を無理やりに解釈すると、「全てのセッションでのページ滞在時間の合計が60秒を超えるセッション」となり意味が通らないため、このようなセグメント条件は無効です。


どのような組み合わせで引き上げ可能なのか、逆にどのようなときは無効なのか。それについてはシンプルな公式があります。

f:id:todays_mitsui:20160313223355p:plain

この図にあるように、指標スコープの指定は常に スコープ と 指標のレベルの間でなければいけません。
指標に割り当てられているレベルは公式ドキュメントにリファレンスが附属しています。指標: プライマリ スコープ リファレンス を参照してください。


Tips

ここからは Google Analytics を深掘りしていく上で知っておくと便利な Tips を紹介します。


日本語名と英語名の変換

Google Analytics はお馴染みの管理画面もすでに日本語化が行き届いています。「平均滞在時間」「直帰率」「新規セッション率」など、日本語名で書かれればなじみ深い言葉も多いのではないでしょうか。

しかし、日本語名に馴染みすぎていることが Reporting API を活用する上では逆にハードルになります。
API で指標を指定するとき、セグメントを指定するとき、そのときには英語名での入力を迫られます。突然「直帰率の英語名を書け」と言われても戸惑いますよね。

というわけで、日本語名と英語名の変換方法を紹介します。


ずばり、いつもの管理画面を利用しましょう。
英語名を知りたい指標やディメンションを組み込んでカスタムレポートを作成しましょう。

f:id:todays_mitsui:20160313223410p:plain

レポートを保存したら、メニューから表示言語を「US English」に変更します。

f:id:todays_mitsui:20160313223419p:plain

f:id:todays_mitsui:20160313223428p:plain

もう一度カスタムレポートの設定画面を見に行くと...

f:id:todays_mitsui:20160313223435p:plain

ご覧のように指標・ディメンションの英語名を見ることができます。
闇雲にググって調べるよりも確実でスムースです。


Query Explorer の利用

Reporting API に投げるクエリを設計するうえで一番の近道は試行錯誤です。
とにかく様々なディメンション・様々なセグメントでクエリを投げて見て Analytics 内部での集計の仕組みを感じ取ることが最も早いでしょう。まさに習うより慣れろの世界です。

そんな試行錯誤に欠かせないのが Google が公式に提供している Query Explorer というツールです。

f:id:todays_mitsui:20160313223444p:plain

画面上でアカウントの認証をするだけで、集計期間・指標・ディメンション・フィルター・セグメントなどを様々に変え集計を簡単に試すことができます。
いつもの管理画面よりも細かい事ができるので Google Analytics を極めたいと思ったならば親しんでおくことをおすすめします。


まとめ

いささか散文的になりましたが、Google Analytics のセグメント集計の知識についてできる限りまとめてみました。

いま書いておかないと、せっかく必死で体得した知識をあっという間に忘れてしまいますからね。こっちも必死です。


私からは以上です。

Scrapy で相対パスを解決して絶対パスに変換 for v1.0.4

スクレイピングネタです。

Scrapy は Spider の perse() メソッドの中で新しい Request オブジェクトを yield してあげるだけで、次々に URL を辿ってクローリングしていけるので便利ですね。
例えば、response.xpath("//a/@href").extract() とかすればページの中のリンクを取得するのも容易です。

ただし、取得したリンクの href が全て絶対パスで書かれている保証はありません。
もしも、相対パスで書かれていた場合は(そういう場合は多いでしょう)、相対パスを解決して絶対パスにしてあげなくてはいけません。


Python で相対パスの解決をしようと思えば、urlparse モジュールの urljoin() を使うのが普通です。
が、Scrapy には urlparse.urljoin() 相当のメソッドが最初から組み込まれているので、そちらを利用するのが便利です。


サイトの全ページを辿って <title> を取得する Spider を例に見ると、

# -*- coding: utf-8 -*-
import scrapy


class SampleSpider(scrapy.Spider):
    name = "sample"
    allowed_domains = ["example.com"]

    start_urls = [
        "http://example.com/",
    ]

    def parse(self, response):
        # <title> タグの中を取得して返す
        title = response.xpath("//title/text()").extract_first()
        yield {
            "url": response.url,
            "title": title,
        }

        for href in response.xpath('//a/@href').extract():
            # href を urljoin() で絶対パスに変換
            next_url = response.urljoin(href)

            # 新しい Request オブジェクトを返すことで、その URL が続いてクロールされる
            yield  scrapy.Request(next_url)

for文の中で response.urljoin(href) としています。
response.urljoin() は現在の URL をベースに相対パスを解決するので、これで urlparse.urljoin(response.url, href) と書いたときと同じ処理になります。

シンプルで、分かりやすいですね。


私からは以上です。