パターンマッチ処理

与えられたデータを自動的に処理するプログラムの必要性と有用性は高い。 そのことを見越して、自動処理しやすい形式でデータファイルを 作成しておくことが重要である。 また,Webページのアクセスログなど,計算機が自動的に生成する ログファイルは,テキスト形式で書き出されるものが多い。

テキストを処理するプログラムは,その入力パターンに応じて処理を 決定する。入力パターンの照合は対象となる書式を元に決める。

カンマ「,」やタブ文字などの特定文字列による分解(例: CSVファイル)

splitメソッドで分解

特定キーワードで分解

正規表現を用いて解析

splitによる処理

ある特定の文字でフィールドを区切られている書式(DSV; Delimiter-Separated Value)で,フィールド値に区切り文字が現れないようなものは split で簡単に解析できる。CSVファイルの処理例を示す。

yamagata.csv(※)は, 文字列もクォートなしで書かれたcsvファイルで,

※ e-Stat 政府統計の総合窓口 http://www.e-stat.go.jp/SG1/estat/List.do?bid=000001034995cycode=0 表番2. 男女別人口及び世帯の種類(2区分)別世帯数 都道府県,市部,郡部, 市町村・旧市町村 より抜粋

第6フィールド市町村名
第9フィールド総人口
第10フィールド男人口
第11フィールド女人口

となっている。

7,大項目,地域コード,地域識別コード,境域年次(2010),境域年次(2000),,人口 総数,人口 男,人口 女,世帯数 総数,世帯数 一般世帯,世帯数 施設等の世帯
11,,6201,2,2010,2000,山形市,254244,121433,132811,96560,96425,135
12,,6202,2,2010,2000,米沢市,89401,43953,45448,33013,32920,93
13,,6203,2,2010,,鶴岡市,136623,64846,71777,45514,45395,119
20,,6204,2,2010,,酒田市,111151,52610,58541,38955,38860,95
25,,6205,2,2010,2000,新庄市,38850,18432,20418,12980,12958,22
26,,6206,2,2010,2000,寒河江市,42373,20497,21876,12717,12702,15
27,,6207,2,2010,2000,上山市,33836,16036,17800,10751,10709,42
28,,6208,2,2010,2000,村山市,26811,12846,13965,7865,7860,5
29,,6209,2,2010,2000,長井市,29473,14211,15262,9269,9228,41
30,,6210,2,2010,2000,天童市,62214,30148,32066,20404,20338,66
31,,6211,2,2010,2000,東根市,46414,22934,23480,14388,14343,45
32,,6212,2,2010,2000,尾花沢市,18955,9138,9817,5332,5320,12
33,,6213,2,2010,2000,南陽市,33658,16025,17633,10567,10538,29

これを split で分解して, 市町村と総人口のみの集計出力を得るには以下のようにする。

split-yg.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

while line=gets
  data = line.chomp.split(",")
  if /\d+/ =~ data[7]			# 第8フィールドが数字の連続なら
    printf("%s,%s\n", data[6], data[7])
  end
end

実行すると以下のようになる。

./split-yg.rb yamagata.csv
山形市,254244
米沢市,89401
鶴岡市,136623
酒田市,111151
新庄市,38850
寒河江市,42373
上山市,33836
村山市,26811
長井市,29473
天童市,62214
東根市,46414
尾花沢市,18955
南陽市,33658

このように,CSVやタブ文字区切りなどで明確にフィールド分けされた テキストファイルはsplitメソッドで必要なフィールドを抽出できる。

ただし,CSVファイルでも文字列をダブルクォートで括ったり, なおかつその内部にさらにカンマやダブルクォートが含まれるようなものは splitで単純にフィールド分けできない。たとえば以下のようなCSVファイルが該当する。

quoted.csv

番号,氏名,ローマ字,所属
1,公益太郎,"KOEKI, Taro",野球部
2,飯森花子,"IIMORI, Hanako",茶道部
3,三川一二三,"MIKAWA, Hifumi",SKIP

クォートを含むCSVを読んで加工し,CSVに書き出す処理は, csvライブラリ を利用する。これを利用して上記のCSVファイルをフィールドごとに分解してみる。

csv-split.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'csv'			# csvライブラリの利用

CSV.foreach("quoted.csv") do |row|
  puts row.join("|")		# 縦棒(|)でフィールドを区切って出力
end

フィールド値にカンマが含まれていてもクォートされているため 一つのフィールド値として処理されていることが以下の実行例から分かる。

./csv-split.rb
番号|氏名|ローマ字|所属
1|公益太郎|KOEKI, Taro|野球部
2|飯森花子|IIMORI, Hanako|茶道部
3|三川一二三|MIKAWA, Hifumi|SKIP

正規表現を用いた処理

固定区切りでないものは正規表現でパターンマッチを行ない, 後方参照を用いてマッチした一部を取り出す。

以下のような電子メイルのファイルから差出人を抽出してみる。

1217.txt

Return-Path: <ta01002@e.koeki-u.ac.jp>
Delivered-To: yuuji@itl.koeki-u.ac.jp
Received: (qmail 12673 invoked by uid 1010); 26 Apr 2014 22:52:17 -0000
Received: (qmail 12659 invoked from network); 26 Apr 2014 22:52:17 -0000
Received: from broy.e.koeki-u.ac.jp (HELO localhost) (172.17.54.112)
  by pan.e.koeki-u.ac.jp with SMTP; 26 Apr 2014 22:52:17 -0000
Received: from broy.e.koeki-u.ac.jp (HELO localhost) (172.17.54.112)
  by pan.e.koeki-u.ac.jp (antibadmail 1.38) with SMTP; Apr 27 07:52:17 JST 2014
