月別アーカイブ: 2017年8月

Python3 + Selenium + Firefox でチャットワークの表示名を自動で切り替える

前提

以下の環境で実装、動作確認済み

要素 バージョン
debian 8.6
python 3.6.2

概要

非常にニッチな目的に感じる題だが、以下の流れでこうなった

  • 諸事情でチャットワークの表示名を自動で変更する手段を作りたい
  • チャットワークAPIを使えばいけそう
  • ユーザ情報を変更するAPIが存在しなかった(見落としているだけであったら教えてください)
  • 仕方ないのでスクレイピングで強引に変更しよう
  • チャットワークにログインする必要もあるのでSeleniumを使うのが良いのかな
  • せっかくだから最近勉強してるPython3を使おう

以上より、Seleniumを用いてFirefoxを自動操作し、チャットワークにログイン、表示名を任意のに変更するスクリプトを実装する。

なお、今回はGUIを持たない環境で実装するので、Xvfbを用いた仮想画面を使って動かす(この辺理解が怪しい)

下準備

まずFirefoxを入れる。画面無くて良いならPhantomJSとかヘッドレスブラウザ使えばいいじゃんと思い最初はそうしたのだが、どうしてもチャットワークが正しく表示できなかったので安定を取ってFirefoxにした。その分容量が大きいけど仕方ない。

$ apt-get install firefox

Firefoxのレンダリングエンジンであるgeckoをseleniumから利用するためのドライバーが別途必要。
こちらからダウンロードできるので、OSの種類に応じてダウンロードする。
今回は64ビット版のLinuxなので以下のように。パスが通ってるところで展開したほうが後々楽。

$ cd /usr/local/bin
$ wget https://github.com/mozilla/geckodriver/releases/download/v0.18.0/geckodriver-v0.18.0-linux64.tar.gz
$ tar -zxvf geckodriver-v0.18.0-linux64.tar.gz
$ rm geckodriver-v0.18.0-linux64.tar.gz

仮想モニタが必要になるのでインストール

$ apt-get install xvfb

必要なPythonモジュールをpipでインストール

$ pip install selenium
$ pip install PyVirtualDisplay

ログイン情報を定義

チャットワークにログインするためのメールアドレスをパスワードが必要になるが、ソースコード中にハードコーディングしてしまうとコードの共有ができなくなってしまうので、今回はJSONファイルに別途書き出すようにする。

以下のようなsecret.jsonを作成した。

{
  "email":    "hogehoge@fugafuga.com",
  "password": "qawsedrftgyhujikol"
}

実装

今回は一つのファイルで全て完結させるので、前項で作成したsecret.jsonと同じフォルダに、main.pyを作成する。

モジュールインポート

今回使うモジュールは以下の通りなのでimportする。

import time
import json
import sys
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.common.by import By
from pyvirtualdisplay import Display

ログイン情報を取得する

secret.jsonを開いて、jsonモジュールでデシリアライズ。メールアドレスとパスワードを抜き出す

with open('secret.json', 'r') as file:
  secret   = json.load(file)
  email    = secret['email']
  password = secret['password']

Seleniumの初期設定

仮想モニタを立ち上げ、SeleniumDriverをFirefoxで初期化。画面サイズがやたらと大きいのはデバッグ時のスクリーンショットを見やすくするためなので最終的に意味はない。

display = Display(visible=0, size=(1920, 1080))
display.start()
driver = webdriver.Firefox()

チャットワークへのアクセス

SeleniumDriverのgetメソッドで簡単にWebサイトにアクセスできる。

driver.get('https://www.chatwork.com/login.php?lang=ja&args=')

メールアドレス/パスワードを入力

find_element_by_css_selectorメソッドで、CSSセレクターを用いてDOMを取得し、それに対してsend_keysメソッドを用いてキー入力を行うことができる。JSONファイルから取得しておいたメールアドレス/パスワードを入力する。今後他にも出てくるけど、SeleniumDriverって何かとメソッド名が冗長だなと思った。

driver.find_element_by_css_selector('form[name="login"] input[name="email"]').send_keys(email)
driver.find_element_by_css_selector('form[name="login"] input[name="password"]').send_keys(password)

ログインする

同じくfind_element_by_css_selectorメソッドを用いてログインボタンを取得し、clickメソッドでクリックすることでログインする。

driver.find_element_by_css_selector('form[name="login"] input[name="login"]').click()

ログイン処理には時間がかかる上、チャットワークはJavaScriptで画面を構築していくので、構築の完了まで待機する必要がある。
Seleniumでは、以下のようにWebDriverWaitを用いることで、「特定の何かが確認できるまで待機する」という記述を行うことができる。
以下では、IDが_myStatusNameのDOMが確認できるまで待機するという処理(10秒でタイムアウト)

wait = WebDriverWait(driver, 10)
wait.until(expected_conditions.visibility_of_element_located((By.ID, "_myStatusName")))

_myStatusNameは、この部分。これが表示されたら読み込み完了と捉える。

読み込み完了でようやくチャットワークの画面に到達する。

プロフィール画面を開く

ヘッダのプルダウンメニューを開いて、編集メニューをクリックしてプロフィール編集画面を表示する。
かなりまどろっこしい事してる気がする。可能であれば最初からプロフィール編集画面を表示するためのJavaScriptコードを実行させれば一発だと思うが、Seleniumの練習も含めて地道に行う。

0.5秒のスリープを毎回挟んでるのは、UIのアニメーションがあるのでスグに次のUIを操作できないため。これもっと良い方法あったら教えて下さい。

driver.find_element_by_css_selector('#_myStatusName').click()
time.sleep(0.5)
driver.find_element_by_css_selector('#_myProfile .myAccountMenu__anchor').click()
time.sleep(0.5)

表示名を編集する

編集ボタンをクリックし、テキストボックスの内容を書き換え、保存ボタンをクリックする。
変更後の表示名は、コマンドライン引数から取ることを想定し、sys.argvを参照する。

driver.find_element_by_css_selector('#_profileContent ._profileEdit').click()
time.sleep(0.5)
driver.find_element_by_id('_profileInputName').clear()
driver.find_element_by_id('_profileInputName').send_keys(sys.argv[1])
driver.find_element_by_css_selector("div[aria-label='保存する']").click()

実行

以下コマンドで実装できる。ネットワーク環境の影響を大きく受けるが、手元の環境で実行時間は10秒程度。APIがあれば1秒で終わるというのに。

$ python main.py "ふー ばーのすけ"

最新のソースコードはこちら

所感

  • Seleniumの入門としてはえらくニッチな使い方になったが、個人的にやりたかったことが実現できたので良かった
  • 全体的冗長なコードになりやすそう。ラッパーライブラリが多いのも頷ける
  • JavaScriptで動いてるページをうまく制御したければ関連ライブラリ/フレームワークを使うべきか
  • このやり方だとチャットワーク側がちょいとHTML変更したら動かなくなったりするのでやっぱりAPIが欲しい

RubyプログラマがPython3を勉強した話

概要

Rubyを使い始めて2年弱の私が、前々から気にはなっていたが使ったことのないPythonにようやく手を出したお話。
私がひと夏の間に行った、基本的な勉強についての備忘録を残す。

Pythonのバージョンについて

Pythonのバージョンは2.x系(Python2)と3.x系(Python3)が存在するが、2と3では互換性のない変更が多く含まれており、実質別言語として扱われることが多い。

