読者です 読者をやめる 読者になる 読者になる

_RJ 技術メモ

人生を豊かにする技術を提供する、筋肉

Noto Serif(源ノ明朝)のCSS指定

CSS HTML font NotoSans NotoSerif

はじめに

Screen Shot 2017-04-04 at 12.14.22.png 最近提供されたばかりのNoto Serif(源ノ明朝)を試してみた。 まだCDN提供されていないので、現状はfont-faceでローカルファイルを指定しなければいけない。

フォントのダウンロード

https://www.google.com/get/noto/#serif-jpan からフォントをダウンロードして、fonts/NotoSerifCJKjp/などのディレクトリにファイルを展開する

ソースコードの記述

CSSなどに下記ソースコードを記述する srcのディレクトリは環境に合わせて変更する

@font-face {
  font-family: 'Noto Serif Japanese';
  font-style: normal;
  font-weight: 100;
  src: url(../fonts/NotoSerifCJKjp/NotoSerifCJKjp-ExtraLight.otf) format('opentype');
}
@font-face {
  font-family: 'Noto Serif Japanese';
  font-style: normal;
  font-weight: 200;
  src: url(../fonts/NotoSerifCJKjp/NotoSerifCJKjp-Light.otf) format('opentype');
}
@font-face {
  font-family: 'Noto Serif Japanese';
  font-style: normal;
  font-weight: 400;
  src: url(../fonts/NotoSerifCJKjp/NotoSerifCJKjp-Regular.otf) format('opentype');
 }
@font-face {
  font-family: 'Noto Serif Japanese';
  font-style: normal;
  font-weight: 500;
  src: url(../fonts/NotoSerifCJKjp/NotoSerifCJKjp-Medium.otf) format('opentype');
 }
@font-face {
  font-family: 'Noto Serif Japanese';
  font-style: normal;
  font-weight: 600;
  src: url(../fonts/NotoSerifCJKjp/NotoSerifCJKjp-SemiBold.otf) format('opentype');
 }
@font-face {
   font-family: 'Noto Serif Japanese';
   font-style: normal;
   font-weight: 700;
  src: url(../fonts/NotoSerifCJKjp/NotoSerifCJKjp-Bold.otf) format('opentype');
 }
@font-face {
  font-family: 'Noto Serif Japanese';
  font-style: normal;
  font-weight: 900;
  src: url(../fonts/NotoSerifCJKjp/NotoSerifCJKjp-Black.otf) format('opentype');
 }

