ようじょのおえかきちょう

ふぇぇ お医者さんにペン持ったらダメっていわれた〜〜

言語処理100本ノック2015 をRubyでやる【第2章】

コードは GitHub に随時上げていきます。

github.com

実は 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 行ずつ読んだりします。

qiita.com

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"]

qiita.com

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 個のファイルに分割するが、行やレコード内の分割は行わない」を表すそうです。

Man page of SPLIT

出力例(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 とかで教えてもらえるとうれしいです!

github.com