既存システムの多くはPython2で書かれていること、Python3では動かない有用なライブラリが存在することなどから、単に新しいという理由でPython3を採用することはできないが、今回は以下の理由からPython3を採用する。

  • Python2は2010年以降更新されていないため
  • Python2は2020年で公式のサポートが打ち切られるため
  • Python3は現在も積極的なバージョンアップが行われており、今後は完全に3に以降する流れのため

Python3.6.2のインストール

今回は現在の最新のPythonである3.6.2を、Debianにインストールした。
インストールには、Pythonのバージョン管理ツールであるpyenvを用いることにした。インストール手順については、以下記事にまとめた

Python3.6.2をインストールして、Wikipediaをクロールするスクリプトを書く | QSのウェブ開発とか

Python3の基本の勉強

はじめに、Python3の基本的な使い方、文法などの勉強には以下ページを利用した。広く浅く、かつ丁寧にPython3の基本が書かれた良い記事だったので、自身を持ってオススメできる。

Python3基礎文法 – Qiita

Rubyなどのスクリプト言語をある程度経験している人なら、Python3の基本的な範囲については特に戸惑うこと無く馴染めると思う。

Python3のトレーニング

前項で基本的なPython3の書き方、文法は抑えられた。
しかし、まだまだPythonの感覚は掴めていないので、競技プログラミングの問題をPython3を用いて沢山解くことで、Pythonの感覚を掴むことにした。

今回は、私が個人的にもよく利用していたpaizaのスキルチェックを利用した。

paizaのスキルチェック問題は、以下の5段階の難易度がある

  • Dランク: ウォーミングアップレベル
  • Cランク: 初級レベル
  • Bランク: 中級レベル
  • Aランク: 上級レベル
  • Sランク: 超上級レベル

Aランク以上は、言語の使い方よりも、高度なアルゴリズム設計が求められる問題(そもそも解けないのが殆ど)となるので対象外とし、D,C,BランクをPython3を用いて解くことにした。

実際に問題を解いてみると、多言語ではすんなり書けることがPython3ではなかなか書けない。やりたいことをPython3で実現する方法が全然わらないのだ。そういった壁に直面するたびに、実現方法を調べては、Python節を身につけることを繰り返し、D,C,Bランク合わせて30問ぐらい解いたところである程度の理解は出来たと判断して終了した。

Python3によるWebスクレイピング

次に、Pythonと言えばWebスクレイピング/クローリングなので(偏見?)、HTTPライブラリと、XMLパーサライブラリを用いて色々遊んだ。
その中で作成した、Wikipediaをランダムにクロールするスクリプトについては、以下記事にまとめた

Python3.6.2をインストールして、Wikipediaをクロールするスクリプトを書く | QSのウェブ開発とか

Backlog + Chatwork + Sinatraで課題の更新通知を行う

前提

以下の環境で実装、動作確認

要素 バージョン
debian 8.6
ruby 2.2.2
sinatra 1.4.7

概要

BacklogのWebhook機能を用いて、Sinatraで実装したWebアプリケーションを経由し、チャットワークにリアルタイム更新通知を行ったお話。

メール通知より圧倒的に早いリアルタイム通知による、プロジェクト管理の効率化ができる可能性を探る。

今回は試験的実装のため、課題作成時に課題の作成者、タイトル、URLをチャットワークのマイチャットに投稿するだけの簡単なシステムを実装した。

なお、今回は非常に軽量なシステムとなるので、Rubyの軽量WebフレームワークであるSinatraを採用しているが、Sinatraについては本記事から脱線するので詳しい解説は割愛する。

Webhookとは

端的に言えばアプリケーションで何らかの更新アクションが発生した時、その内容を特定のURLに対してPOSTしてくれる機能。
Backlogの場合、課題が追加された、更新された、削除されたなどの情報を、指定したURLに対してJSONでPOSTさせることができる。
技術とかツールでなく、ただの機能の名称なので注意。

参考 Webhookとは? on @Qiita

Webhookの設定を行う

BacklogでのWebhookの設定はプロジェクト単位で行う。
プロジェクト設定に、Webhookの項目があるので、そこから設定する。

Webhook名と説明は適当に入力し、WebHookURLに更新通知をPOSTさせたいURLを入力する。当然、インターネットからアクセスできる場所でないといけないので、ローカル開発環境などを指定しても動作しない。

「通知するイベント」は、Backlogのどの更新アクションをWebhookで通知するかを指定できる。今回は課題の追加時にチャットワークで通知したいので、「課題の追加」にのみチェックを入れる

Webhookの受け口作成(Sinatra)

本記事では、Webhookの受け口に、Rubyの軽量WebアプリケーションフレームワークであるSinatraを用いる。といっても、POSTされたデータをチャットワークに流すだけなので、フレームワークも不要なレベルだが、導入が簡単なので今回はSinatraを用いることにした。

なお、Sinatra環境の構築には、手前味噌だが以下を用いた。
Sa2Knight/Sinatra-Skeleton: Sinatraアプリケーションを最短で構築する個人用リポジトリ

Backlogの更新内容を出力

Sinatraの詳細的な説明は割愛するが、以下がWebhookの受け口となるエンドポイントの実装である

require 'sinatra/base'
require 'json'
require 'pp'

class App < Sinatra::Base

  post '/' do
    pp JSON.parse request.body.read
    return true
  end

end

BacklogのWebhookでは、指定したURLに対して、リクエストボディがJSONのPOSTリクエストが飛んで来る。
上記コードでは、それを受け取ってJSONをparseし、標準出力している。

サーバを立ち上げてから、以下のような課題を作成すると

WebhookによってJSONがPOSTされ、以下のような標準出力が得られる

{"created"=>"2017-08-15T13:44:56Z",
 "project"=>
  {"archived"=>false,
   "projectKey"=>"DEV",
   "name"=>"個人開発",
   "id"=>38382,
   "subtaskingEnabled"=>false},
 "id"=>19571304,
 "type"=>1,
 "content"=>
  {"summary"=>"課題タイトル",
   "key_id"=>275,
   "customFields"=>[],
   "dueDate"=>"2017-08-09",
   "description"=>"課題詳細",
   "priority"=>{"name"=>"中", "id"=>3},
   "resolution"=>{"name"=>"", "id"=>nil},
   "actualHours"=>nil,
   "issueType"=>
    {"color"=>"#7ea800",
     "name"=>"タスク",
     "displayOrder"=>0,
     "id"=>172585,
     "projectId"=>38382},
   "milestone"=>[],
   "versions"=>[],
   "parentIssueId"=>nil,
   "estimatedHours"=>nil,
   "id"=>2889963,
   "assignee"=>
    {"name"=>"sa2knight",
     "id"=>85748,
     "roleType"=>255,
     "lang"=>"null",
     "userId"=>"sa2knight"},
   "category"=>[{"name"=>"開発関係", "displayOrder"=>0, "id"=>79173}],
   "startDate"=>"",
   "status"=>{"name"=>"未対応", "id"=>1}},
 "notifications"=>[],
 "createdUser"=>
  {"nulabAccount"=>nil,
   "name"=>"sa2knight",
   "mailAddress"=>nil,
   "id"=>85748,
   "roleType"=>1,
   "userId"=>nil}}

