SHOYAN BLOG

I am a pragmatic programmer.

[Ruby]CSVファイルのエンコードをsjisに指定する

日本語の場合、CSVのファイルエンコードをShift JISにする要件がけっこうあると思います。
RubyのCSVライブラリは、encodingというオプションが用意されており、encoding: ’sjis’ のようにファイルエンコーディングを指定できます。

1
2
3
4
require 'csv'
CSV.open("hoge.csv", "wb", encoding: 'sjis') do |csv|
  csv << ["ID", "担当者", "メールアドレス"]
end

nkfコマンドを使ってファイルエンコーディングを確認します。

1
2
  nkf -g hoge.csv
Shift_JIS

Shift_JISで作成されていることが確認できました。

FTPについて調べてみた

FTPはすでにご存知のかたも多いと思います。
自分はFTPについて、「ファイル転送に使われるプロトコルであり、セキュリティが脆弱である」というくらいしか理解していなかったので詳細を把握するために調べました。

FTPとは

FTPとはインターネットの初期の頃から存在するプロトコルで、今でもインターネットでよく使われています。
用途としては、以下に使われます。

  • ウェブページの各種ファイル(HTMLや画像)をクライアントのPCからサーバーへアップロードする
  • ソフトウェアの配布サイトやFTPファイルサーバーからクライアントPCへダウンロードする

FTPを利用するにはユーザー名とパスワードが必要です。
ソフトを配布するための目的で使う匿名でアクセスできるAnonymous(匿名)FTPサーバーもあります。
しかし、形式上ユーザー名とパスワードは必要なので、ユーザー名にanonymousやftpを使います。パスワードは何でもよいですが、慣習としてメールアドレスを入力するようです。

プロトコルの概要

FTPのプロトコルはRFC959に定義してあります。
RFC959が書かれたのが1985年なのでおよそ30年前に作られたプロトコルです。

FTPは大きくわけると、コネクションの確立とデータ転送にわけることができます。
そして、使うポートも2つが用意してあります。

  • 20: データ転送ポート
  • 21: コントロールポート

20番がデータを転送するときに使うポートで、21番は認証などの接続に使うためのポートです。

コネクションの確立

FTPは アクティブモードパッシブモード のいずれかで動作し、サーバーとの接続をする際にどちらかを選択します。

アクティブモードの場合

アクティブモードの場合、クライアントはデータ転送用のポートを用意しPORTコマンドを使って待ち受けポートをサーバーに伝えます。
サーバーはPORTコマンドを受け取ると、そのポートに対して接続を行います。
しかし、Firewallがある環境の場合、サーバーからの接続が拒否されうまくいかない場合があります。
その際はパッシブモードを使います。

パッシブモード

パッシブモードでは、まず最初にクライアントがPASVというコマンドを使いサーバーに送信します。
そのコマンドを受け取ったサーバーは自身のIPアドレスとポート番号をクライアントに送信します。
クライアントは受け取ったIPアドレスとポート番号へコネクションを確立しにいきます。

アクティブモードはサーバーより接続を行う、パッシブモードはクライアントより接続を行うという違いがあります。
ちなみに、FTPはよく使われるプロトコルのため、PORTコマンドがFirewallを通過する際にPORTコマンドに書かれたPORTは通過できるようにしてくれるFirewallもあるとのことです。

データ転送

接続ができたら、次はデータ転送を行います。
FTPにはデータタイプという考え方があって、現在は2つのモードが使われています。

アスキーモード

アスキーモードは、必要であればデータを変換します。
例えば異なるOS間でファイルを送ると改行コードが違ったりする場合があると思いますが、アスキーモードはファイルを受け取るホストに適した改行コードにファイルを変換します。

Imageモード(バイナリモード)

Imageモードは一般的にバイナリモードと呼ばれます。
バイト単位でデータを転送します。
アスキーモードのようにデータの変換は行われません。

データの転送モードには、以下の3つのモードがあります。

  • ストリームモード
  • ブロックモード
  • 圧縮モード

ストリームモード

データをそのまま転送するモードです。

ブロックモード

データをブロックに分割して転送するモードです。

圧縮モード

ランレングスエンコーディングを使ってデータを圧縮して転送するモードです。

セキュリティ上の問題

FTPはセキュアなプロトコルとして設計されていません。
ユーザー名やパスワードを暗号化せずに送信する問題のほかにも数多くのセキュリティ脆弱性があげられています。

  • 総当たり攻撃
  • en:FTP bounce attack
  • パケットキャプチャ (sniffing)
  • Port stealing
  • en:Spoofing attack
  • ユーザ名保護

