バグですか?仕様ですか?筋肉です💪

筋肉とプログラミング、その他アウトプットをしていきます。

クールなURIとは

前回と引き続き

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

の本からURIについて書いていきます!!

この記事を読んでわかること

  • なぜ変わらないURIはクールなのか
  • 変わりにくいURIするには
  • URIを変えなくてはいけない時はどうするか

なぜ変わらないURIはクールなのか

変わらないURIがクールである

という言葉があります。なぜ変わらないURIがクールなのでしょうか?

Webはそれぞれのリソースに他のリソースが埋め込まれたハイパーメディアシステムのため、URIを変更しリンク切れを起こすと機能しなくなってしまいます。

通常ユーザは、ページにあるリンクをクリックすることで、見たいページに遷移します。 しかし、そのリンクのURIが変わってしまうとユーザはページ遷移ができなくなってしまうので、webとして機能しなくなってしまうのです。

つまり、URIは出来るだけ変わらないものが好ましい(クールである)のです。

本では主に3つ紹介されていましたので、ダメなURIを例にあげて説明していきたいと思います。

プログラミング言語に依存した拡張子やパスを含めない

http://example.jp/login.pl

例えばこのURIの場合、.plというPerlというプログラミング言語のソースに使われる拡張子が使われています。
もし仮に、PerlからRubyに言語を変えてアプリケーションを実装するとなると、URIを変更しなければなりません。 (.plでRubyスクリプトを動かすこともできますが、あまりよくはありませんよね。)

このように実装言語に依存した文字列をURIに含めてしまうと、言語を変更した際にそのURIは使えなくなってしまします。

メソッド名やセッションIDを含めない

http://eample.jp/Login.do?action=showPage

.doという拡張子も問題ですが、今回はshowPageが問題です。これはメソッドの名前なのですが、 もしリファクタリングを行なってメッソド名を変更してしまうとURIも変更になってしまいます。

セッションIDの場合、

http://example.jp/home.jsp?jsesionid=123456789

セッションIDをCookieではなくURIに埋め込むとログインのたびにセッションIDは変わるので、その度にURIも変更になってしまいます。

URIはリソースの名詞にする

http://example.jp/sample/people/show/1234

URIはリソースの名前です。なので本来名詞であるべきです。

あるリソースを取得するのか更新するのかは、URIで指定するのではなく、HTTPメソッドで決定されます。 つまりURIとHTTPメソッドは、名詞と動詞となるように設計されるべきで、showという動詞をURIに含めることはよくありません。

URIを変えないといけない時は?

変わらないURIがクールな理由と、具体的に変わりにくくするためにはどうするべきかについて書きました。

しかし、システムには変更はつきものであるように、URIも変更しなければいけない時があります。 その場合の対処法として、本では出来るだけリダイレクトをするようにと書かれていました。

リダイレクトとは、古いURIから新しいURIに転送するHTTPの仕組みで、リダイレクトしてあげればリンク切れでユーザが見たいページに遷移できなくなる心配も無くなります。

URI

この記事を読んでわかること

  • URIとは何か
  • URLやURNとの違いは何か
  • 実際のURLの構文はどのような構造になっているのか

URIとは

URI(Uniform Resource Identifier)とは、直訳すると「統一リソース識別子」です。

リソースは前回、説明しましたが、「web上のありとあらゆる情報」のことです。

HTTPとは - バグですか?仕様ですか?筋肉です💪

なので、URIとは「web上の情報を統一的に識別するID」のことであり、それを用いるとリソースを一意に識別することができます。

URIとURLとURN

URIと似た用語でURL(Uniform Resource Locator)やURN(Uniform Resource Name)という用語もありますが、違いは以下のようになります。

URLは、リソースの場所を指し示すもの

URNは、リソースの名前を示すもの

URIは、URLとURNを総称する名前でリソースを識別するもの

URIの構文

http://takuma:pass@blog.example.jp:8000/search?q=test&debug=true#10
※ダミーです

このURIは次のように分けられます。

  • URIスキーム:http
  • ユーザ情報:takuma:pass
  • ホスト名:blog.example.jp
  • ポート番号:8000
  • パス:/search
  • クエリパラメータ:q=test&debug=true
  • URIフラグメント:#n10

URIスキーム:http

URIスキームは、そのURIが利用するプロトコルを示すのが一般的です。
この場合は、リソースにHTTPでアクセスできることを示します。