フォーマットはBacklog APIと概ね一緒なのでここでは割愛する。
BacklogAPIについては[Ruby] BacklogをCUIで操作してみる | QS-DEVSでも触れている。

通知に必要な情報のみ抜き出す

受け取ったデータのうち、今回必要なのは以下の3種類

  • 課題キー
  • 課題名
  • 課題作成者

以下のようにコードを修正して、必要な情報のみ抜き出す。

  post '/' do
    params = JSON.parse request.body.read
    @issue = {
      key:     "#{params['project']['projectKey']}-#{params['content']['key_id']}",
      summary: params['content']['summary'],
      creator: params['createdUser']['name'],
    }
    pp @issue
    return true
  end

これで出力は以下のようになる

{:id=>"DEV-137", :summary=>"課題タイトル", :creator=>"sa2knight"}

チャットワーク連携の準備

次に、チャットワーク連携用のロジックを用意する。
こちらについても手前味噌だが、RubyでチャットワークAPIを利用するクラスを以前作っていたので、それをベースにする。
Sa2Knight/chatwork-ruby: RubyでチャットワークAPI呼んで遊ぶ

チャットワークAPIについては、以下参照
チャットワークAPIをRubyで利用する | QS-DEVS

今回はAPIの認証と、メッセージの送信ができればいいので、大幅にコードを削って以下のようなクラスに仕上げた。

require 'net/http'
require 'uri'
require 'json'
require 'date'

class Chatwork

  @@API_BASE = 'https://api.chatwork.com/v2'
  @@ROOM_ID  = 'hogehogefugafuga'

  # tokenを指定してオブジェクトを生成
  # tokenを省略した場合、環境変数を参照する
  def initialize(token = nil)
    @token = token || ENV['CHATWORKAPI']
  end

  # ルームに新規メッセージを送信
  # room_id: 対象のroomID
  # body:    投稿する本文
  def sendMessage(body)
    url = '/rooms/' + @@ROOM_ID + '/messages'
    res = createHttpObject(url, :post, {:body => body})
    return res.body ? JSON.parse(res.body) : []
  end

  private
    # HTTPリクエストを送信する
    def createHttpObject(url, method, params = {})
      api_uri = URI.parse(@@API_BASE + url)
      https = Net::HTTP.new(api_uri.host, api_uri.port)
      https.use_ssl = true
      api_uri.query = URI.encode_www_form(params) if method == :get
      req = createRequestObject(method, api_uri)
      req["X-ChatWorkToken"] = @token
      req.set_form_data(params) unless method == :get
      https.request(req)
    end
    # リクエストオブジェクトを生成する
    def createRequestObject(method, uri)
      case method
        when :get
          return Net::HTTP::Get.new(uri.request_uri)
        when :post
          return Net::HTTP::Post.new(uri.request_uri)
        when :put
          return Net::HTTP::Put.new(uri.request_uri)
        when :delete
          return Net::HTTP::Delete.new(uri.request_uri)
      end
    end
end

環境変数”CHATWORKAPI”にAPIキーを設定し、このクラスのインスタンスを生成後、sendMessageメソッドを呼び出せばチャットワークにメッセージを送信できる。

なお、今回はメッセージの送信先として、マイチャットを指定している。

課題の更新を通知する

前項で実装したChatworkクラスを用いて、Webhookで送られた更新情報をチャットワークに通知する。
Sinatraの使い方としてはかなりお行儀が悪いが、プライベートメソッドmakeChatworkMessageを実装し、チャットワークに送信するメッセージを作成、それをチャットワークに送信するようにした。

require 'sinatra/base'
require 'json'
require 'pp'
require_relative 'chatwork'

class App < Sinatra::Base

  post '/' do
    params = JSON.parse request.body.read
    issue = {
      key:     "#{params['project']['projectKey']}-#{params['content']['key_id']}",
      summary: params['content']['summary'],
      creator: params['createdUser']['name'],
    }
    Chatwork.new.sendMessage(makeChatworkMessage(issue))
    return true
  end

  private
  def makeChatworkMessage(issue)
    message =  "[info][title]#{issue[:creator]}さんが課題を作成しました[/title]"
    message += "[#{issue[:key]}] #{issue[:summary]}\n"
    message += "https://saknight.backlog.jp/view/#{issue[:key]}[/info]"
  end

end

動作確認

サーバを起動し、課題の作成を行うと、チャットワークのマイチャットにリアルタイムで通知が届くことが確認できた。

所感

  • 初めてWebhookを使ったが、かなりリアルタイムに近い。APIでpull型で取得するよりもコードもスマートに書けて良い
  • メール通知より遥かに確実に、高速に通知できるので使いみちはあると思う
  • BacklogWebhookの仕様上、インターネットで繋がる場所にサーバを建てる必要があるため、セキュリティにちょっと気をつける必要ありそう
  • 以下のような工夫ができる余地がある
    — プロジェクトごとに通知するチャットルームを切り替える
    — 担当者に対応するユーザに対してToを付ける
    — 課題のクローズ時にも作成者にToを付けて通知する

CentOS7にphpenv/php-buildを入れてPHPのバージョンを自在に切り替える

前提

以下の環境で動作確認

要素 バージョン
CentOS 7.3.1611
git 1.8.3.1
phpenv 1.1.1-2

概要

Dockerで構築したほぼ空っぽのCentOS7に対し、各種パッケージをインストール後、phpenv/php-buildをインストールし、PHPのバージョンを自在に切り替えたお話。PHPのバージョンを上げたり下げたりすることを容易に行うことが目的。

本記事ではほぼ空っぽのCentOS7を利用しているので、色々な基本となるパッケージの導入も行ってるが、多くの環境では既に入っているものが殆どなので必要に応じてインストールする形で問題ない。

phpenv/php-buildとは

phpenv: PHPの複数のバージョンを一元管理し、バージョンの切り替えを容易に行うためのツール
php-build: phpenvで使用するPHPのインストールツール

下準備

ツールはGithubで公開されてるのでgitが必要。ファイルの書き換えなどでvimを使う。パッケージのダウンロードにwgetを使う。

$ yum install -y git vim wget

CentOS7の標準リポジトリだけだと足りないので、EPELを追加する。

$ yum install epel-release

EPELリポジトリを有効にする(デフォルトで有効になってる?)

$ vim /etc/yum.repos.d/epel.repo
enabled=1

PHPに依存するパッケージ、コンパイルに必要パッケージなどなどを纏めてドン(元々入ってるものが多数、不要なものもあるかも)
PHPのバージョンによっては依存モジュールが異なる場合がある模様。その都度エラーメッセージが出るのでインストールすればよい。

$ yum install -y yum install gcc bison libxml2 libxml2-devel openssl-devel libcurl-devel libjpeg-turbo-devel libpng-devel libmcrypt-devel readline-devel libtidy-devel libxslt-devel bzip2 bzip2-devel libicu-devel  gcc-c++ tidyp make re2c file libtool-ltdl-devel autoconf automake patch mysql-devel

phpenvのインストール

Githubから本体を落とす。/usr/srcあたりに置くのが定石なのでそこでclone

$ cd /usr/src
$ git clone https://github.com/CHH/phpenv.git

本体の中にインストールスクリプトが含まれているのでそれを実行する

