Scheme:テキスト処理

Scheme:テキスト処理

テキスト処理に出て来るちょっとしたパターンを集めてってみる。 処理系はGauche


行末のコメントを除く

ここで見かけた話。 セミコロンから行末までがコメントであるような構文を持つファイルの、 コメント(と余分な行末の空白)を除いたものを出力する。

(use srfi-13)
(use util.match)

(define main
  (match-lambda
   ((program filename)
    (with-input-from-file filename
      (cut port-for-each
           (lambda (line)
             (print (string-trim-right
                     (or (string-scan line #\; 'before) line))))
           read-line))
    0)
   ((program . _) (print "Usage: " program " <filename>") 1)))

文字列リテラルや文字リテラルとして出現する';' は考慮してない。 リテラル構文によるな。

base64エンコーディング

Shiro (2002/12/13 15:01:23 PST) : sakaeさんのページより。メールの添付ファイルをBase64エンコーディングする効率の良い方法は?

バージョン1.

(define (file->base64-encode path)
  (call-with-input-file path
    (lambda (inp)
      (string-join 
          (port-map base64-encode-string 
             (lambda ()(read-block 45 inp)))
          "\r\n" 'suffix))))

gosh> (time (file->base64-encode "Gauche-0.6.5.tgz") (values))
;(time (file->base64-encode "Gauche-0.6.5.tgz") (values))
; real   7.505
; user   7.460
; sys    0.040

string-joinは最初に全文字列長を計算するので普通は効率が良いが、 与えられたリストが長くなると2回リストをスキャンするのが負担になって くるかも。また、Gaucheはストリーム指向の部分が多く、 base64-encode-stringもベースにストリーム指向のbase64-encodeがあって、 with-output-to-stringを使って文字列に直している。それをまたjoin するよりは、全体でひとつoutput string portを用意して全部そっちに 吐き出したほうが良いかも。

バージョン2.

(define (file->base64-encode path)
  (call-with-input-file path
    (lambda (inp)
      (with-output-to-string
        (lambda ()
          (port-map (lambda (s) 
                       (with-input-from-string s base64-encode) 
                       (display "\r\n"))
             (lambda ()(read-block 45 inp))))))))

gosh> (time (file->base64-encode "Gauche-0.6.5.tgz") (values))
;(time (file->base64-encode "Gauche-0.6.5.tgz") (values))
; real   7.195
; user   7.140
; sys    0.050

あんまり変わらんね。エンコード時の一行の長さを切るのにいちいち inpから文字列に切り出しているのがいやーんな感じで、base64-encode 等をフィルターっぽく使ってlazy streamをつなげてくような書き方が 出来ると綺麗なんだが、Gaucheではちょいとそれが出来ない。

まあ、大きなファイルを扱う時にはfile->base64-encodeを直接portに 出力するように作っておいて、ファイルを送りたい時にsendmailに つながったsocketに流し込んでやるとか、それが出来なくとも全部を stringにつなげないで最後の最後までlistで持っといて最後にwrite-tree するとか、そのへんが常套手段ではある。

実は、base64-encodeには既に出力行を72文字単位で区切る機能が入っている (RFC2045, section 6.8)。 だから呼び出し側で区切ってやる必要は無い:

バージョン3.

(define (file->base64-encode path)
  (with-input-from-file path
    (lambda ()
      (with-output-to-string base64-encode))))

gosh> (time (file->base64-encode "Gauche-0.6.5.tgz") (values))
;(time (file->base64-encode "Gauche-0.6.5.tgz") (values))
; real   6.588
; user   6.580
; sys    0.010

ただ、今見直してみたら単にnewlineで区切っているのでちとまずい。 CRLFで区切るように直さなきゃ。

さて、そもそもbase64-encodeが律速なのでこのへんが限界かと思われるのだが、 base64-encodeそのものに実はひどく非効率な部分がある。 base64-encodeだけでなくstream系のライブラリ全般に言えることだが。 threadを導入した際、SRFI-18の要請に合わせるために、 ポートのアクセスにはデフォルトで排他制御がかかるようになった。 大きなファイルを1バイトづつ読むような処理ではそのオーバヘッドが 大きく効いてくるのだ。

この点に関しては、アプリ側でロックの粒度が指定できるようなクリーンな APIが出来てから直そうと思っている。今試しに低レベルAPIを使って、 base64-encode内全体でポートをロックするようにしてみると、 バージョン2と同じコードでこのくらいになった。

gosh> (time (file->base64-encode "Gauche-0.6.5.tgz") (values))
;(time (file->base64-encode "Gauche-0.6.5.tgz") (values))
; real   5.546
; user   5.440
; sys    0.100

バージョン3のコードだとこんなもん。

gosh> (time (file->base64-encode "Gauche-0.6.5.tgz") (values))
;(time (file->base64-encode "Gauche-0.6.5.tgz") (values))
; real   4.536
; user   4.540
; sys    0.000

grep

Shiro (2002/12/09 05:10:09 PST): Gaucheではregexpがapplicableになったので、 SRFI-1のfilterと組み合わせるとPerlのgrepと同じことが出来る、ことに気づいた。

gosh> (filter #/\.c$/ '("foo.h" "foo.c" "bar.c" "a.out"))
("foo.c" "bar.c")

条件を反転させたいときはremoveが使える。partitionを使えば一度で仕訳けが できる。

TABを空白に変換する

satoru こんな感じでよいでしょうか。

  (define (string-untabify str)
    (regexp-replace-all #/(\t+)/ str
                        (lambda (m)
                          (make-string (- (* (string-size
                                              (rxmatch-substring m 1))
                                             8)
                                          (modulo (string-size
                                                   (rxmatch-before m)) 
                                                  8))
                                       #\space))))
  
  (display (string-untabify "\taaaa")) (newline)
  (display (string-untabify "123\taaaa")) (newline)
  (display (string-untabify "12345678\t12345678")) (newline)
  (display (string-untabify "12345\ta\t12\tbbb")
  (display (string-untabify "あいう\ta\t12\tbbb")) (newline)) (newline)
  

string-length だと (string-length "あ") => 1 なので、 とりあえず string-size で逃げときました。UTF-8とかだとこれでも困りますけど。

実行結果:

          aaaa
  123     aaaa
  12345678        12345678
  12345   a       12      bbb
  あいう  a       12      bbb

ファイルfoo.txtを一行づつ読み込む

  (with-input-from-file "foo.txt"
    (lambda ()
      (port-for-each 処理 read-line)))

全部の行を読み込んでリストにして返したい場合は

  (call-with-input-file "foo.txt" port->string-list)

ファイル全部を一気に読み込んで文字列にしたい場合は

  (call-with-input-file "foo.txt" port->string)

では標準入力から一行ずつ読み込むには? ポートを省略したら標準入力を仮定するようになっている関数の 場合は何もしないでも良く、ポートが必要な関数の場合は(current-input-port) を渡してやればよい。上の例を書き直せば

  (port-for-each 処理 read-line)
  (port->string-list (current-input-port))
  (port->string (current-input-port))

コマンドの出力を取る

Gaucheでは上のwith-input-from-fileを with-input-from-processに変えれば コマンドを起動してその結果を一行づつ読み込んだりできる。

  (use gauche.process)
  (with-input-from-process "ls -l"
    (lambda ()
      (port-for-each 処理 read-line)))

コマンドの結果を直接取るにはこんなのもある。

  (use gauche.process)
  (process-output->string-list "ls")
  (process-output->string "uname -a")

文字列を(空白などで区切られた)単語に分割

SRFI-13のstring-tokenizeが便利。

  (string-tokenize "this is a pen.\n") => ("this" "is" "a" "pen.")

単語を構成すべき文字セットを指定することもできる。

  (string-tokenize "* this is a pen.\n" #[A-Za-z]) => ("this" "is" "a" "pen")

行末の改行文字を削除(perlのchopやchompみたいなの)

read-lineは改行文字を削除するので、perl程に必要なオペレーション ではないが…

  安全な方法:  (string-trim-right 行 #[\n])
  有無をいわさず一文字削除:  (string-drop-right 行 1)

文字列の最初のn文字だけを落す

substringは部分文字列の始点と終点を要求するが、しばしばn文字目から 文字列の最後までを取りたいことがある。SRFI-13のstring-dropが使える。

  (string-drop 文字列 n)

CSVファイルを読む

リクエストにお応えして。 単純なのでよくデータ交換に使われるCSV形式。単に値がコンマで区切られているだけ だからregexpの一行野郎でいけそうだけど、実は真面目にやろうとするとめんどくさい。 Gaucheではtext.csvモジュールとしてライブラリになっている。 今のところは間接的な手続きしかないが。

  (use text.csv)
  (define read-csv (make-csv-reader #\,))

これでread-csvはポートを引数に取り、CSVファイルの1レコードをパーズして フィールドのリストにして返す手続きとなる。次のように呼ぶとファイルの 全レコードをパーズしてリストのリストが返る。

  (call-with-input-file ファイル (cut port->list read-csv <>))

コンマ区切りだけでなく、タブ区切りなどのフォーマットに対応することもできる。

  (define read-tab-separated (make-csv-reader #\tab))

ホントはこのAPIの上に、抽象的なrecordを取り扱うAPIを載せる予定なのだが まだ出来ていない。

Scheme:イテレータスタイル

算譜の記 にあったちょっとしたテキスト処理。引用すると:

    <ID> <keyword1> <keyword2> ... <keywordi>

    のようなフォーマットのレコード行が複数あるテキストデータ(各行のkeyword の数は可変)を 

    <ID> <keyword1>
    <ID> <keyword2>
    ...
    <ID> <keywordi>

    のように ID と <keyword> との対を1行とするテキストデータに変換したい
More ...