ユーザ情報:takuma:pass

ユーザ情報は、このリソースにアクセスする際に利用するユーザ名とパスワードからなります。

ホスト名:blog.example.jp

ホスト名はDNS(Domain Name System)で名前が解決できるドメイン名かIPアドレスで、 インターネット上で必ず一意になります。

ポート番号:8000

ポート番号は、このホストにアクセスするときのプロトコルで用いるTCPのポート番号を示します。 同じコンピュータ内で動作する複数のソフトウェアのどれが通信するかを指定します。

パス:/search

ホストの中でリソースを一意に示します。 このように、ホスト名とパスを組み合わせることで、あるリソースのURIが世界中で重複せず一意になります。

クエリパラメータ:q=test&debug=true

検索サービスや検索キーワードを渡す時など、クライアントから動的にURIを生成する時に利用します。

URIフラグメント:#n10

その前までの文字列で表現するURIが指し示すリソース内部の、さらに細かい部分を指定する時に利用します。

最後に

今回も前回に引き続き、「webを支える技術」をまとめました。
少しずつでも今まで理解していなかったweb関連の用語をアウトプットしていき、使いこなせるように続けていきます💪

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

HTTPとは

この記事を読んでわかること

  • HTTPとは何か。
  • HTTPの特徴であるステートレス性とはどういうものか。

背景

Web業界に入ってから数ヶ月経ち、プログラミングのこともまだまだですが、そもそもwebの知識が乏しいと感じていたので、 今日は、HTTPについて簡単にまとめてみました。

参考:

Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESS plus)

HTTPとは

HTTP(Hyper Text Transfer Protocol)はweb上でやり取りするリソースの表現を、クライアントとサーバの間でやり取りするためのTCP/IPをベースとしたプロトコル

と言われても、なんのこっちゃわかりません。 こういうわからない用語を調べて、さらにわからない用語が出た時は、さらにその用語を調べていくしかありません。。。 ここで出てきたよくわからない言葉、

を先に説明します。

リソース

リソースとは、web上のありとあらゆる情報。

例えば、

など。

特に、あるリソースを他のリソースと区別して指し示すときに使う名前をURIと呼びます。

クライアントとサーバ

クライアント(webブラウザ)が、情報を提供するサーバ(webサーバ)に接続し、各種のリクエストを出して、レスポンスを受け取る。 サーバでの処理に時間がかかる場合でも、リクエストを出したクライアントはレスポンスが返るまで待機する。

プロトコル

プロトコルとは、通信をする上での約束事

TCP/IP

TCP/IPとは、TCP(Transmission Control Protocol)とIP(Internet Protocol)は、インターネットの基盤を構成する重要なネットワークプロトコル。 HTMLやXMLなどのハイパーテキストだけでなく、静止画、音声、動画、Javascriptプログラム、PDFや各種オフィスドキュメントなど、 コンピュータで扱えるデータであればなんでも転送できる。

IPは、インターネット層でデータを実際にやり取りする部分を担当している。 指定したIPアドレスを送り先として、パケット単位でデータをやり取りして通信する。しかし、多数のルータを経由して最終的な送り先まで届くかは保証しない。

TCPは、IPが保証しなかったデータの転送を保証するトランスポート層に相当する。

で、ここまでを踏まえて改めて先ほどの説明を言い換えると、

HTTP(Hyper Text Transfer Protocol)はweb上のありとあらゆる情報を、クライアントとサーバの間でやり取りするためのTCP/IPをベースとした通信の約束事。

(あまり変わってない笑)

HTTPのステートレス性

次にHTTPの特徴であるステートレス性ついて触れてみたいと思います。

ステートレス性とは、サーバがクライアントのアプリケーション状態を保存しない制約のことです。 ステートレスとは、逆にステートフルという言葉がありますが、両者の違いについて日常でよくあるやりとりを例に挙げて、説明します。

Aをクライアント、Bをサーバだと思ってください。

ステートフルなやりとり

A「筋トレを教えてください!!」

B「どうして筋トレを教えて欲しいの?」

A「筋肉を大きくしたいんです!!」

B「どこの筋肉を大きくしたいの??」

A「足の筋肉を大きくしたいんです!!」

B「じゃあスクワットやろうか(^∀^)ᕗ」

A「はい!!」

ステートレスなやりとり

A「筋トレを教えてください!!」

B「どうして筋トレを教えて欲しいの?」