$ cd phpenv/bin/
$ ./phpenv-install.sh
Installing phpenv in /root/.phpenv
remote: Counting objects: 2620, done.
remote: Total 2620 (delta 0), reused 0 (delta 0), pack-reused 2620
Receiving objects: 100% (2620/2620), 491.71 KiB | 368.00 KiB/s, done.
Resolving deltas: 100% (1640/1640), done.
Success.

export PATH="/root/.phpenv/bin:$PATH"
eval "$(phpenv init -)"

Add above line at the end of your ~/.bashrc and restart your shell to use phpenv.

これでphpenvは使えるようになったが、コマンドですぐに利用できるようにパスを通す。
以下をbashrcなりに追記する

export PATH="/root/.phpenv/bin:$PATH"
eval "$(phpenv init -)"

bashrcを再読込

$ source ~/.bashrc

phpenvが読めることを確認

$ phpenv --version
rbenv 1.1.1-2-g615f844

php-buildのインストール

PHPをインストールするためのphp-buildを取得し、phpenvのプラグインディレクトリに配置する。こちらもGithubから取得できるので以下の手順でCone

$ git clone https://github.com/CHH/php-build.git ~/.phpenv/plugins/php-build

以下コマンドでインストール可能なPHPの一覧が出てくるので、これが出ればOK

$ phpenv install --list
(中略)
  7.1.0
  7.1.1
  7.1.2
  7.1.3
  7.1.4
  7.1.5
  7.1.6
  7.1.7
  7.1.8
  7.1snapshot
  7.2.0beta1
  7.2.0beta2
  7.2snapshot
  master

最新のPHPをインストールする

前項でインストール可能なPHPの一覧を確認した所、開発段階の7.2までインストール可能になっているが、今回は安定版の最新版である7.1.8をインストールする。

インストール時間が長い。その上インストールログがあんまり出ないので固まったとも思ってしまうが、気長に待てば終わるか、エラーメッセージがちゃんと出るので待機する。(本環境では11分程度)

$ phpenv install 7.1.8
[Info]: Loaded extension plugin
[Info]: Loaded apc Plugin.
[Info]: Loaded composer Plugin.
[Info]: Loaded github Plugin.
[Info]: Loaded uprofiler Plugin.
[Info]: Loaded xdebug Plugin.
[Info]: Loaded xhprof Plugin.
[Info]: Loaded zendopcache Plugin.
[Info]: php.ini-production gets used as php.ini
[Info]: Building 7.1.8 into /root/.phpenv/versions/7.1.8
[Downloading]: https://secure.php.net/distributions/php-7.1.8.tar.bz2
[Preparing]: /tmp/php-build/source/7.1.8
[Compiling]: /tmp/php-build/source/7.1.8
[xdebug]: Installing version 2.5.5
[xdebug]: Compiling xdebug in /tmp/php-build/source/xdebug-2.5.5
[xdebug]: Installing xdebug configuration in /root/.phpenv/versions/7.1.8/etc/conf.d/xdebug.ini
[xdebug]: Cleaning up.
[Info]: Enabling Opcache...
[Info]: Done
[Info]: The Log File is not empty, but the Build did not fail. Maybe just warnings got logged. You can review the log in /tmp/php-build.7.1.8.20170815015550.log
[Success]: Built 7.1.8 successfully.

PHP7.1.8がインストールできたので、この環境で利用するデフォルトのPHPを7.1.8に設定する

$ phpenv global 7.1.8

PHP7.1.8が利用できる状態になったことが確認できる

$ php -v
PHP 7.1.8 (cli) (built: Aug 15 2017 02:06:59) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
    with Zend OPcache v7.1.8, Copyright (c) 1999-2017, by Zend Technologies
    with Xdebug v2.5.5, Copyright (c) 2002-2017, by Derick Rethans

古いPHPをインストールし、切り替える

開発内容によっては意図的に古いPHPを使用せざるを得ない場面もある。そんな場合にphpenvは効果的。ここではPHP5.2.17(デフォルトでインストールできる最も古いPHP)をインストールする。

$ phpenv install 5.2.17
[Info]: Loaded extension plugin
[Info]: Loaded apc Plugin.
[Info]: Loaded composer Plugin.
[Info]: Loaded github Plugin.
[Info]: Loaded uprofiler Plugin.
[Info]: Loaded xdebug Plugin.
[Info]: Loaded xhprof Plugin.
[Info]: Loaded zendopcache Plugin.
[Info]: php.ini-production gets used as php.ini
[Info]: Building 5.2.17 into /root/.phpenv/versions/5.2.17
[Downloading]: https://git.php.net/?p=web/php-distributions.git;a=blob;f=php-5.2.17.tar.bz2;h=90d654f7b9c60320f6228194ed1d2f12976a3a59;hb=bed93a30bbc2104f6e88e6ef142891a4c289502f
[Info]: Applying patches: /root/.phpenv/plugins/php-build/bin/../share/php-build/patches/gmp.c.patch /root/.phpenv/plugins/php-build/bin/../share/php-build/patches/xp_ssl.c.patch /root/.phpenv/plugins/php-build/bin/../share/php-build/patches/zip_direct.c.patch /root/.phpenv/plugins/php-build/bin/../share/php-build/patches/php-5.4.6-libxml2-2.9.patch
[Preparing]: /tmp/php-build/source/5.2.17
[Compiling]: /tmp/php-build/source/5.2.17
[xdebug]: Installing version 2.2.7
[xdebug]: Compiling xdebug in /tmp/php-build/source/xdebug-2.2.7
[xdebug]: Installing xdebug configuration in /root/.phpenv/versions/5.2.17/etc/conf.d/xdebug.ini
[xdebug]: Cleaning up.
[Info]: The Log File is not empty, but the Build did not fail. Maybe just warnings got logged. You can review the log in /tmp/php-build.5.2.17.20170815021852.log
[Success]: Built 5.2.17 successfully.

versionsコマンドを叩くと、以下のようにインストール済みのPHP一覧と、デフォルトに設定されたバージョン(7.1.8)が確認できる

$ phpenv versions
  5.2.17
* 7.1.8 (set by /root/.phpenv/version)

globalコマンドでバージョンを切り替えると

$ phpenv global 5.2.17

デフォルトが変わり

$ phpenv versions
* 5.2.17 (set by /root/.phpenv/version)
  7.1.8

5.2.17が利用できる状態になる

$ php --version
PHP 5.2.17 (cli) (built: Aug 15 2017 02:23:16)
Copyright (c) 1997-2010 The PHP Group
Zend Engine v2.2.0, Copyright (c) 1998-2010 Zend Technologies
    with Xdebug v2.2.7, Copyright (c) 2002-2015, by Derick Rethans

備考

phpenv installで、以下のようなエラーが出て

-----------------
|  BUILD ERROR  |
-----------------

Here are the last 10 lines from the log:

何らかの対応を行った後は、中途半端にデータが残っている可能性のある下記ディレクトリを削除しておいたほうが無難な模様

$ rm /tmp/php-build -rf

所感

  • これまでrbenv(ruby),pyenv(python),nodenv(node)を使ってきたが、それ比べて導入に一番苦労した。依存が多すぎる。
  • 同様に一番インストールにも時間がかかる。コンパイルが長すぎる
  • いくつかのバージョンを一度入れてしまえば後は他の言語と同様なので容易
  • apacheとか関連モジュールは結局バージョンに依存してしまうからあんまり効果的でないのかも
  • 他言語含めて、anyenvというのを使えばまとめて出来たりするらしい。今度調べたい。