body{
    font-family: 'Noto Serif Japanese', serif;
}
 ```

Amazonの商品を文字列検索と切り取りでスクレイピングする

Amazon スクレイピング PHP

概要

Slackで商品名を投稿するとAmazonの商品を返すBotを作るために作成。 面倒だったので、APIXMLパースライブラリも、正規表現も使わずに文字列検索と切り取りのみでのスクレイピング

DOMの構造が変わったらこのプログラムは終わり。 商品によっては発生するバグは修正予定。

ソースコード

<?php

if(!isset($_GET['keyword'])){
    echo 'URL?keyword=検索キーワード';
    die();
}

//ソースの取得
$source = file_get_contents('https://www.amazon.co.jp/s/?url=search-alias%3Daps&field-keywords=' . urlencode($_GET['keyword']));
$item = subStrHtml($source, 'id="result_1"', 'id="result_2"');

//各項目の抜き出し
$title = subStrHtml($item, 's-access-title  a-text-normal">', '</h2>');
$link = subStrHtml(subStrHtml($item, 'a-text-normal" target="_blank"', '</a>'), 'href="', '"><img ');
$image = subStrHtml($item, '<img src="', '" width="160"');
$price = subStrHtml($item, 'a-color-price s-price a-text-bold">', '</span>');
$star = subStrHtml($item, '<span class="a-icon-alt">5つ星のうち ', '</span>');
$prime = subStrHtml($item, '<span class="a-icon-alt">', '</span>');

//出力
echo '<img src="' . $image . '">' . '<br>';
echo '<a href="' . $link . '">' . $title. '</a>' . '<br>';
echo '価格: ' . $price . '<br>';
echo '★' . $star . '<br>';
echo $prime . '<br>';


function subStrHtml($data, $startStr, $endStr){
    $start = mb_strpos($data, $startStr) + mb_strlen($startStr);
    $end = mb_strpos($data, $endStr, $start);
    $result = mb_substr($data, $start, $end - $start);
    return $result;
}

実行結果

Screen Shot 2017-04-03 at 22.13.35.png

各種スクレイピングのために、レンダリング後(JavaScript実行後)のHTMLを出力するAPIを作る

api PhantomJS JavaScript GoogleAppsScript PHP

はじめに

スクレイピングをしようとして、いざJavaScriptPHPGoogle Apps ScriptでHTMLの取得をすると、JavaScript実行前のソースが取得されてしまう。Seleniumなどを使えばいいが、環境を構築したり起動させっぱなしにしなければいけないので、コストやハードルが高いと感じる。 なのでレンダリング後のHTMLを取得してくれるAPIがあれば様々なプログラムから参照でき、スクレイピングが捗るのではないかと思う。 自分はGoogle Apps Scriptで株価を取得しようとしたところ、参照先がAjaxで株価を動的に生成していたため、スクレイピングできなかっためこのようなAPIが欲しいと思った。 今回は、簡単に実装したかったので、コマンドラインブラウザとしてPhantomJS、API実装としてPHPを使用した。

やりたいこと

  • 簡単にレンダリング後のHTMLを取得できるようにする
  • APIサーバーのメモリ使用量できるだけを抑える
  • GoogleAppsScriptでレンダリング後のHTMLを取得できるようにする

開発環境

実装の手順

CentOSHttpdPHPはインストールされている前提とする

PhantomJSのインストール

Macの場合はbrew installで簡単にインストールできる

$ sudo yum install epel-release 
$ sudo rpm -ivh http://repo.okay.com.mx/centos/6/x86_64/release/okay-release-1-1.noarch.rpm 
$ sudo yum search all phantomjs

PhantomJSでレンダリング後のHTMLを取得する

作業ディレクトリ(/var/www/html/rendered-html/など)に以下のファイルを作成する

下記のソースコードでは、 1. 第二引数で指定したURLの取得 2. ページを取得 3. ページの評価(evalute)を行い、レンダリング後のhtmlタグ内のソースを取得 4. htmlをコマンドラインに出力する を行なっている。

var page = require('webpage').create();
var system = require('system');
var args = system.args;

var url = args[1];
if (url == undefined) {
    phantom.exit();
}

page.open(url, function(status) {
        if (status === 'success') {
                var body = page.evaluate(function() {
                        return '<html>' + document.getElementsByTagName('html')[0].innerHTML + '</html>';
                });
                console.log(body);
        }
        phantom.exit();
});

上記のソースコードを実行すると、HTMLが取得できる

$ phantomjs phantomjs-get-html.js http://google.com
<html><head><meta content="世界中のあらゆる情報を検索するためのツールを提供しています。さまざまな検索機能を活用して、お探しの情報を見つけてください。" name="descrip...

PHPからPhantomJSを実行する

PHPのPhantomJSを使うという方法もあったが、Composerが多少コストが高いため使用せず execでPhantomJSを実行し、最終的にJSON形式でHTMLを出力している

<?php
header('content-type: application/json; charset=utf-8');

//URLの取得 URLがない場合エラー
if(!isset($_GET['url'])){
  statusFailure();
}
$url = $_GET['url'];

//PhantomJSの実行
$cmd = 'phantomjs phantomjs-get-html.js ' . $url;
exec($cmd, $arr);

//データがない場合エラー
if(!$arr){
  statusFailure();
}

//文字列結合
$html = '';
foreach($arr as $val){
  $html .= $val;
}

//JSONにてHTMLを出力
echo json_encode([
  'status' => 'success',
  'html' => $html
]);

//エラー出力関数
function statusFailure(){
  echo json_encode([
    'status' => 'failure',
    'html' => ''
  ]);
  die();
}

実行結果

ブラウザにて、http://IPアドレス/rendered-html/?url=http://google.com にアクセスする 正常に取得できれば、successがかえってきて、HTMLが取得できる

{"status":"success","html":"<html><head><meta content=\"\u4e16\u754c...

おわりに

クライアントJavaScriptやGASでもレンダリング後のスクレイピングができるようになったので、やれることが多くなった。今後はクリックやDOM操作もできるAPIに発展させていきたい。

コマンドラインでURLエンコードをする (他コマンド未使用)

URLEncode URL Terminal Bash alias

はじめに

たまにURLエンコードが必要な時があるときは、検索エンジンを使いエンコードしていたが、ターミナルで変換したいと思い、いざターミナルでやってみようとするとnkfというコマンドが必要になることが分かった。インストールするのはしゃくなので、他のコマンドを使わずにforなどを駆使しURLエンコードするaliasを作成。

ソースコード

forにより一文字ずつ、変換対象の文字かそれ以外かを判定して、文字列結合して出力する

導入

~/.bash_profileなどに下記コードを追記をする

function url-encode() {
  local input="${1}"
  local output=""

  ##一文字ずつURL形式に変換
  local i=0
  for (( i=0 ; i<${#input} ; i++ )); do
     local o=""
     local c=${input:$i:1}
     case "$c" in 
        [-_.~a-zA-Z0-9] ) o="${c}" ;;   ##変換せず
        * ) printf -v o '%%%02x' "'$c"  ##URL形式に変換
     esac
     output+=$o
  done

  echo $output
}

alias url-encode=url-encode

コマンドの使用例

url-encode ‘エンコード対象’

$ url-encode '$$url##encode@@‘
%24%24url%23%23encode%40%40

ターミナルから簡単にSlackに投稿するエイリアス

Slack Bash Terminal alias bashrc

実行結果

Screen Shot 2017-03-30 at 21.41.08.png

ソースコード

メッセージをURLエンコードをかけ、curlにてslackに投稿する。

function param-url-encode() {
  local input="${1}"
  local output=""

  local i=0
  for (( i=0 ; i<${#input} ; i++ )); do
     local o=""
     local c=${input:$i:1}
     case "$c" in 
        [-_.~a-zA-Z0-9] ) o="${c}" ;;  
        * ) printf -v o '%%%02x' "'$c"
     esac
     output+=$o
  done

  echo $output
}

function post-slack(){
  local token="xoxp-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ##Slackのアクセストークン
  local message=`param-url-encode $3` ##メッセージをURLエンコード
  curl -w "%{http_code}\n" "https://slack.com/api/chat.postMessage?token=${token}&channel=${1}&username=${2}&text=${message}" ##curlで送信する
}

alias post-slack=post-slack

エイリアスの導入方法

  1. tokenに自分のslackのアクセストークンを記述する
  2. ~/.bash_profileを編集し、下記のコードを追記をする

エイリアスの使用例

$ post-slack general username message  #post-slack チャンネル ユーザー名 メッセージ

{"ok":true,"channel":"C0R3AKR8","ts":"149087635.112142","message":{"text":"message","username":"username","bot_id":"B4DCDQML","type":"message","subtype":"bot_message","ts":"149076935.112142"}}200

人生の残り時間と一年の残り時間を定期的にSlackに通知する

GoogleAppsScript Slack slackbot JavaScript ライフハック

プログラムについて

人生の時間を大切にする為に、来年までの残りの時間日数月数と、人生の時間日数月数年数を定期的に通知する。また意識を高めるためにタイマーを8時間に設定。

スクリーンショット 2017-03-30 1.59.28.png

ソースコードの使い方

  • 時間操作はMomentのライブラリを導入 (プロジェクトキー: MHMchiX6c1bwSqGM1PZiW_PxhMjh3Sh48)
  • 誕生日(birthday)と予想寿命(lifespan)を入力する
  • xoxp-xxxxxxxxxxxxxxxxxxxxxxにはSlackのAccessTokenを入力する
  • lifespanチャンネルを事前に作成しておく
  • トリガーを8時間に設定して、定期的に通知する
function myFunction() {
  var birthday = '1980/1/1';
  var lifespan = 80;

  var now = Moment.moment();
  var end = Moment.moment(birthday).add('years', lifespan);
  var nextYear = Moment.moment(Moment.moment().add('years', 1).get('year') + '/1/1');
  
  var message = '';
  message += '来年までの残り時間はあと *' + -now.diff(nextYear, 'hours') + '時間* です\n';
  message += '来年までの残り日数はあと *' + -now.diff(nextYear, 'days') + '日* です\n';
  message += '来年までの残り月数はあと *' + -now.diff(nextYear, 'month') + 'ヶ月* です\n\n';
  message += '人生の残り時間はあと *' + -now.diff(end, 'hours') + '時間* です\n';
  message += '人生の残り日数はあと *' + -now.diff(end, 'days') + '日* です\n';
  message += '人生の残り月数はあと *' + -now.diff(end, 'month') + 'ヶ月* です\n';
  message += '人生の残り年数はあと *' + -now.diff(end, 'years') + '年* です\n';
  message += '--------------------';

  postSlack('xoxp-xxxxxxxxxxxxxxxxxxxxxx', 'lifespan', 'lifespan-bot', message);
}

function postSlack(token, channel, username, text) {
  var url = 'https://slack.com/api/chat.postMessage?token=' + token + '&channel=' + channel + '&username=' + username + '&text=' + encodeURIComponent(text);
  response = UrlFetchApp.fetch(url).getContentText("UTF-8");
}

おわりに

通知を見るたび、一年と人生の残り時間はとても短いことに気づかされる。

QiitaのContribution数が増えるたびにSlackに通知する

Qiita Slack slackbot GoogleAppsScript JavaScript

概要

環境はGoogle Apps Scriptを使用。

スクリーンショット 2017-03-26 17.44.59.png

事前準備

SlackのAPIキーを取得する

https://api.slack.com/custom-integrations/legacy-tokens Appを登録するとxoxp-xxxxxxxxxxxxxxxxxxxxのようなコードが発行されるので、ソースコード内のslackTokenに設定する

Slackにチャンネルを追加する

Slackにて、qiita-contributionのチャンネルを新規で追加する

ライブラリにSlackAppを追加する

Slackを使用するためにsoundTrickerさんの以下のライブラリを導入する http://qiita.com/soundTricker/items/43267609a870fc9c7453 Library Keyをコピーして、リソース-ライブラリ-ライブラリを追加に入力する 今回はバージョン22を使用

取得したいユーザーのアカウント名を設定する

ソースコード内のuserNameにContributionを取得したいユーザーアカウント名を設定する

トリガーの設定

プロジェクトのトリガーを時間主導型で1分に設定する

ソースコード解説

今回はQiitaAPIを使わずに、簡易的な正規表現でのスクレイピングでContributeを取得 1. Qiitaのユーザーページをフェッチ 2. ユーザーページHTMLのContributeに該当する箇所のみ正規表現で抜き出し 2. Slackの以前の最後のメッセージと、今回の送信するメッセージを比較 3. 以前のメッセージと違ければ(Contributionが増えて入れば)新しいContribution数を通知する

function myFunction() {
  //各種設定
  var userName = '_RJ';
  var slackToken = 'xoxp-xxxxxxxxxxxxxxxxxxxx';
  var slackChannel = 'qiita-contribution';
  var slackUserName = 'contributin-bot';
  var additionalStartMessage = 'QiitaのContribution数が'
  var additionalEndMessage = 'に増えました!'
  
  //Contributionの取得
  var html = UrlFetchApp.fetch('http://qiita.com/' + userName).getContentText();
  var contribution = /contributions\"><span class=\"userActivityChart_statCount\">([\s\S]*?)<\/span>/i.exec(html)[1].replace(/(^\s+)|(\s+$)/g, "")
 
  //Logger.log(contribution);
  
  //以前のメッセージと比較してContributionが更新されていれば投稿する
  var message = additionalStartMessage + contribution + additionalEndMessage;
  var lastMessage = getSlackMessage(slackToken, slackChannel)[0].text;
  if(lastMessage != message){
    postSlackMessage(slackToken, slackChannel, slackUserName, message);
  }else{
    //Logger.log('Duplicated');
  }
}


function postSlackMessage(accessToken, channelName, userName, message){
  //Get channels
  var slackApp = SlackApp.create(accessToken);
  var channels = slackApp.channelsList().channels;
  
  //Find channel by channel name
  var channel = null;
  channels.forEach(function(v, i){
    if(v.name == channelName){
     channel = v; 
    }
  });

  //Post Message to slack
  slackApp.postMessage(channel.id, message, {
    username : userName
  }); 
}


function getSlackMessage(accessToken, channelName){
  //Get channels
  var slackApp = SlackApp.create(accessToken);
  var channels = slackApp.channelsList().channels;
  
  //Find channel by channel name
  var channel = null;
  channels.forEach(function(v, i){
    if(v.name == channelName){
     channel = v; 
    }
  });

  //Get Message at slack
  return slackApp.channelsHistory(channel.id).messages;
}