A「筋肉を大きくしたいので、筋トレを教えてください!!」

B「どこの筋肉を大きくしたいの??」

A「足の筋肉を大きくしたいので、筋トレを教えてください!!」

B「じゃあスクワットやろうか(^∀^)ᕗ」

A「はい!!」

このように、ステートフルなやりとりはそれまでのやりとりをずっと覚えていることを前提に会話をしています。 逆にステートレスなやりとりは、記憶することができないので、都度「筋トレを教えてください!!」を言わないと教えてくれません笑笑

側から見たら、ステートフルなやりとりの方が簡潔で良いのではと思いますが、欠点があります。

それは、サーバがクライアントのアプリケーションの状態を覚えることは、クライアントの数が増えるにしたがい難しくなるからです。

不特定多数のクライアントを相手にする場合、サーバ間でデータを同期しなければいけませんが、それが100台となるととても負荷のかかることです。 なので、ステートフルでは、サーバの数を増やしづらいのです。

その点ステートレスは、アプリケーションの状態を覚える必要がないため、サーバ側のシステムは単純になり、拡張するときにはただサーバを増設するだけで済んでしまうのです。

このように、プロトコルをシンプルに保つことで、PCだけでなく様々なデバイス上でHTTPは使われています。

最後に

本の数10ページについてまとめましたが、まだまだ浅い記事しか書けないなと思いました。 ただ、続けることが大切なので自分のできる範囲でまずはやっていきたいと思います。

Google Analytics APIを使用しページごとのセッション数を取得

この記事に書かれていること

背景

コーポレートサイトのブログ記事の人気ランキングを実装のために、ブログの記事ごとのセッション数を取得する必要がありました。

そこで、Google Analytics APIを使用しデータを取得し、実装しました。

Google Analyticsでできること。

Google Analyticsは、シェアNo.1のアクセス解析ツールで、Googleアナリティクスを使えば、Webサイト運営に必要なデータはほぼ見ることができます。

  • リアルタイムの利用状況
  • ユーザーの基本属性
  • ユーザーがどこから来たか
  • サイト内でのユーザーの動き
  • Webサイトの成果

などです。

実装

準備

まず必要な登録や、設定、情報を取得する必要がありますが、 以下の記事をご参照ください。

また今回の実装においては、大変参考になった記事です🙇‍♂️

https://qiita.com/Sa2Knight/items/0b61efc2be0bdf33ec48

https://qiita.com/ryota-sasabe/items/a5efd2aac244cfcce5c7

全体像

lib/tasks/google_analytics.rakeに具体的な処理、

lib/google_analytics_reporting.rbにGoogleAnalyticsReportingクラスを定義、

必要な情報、秘匿情報は各種ファイルに記述しました。

config/environments/

config/credentials

# lib/tasks/google_analytics.rake
namespace :google_analytics do
  desc 'update_articles_sessions_count_with_Google_Analytics'
  task update_articles_sessions_count: :environment do

    reporting = GoogleAnalyticsReporting.new(
      '2019-01-01',
      'today',
      'ga:uniquePageviews',
      'ga:pagePath',
    )
    reporting = reporting.get_reporting
    articles_pages = reporting.rows.select { |row| row.dimensions.first.match(%r{articles/.+}) }

    articles = []
    Article.published.find_each do |article|
      article = article.attributes
      article['category'] = Article.categories[article['category']]
      article['public_status'] = Article.public_statuses[article['public_status']]

      articles_page = articles_pages.find { |articles_page| articles_page.dimensions.first.gsub("/articles/",  "") == article['uid'] }
      next if articles_page.nil?
      article['sessions_count'] = articles_page.metrics.first.values.first.to_i
      articles << article
    end

    if articles.present?
      Article.upsert_all(articles)
      Article.reindex
    end
  end