Python3.6.2をインストールして、Wikipediaをクロールするスクリプトを書く

前提

本記事は以下の環境で動作確認済み

要素 バージョン
Debian 9.1
pyenv 1.1.3-7
python 3.6.2

概要

Pythonを使ったことのない私が、pyenvを用いて現在の最新版であるPython3.6.2をインストールし、Wikipediaの特定ページからランダムにリンクを辿り続けるスクリプトを書いたお話。

Python3.6.2のインストール

pyenvについて

pyenvは、複数のPythonのバージョンを容易にインストール、切り替えを行うツール。Rubyのrbenvや、nodeのnvm,nなどと同じようなもの。

pyenvのインストール

以下ページをそのまま参考にした。
PyenvによるPython3.x環境構築(CentOS, Ubuntu) – Qiita

3.6.2のインストール

前述のページだと若干古いPythonを使っているので、pyenvを用いて以下のように3.6.2をインストール

$ pyenv install -v 3.6.2
$ pyenv global 3.6.2

確認

$ python --version
Python 3.6.2

Python3入門

初Pythonだったのでまずは基本的な使い方を確認。以下のページが広く浅くしっかり丁寧だったので大変お世話になった。
Python3基礎文法 – Qiita

Wikipediaをクロールするスクリプト作成

概要

Pythonの基本を学んだので、手始めに、以下のような簡単なWebクロールスクリプトを書いてみる。

  • 特定のWikipediaのページを開始地点とする
  • 記事内の、他の単語へのリンク一覧を取得する
  • ノイズとなるリンクを排除し、関連する単語へのリンクのみを取り出す
  • 残ったリンク一覧から、ランダムに1件選出し、そのリンク先へ移動する
  • リンク先のページタイトルを標準出力し、同様の操作を繰り返す

Wikipediaを使った連想ゲームのような物?

使用ライブラリ

調べてみると、PythonにはWebクロールのためのフレームワーク/ライブラリも充実してるようだが、今回はPythonのお試しも含んでいるので、HTTPにrequests、HTML/XMLパーサにBeautifulSoupというライブラリを用いることにする。

実装

そんなに長くないので先に全文

BASEURL = 'https://ja.wikipedia.org'

# 指定したWikipediaページ内のaタグからランダムで一つ戻す
def get_wiki_a_tag(url):
  response = requests.get(url)
  soup = BeautifulSoup(response.text, 'lxml')
  print(soup.find('h1').text)
  a_tags = filter(
    lambda a: 'href' in a.attrs and
               a.text and
               a.attrs['href'].startswith('/wiki') and
               a.attrs['href'].find('Wikipedia') and
               a.text.find('年') == -1,
    soup.select('.mw-parser-output p a')
  )
  a_tags_list = list(a_tags)
  if not len(a_tags_list):
    print("end")
    sys.exit()
  return random.sample(a_tags_list, 1)[0]

# 開始地点を設定
word  = 'Python'
url   = f"{BASEURL}/wiki/{word}"
# ランダムにWikipediaのリンクを飛び続ける
while True:
  a_tag = get_wiki_a_tag(url)
  url  = f"{BASEURL}{a_tag.attrs['href']}"
  time.sleep(1)

まず、以下の部分で特定URLのHTTPレスポンスを取得する。BeautifulSoupを用いることで、レスポンスのXML/HTMLをparseできるようにする。

response = requests.get(url)
soup = BeautifulSoup(response.text, 'lxml')

関連する単語へのリンクは概ねdiv.mw-parser-output配下のp要素に含まれているので、その中のaタグの一覧を取得する。あくまで概ねなので正確ではない。

soup.select('.mw-parser-output p a')

ノイズとなるリンクはpythonのfilter関数で排除する。排除対象は以下の通り

  • href属性が設定されてないaタグ
  • テキストが設定されていないaタグ
  • リンク先が/wiki で始まらないaタグ
  • リンク先にWikipediaが含まれるaタグ(要出典、検証可能性など)
  • テキストに「年」が含まれるaタグ(‘2010年’などの記事が頻出するため)
lambda a: 'href' in a.attrs and
           a.text and
           a.attrs['href'].startswith('/wiki') and
           a.attrs['href'].find('Wikipedia') and
           a.text.find('年') == -1,

randomパッケージのsample関数を用いて、aタグの一覧からランダムに一つ戻す。RubyだとArrayにsampleメソッドがあるのでちょっと不便だと思った。

return random.sample(a_tags_list, 1)[0]

動作確認

Pythonスタートでやってみたところ、初めのうちは良い感じだったが、トレインチャンネルあたりからおかしくなった。

もう一度。Googleから何故サウナ風呂につながるのか‥‥。
※確認した所、社内にサウナがあるらしい

試しに開始地点を阿部寛にしたらやたらとグローバリゼーションになった。阿部寛だしそんなものか。

所感

  • とりあえず何かを作ることを通じてPythonに世界に飛び込んでみたが、まだまだ魅力は掴めてない。インデントブロックがPythonの特徴の一つだが、これもまだ見やすいとは思えない(インデント幅を4字にしたらまた違うかも)
  • Rubyに慣れてて、Rubyが一番好きな分、Pythonとはウマが合わない可能性もあるが、もっと使ってみて自分に合うとこを見つけていきたい。
  • 今回作ったスクリプト、ずっと眺めてられそう。

はじめてのvimscriptとvimプラグインの作成

前提

以下の環境で作業を行った

要素 バージョン
debian 8.6
vim 7.4
neobundle 7.2.051
git 2.11.0

概要

vimは長らく使ってきたけどvimscriptを書いたことがない私が、vimscriptの基本を勉強し、簡単なvimプラグインを作成、NeoBundleでインストールできる状態にしたお話。

Vim scriptとは

Vim上で動作するスクリプトのこと。

:set filetype=html

のような、コマンドモードで”:”から始まるコマンドの集合。

最も身近なvimscriptは.vimrcで、これはvim起動時に読み込まれるvimscriptである。
つまり、vimscriptの書き方を知っていれば、より効果的なvimrcを記述することができる。

Vim script入門

vim scriptの入門は、以下を参考にした。
Vim script と vimrc の正しい書き方

本記事は簡単に基本文法を紹介する程度にするので、具体的に学びたい場合は各種参考ページを利用すること。

実行方法

vimscriptは基本的にscript.vimのように、拡張子をvimにしてファイルを作成する。
vimファイルはvimコマンドから実行することもできるが、多分以下のように、スクリプトをvimで編集中にsourceコマンドを叩くのが一番早い。

変数と代入

vim scriptには変数宣言の概念はなく、変数への代入と同時に変数が作成される模様。
let で変数への代入を行う。echoを見てもわかるように、シェルスクリプトに似ていると感じる。

let number = 1
let string = "hoge"
let array  = [1,2,3,4,5]

echo number
echo string
echo array

実行結果

1
hoge
[1, 2, 3, 4, 5]

条件分岐

ifとelseifとendifを使う。これもシェルスクリプトと同じ。

let number = 1
if number == 1
  echo "number is 1"
elseif number == 2
  echo "number is 2"
else
  echo "other"
endif

繰り返し

