jQueryで複数のフォームに文字数カウンターを表示する

文字数カウンターを複数フォームに表示するためのjQueryです。

コード

HTMLは以下のようにします。

.input-field以下に文字数を表示する.input-field__countをつくり、データ属性(data-count)に最大文字数を設定します。

<div class="input-field">
  <input type="text">
  <div class="input-field__count" data-count="45"></div>
</div>

jQueryは以下のようにします。 すべてのtextarea, input[type=text]に文字数カウンターを表示します。

$(function(){
  function countInputText(input) {
    //文字数を表示する.input-field__countを取得
    var counter = $(input).parents('.input-field').find(".input-field__count");

    // データ属性(data-count)から最大文字数を取得
    var maxLength = counter.data("count");

    // 現在の文字数を取得
    var currentLength = $(input).val().length;
    var htmlVal = currentLength + " / " + maxLength;

   // 現在の文字数と最大文字数を比較して、
   // 現在の文字数が多ければ赤く表示する
    if ( parseInt(currentLength) >= parseInt(maxLength) ) {
      counter.html("<span style='color:red'>" + htmlVal + "</span>");
    } else {
      counter.html(htmlVal);
    }
  }

  // ページ表示時に文字数をカウントして表示
  $('textarea, input[type=text]').each(function(){
    countInputText($(this));
  });

  // フォーム入力時に文字数をカウントして表示
  $('textarea, input[type=text]').on('keydown keyup keypress change',function(){
    countInputText($(this));
  });
});

RailsでRSSフィードを作成する:builder

Railsでbuilderを使ってRSSフィードを追加するためのメモです。

要件

  • Gemを追加しない
  • ルーティングを指定できる
  • 動的に更新できる

最終的には、http://example.com/feed/RSSフィードを提供することが目的です。

今回は、上記の要件を満たすためにbuilderを使ってRSSフィードを作成しました。

参照: Builder::XmlMarkup - APIdock

ルーティング:routes.rb

routes.rbにルーティングを記載します。このとき、デフォルトのフォーマットを指定するのを忘れないようにしてください。

get :feed, to: 'rss#index', defaults: { format: :rss }

コントローラー:rss_controller.rb

コントローラー側ではあまりやることはなく、@postsを取得するのみです。

atomなどフォーマット毎に、RSSを提供する場合はrespond_toを使って指定します。

class RssController < ApplicationController
  layout false
  def index
    @posts = Post.published.limit(10)

    respond_to do |format|
      format.rss
      format.atom
    end
  end
end

ビュー:index.rss.builder

appview/rssにindex.rss.builderを作成します。

xml.instruct! :xml, :version => "1.0"
xml.rss(
  "version" => "2.0",
  "xmlns:content" => "http://purl.org/rss/1.0/modules/content/",
  "xmlns:wfw" => "http://wellformedweb.org/CommentAPI/",
  "xmlns:dc" => "http://purl.org/dc/elements/1.1/",
  "xmlns:atom" => "http://www.w3.org/2005/Atom",
  "xmlns:sy" => "http://purl.org/rss/1.0/modules/syndication/",
  "xmlns:slash" => "http://purl.org/rss/1.0/modules/slash/"
) do
  xml.channel do
    xml.title "サイト名"
    xml.description "説明"
    xml.link "サイトURL"
    xml.language "ja-ja"
    xml.ttl "40"
    xml.pubDate(Time.now.strftime("%a, %d %b %Y %H:%M:%S %Z"))
    xml.atom :link, "href" => "RSSのURL", "rel" => "self", "type" => "application/rss+xml"
    @posts.each do |p|
      xml.item do
        xml.title p.title #タイトル
        xml.description do
          xml.cdata! strip_tags(p.content)[0..110] #本文
        end
        xml.pubDate p.created_at #公開日
        xml.guid "http://example.com/#{p.id}"
        xml.link "http://example.com/#{p.id}"
      end
    end
  end
end

これで完了です。

Rails5でコントローラー別に実行するJavaScriptを分ける