end
# lib/google_analytics_reporting.rb
class GoogleAnalyticsReporting
  require 'google/apis/analyticsreporting_v4'
  SCOPE = ['https://www.googleapis.com/auth/analytics.readonly'].freeze
  def initialize(start_date, end_date, metric_type, dimension_type)
    @analytics = Google::Apis::AnalyticsreportingV4
    @client = @analytics::AnalyticsReportingService.new
    ENV['GOOGLE_PRIVATE_KEY'] = Rails.application.credentials.google_analytics[:private_key]
    ENV['GOOGLE_CLIENT_EMAIL'] = Rails.application.credentials.google_analytics[:client_email]
    ENV['GOOGLE_PROJECT_ID'] = Rails.application.credentials.google_analytics[:project_id]
    @start_date = start_date
    @end_date = end_date
    @metric_type = metric_type
    @dimension_type = dimension_type

    @client.authorization = Google::Auth::ServiceAccountCredentials.make_creds(
      scope: SCOPE
    )
  end

  def get_reporting
    request = build_request
    response = @client.batch_get_reports(request)
    response.reports.first.data
  end

  def build_request
    date_range = @analytics::DateRange.new(start_date: @start_date, end_date: @end_date)
    metric = @analytics::Metric.new(expression: @metric_type)
    dimension = @analytics::Dimension.new(name: @dimension_type)
    @analytics::GetReportsRequest.new(
      report_requests: [@analytics::ReportRequest.new(
        view_id: Rails.configuration.google_analytics_view_id,
        metrics: [metric],
        dimensions: [dimension],
        date_ranges: [date_range]
      )]
    )
  end
end

コード解説

# lib/google_analytics_reporting.rb
class GoogleAnalyticsReporting
  require 'google/apis/analyticsreporting_v4'
  SCOPE = ['https://www.googleapis.com/auth/analytics.readonly'].freeze
  def initialize(start_date, end_date, metric_type, dimension_type)
    @analytics = Google::Apis::AnalyticsreportingV4
    @client = @analytics::AnalyticsReportingService.new

まずクラスので必要な情報を定義していきます。

# lib/google_analytics_reporting.rb
    ENV['GOOGLE_PRIVATE_KEY'] = Rails.application.credentials.google_analytics[:private_key]
    ENV['GOOGLE_CLIENT_EMAIL'] = Rails.application.credentials.google_analytics[:client_email]
    ENV['GOOGLE_PROJECT_ID'] = Rails.application.credentials.google_analytics[:project_id]

は、認証のために必要な情報で、参考にしたqiitaにはjsonファイルで記述されていますが、環境変数に定義しても認証が行えるので 今回はこのように定義しました。 中身は、秘匿情報、環境ごとに取得したいのでcredentialsに記述しました。

# lib/google_analytics_reporting.rb
    @start_date = start_date
    @end_date = end_date
    @metric_type = metric_type
    @dimension_type = dimension_type

データの取得の期間、メトリックとディメンション(条件のようなもの)を引数で渡せるように定義しました。 将来、異なる情報を取得したいときにもこのクラスを使用できます。

# lib/google_analytics_reporting.rb
    @client.authorization = Google::Auth::ServiceAccountCredentials.make_creds(
      scope: SCOPE
    )
  end

initializeが呼び出されたときに認証を行なっています。

# lib/tasks/google_analytics.rake
    reporting = GoogleAnalyticsReporting.new(
      '2019-01-01',
      'today',
      'ga:uniquePageviews',
      'ga:pagePath',
    )

rake taskにてクラスをインスタンス化させます。 その際に、今回取得したいデータの条件を引数として渡してあげます。

# lib/tasks/google_analytics.rake
    reporting = reporting.get_reporting
    articles_pages = reporting.rows.select { |row| row.dimensions.first.match(%r{articles/.+}) }

get_reportingメソッドでGAからのデータを取得します。中身は、

# lib/google_analytics_reporting.rb
def get_reporting
    request = build_request
    response = @client.batch_get_reports(request)
    response.reports.first.data
  end
# lib/google_analytics_reporting.rb
  def build_request
    date_range = @analytics::DateRange.new(start_date: @start_date, end_date: @end_date)
    metric = @analytics::Metric.new(expression: @metric_type)
    dimension = @analytics::Dimension.new(name: @dimension_type)
    @analytics::GetReportsRequest.new(
      report_requests: [@analytics::ReportRequest.new(
        view_id: Rails.configuration.google_analytics_view_id,
        metrics: [metric],
        dimensions: [dimension],
        date_ranges: [date_range]
      )]
    )
  end

となっており、先ほど引数で渡した条件からデータを取得しています。 その際にview_idを渡していますが、これはconfig/environments/以下の環境ごとのファイルにそれぞれ定義しています。

# lib/tasks/google_analytics.rake
    articles = []
    Article.published.find_each do |article|
      article = article.attributes
      article['category'] = Article.categories[article['category']]
      article['public_status'] = Article.public_statuses[article['public_status']]

      articles_page = articles_pages.find { |articles_page| articles_page.dimensions.first.gsub("/articles/",  "") == article['uid'] }
      next if articles_page.nil?
      article['sessions_count'] = articles_page.metrics.first.values.first.to_i
      articles << article
    end

    if articles.present?
      Article.upsert_all(articles)
      Article.reindex
    end