for文とwhile文が使える。while文の中でletを用いて変数を再宣言しているように見えるが、vim scriptにおけるletは代入のコマンドなので代入する場合は必ず必要。

for i in [1,2,3]
  echo i
endfor

let n = 0
while n < 10
  echo n
  let n += 1
endwhile

関数

関数もシェルスクリプトと同じようだが、気をつけたい所が2点。

まず、function! と”!”をつけることで、同名の関数が既にロード済みの場合上書きすることができる。こうしないと、同じバッファ内で複数回同じスクリプトを読んだ時に、同名の関数が既に定義済みだよとエラーが出るので”!”を付ける必要がある。

次に、関数の引数であるnumberを参照するために”a:”を頭につけている。これは変数のスコープの指定を表し、aとつけることでnumberは関数内にあるnumberであることを表す。aの他に、グローバルスコープ用の接頭辞やスクリプトファイル内スコープ用の接頭辞があるので、必要に応じて書き換える必要がある。

function! Double(number)
  return a:number * 2
endfunction

let result = Double(10)
echo Double(10)

vimプラグインの作成

本記事では、簡単すぎず難しくもないシンプルなvimプラグインを実装する。

今回作成するプラグインは、:RandDateTimeと入力することで、1980年から、2017年までの範囲内のランダムな日付を、Y-M-D H:m:sの形式でカーソル位置に挿入するコマンドを利用できるようにするプラグインで、まったくもって実用性はないが、個人的にもう少し改良して実用的なものにする予定。

ディレクトリ構成

作成するプラグイン名をmy-vim-pluginとした場合、同名のディレクトリを作成し、その中にpluginというディレクトリを作成、さらにその下にvim scriptを記述する。my-vim-plugin直下にはREADMEを入れたりするみたいだが、今回はスクリプトファイル一つで完結させるので、ディレクトリ構成は下記のようになる。

vagrant$ tree my-vim-plugin/
my-vim-plugin/
└── plugin
    └── script.vim

1 directory, 1 file

乱数を生成する関数

まず、ランダムな日付を生成するために、指定した範囲内での数値をランダムに戻す関数を実装する。
残念ながら、vim scriptには乱数を生成するための仕組みは存在しない。とは言え、今回は時刻データを用いて擬似的な乱数を生成することにした。

作成した関数は以下の通り。GetRandom関数は、minとmaxを引数にとり、min <= n <= max の範囲のランダムな整数を戻す。
reltime関数は、現在のUNIXTIMEを秒とマイクロ秒の配列で戻す。配列の二番目の要素がマイクロ秒になるので、その数値を使って擬似乱数を生成している。

function! GetRandom(min, max)
  return reltime()[1] % (a:max - a:min + 1) + a:min
endfunction

ランダムな日付を取得する関数

前項で作成したGetRandom関数を用いて、1980年から2017年までのランダムな日付文字列を戻す、GetRandomDate関数を以下のように実装する。なお、今回は実装の簡略化のため、日は1日から28日までの確実に存在する日付のみを対象とするようにした。

function! GetRandomDate()
  return GetRandom(1980, 2017) . '-' . GetRandom(1, 12) . '-' . GetRandom(1, 28)
endfunction

ランダムな日付時刻を取得する関数

同様に、前項で作成したGetRandomDate関数を用いて、ランダムな時刻までを含めて戻すGetRandomDateTime関数を以下のように実装する

function! GetRandomDateTime()
  return GetRandomDate() . ' ' . GetRandom(0, 23) . ':' . GetRandom(0, 59) . ':' . GetRandom(0, 59)
endfunction

ランダムな日付時刻をカーソル位置に挿入する関数

前項で作成したGetRandomDateTime関数は、ランダム日付時刻を取得するだけなので、新たにInsertRandomDateTime関数を以下のように実装する。
executeコマンドは、以降の任意のコマンドをvim上で実行するためのコマンド。:normalは以降のコマンドをノーマルモードで実行するコマンド。aでカーソルの次の位置から挿入モードを開始、そしてdatetimeを入力するという手はずになる。

function! InsertRandomDateTime()
  let datetime = GetRandomDateTime()
  execute ":normal a" . datetime
endfunction

作成した関数を呼び出すコマンドを定義

ここまででランダム日付時刻文字列を挿入する仕組みができあがったので、それを呼び出すためのコマンドを定義する。コマンドの定義にはcomman関数を用い、コマンド名と実行内容を引数に設定する。(callは、指定した関数を実行するコマンド)

command! RandDateTime call InsertRandomDateTime()

動作確認

以上で、以下のような自作のvimプラグインが完成した。

function! GetRandom(min, max)
  return reltime()[1] % (a:max - a:min + 1) + a:min
endfunction

function! GetRandomDate()
  return GetRandom(1980, 2017) . '-' . GetRandom(1, 12) . '-' . GetRandom(1, 28)
endfunction

function! GetRandomDateTime()
  return GetRandomDate() . ' ' . GetRandom(0, 23) . ':' . GetRandom(0, 59) . ':' . GetRandom(0, 59)
endfunction

function! InsertRandomDateTime()
  let datetime = GetRandomDateTime()
  execute ":normal a" . datetime
endfunction

command! RandDateTime call InsertRandomDateTime()

これをsourceコマンドで読み込むことで、現在開いているバッファ上でRandDateTimeコマンドが利用できるようになる。

NeoBundleでインストールできるようにする

NeoBundleはvimプラグインを管理するためのツールで、決まったディレクトリ構成でGithub上にVimプラグインを公開すれば、誰でも手軽にそれをインストールできるようになる。

今回は既にNeoBundleを想定したディレクトリ構成にしているので、これをこのままGithub上にあげる。
Sa2Knight/my-vim-plugin

この状態で、既にNeoBundleが利用できる状態のvimrcに以下を追加

NeoBundle "Sa2Knight/my-vim-plugin"

vimを起動すると、以下のようにインストールするかの確認が出るので、Yを入力する

Not installed bundles:  ['my-vim-plugin']
Press ENTER or type command to continue
Install bundles now?
(y)es, [N]o:

Github経由で先程作成した自作プラグインがインストールされ、次回起動時から自動で:RandDateTimeコマンドが使えるようになる。

所感

vimscript及びvimプラグインの非常に基礎的な部分にようやく手を付けられたので、今後もっと深い部分を勉強して実用的なプラグインを作りたい。とりあえず今回作ったプラグインには以下のような改良点があるので、それを実装しつつノウハウを見つけていく。

  • ランダムの範囲を外から指定できるようにする
  • 日付時刻文字列のフォーマットを外から指定できるようにする
  • 乱数値が一桁になった場合0で詰めるようにする
  • コマンドを叩かずともキーバインドですぐに実行できるようにする

HTMLスケッチでHTML/CSSトレーニング(Backlog編)

HTMLスケッチとは

本記事で言う「HTML」スケッチは、概ね以下の行為を指すものとする

  • 実在するWebページをHTMLとCSSを使って自らの手で再現する
  • 見本となるWebページは単一のページとし、それのソースコードを参照せずに、再現する
  • 再現に使用する手法/技術は、見本に合わせる必要はなく、自分の持っている知識から試行錯誤して再現する

参考 プロデザイナーが実践するHTML/CSSスキルアップ術