また、通信内容を暗号化できないので通信経路上でパケットキャプチャすることで盗聴することができてしまいます。
セキュアにする一般的な方法として、SSL/TLSセッション上で通信を行うようにします。
これをFTPSと言います。
また、SSHを介してファイル転送を行うSFTP、SCPを使います。

ちなみにFTPSとSFTPの違いは以下のようになります。

  • FTPS : FTPの通信をSSL/TLSで暗号化 → FTPの拡張
  • SFTP : SSHの通信を使って、FTPを行う → SSHで動作するアプリケーション

理解を深めるためにtelnetを使って実際にFTPサーバーと通信してみます。

実際にFTPを試してみる

実際にFTPを試してみる場合、FTPサーバーが必要です。
ロリポップ!レンタルサーバーを使えば無料でFTPサーバーが利用できます。

ちなみにtelnetでファイルの送受信はできません。
というのも、telnetでは1つのポートを使った通信しかサポートしていないからです。
FTPはデータ転送用のポートと制御用のポートの2つを利用するため、telnetでは認証しか行えません。

まずは、telnetで認証をやってみます(localhostにftpサーバーが起動しているという前提です)。

1
2
3
4
5
$ telnet localhost 21
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 FTP server ready.

まず、ユーザ名を入力します。

1
2
USER ftp
331 Password required for example.com

次に、パスワードを入力します。

1
2
PASS 123
230 User example.com logged in.

telnetでは他に何もできませんのでQUITします。

1
2
3
QUIT
221 Goodbye.
Connection closed by foreign host.

FTPでファイルの送受信を行う

FTPでファイルの送受信を行うには、ftpコマンドを使います。
-d オプションはデバッグモードです。

1
ftp -d localhost 21

ユーザー名とパスワードを聞かれるので入力します。

lsでファイルの一覧をみることができます。

1
2
3
4
5
6
7
8
9
10
ftp> ls
---> EPSV
229 Entering Extended Passive Mode (|||65086|)
229 Entering Extended Passive Mode (|||65086|)
---> LIST
150 Opening ASCII mode data connection for file list
drwx---r-x   6 shoyan shoyan      4096 Jun 17 11:31 .
drwx---r-x   6 shoyan shoyan     4096 Jun 17 11:31 ..
-rw-r--r--   1 shoyan shoyan     3096 Jun 17 11:30 index.html
226 Transfer complete

getコマンドでファイルをダウンロードできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ftp> get index.html
local: index.html remote: index.html
---> SIZE index.html
213 3096
---> EPSV
229 Entering Extended Passive Mode (|||65011|)
229 Entering Extended Passive Mode (|||65011|)
---> RETR index.html
150 Opening BINARY mode data connection for index.html (3096 bytes)
100% |************************************************************************************************************************************************************************************************|  3096        4.61 MiB/s    00:00 ETA
226 Transfer complete
3096 bytes received in 00:00 (163.72 KiB/s)
---> MDTM index.html
213 20160617023028
parsed date `20160617023028' as 1466130628, Fri, 17 Jun 2016 11:30:28 +0900

参考リンク

Rubyでファイルエンコーディングを確認する

Rubyでファイルエンコーディングを確認するにはNKFモジュールを使って確認します。

http://docs.ruby-lang.org/ja/2.3.0/class/NKF.html

NKFモジュールとは、nkf(Network Kanji code conversion Filter, http://sourceforge.jp/projects/nkf/) をRubyから使うためのモジュールです。

1
2
3
4
5
require 'nkf'

content = File.read("path/to/file.txt")
NKF.guess(content).to_s
=> "Shift_JIS"

このような感じでrspecでテストするときにも使えます。

1
2
3
4
5
6
require 'nkf'

it 'file encoding is Shift_JIS' do
 content = File.read("filename.csv")
 expect(NKF.guess(content).to_s).to eq 'Shift_JIS'
end

Capistrano/wheneverで Cannot Load Such Fileがでる

wheneverを導入するためCapfilerequire "whenever/capistrano"と定義して bundle exec cap -T とすると以下のエラーがでた。

1
LoadError: cannot load such file -- /Users/shoyan/app/vendor/bundle/ruby/2.2.0/gems/whenever-0.9.5/lib/whenever/tasks/whenever.rake

実際にvendor/bundle/ruby/2.2.0/gems/whenever-0.9.5/lib/whenever/tasks/whenever.rake を確認してみると、たしかにそのファイルが存在しない。

wheneverのリポジトリを見てみると、シンボリックリンクがはってあった。

bundle install したときにはシンボリックリンクがはられないようだ。

手元のRubyのバージョンは、2.2.4だった。

ruby2.3.1で実行してみると Gem::Package::PathError が発生した。

1
2
ERROR:  While executing gem ... (Gem::Package::PathError)
    installing into parent path /Users/shoyan/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/capistrano/v3/tasks/whenever.rake of /Users/shoyan/.rbenv/versions/2.3.0/lib/ruby/gems/2.3.0/gems/whenever-0.9.5 is not allowed

このエラーはRuby2.3.2では直っていることが期待される。

回避策

Capfileでの指定はやめて、lib/capistrano/tasks 以下にwhenever.taskを作成した。
これでcapのタスク自体は実行できるようになった。
しかし、ruby2.3.1の環境ではbundle installがこけるのでwheneverの導入自体ができないという状況。

その後

bundle installがこけるバグは以下のPRがでており0.9.6で修正されていた。

また、0.9.5では cap whenever:update_crontabinstance variable @_env not defined が発生してコマンドが正常に実行できないバグがあったが、0.9.7で修正されている。

実はこのバグは自分が踏んでおり、その修正案をPRしてCHANGELOGに名前が刻まれた。

SSHKitを実際に使ってみて理解する

Capistrano/sshkitを紹介します。
https://github.com/capistrano/sshkit

SSHKitはリモートサーバーに対してコマンドを実行するためのツールキットです。
CapistranoやCapistranoプラグインではSSHKitが使われています。

インストール

1
gem install sshkit

コマンドのサンプル

実際に使ってみて理解していきます。
使うには、sshkitをロードする必要があります。

1
2
require 'sshkit'
include SSHKit::DSL

ホスト名を取得する

まずはログインしてホスト名を取得してみましょう。

1
2
3
4
on ['deploy@example.com] do |host|
 puts capture(:hostname)
end
=> example.com

on メソッドに対象のサーバーとブロックを渡します。
対象のサーバーは複数設定することも可能です。
ブロックにはサーバー上で実行するコマンドを設定します。
captureメソッドは渡された引数をコマンドとして実行し、結果をログに出力します。

特定のユーザーでコマンドを実行する

特定のユーザーでコマンドを実行する場合は、asメソッドで指定します。

1
2
3
4
5
on ['example.com'] do |host|
  as 'deploy' do
   puts capture(:whoami)
  end
end

特定のディレクトリを指定する

特定のディレクトリを指定する場合は、withinメソッドを指定します。

1
2
3
4
5
on ['deploy@example.com'] do |host|
  within '/var/log' do
    puts capture(:head, '-n5', 'messages')
  end
end

/var/loghead -n5 messages を実行します。

環境変数を指定する

withメソッドで環境変数を指定することができます。

1
2
3
4
5
on hosts do |host|
  with rack_env: :test do
    puts capture("env")
  end
end

rack_envに:test を設定しています。

ファイルをチェックして存在すればメッセージを表示、なければファイルを作成する

1
2
3
4
5
6
7
8
9
10
on ['deploy@example.com'] do |host|
  f = '/tmp/file'
  if test("[ -f #{f} ]")
    info "#{f} already exist on #{host}!"
  else
    execute :touch, f
  end
end
INFO [790b6aaa] Running /usr/bin/env touch /tmp/file as deploy@example.com
INFO [790b6aaa] Finished in 0.052 seconds with exit status 0 (successful).

testメソッドでファイルをチェックし、executeメソッドでtouchコマンドを実行しています。

ファイルをアップロードする

ファイルをアップロードすることもできます。

1
2
3
on ['deploy@example.com'] do |host|
  upload! 'README.md', '/tmp/README.md'
end

第1引数がローカルのファイルのパス、第2引数がサーバーに配置するファイルのパスです。

また、recursiveオプションをtrueに設定することでディレクトリをアップロードすることもできます。

1
2
3
on hosts do |host|
  upload! '.', '/tmp/mypwd', recursive: true
end

ローカルで実行する

ローカルで実行することもできます。

1
2
3
4
5
6
7
8
9
10
11
12
13
run_locally do
  within '/tmp' do
    execute :whoami
  end
end

# もしくは

on(:local) do
  within '/tmp' do
    execute :whoami
  end
end

Rakeタスクで利用する

RakeタスクでSSHKitのDSLを使うこともできます。
この仕組みを利用してCapistranoのプラグインは作成されています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
require 'sshkit'

SSHKit.config.command_map[:rake] = "./bin/rake"

desc "Deploy the site, pulls from Git, migrate the db and precompile assets, then restart Passenger."
task :deploy do
  include SSHKit::DSL

  on "example.com" do |host|
    within "/opt/sites/example.com" do
      execute :git, :pull
      execute :bundle, :install, '--deployment'
      execute :rake, 'db:migrate'
      execute :rake, 'assets:precompile'
      execute :touch, 'tmp/restart.txt'
    end
  end
end

サンプルがこちらにたくさんあるので、参考になると思います。

https://github.com/capistrano/sshkit/blob/master/EXAMPLES.md