ここで、dbに存在する。ブログ記事のデータを取得し、GAと同じuid(url)のもののsessions_countを更新させる処理を行なっています。 レコード数は少ないとは思いますが、大量にデータを扱っても良いようにfind_eachを使用しました。 https://blog.toshimaru.net/rails-find_each/

また、1件ずつsqlを発行するのは、処理的によくないのでbulk_insertのupsert_allを使いました。 最後に、reindexをしているのは、elastic searchを使用しているため、upsert_allして値を更新したらreindexをしないといけないからです。

最後に

APIを使用するのは初めてだったので、最初はqiitaの丸写しに近いものでしたが、コードレビューを行なってもらい、より自分の実装したいものになっていきました。 また、内容の理解やrubyの理解も深まりました。 同じ処理でも、よりパフォーマンスの良い処理、実は省くことのできる記述などがありました。例えばjsonファイルではなく必要な情報だけ環境変数に記述すれば良いところなど。

最初にqiitaからヒントを得るのは悪くないと思いますが、そこから

  • コピペしたものの中身はどうなっているのか
  • 何が必要なもので、何が不必要なのか
  • 削れる処理はないか
  • よりパフォーマンスをあげるには

をもう少し自分で考え、実装できるようにしないとなと思いました。

gitで派生元branchを間違えた時の対処法

作業ブランチの派生元を修正する

  1. git rebase --onto {本来親にしたかったブランチ} {間違って親にしてしまったブランチ名} {親を変更する作業ブランチ名}

  2. conflictを解消する。

  3. ブランチの派生元を調べる。
    ブランチがどこまでマージされているかを調べる
    git show-branch branchA branchB
    ブランチの派生元を調べる
    git show-branch branchA branchB | tail -1

  4. 一度pushしている場合は、コミット履歴の改変になってしまうので、注意が必要。
    他の人も同じ作業をしている場合はNG。自分だけなら-fオプションをつけて強制的にpushする。
    git push -f origin {親を変更する作業ブランチ名}

新しいブランチを作り直す。

新しい作業ブランチを正しく作っておき、git cherry-pickを使って欲しいコミットだけを適用する。

  1. 正しくブランチを作り直す
    git checkout -b {新しく作るブランチ} {本来親にしたかったブランチ}

  2. 欲しい作業だけを適用する
    git cherry-pick {コミットID}

  3. ブランチの派生元を調べる。
    ブランチがどこまでマージされているかを調べる。
    git show-branch branchA branchB
    ブランチの派生元を調べる
    git show-branch branchA branchB | tail -1

正規表現

^行頭
$行末
\tタブ文字
\n改行文字
\s空白文字(スペース、タブ文字、改行文字等)
\d半角数字1文字
\w英単語を構成する文字(半角英数字とアンダースコア)
\b単語の境界

{n,m}直前の文字が n 文字以上、m 文字以下
{n}直前の文字がちょうど n 文字

.任意の1文字
+直前の文字が1個以上
*直前の文字が0個以上
?直前の文字が1個、または無し

ABC|DEF「文字列ABCまたは文字列DEF」のOR条件
[AB]AまたはBが1文字
[^AB]AでもなくBでもない任意の1文字

[a-z][-az]ではハイフンの意味が異なる
[a-z]aからzのアルファベット
[-az]-またはaまたはz

( )マッチする部分をキャプチャ(捕捉)する
キャプチャした部分は置換するときに $1 や \1 で参照できる
( )はキャプチャだけでなく、グループ化にも使われる
(ABC)?文字列 ABC があり、または無し

正規表現中の特別な文字は \ でエスケープする

 

I18n (internationalization )

アプリケーションで時刻や日付のフォーマット、validationの文言などを国ごとに定義すると、設定によりその国に適した形式に切り替えることができる。

  • 文言系:メニューなどの表示や入出力される文字
  • 時間系:時刻(標準時)や暦(西暦や国・宗教ごとの暦)、日付や時刻の表記(月や曜日が名前か数字か、年月日の順序など)
  • 数値系:数値の表記(小数点の記号や桁区切り)
  • 単位系:通貨や度量衡の単位など

e-words.jp