HTMLスケッチを行うメリット

  • HTML/CSSのスキル底上げには実際にモノを作るのが一番だが、何を作ったら良いかわからない時に手っ取り早い
  • 公開されているWebページがどのような構造で実装されているかが見えてくるので、Webデザイン力の底上げにもなる
  • 自分でWebデザインした場合には使わないようなレイアウト/スタイルを使うことも出てくるので、技術の幅が広がる

フロントエンジニアがWebデザインスキルを身につける必要はあるか?

これについては色々な意見が飛び交ってるが、個人的にはモダンなフロントエンジニアになるためにはWebデザインスキルも必須だと思う。

最近はリッチクライアント(死語?)が当たり前になってきたため、クライアント側でも各種ライブラリ/フレームワークの導入が一般的になってきてるが、その中でもReactやVue(Component)など、ビューとそのロジックが密になったライブラリ/フレームワークを使う場面も多くなって来ている。ビューとロジックが密ということは、それだけエンジニアとデザイナの作業範囲を分離しづらいということだ。

そうなると、デザインの諸々をデザイナに投げて放ったらかすというもの難しくなってくるため、エンジニアが最低限のデザインを施す必要がある。

よって、特にフロントエンジニアにもWebデザインスキルは必須になってくる(と思う)ので、本記事ではデザイナでなくフロントエンジニアの視点でHTMLスケッチを行う

HTMLスケッチのルール

以下のルールは本記事でのルールなので、実践する場合は各自やりやすいようにルール作りをしたほうが良い

  • 見本は既存Webサービスの特定の1画面をスクリーンショットで収めた範囲とする
  • 基本的にはスクリーンショットの画面を再現し、見えない範囲(オンマウスで見た目が変わるなど)については対応しない
  • JavaScriptが影響する部分は対応しない
  • bootstrapなどのCSSフレームワークの利用を禁止
  • 見本のソースコードを閲覧することは原則禁止する(スケッチ後に答え合わせとして確認する)
  • Saasを使う。今時生のCSSを直接書くのもナンセンスなので。と言ってもネスト構造ぐらいしか使う予定はない
  • 画像ファイルは見本のページから直接ダウンロードする(学習用なので見逃してください)
  • アイコンファイルは、今回はfontawesomeを用いて見本に近いアイコンを利用する
  • ブラウザ対応はそこまで拘らず、とりあえず見本撮影時に用いたChromeのみで検証する

今回の見本

例によってBacklogが好きなのでBacklogの課題一覧画面を対象とする。初めてのHTMLスケッチなので比較的簡単そうで、かつ身近な普段から見慣れている画面を見本とすることにした。

以下が今回見本とするスクリーンショット

作業環境の構築

以下の環境で作業を行う

要素 バージョン
Debian 8.6
sass 3.5.1
Compass 1.0.3

sassのインストール

sassはcssのプリプロセッサで、cssの弱い部分などを改良したメタ言語で書かれたファイルをcssに変換する。
簡単に言えばcssを効率的に記述するために必要なもの。

sassはgemライブラリで公開されてるので、rubyとgemが必要。本記事では割愛するので必要に応じてそれらもインストールすること。

vagrant$ sudo gem install sass

Compassのインストール

Compassは、sassで記述されたファイルの変更を検知し、自動でsassを実行し、変換後のcssファイルを生成してくれるツール。これが無いとファイルを変更するたびに変換コマンドを叩く必要があるので実質必須。

こちらもgemを用いてインストールする

vagrant$ sudo gem install compass

動作確認

compass createで作業環境をまとめて作成できる。

vagrant$ compass create
directory sass/
directory stylesheets/
   create config.rb
   create sass/screen.scss
   create sass/print.scss
   create sass/ie.scss
    write stylesheets/ie.css
    write stylesheets/print.css
    write stylesheets/screen.css
(以下略)

が、今回はscssファイル1個に全てまとめるつもりなので、生成されたsassファイルは消しておく

vagrant$ rm sass/*

compassを実行。watchコマンドで常駐化して変更を検知してくれる

vagrant$ compass watch
Compass is watching for changes. Press Ctrl-C to Stop.

この状態でsassディレクトリいかに、sassファイル(style.sass)を作成する

body {
  .hoge {
    font-size: 12px;
  }
  .fuga {
    font-size: 14px;
  }
}

するとcompassがそれを検知して

Compass is watching for changes. Press Ctrl-C to Stop.
  created sass/style.scss
    write stylesheets/style.css

自動でstyleseets/style.cssを生成してくれる

/* line 2, ../sass/style.scss */
body .hoge {
  font-size: 12px;
}
/* line 5, ../sass/style.scss */
body .fuga {
  font-size: 14px;
}

あとはこの生成されたcssファイルを参照するHTMLファイルを用意すれば準備完了

<html>
  <head>
    <link rel="stylesheet" type="text/css" href="stylesheets/style.css">
  </head>
  <body>
  </body>
</html>

HTMLスケッチの実施

大枠の実装

まずはページ全体の大枠をマークアップすることに。見本から見える必要な構造を想像してHTMLに書き出す(コードが長いのでGithub参照)

ある程度全体構造を意識しながら主にdivで囲って、適切なclassを振ってCSSの適用に備えている。

まだスタイルがまったく適用されていないので、そのまま画面に表示されるだけ。色々足りてない構造もあるが、それは随時追加していくものとする。
ここからスタイルの適用と、それに応じてHTMLの書き換えも行っていく。

カラム割

ページ全体を以下の3カラム構成にする

  • ヘッダー
  • サイドメニュー
  • コンテンツ

カラム割の流行りはflexboxらしいけどCSS弱者なので先ずはfloatを使ったカラム割の経験を積む。サイドメニューの背景には画像ファイルがあったのでBacklogから拝借する。コンテンツ内にもヘッダーがあるので、そこにはprojectと名付けて50px確保する。

body {
  margin:  0;
  padding: 0;
  border:  0;
}
.header {
  height: 50px;
  background-color: #ebf0f2;
}
.wrap {
  .sidebar {
    width: 200px;
    height: 100%;
    float: left;
    background-color: #3b9dbd;
    background-image: url('../images/bg_dot.png');
  }

  .content {
    margin-left: 200px;
    background-color: #f0f0f0;
    .project {
      height: 50px;
      background-color: white;
    }
  }
}

Github

HTMLも微修正して以下の感じに。少しだけ全体像が見えてくる。

サイドメニューの実装

ぱっと見一番簡単そうなのがサイドメニューなので安直にこちらから手を出す。HTMLは、メニュー項目をul/liで記述し、アイコンはfontawsomeを利用する。

      <div class="sidebar">
        <div class="burger"><i class="fa fa-bars fa-2x" aria-hidden="true"></i></div>
        <ul class="sidemenu">
          <li><i class="fa fa-home fa-lg" aria-hidden="true"></i><span>ホーム</span></li>
          <li><i class="fa fa-list fa-lg" aria-hidden="true"></i><span>課題</span></li>
          <li><i class="fa fa-plus fa-lg" aria-hidden="true"></i><span>課題の追加</span></li>
          <li><i class="fa fa-wikipedia-w fa-lg" aria-hidden="true"></i><span>Wiki</span></li>
          <li><i class="fa fa-files-o fa-lg" aria-hidden="true"></i><span>ファイル</span></li>
          <li><i class="fa fa-git fa-lg" aria-hidden="true"></i><span>Git</span></li>
          <li><i class="fa fa-cog fa-lg" aria-hidden="true"></i><span>プロジェクト設定</span></li>
        </ul>
      </div>
    .burger {
      text-align: right;
      .fa {
        padding: 10 20 10 0;
      }
    }
    ul.sidemenu {
      padding-left: 0;
      font-size: 95%;
      list-style-type: none;
      li {
        padding-left: 12px;
        margin: 20 0 20 0;
        .fa {
          width: 20px;
        }
        span {
          padding-left: 15px;
        }
      }
    }
  }