Railsでコントローラー毎に実行するJavaScriptを分けたいときの対応方法です。

対応方法としては、

  1. コントローラー毎にファイルを作り読み込む
  2. ファイルは1つにしてコントローラー毎に実行するJavascriptを分ける

という方法があるかと思います。

1の方法でも良いのですが、Turbolinksを使うのであればファイルを分けるのは望ましくないので2の方法で実装します。

1. HTMLにコントローラー名を設定

JavaScriptからコントローラーを取得するために、HTMLのカスタムデータ属性を使いコントローラー名を設定します。 bodyタグでカスタムデータ属性を使っていますが、ほかの方法でも問題ありません。

Slimでカスタムデータ属性を設定する場合。

body data-controller="#{params[:controller]}"

2. コントローラー名を取得する

HTMLからコントローラー名を取得します。

var controller = $('body').data('controller');

JavaScriptの量が少なく、とにかくシンプルにコントローラー毎に処理を分けたいという場合は、以下のように単純な分岐でも良いかもしれません。

if ( controller == "コントローラー名") {
  //コントローラー別の処理
}

3. 初期化処理をする

しかし、これでは分岐が増えると大変なことになるので、以下のような初期化処理をつくります。 コントローラー名と同じコンストラクタをインスタンス化して実行するという非常にシンプルなものです。

$(document).on('turbolinks:load', function(){
  var controller = $('body').data('controller');
  if (window[controller]) {
    new window[controller];
  }
});

JavaScriptの場合は、以下のようにコントローラー別にコンストラクタをつくります。

var user = function() {
  console.log("userコントローラーで実行されます");
}

CoffeeScriptを使う場合は、以下のようにコンストラクタの名前に@を付けて外部から参照できるようにしてください。

@user = ->
    console.log("userコントローラーで実行されます")

コントローラー名と同じコンストラクタを作ればインスタンス化して実行するようになります。これで完了です。

[おまけ] RailsScriptを使う

RailsScriptというGemを使うことでも可能です。

RailsScriptも内部の実装は、上記の方法とほぼ同じようなものです。

しかし、使い方が悪かったのか、Turbolinksとあわせると画面遷移したときにJavaScriptが実行されなかったので採用していません。

Gemfileにgem 'rails_script', '~> 2.0'を追加をしてインストール。

bundle install
bundle exec rails g rails_script:install
Running via Spring preloader in process 3112
      create  app/assets/javascripts/base.coffee
      create  app/assets/javascripts/global.coffee
      append  app/assets/javascripts/application.js

layout/application.html.slimnclude_rails_scriptを追加

application.jsに設定を追加。

//= require rails_script
//= require_tree .

こんな感じで使えます。

class App.Users extends App.Base

   show: =>
      console.log 'users#show'

参考

日付や時間を数値として計算する方法

給与計算や時間あたりのコスト計算などで、日付や時間などの型のものを数値として計算したいという場合の対処法です。

日付計算の時のよくあるミス。

HOUR()やMINUTE()といった関数を使って計算するというパターン。これ自体は問題ないと言えばないのですが、24時間を超える時間を計算する時に困ったことになります。

例えば、32時間30分00秒という時間を計算する場合。HOUR関数では以下のような結果になります。

spreadsheet1

そうなんです。32時間を24時間+8時間と計算し、8時間だけを返してくるのです。良さそうな関数を探したのですが、見つかりませんでした。

日付型を数値に変換して扱う

対処法は以外とシンプルで、日付型を数値に変換することで計算ができます。

使うのは、VALUE()関数。公式マニュアルには以下のように説明があります。

VALUE()
Google スプレッドシートで認識される日付、時刻、番号の書式の文字列を数値に変換します。

具体的には、以下のようにVALUE()関数に日付をキャストして数値に変換し、24時間表示にするために×24します。 spreadsheet3

分表示にするには、×24×60すればOKです。 spreadsheet4

補足:VALUE()関数とTO_PURE_NUMBER()関数

