言語処理100本ノック2015 をRubyでやる【第2章】
コードは GitHub に随時上げていきます。
実は 12/28 現在、6 章まで進んでいるのであともうちょっと! って感じです。見た感じ 7 章もいけそうなので、8 章以降は Ruby で機械学習ライブラリ使えるかにかかっています。
今回は第 2 章「UNIXコマンドの基礎」です。
hightemp.txtは,日本の最高気温の記録を「都道府県」「地点」「℃」「日」のタブ区切り形式で格納したファイルである.以下の処理を行うプログラムを作成し,hightemp.txtを入力ファイルとして実行せよ.さらに,同様の処理をUNIXコマンドでも実行し,プログラムの実行結果を確認せよ.
10. 行数のカウント
行数をカウントせよ.確認にはwcコマンドを用いよ.
解答
filename = "hightemp.txt" puts File.readlines(filename).count
IO.readlines(path) -> [String]
は、指定されたファイルを全て読み込んで、その各行を要素としてもつ配列を返します。今回は行数少ないので readlines で一気に読み込んじゃいましたが、ちゃんとやるなら N 行ずつ読んだりします。
cat hightemp.txt | wc -l
シェルだとこんな感じです。
出力
24
11. タブをスペースに置換
タブ1文字につきスペース1文字に置換せよ.確認にはsedコマンド,trコマンド,もしくはexpandコマンドを用いよ.
解答
filename = "hightemp.txt" lines = File.readlines(filename) lines.each do |line| puts line.gsub("\t", " ") end
String#gsub(pattern, replace) -> String
は、pattern にマッチする部分全てを replace で置き換えた文字列を生成して返します。非破壊です。gsub!
だと破壊的。
sed -e "s/ / /g" hightemp.txt cat hightemp.txt | tr "\t" " " expand -t 1 hightemp.txt
シェルだとこんな感じです。挙げられたコマンド 3 種それぞれでやってみました。
出力
高知県 江川崎 41 2013-08-12 埼玉県 熊谷 40.9 2007-08-16 岐阜県 多治見 40.9 2007-08-16 山形県 山形 40.8 1933-07-25 山梨県 甲府 40.7 2013-08-10 和歌山県 かつらぎ 40.6 1994-08-08 静岡県 天竜 40.6 1994-08-04 山梨県 勝沼 40.5 2013-08-10 埼玉県 越谷 40.4 2007-08-16 群馬県 館林 40.3 2007-08-16 群馬県 上里見 40.3 1998-07-04 愛知県 愛西 40.3 1994-08-05 千葉県 牛久 40.2 2004-07-20 静岡県 佐久間 40.2 2001-07-24 愛媛県 宇和島 40.2 1927-07-22 山形県 酒田 40.1 1978-08-03 岐阜県 美濃 40 2007-08-16 群馬県 前橋 40 2001-07-24 千葉県 茂原 39.9 2013-08-11 埼玉県 鳩山 39.9 1997-07-05 大阪府 豊中 39.9 1994-08-08 山梨県 大月 39.9 1990-07-19 山形県 鶴岡 39.9 1978-08-03 愛知県 名古屋 39.9 1942-08-02
12. 1列目をcol1.txtに,2列目をcol2.txtに保存
各行の1列目だけを抜き出したものをcol1.txtに,2列目だけを抜き出したものをcol2.txtとしてファイルに保存せよ.確認にはcutコマンドを用いよ.
解答
require "csv" filename = "hightemp.txt" table = CSV.table(filename, col_sep: "\t", headers: %w(pref city temp day)) File.open("col1.txt", "w") do |file| file.puts table[:pref] end File.open("col2.txt", "w") do |file| file.puts table[:city] end
%w
で配列を作っています。
%w(hoge fuga) # => ["hoge", "fuga"]
cut -f 1 hightemp.txt cut -f 2 hightemp.txt
シェルだとこんな感じです。
出力
# col1.txt 高知県 埼玉県 岐阜県 山形県 山梨県 和歌山県 静岡県 山梨県 埼玉県 群馬県 群馬県 愛知県 千葉県 静岡県 愛媛県 山形県 岐阜県 群馬県 千葉県 埼玉県 大阪府 山梨県 山形県 愛知県
#col2.txt 江川崎 熊谷 多治見 山形 甲府 かつらぎ 天竜 勝沼 越谷 館林 上里見 愛西 牛久 佐久間 宇和島 酒田 美濃 前橋 茂原 鳩山 豊中 大月 鶴岡 名古屋
13. col1.txtとcol2.txtをマージ
12で作ったcol1.txtとcol2.txtを結合し,元のファイルの1列目と2列目をタブ区切りで並べたテキストファイルを作成せよ.確認にはpasteコマンドを用いよ.
解答
col1 = "col1.txt" col2 = "col2.txt" prefs = File.readlines(col1).map(&:chomp) cities = File.readlines(col2).map(&:chomp) output = [prefs, cities].transpose.map { |row| row.join("\t") } File.open("hightemp13.txt", "w") do |file| file.puts output end
単純にくっつけてから転置行列を作っています。String#chomp -> String
では、末尾から改行コードを取り除いています。
paste -d "\t" col1.txt col2.txt
シェルだとこんな感じです。
解答
高知県 江川崎 埼玉県 熊谷 岐阜県 多治見 山形県 山形 山梨県 甲府 和歌山県 かつらぎ 静岡県 天竜 山梨県 勝沼 埼玉県 越谷 群馬県 館林 群馬県 上里見 愛知県 愛西 千葉県 牛久 静岡県 佐久間 愛媛県 宇和島 山形県 酒田 岐阜県 美濃 群馬県 前橋 千葉県 茂原 埼玉県 鳩山 大阪府 豊中 山梨県 大月 山形県 鶴岡 愛知県 名古屋
14. 先頭からN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち先頭のN行だけを表示せよ.確認にはheadコマンドを用いよ.
解答
filename = "hightemp.txt" lines = File.readlines(filename) n = ARGV[0].to_i puts lines[0...n]
String#to_i
は、整数とみなせない文字が出てくるまで、文字列を整数に変換します。
"".to_i # => 0
head -n 4 hightemp.txt
シェルだとこんな感じです。
出力例(n == 5)
高知県 江川崎 41 2013-08-12 埼玉県 熊谷 40.9 2007-08-16 岐阜県 多治見 40.9 2007-08-16 山形県 山形 40.8 1933-07-25 山梨県 甲府 40.7 2013-08-10
15. 末尾のN行を出力
自然数Nをコマンドライン引数などの手段で受け取り,入力のうち末尾のN行だけを表示せよ.確認にはtailコマンドを用いよ.
解答
filename = "hightemp.txt" lines = File.readlines(filename) n = [ARGV[0].to_i, lines.length].min exit if n == 0 puts lines[-n..-1]
基本的に 14 の逆をすれば良いのですが、
[-n..1]
で n がlines.length
より大きいと lines が全部出ちゃうn == 0
だと lines が全部出ちゃう
のでこうなっています。
tail -n 4 hightemp.txt
シェルだとこんな感じです。
出力例(n == 5)
埼玉県 鳩山 39.9 1997-07-05 大阪府 豊中 39.9 1994-08-08 山梨県 大月 39.9 1990-07-19 山形県 鶴岡 39.9 1978-08-03 愛知県 名古屋 39.9 1942-08-02
16. ファイルをN分割する
自然数Nをコマンドライン引数などの手段で受け取り,入力のファイルを行単位でN分割せよ.同様の処理をsplitコマンドで実現せよ.
解答
require "activerecord" filename = "hightemp.txt" lines = File.readlines(filename) l = lines.length n = ARGV[0].to_i exit if n == 0 lines.in_groups_of(n).each_with_index do |line, i| File.write("hightemp16-#{i}.txt", "w") { |file| file.puts(line) } end
「なるべく均等な N 分割」と考えて、ActiveRecord の in_groups_of を使いました。24 行 5 分割なら 5-5-5-5-4
になります。
split -n l/5 -a 1 -d --additional-suffix=.txt hightemp.txt hightemp16sh-
シェルだとこんな感じです。普通に分割すると単純にバイト数を見ているのか、文字の途中で分割されたりします。l/N
で「N 個のファイルに分割するが、行やレコード内の分割は行わない」を表すそうです。
出力例(n == 5)
# hightemp16-0.txt 高知県 江川崎 41 2013-08-12 埼玉県 熊谷 40.9 2007-08-16 岐阜県 多治見 40.9 2007-08-16 山形県 山形 40.8 1933-07-25 山梨県 甲府 40.7 2013-08-10
# hightemp16-1.txt 和歌山県 かつらぎ 40.6 1994-08-08 静岡県 天竜 40.6 1994-08-04 山梨県 勝沼 40.5 2013-08-10 埼玉県 越谷 40.4 2007-08-16 群馬県 館林 40.3 2007-08-16
# hightemp16-2.txt 群馬県 上里見 40.3 1998-07-04 愛知県 愛西 40.3 1994-08-05 千葉県 牛久 40.2 2004-07-20 静岡県 佐久間 40.2 2001-07-24 愛媛県 宇和島 40.2 1927-07-22
# hightemp16-3.txt 山形県 酒田 40.1 1978-08-03 岐阜県 美濃 40 2007-08-16 群馬県 前橋 40 2001-07-24 千葉県 茂原 39.9 2013-08-11 埼玉県 鳩山 39.9 1997-07-05
# hightemp16-4.txt 大阪府 豊中 39.9 1994-08-08 山梨県 大月 39.9 1990-07-19 山形県 鶴岡 39.9 1978-08-03 愛知県 名古屋 39.9 1942-08-02
17. 1列目の文字列の異なり
1列目の文字列の種類(異なる文字列の集合)を求めよ.確認にはsort, uniqコマンドを用いよ.
解答
require "csv" filename = "hightemp.txt" table = CSV.table(filename, col_sep: "\t", headers: %w(pref city temp day)) puts table[:pref].uniq.sort
cut -f 1 hightemp.txt | sort | uniq
シェルだとこんな感じです。
出力
千葉県 和歌山県 埼玉県 大阪府 山形県 山梨県 岐阜県 愛媛県 愛知県 群馬県 静岡県 高知県
18. 各行を3コラム目の数値の降順にソート
各行を3コラム目の数値の逆順で整列せよ(注意: 各行の内容は変更せずに並び替えよ).確認にはsortコマンドを用いよ(この問題はコマンドで実行した時の結果と合わなくてもよい).
解答
require "csv" filename = "hightemp.txt" table = CSV.table(filename, col_sep: "\t", headers: %w(pref city temp day)) sorted = table.sort_by { |row| -row[:temp] } sorted.each do |row| puts row.to_s.gsub(/,/, "\t") end
ソートのところで -
を付けているのは逆順の意味です。
sort -k 3nr,3 hightemp.txt
シェルだとこんな感じです。
出力例
高知県 江川崎 41 2013-08-12 埼玉県 熊谷 40.9 2007-08-16 岐阜県 多治見 40.9 2007-08-16 山形県 山形 40.8 1933-07-25 山梨県 甲府 40.7 2013-08-10 静岡県 天竜 40.6 1994-08-04 和歌山県 かつらぎ 40.6 1994-08-08 山梨県 勝沼 40.5 2013-08-10 埼玉県 越谷 40.4 2007-08-16 群馬県 館林 40.3 2007-08-16 群馬県 上里見 40.3 1998-07-04 愛知県 愛西 40.3 1994-08-05 千葉県 牛久 40.2 2004-07-20 静岡県 佐久間 40.2 2001-07-24 愛媛県 宇和島 40.2 1927-07-22 山形県 酒田 40.1 1978-08-03 群馬県 前橋 40 2001-07-24 岐阜県 美濃 40 2007-08-16 大阪府 豊中 39.9 1994-08-08 山梨県 大月 39.9 1990-07-19 山形県 鶴岡 39.9 1978-08-03 愛知県 名古屋 39.9 1942-08-02 千葉県 茂原 39.9 2013-08-11 埼玉県 鳩山 39.9 1997-07-05
19. 各行の1コラム目の文字列の出現頻度を求め,出現頻度の高い順に並べる
各行の1列目の文字列の出現頻度を求め,その高い順に並べて表示せよ.確認にはcut, uniq, sortコマンドを用いよ.
解答
require "csv" filename = "hightemp.txt" table = CSV.table(filename, col_sep: "\t", headers: %w(pref city temp day)) prefs = table[:pref].group_by(&:itself).map { |k, v| { "#{k}": v.count } } prefs.sort_by{ |pref| pref.values }.reverse.each do |hash| puts "#{hash.keys.first} #{hash.values.first}" end
① group_by(&:itself)
で、同じ都道府県を集めて配列をつくり、都道府県名を key、配列を value とした Hash をつくります。力技です。
table[:pref].group_by(&:itself) # => {"高知県"=>["高知県"], # "埼玉県"=>["埼玉県", "埼玉県", "埼玉県"], # "岐阜県"=>["岐阜県", "岐阜県"], # "山形県"=>["山形県", "山形県", "山形県"], # "山梨県"=>["山梨県", "山梨県", "山梨県"], # "和歌山県"=>["和歌山県"], # "静岡県"=>["静岡県", "静岡県"], # "群馬県"=>["群馬県", "群馬県", "群馬県"], # "愛知県"=>["愛知県", "愛知県"], # "千葉県"=>["千葉県", "千葉県"], # "愛媛県"=>["愛媛県"], # "大阪府"=>["大阪府"]}
② Hash に対して、key そのままで value を配列のサイズにした Hash をつくります。
③ Hash in Array の value に対してソートします。ちょっと苦しい。
cut -f 1 hightemp.txt | sort | uniq -c | sort -r
シェルだとこんな感じです。
出力
山梨県 3 埼玉県 3 群馬県 3 山形県 3 千葉県 2 岐阜県 2 静岡県 2 愛知県 2 大阪府 1 愛媛県 1 和歌山県 1 高知県 1
ソートは安定ソートだったり不安定ソートだったり、使ってる言語の仕様によって出力が微妙に変わります。あと UNIX コマンドすごい、プログラミング言語なんていらないですね(!)
もっと良い書き方あるよ〜などあれば issue とかで教えてもらえるとうれしいです!