Github
まだ比較的まともにCSSも書けてそれっぽい画面が出来上がってる。

ヘッダーの実装

コンテンツは最後として、次にヘッダーに着手する。結果から言えばここが一番苦労した。CSSよくわからない。ユーザアイコン的な画像はBacklogから拝借。li要素を横並びに表示するテクニックを身に着けたのでさっそく使ってる。

    <div class="header">
      <img class="logo" src="images/logo.svg">
      <ul class="menus">
        <li>ダッシュボード</li>
        <li>プロジェクト</li>
        <li>最近みた課題</li>
        <li>最近見たWiki</li>
        <li>フィルタ</li>
      </ul>
      <ul class="icons">
        <li><i class="fa fa-ellipsis-h fa-lg" aria-hidden="true"></i></li>
        <li><i class="fa fa-plus fa-lg" aria-hidden="true"></i></li>
        <li><i class="fa fa-eye fa-lg" aria-hidden="true"></i></li>
        <li><i class="fa fa-bell fa-lg" aria-hidden="true"></i></li>
        <li><i class="fa fa-search fa-lg" aria-hidden="true"></i></li>
        <li class="bars"><i class="fa fa-bars fa-lg" aria-hidden="true"></i></li>
        <li>
          <img class="user-menu" src="images/user.gif">
          <i class="fa fa-angle-down" aria-hidden="true"></i>
        </li>
      </ul>
    </div>
.header {
  background-color: #ebf0f2;
  * {
    vertical-align: middle;
  }
  ul {
    padding: 0;
    margin: 10px 0 10px 0;
    list-style-type: none;
    display: inline-block;
    li {
      display: inline-block;
      margin-left: 12px;
    }
  }
  img {
    width: 30px;
    max-height: 100%;
  }
  img.logo {
    padding-left: 20px;
  }
  .menus {
    font-size: 95%;
    width: 70%;
  }
  .icons {
    width: 25%;
    color: gray;
    text-align: right;
    .bars {
      padding-left: 20px;
      border-left: 1px solid gray;
    }
  }
}

Github

課題一覧の実装

メインの部分。この辺から妥協が生まれる。細かい部分を合わせるのが難しくなってきて、ゴリ押し気味なスタイリングが増えてくる。
テーブルは普通にtableタグを用いて、列単位で装飾。聞くところによるとTableの内部要素(tr/td)にmarginやpaddingを設定するのはナンセンスらしいけど、それを避けて装飾するのが上手く行かなかったので妥協。

        <div class="issues">
          <table>
            <thead>
              <tr>
                <th>種別</th>
                <th>キー</th>
                <th>件名</th>
                <th>状態</th>
                <th>カテゴリー</th>
                <th>登録日</th>
                <th>更新日</th>
                <th>登録者</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td><div class="task mark">タスク</div></td>
                <td><a href="">DEV-240</a></td>
                <td>githubのコミットログを@nullでツイートする方法の検討</td>
                <td><div class="status mark">未対応</div></td>
                <td>開発関係</td>
                <td>2017/07/30</td>
                <td>2017/07/30</td>
                <td class="user">
                  <img class="user-menu" src="images/user.gif">
                  <span>sa2knight</span>
                </td>
              </tr>
             <tr>
                <td><div class="task mark">タスク</div></td>
                <td><a href="">DEV-237</a></td>
                <td>vimでもっと強力なコード補完を導入する</td>
                <td><div class="status mark">未対応</div></td>
                <td>開発関係</td>
                <td>2017/07/30</td>
                <td>2017/07/30</td>
                <td class="user">
                  <img class="user-menu" src="images/user.gif">
                  <span>sa2knight</span>
                </td>
              </tr>
             <tr>
                <td><div class="task mark">タスク</div></td>
                <td><a href="">DEV-235</a></td>
                <td>[でぐろぐ]元ツイートページから画像ファイルのパスを抜き出すロジック</td>
                <td><div class="status mark">未対応</div></td>
                <td>開発関係</td>
                <td>2017/07/30</td>
                <td>2017/07/30</td>
                <td class="user">
                  <img class="user-menu" src="images/user.gif">
                  <span>sa2knight</span>
                </td>
              </tr>
            </tbody>
          </table>
        </div>
    .issues {
      padding: 0 30 0 30;
      table {
        width: 100%;
        border: solid 1px #c2c2c2;
        border-radius:   4px;
        border-collapse: collapse;
        thead {
          background-color: white;
          color: #00759b;
          font-size: 85%;
          tr {
            border-bottom: 2px solid #ccc;
          }
          th {
            padding: 7px 0px 7px 0px;
          }
        }
        tbody {
          font-size: 80%;
          a {
            color: #00759b;
            text-decoration: none;
          }
          tr {
            border-collapse: separate;
          }
          tr:nth-child(even) {
            background-color: #f6f6f6;
          }
          tr:nth-child(odd) {
            background-color: white;
          }
          td {
            padding: 5px 15px 5px 15px;
          }
          td > .mark {
            text-align: center;
            color: white;
            padding: 0 10 0 10;
            border-radius: 20px;
          }
          td > .task {
            background-color: #b0be3c;
          }
          td > .status {
            background-color: #ed8077;
          }
          td.user {
            img {
              vertical-align: middle;
              width:  24px;
              height: 24px;
            }
          }
        }
      }
    }

Github
ゴリ押したけど見た目はそれっぽい

課題一覧周辺のUI実装

ページャみたいなモノとかボタンとかのスタイリング。この当たりから作業がとても地味でCSSも汚くなってきた。
コードの修正が分散してきたのでGithub参照

検索フォームの実装とその他修正

あとは検索フォームをスタイリングして全体の仕上げをして完成。
最終的な成果物はこちら

上が今回作成したページで、下が見本となる本家Backlog。比べてみると中国産のパチもんみたいにも見えるが、概ね同じように出来ているので個人的には満足。

所感

良かった点

  • スケッチの過程でスタイリングに関する知らなかった定石/手法を身につけることができた
  • 最後にBacklogのCSSを見て答え合わせした中で、知らなかった属性などを学ぶことができた
  • 自分で一からスタイリングする経験が少なかったので、そういった経験を得られた

失敗した点

  • flota,absolute,text-alignなど、現在はあまり使われないスタイリング手法に頼りすぎて、変更に弱いCSSになったと自負してる。もう少しモダンなスタイリング技術を学びたい
  • BacklogのようなWebサービスは、通常動的にページが生成されるので必然的にHTMLの可読性が低くなるので、参考にしづらかった。俳優のホームページなど、静的なHTMLページを見本にしたほうが勉強になると思う
  • ファイルを更新した後にブラウザも更新するのが面倒だった。ブラウザのオートリロードツールなどを導入すればもう少し楽だったかも