ちなみに、似たような関数でTO_PURE_NUMBER()という関数があり、ほとんどVALUE()と同じような説明がマニュアルには記載されています。

TO_PURE_NUMBER()
日付/時刻、百分率、通貨などの表示形式を持つ数値を表示形式なしの純粋な数値に変換します。

今回の日付型を計算する場合は、どちらでも使えると言えば使えます。

ただし、引数に文字列を渡した時の挙動が違うようです。引数に文字列「Time」を渡した場合の具体例は、以下のようになります。

spreadsheet2

VALUE()ではエラーに、TO_PURE_NUMBER()ではなぜか引数そのままの「Time」が表示されることに。

ということで、普通に使う場合はエラー表示もしてくれて、タイプ数も少なくなるVALUE()関数を使えば良いのかなと思います。

Scrapyをインストールしてバックグラウンドでスクレイピングするまで

1. Python2.7とpipのインストール

まずはPythonをソースからインストールします。

$ yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel
$ cd /usr/local/src
$ wget https://www.python.org/ftp/python/2.7.11/Python-2.7.11.tgz
$ tar xvfz Python-2.7.6.tgz
$ cd Python-2.7.6
$ ./configure --prefix=/usr/local
$ make
$ make altinstall

次にpipのインストールです。easy_installでpipを入れることもできますが、今では下記のコマンドで一発でpipを入れることができます。

$ curl -kL https://raw.github.com/pypa/pip/master/contrib/get-pip.py | python

また、easy_installを使ってpipをインストールする方法は以下の通りです。easy_installを使うのにsetuptoolsではなくdistributeを利用しています。

$ wget http://pypi.python.org/packages/source/d/distribute/distribute-0.6.27.tar.gz
$ tar zxvf distribute-0.6.27.tar.gz
$ cd distribute-0.6.27
$ sudo python2.7 setup.py install
$ easy_install-2.7 pip

2. Scrapyのインストール

次にpipを使ってscrapyをインストールします。

$ pip install scrapy
    Compile failed: command 'gcc' failed with exit status 1
    creating tmp
    cc -I/usr/include/libxml2 -c /tmp/xmlXPathInitacjqEE.c -o tmp/xmlXPathInitacjqEE.o
    /tmp/xmlXPathInitacjqEE.c:1:26: error: libxml/xpath.h: そのようなファイルやディレクトリはありません
    *********************************************************************************
    Could not find function xmlCheckVersion in library libxml2. Is libxml2 installed?
    *********************************************************************************
    error: command 'gcc' failed with exit status 1

エラーですね。依存関係をクリアできなかったみたいです。libxml/xpath.hがないとのことなのでインストールします。

$ yum -y libxslt-devel

これで再度インストールすればうまくいきました。

$ pip install scrapy

3. Scrapyを使ってみる

Scrapyを使う簡単な流れは以下のようになります。

  1. Scrapy プロジェクトの作成
  2. 基本設定と抽出するアイテムの定義
  3. スクレイピング&クロール用のSpider作成
  4. 抽出したアイテムのパイプライン処理作成

3-1. Scrapy プロジェクトの作成

scrapyコマンドを使うことでプロジェクトを作成できます。プロジェクト名は英数字またはアンダースコアしか使えないので注意です。

$scrapy startproject sample_crawler

すると以下のようなディレクトリが作成されます。

sample_crawler/
├ sample_crawler
│ ├ __init__.py
│ ├ items.py
│ ├ pipelines.py
│ ├ settings.py
│ └ spiders
│    └ __init__.py
└── scrapy.cfg

ここで主に編集するのは、以下のファイルです。

items.py      データ保存のためのクラス
settings.py   基本的な設定
pipelines.py  抽出データの出力先を指定
spiders/      spiderのディレクト

3-2. 基本設定と抽出するアイテムの定義

settings.pyでクローラーの設定をすることができます。今回はサンプルなので、以下の部分のみ変更しました。

DOWNLOAD_DELAY=5
BOT_NAME = 'samplespider'