Date: Mon, 27 Apr 2014 07:52:10 +0900 (JST)
Message-Id: <20140427.075210.193698131.ta01002@e.koeki-u.ac.jp>
To: yuuji@e.koeki-u.ac.jp
Subject: Nice to meet you
From: HIROSE Yuuji <ta01002@e.koeki-u.ac.jp>
Mime-Version: 1.0
Content-Type: Text/Plain; charset=iso-2022-jp
Content-Transfer-Encoding: 7bit
Status:   

はじめまして、こんにちは、
では、さようなら。

おしまい。

-- 
公益太郎

差出人はメイルヘッダの Return-path か,From の値から取得する。電子メイルのヘッダは,1行目から始まり,空行で終わる。 行頭(先頭カラム)から始まり,コロンまでのものがフィールド名, その後ろがフィールド値である。

メイルのヘッダ部分のみを繰り返す処理は,

while line=gets
  break if /^$/ =~ line    # 行頭(^)の直後に行末($)で空行を表すパターン
  〜処理〜
end

のように書ける。この間で,Return-pathまたは Fromを得る処理は以下のようにする。

while line=gets
  break if /^$/ =~ line
  if /^(return-path|from): *(.*)/i =~ then
    from = $1
  end
end

これを応用して,1通のメッセージを含むファイルから差出人などの 情報をサマリ表示するプログラムを作ると以下のようになる。

headervalue.rb

#!/usr/bin/env ruby
# coding: euc-jp
# Parse rfc5322 header and display summary

while line=gets
  break if /^$/ =~ line
  if /^(return-path|from): *(.*)/i =~ line then
    from = $2
  elsif /^subject: (.*)/i =~ line then
    subj = $1
  elsif /^date: (.*)/i =~ line then
    date = $1
  end
end

printf("%s: %sさんからの「%s」というメイルです。\n", date, from, subj)

この例では,Return-Path と From に加え,サブジェクトを保持する Subject ヘッダと,日付けを保持する Date ヘッダの値をそれぞれ取得している。

整形出力

プログラムで集計処理した結果を仮想端末画面に出すようなものの場合, 出力項目ごとに桁揃えした形式にすると読みやすくなる。たとえば CSV ファイルの出力例として,

./csv-split.rb
番号|氏名|ローマ字|所属
1|公益太郎|KOEKI, Taro|野球部
2|飯森花子|IIMORI, Hanako|茶道部

のようなものがあったが,以下のようにすると見やすくなる。

./csv-split.rb
番号|氏名 |ローマ字 |所属
1|公益太郎 |KOEKI, Taro |野球部
2|飯森花子|IIMORI, Hanako|茶道部
3|三川一二三|MIKAWA, Hifumi|SKIP

このような場合,printfのフォーマット指定で桁幅を指定すればよい。 各フィールドの桁幅を以下のように見積もる。

第1フィールド4桁右寄せ
第2フィールド16桁左寄せ
第3フィールド25桁左寄せ
第4フィールド指定なし(残り桁すべて)

これを表現するprintfフォーマット指定は以下のようになる。

printf("%4s|%-16s|%-25s|%s\n", *row)

csv-split.rb の出力部分を上のprintfに書き換える。

csv-printf.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'csv'			# csvライブラリの利用

CSV.foreach("quoted.csv") do |row|
  printf("%4s|%-16s|%-25s|%s\n", *row)
end

ところがこれを実行すると以下のような結果が得られる。

番号|氏名              |ローマ字                     |所属
   1|公益太郎            |KOEKI, Taro              |野球部
   2|飯森花子            |IIMORI, Hanako           |茶道部
   3|三川一二三           |MIKAWA, Hifumi           |SKIP

これは,%s では日本語1字が1とカウントされるのに対し, 実際に表示される桁幅が2であるため,表示幅計算がずれるためである。

Ruby1.8以前でeuc-jpやshift_jisコードを用いた場合は, 日本語1字の幅が2と計算されたためずれることなく表示された。 Ruby1.9以降やutf-8を利用する場合で,日本語でのきれいな桁揃えを 行ないたい場合は,文字列を一度euc-jpのような2バイトコードに変換し, なおかつバイト文字列とRubyに見なさせてフォーマットを行なわせ, 出力のときに元の文字コードに戻すようにするとよい。 具体的には以下のプログラムを利用する。

kprintf.rb

class String
  require 'kconv'
  if defined?("".force_encoding)
    def toeucbin()
      self.toeuc.force_encoding("binary")
    end
  else
    def toeucbin()
      self.toeuc
    end
  end
end

class IO
  def printf(*args)
    out = sprintf(*(args.collect{|x| x.is_a?(String) ? x.toeucbin : x }))
    print out.toutf8
  end
end

class Object
  def printf(*args)
    if args[0].is_a?(String)
      $stdout.printf(*args)
    else
      port = args.shift
      port.printf(*args)
    end
  end
end

日本語桁揃え処理をしたいプログラムの冒頭で, 上記 kprintf.rb を読み込むように変えると printf の桁揃えがうまくいく。

csv-kprintf.rb

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'csv'			# csvライブラリの利用
require './kprintf.rb'		# 日本語桁幅揃え(この行を追加)

CSV.foreach("quoted.csv") do |row|
  printf("%4s|%-16s|%-25s|%s\n", *row)
end

実行すると以下のように桁が揃う(等幅フォントの場合)。

./csv-kprintf.rb
番号|氏名 |ローマ字 |所属
1|公益太郎 |KOEKI, Taro |野球部
2|飯森花子|IIMORI, Hanako|茶道部
3|三川一二三|MIKAWA, Hifumi|SKIP

本講では,日本語を含む整形出力処理が必要なプログラムでは この kprintf.rb を読み込むようにする。


本日の目次

yuuji@e.koeki-u.ac.jp