*settings.pyの詳しい設定内容はScrapyの公式ドキュメント: http://doc.scrapy.org/en/latest/topics/settings.html

また、クロールで抽出するアイテムをitem.pyで定義します。以下はURLとタイトルを取得する例です。

# -*- coding: utf-8 -*-
# Define here the models for your scraped items
#
# See documentation in:
# http://doc.scrapy.org/en/latest/topics/items.html
import scrapy

class CrawlerItem(scrapy.Item):
    # define the fields for your item here like:
    URL = scrapy.Field()
    title = scrapy.Field()
    pass

3-3. スクレイピング&クロール用のSpider作成

scrapyコマンドでspiderのテンプレートを作成します。

$ scrapy genspider -t crawl sample_spider example.com

すると、spiders/以下にsample_spider.pyというファイルが作られていると思います。以下は簡単なサンプルです。

# -*- coding: utf-8 -*-
import scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
from scrapy.selector import Selector

class BaseSpider(CrawlSpider):
    name = 'sample_spider'
    # スクレイピングするドメインを指定します
    allowed_domains = ['example.com']

    # スクレイピングを開始するURL
    start_urls = ['http://example.com/']

    # ドメイン以下のURLに関して正規表現で指定します
    # /123など数字が連続しているURLはスクレイピングします
    allow_list = ['/\d+']
    # category, tag, pageなどが含まれるページはスクレイピングしません
    deny_list = ['category', 'tag', 'page']

    rules = (
        # スクレイピングするURLのルールを指定
        # parse_item関数がコールバック
        Rule(LinkExtractor( allow=allow_list, deny=deny_list ), callback='parse_item'),

        # spiderがたどるURLを指定
        # 今回はallowed_domainsの場合は全てたどります。
        Rule(LinkExtractor(), follow=True),
    )

    def parse_item(self, response):
        i = {}
        selector = Selector(response)
        i['URL'] = response.url
        i['title'] = selector.xpath('/html/head/title/text()').extract()
        return i

sitemap.xmlからスクレイピングをする場合は以下のようになります。


# -*- coding: utf-8 -*-
import scrapy
from scrapy.selector import Selector
from scrapy.spiders import SitemapSpider

class SitemapSpider(SitemapSpider):
    name = 'sitemap'
    allowed_domains = ['www.green-japan.com']
    sitemap_urls = ['https://www.green-japan.com/sitemap/companies.xml']

    sitemap_rules = [
        (r'/company/', 'parse_item'),
    ]

    def parse_item(self, response):
        i = {}
        selector = Selector(response)

        i['url'] = response.url
        i['title'] = selector.xpath('/html/head/title/text()').extract()
        return ii

3-4. スクレイピングの実行

以下のコマンドでスクレイピングを実行します。実行結果は、test.csvに出力されます。

$ scrapy crawl sample_spider -o test.csv

クロールをはじめると、以下のような表示がされます。

▼クロール除外時
2015-12-11 16:21:46 [scrapy] DEBUG: Filtered offsite request to 'www.not-target.com': 

▼クロール時
2015-12-11 16:21:49 [scrapy] DEBUG: Crawled (200)  (referer: http://sample.com/)

また、ターミナルを閉じてでもバックグラウンドで実行したい場合は、nohupコマンドで使います。以下はバックグラウンドで実行し、title.csvに実行結果を吐き出す例です。

$ nohup scrapy crawl sample_spidersimple -o titles.csv &
  [1] 11564
  nohup: ignoring input and appending output to `nohup.out'

実行結果は以下のコマンドで確認できます。

$ tail -f nohup.out

【参考】 Scrapy 1.0が公開されました ScrapyでWebサイトのタイトルとURLを再帰的に取得する scrapy を用いてデータを収集し、mongoDB に投入する

SassをMac(El Capitan)にインストールした時のエラー回避

「もうそろそろCSSをどうにかしたい…」と重い腰をあげSassを導入しようとした時に遭遇したエラーの内容と対処法をメモしておきます。

El CapitanのSassインストールエラー

gemをアップデートしておきます

$ sudo gem update --system

sassをインストール。

$ sudo gem install sass
  Fetching: sass-3.4.21.gem (100%)
  ERROR:  While executing gem ... (Errno::EPERM)
    Operation not permitted - /usr/bin/sass

「Operation not permitted」と出てインストールができませんでした。

エラーの原因

El Capitanから導入された「System Integrity Protector(通称SIP)」によって、/usr/binがプロテクトされているからだそう。

これをオフにするにはcsrutilコマンドを使えばいいそうですが、今回は試していません。

エラー解決策

インストール先を/usr/binから/usr/local/binに変更します。

$ sudo gem install -n /usr/local/bin sass
  Successfully installed sass-3.4.21

今度はインストールできました!

$ sass -v
  Sass 3.4.21 (Selective Steve)

これでSassのインストールが完了しました。

投稿時間と現在時間の差を表示する2つの方法

「◯分前の投稿」といった表示をWordPressでしたい時のメモです。

方法1.組み込み関数を使って表示する

echo human_time_diff( get_the_time("U"), current_time('timestamp') ) . __(' ago');

ほとんどのケースはこれで大丈夫です。

この場合、日本語だと例えば「1分前」と表示されて問題ないのですが、英語だと「1 day ago」と表示され「a day ago」と表示することができません。

「1 day ago」でも問題はないのですが、これが気になるという場合は、方法2の独自関数を作って対応するのが良いと思います。

方法2.独自関数で投稿時間と現在時間の差を表示する

本当は別に独自関数を作る必要はないのだけど、方法1.の関数を知らずに作ってしまったのでメモがてら残しておきます。

(本当は英語対応もhuman_time_diff()が1だったらaにするとかすればOKという話は置いておくとして)

注意点は、WordPressではPHPの組み込み関数のtime()が出力するのは常にUTCとなっているので、JSTと比べると9時間ずれてしまうというとこです。

これを回避するには、current_time()かdate_i18n()を使います。

function the_time_diff(){
    global $post;
    $seconds_ago = intval( current_time( 'timestamp' ) ) - intval( get_the_time( 'U', $post->ID ) );

    $years_ago = intval( floor( $seconds_ago / ( 3600 * 24 * 30 * 365 ) ) );
    $months_ago =  intval( floor( $seconds_ago / ( 3600 * 24 * 30 ) ) );
    $weeks_ago = intval( floor( $seconds_ago / ( 3600 * 24 * 7 ) ) );
    $dates_ago = intval( floor( $seconds_ago / ( 3600 * 24 ) ) );
    $hours_ago = intval( floor( ( $seconds_ago / 3600 ) % 24) );
    $minutes_ago = intval( floor( ( $seconds_ago / 60 ) % 60 ) );

    if ( $years_ago === 1 ) :
        _e('a year ago');
    elseif ( $years_ago > 1 ) :
        echo "{$years_ago}".__(' years ago');
    elseif ( $months_ago === 1 ) :
        _e('a month ago');
    elseif ( $months_ago > 1 ) :
        echo "{$months_ago}".__(' months ago');
    elseif ( $weeks_ago === 1 ) :
        _e('a week ago');
    elseif ( $weeks_ago > 1 ) :
        echo "{$weeks_ago}".__(' weeks ago');
    elseif ( $dates_ago === 1 ) :
        _e('a day ago');
    elseif ( $dates_ago > 1 ) :
        echo "{$dates_ago}".__(' days ago');
    elseif ( $hours_ago === 1 ) :
        _e('an hour ago');
    elseif ( $hours_ago > 1 ) :
        echo "{$hours_ago}".__(' hours ago');
    elseif ( $minutes_ago === 1 ) :
        _e('a minute ago');
    elseif ( $minutes_ago > 1 ) :
        echo "{$minutes_ago}".__(' minutes ago');
    elseif ( $seconds_ago === 1 ) :
        _e("a second ago");
    else :
        echo "{$seconds_ago}".__(' seconds ago');
    endif;
}