読者です 読者をやめる 読者になる 読者になる

藻ログ

都会でOLをしています

OLの事務vim日記

vi Windows Python

4月から都会でOLとして働き始めたので, OL的windowsの事務処理環境を手探りで作ってみました.

OLとWindows

事務処理といえばOffice, 当然Windowsで行うことになります.
今時のOLは家ではLinuxを使っているはずなので, 自然とシェル環境で困ることになります.

  • Windowsが本当にわからない
  • linuxコマンド使いたい(DOS音痴)
  • Cygwinは嫌い
  • MinGW+MSYS にしてみたい(けど未だによくわかってない)
  • 事務PCなので, 大掛かりな環境は入れたくない(入れられない)

などのモチベーションから
色々見ていてcmderが良さそうだなと思ったのですが
cmder.net
所属機関でフィルタされて落とせなかった(つらい)ので, ConEmu + msys bash の組み合わせで端末環境を整えることにしました. *1
conemu.github.io

Git for Windows(msys-git)を推奨設定でインストールすると
https://msysgit.github.io/msysgit.github.ioGit - Downloads

git bash(msys-bash)や一部のlinuxコマンド群のportを含めた最低限の環境が手に入るので,
当面はこれで凌ぐことにしました(gitもどうせ使うのでお手軽かと...). git bashだけでも一応使えますが, OLはConEmuからgitbashを起動しています.
sshも使えるので, linux鯖に入るなど最低限の仕事ができる環境が整いました.

OLとVim on Windows

さっき環境ができたといいましたが嘘です.
msys-gitをインストールする時にmsys-vimもついてくるのですが, このvimが微妙*2なのでkaoriyaさんで配布されているvimを展開し, そっちと入れ替えて使います*3
http://www.kaoriya.net/

alias vim="path/to/kaoriya-vim.exe"

OLとExcel

事務処理といえばExcelです. やはり大量のExcelファイルに対して何かする大量の作業に立ち向かうことになります.
今時のOLはVBAとか使えないはずなので適当なLLで*.xlsxをパースすることを考えます.
LLは何でも良いのですが, 諸事情でPythonを使うことにしました. *4

ファイルパス列挙

ディレクトリA以下のxlsxファイル(又は*.(txt|csv|tsv|dat))を列挙します. 後で読み込む時に捗るので絶対パスを格納するリストを返します. os.walkを使うと入れ子ディレクトリ以下も再帰的に列挙できて便利です.

xlsxパーサ

さて次は列挙した大量のxlsxファイルに対して処理を施すことになります.
xlsxを一旦csvに変換してから処理するという手もありますが,

  • 中間ファイルが嫌い
  • 処理対象のxlsxが大量にあって自動化しても処理待ち時間が長い

という点からxlsxを直接パースすることにします.

xlwings | Python For Excel. Free & Open Source.
Python Excel

事務PCには何かと制約*5もあるので, 今回はpython-excelから依存ライブラリの無いxlrdを試してみることにしました.
github.com
xlrdは*.xls, *.xlsxファイルを扱う読み出し専用ライブラリです.
xlsxへの書き込みも行うのであれば読み書きができるopenpyxl(依存でjd-calが必要)が使い勝手良さそうでした. *6

xlrdは,

# ワークブックを開く
wb = xlrd.open_workbook(fpath) 
# シートをインデックスで開く
sheet = wb.sheet_by_idx(idx)
# 行, 列取り出し
row = sheet.row(row_idx) #=> list: [ xlrd.Sheet.Cell, ... ]
col = sheet.col(col_idx)
# セルの取り出し
cell = sheet.cell(row_idx,col_idx)
# セルから値の取り出し
value = cell.value #=> float, unicode

というような形でシンプルにエクセルシートを直接パースすることが出来ます.
cell.value で文字列を取り出すときに少し気をつけたいのが文字列はunicodetypeで格納されていることです.これを踏まえてsample.xlsxから1行ずつ読み出してcsvのフォーマットでprintするスクリプトをこんな風に書いてみました.

後は各セルに対して処理を施し, 別ファイル(csvなど)に出力すればOKですね.
また, cell2strでencodingをcp932(Shift-JIS)としていますが, これはWindowsExcelファイルのデフォルトのエンコーディングがShift-JISだからです. 例えば, utf-8エンコーディングcsvに出力すると, Excelでそのcsvを開いた時に文字化けで禿げ上がることになります.

OLとencoding

※ 以下はPython2.x系列の話です

UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-12: ordinal not in range(128)

で、出〜〜wwww エンコードわかってない奴〜〜〜〜wwwwww

OLは文字コード音痴なのでエンコーディングでよく混乱しますが,(詳しい人教えてください)
基本的には入出力ではバイト列であるstr型を使うことになります.

type detail value(utf-8) len
str バイト列 "あああ" = \xe3\x81\x82\xe3\x81\x82\xe3\x81\x82 len("あああ")=9
unicode 内部文字列 u"あああ" = u'\u3042\u3042\u3042' len(u"あああ")=3

文字列のエンコード/デコードは昔よく逆では....という感覚に悩まされていましたが,

我々は機械様のために文字列をデコードして差し上げている

という友人の名言のおかげであまり混乱しなくなりました.

# 入出力(バイト列str型)→(デコード )→ 内部文字列(unicode型)
"あ".decode("utf-8")       #=> u'\u3042'
# 入出力(バイト列str型)←(エンコード)← 内部文字列(unicode型)
u"あ".encode("utf-8")     #=> \xe3\x81\x82

cp932でエンコーディングされた文字列をUTF-8でエンコされた文字列にしたいときは

def to_utf8(encoded_str, encoding='cp932'):
    return encoded_str.decode(encoding).encode('utf-8')

のようにcp932としてデコードしてunicode文字列にしてからutf-8ととして再エンコーディングしてます.

Python3.x系ではまた全然違って文字列は全てUnicodeであり, 一貫してstr型として扱うようにされています(ややこしい). 2=>3で何故このような思い切った変更が可能なんですかね... 怖い....

VBA学べや

だってWindowsExcelも家にないし....

OLとvimdiff

f:id:nisimur:20150621035043p:plain:w700
データシートの処理をしているとよく差分を見たくなるのですが, vimdiffが便利です*7.

$ vimdiff file1 file2 (file3 file4)

で差分が見れます(4つまで比較可能). ↑のように1行の中で変更があった部分だけハイライトされるので見やすいです.
余談ですがGitを使う場合, fugitive-vimプラグインが:Gdiffでvimdiffを使うので便利です
Fugitive.vim - resolving merge conflicts with vimdiff

OLとログファイル

スクリプトで1万〜数十万行のデータを処理していると, stdoutに雑に出力したログが大量になって辿れないといったことがよくあります. まともなプログラムならログファイルに出力しますが, ワンタイムスクリプトだとログファイルに吐くのも面倒だったりします(ものぐさ). そこで

$ python jimu.py | less

のようにlessにパイプしてスクロールできるようにすることもありますが, OLはvimに流すのがお気に入りです.

$ python jimu.py | vim -R -

こうやって標準出力(-)をreadonlyでvimに流すと, vimでログを閲覧した後, 必要な行だけ残して好きなファイル名で保存するというようなことができます. また, ログファイルに

[ERROR] unsupported format at 100 in /path/to/file.ext

などとabspathを吐いておくとファイルパスにカーソルを合わせたとき, ノーマルモードで

gf

を押して問題のファイルにジャンプに出来るので楽で良いです.

また, ログファイルに吐きたい時は普通に標準出力と標準エラー出力

$ {unix command} 1>stdout.log 2>stderr.log
$ python jimu.py 1>stdout.log 2>stderr.log

してます*8.
Python標準エラー出力に吐くにはsys.stderrを使います.

import sys
sys.stderr.write("[ERROR] ....")

OLとデータソート

Excelでやれ
簡単なソートはvimでもできます.

"昇順ソート
:sort   
"降順ソート
:sort! 
" 重複行を削除してソート
:sort u

sortをかけた後, 2ファイルの差分をvimdiffで見るというような作業をよくしています.
また,

" regexpが含まれる行以外を削除
:v/regexp/d
" regexpが含まれる行を削除
:g/regexp/d

という処理もできます.

OLとVimRegexp

事務作業は何かと正規表現を使うことが多いです. *9

/[a-z0-9]\{40\}

f:id:nisimur:20150621023732p:plain:w300

/.*N\/A.*

f:id:nisimur:20150621023747p:plain:w300
どんな時にも便利な正規表現ですが, vimでは+, ?, |, &, {, ( を\エスケープしないと正規表現のメタキャラクタとして使えないのでエスケープ不要と必要なキャラクタで混乱しがちです.

regexp detail escape
. 任意の1文字にマッチ -
^ 行先頭にマッチ -
$ 行末にマッチ -
* 直前パターンの0回以上の繰り返し -
+ 直前パターンの1回以上の繰り返し \+
? 直前パターンの0,1回の繰り返し \?
{m} 任意文字のm回繰り返し \{m\}
[a-zA-Z] アルファベット -
(abc) パターンabcにマッチ \(abc\)
(abc|def) パターンabcまたはdefにマッチ \(abc\|def\)

などが主にややこしいのですが,

" 田中 or 田中さんにマッチ
田中\(さん\)\?
" c:\\Users\\foo\\bar => c:/Users/foo/bar に置換
:%s/\\\\/\//gc
" m11 => m_{11} のようにtex記法へ変換(後方置換, かなり使う)
:%s/\(\a\)(\d\{2\}\)/\1_\{\2\}/gc

などを書いていると段々
  ノ⌒)(⌒ヽ
 (´  _,人_  `)  わけがわからない
(  )´・ω・`(  )
 (  )ー (  ) ☆
  ヽ _)(_ノ ヽノ
     UU~UU
気持ちになってきます.
そこで\v(very magic)を使うとこのエスケープをしなくて良くなるのですが,
それについては優れたエントリを発見したのでこちらを参考にしてください.
deris.hatenablog.jp

OLと事務vim

  • Excelの使い方が一向に覚えられない
  • 殆どMSYSのシェルで作業してるせいでWindowsも一向に慣れない
  • 端から見ると仕事してないように見える

という様々な問題点が明らかになってきましたが, この調子でがんばってオフィスマスターを目指したいと思います.

*1:cmderも結局端末エミュレータはConEmuを使っていて, full installでmsysのコマンド群が付いてくるので大体同じような環境になるのかと勝手に思っている

*2::syntax on しようがシンタックスハイライトも効かず, 他の挙動もおかしかったが自力で解決できなかった(情弱)

*3:/bin/vi へシンボリックリンクを貼っても良い

*4:OL的には慣れてるRubyが良かった

*5:外のネットに繋げなかったり...

*6:書き込みライブラリであるxlwtは, 既存workbookへの書き込みをサポートしておらず, xlutilsのcopyを使ってworkbookをまるまるコピー → 書き込みの手順を踏まなければならないようだった. また, 基本的にサポートは*.xlsのみで*.xlsx形式での保存には対応していない.

*7:vimdiffない場合は, vim -d file1 file2 かvimを立ち上げてから:diffsplit file1 などで出来ます. 詳しくは:help vimdiff. 今までvimdiffはvim -dのエイリアスだと思ってたんだけど違うんだろうか.

*8:というか>は標準出力のみで, 標準エラー出力出せない

*9:サンプルはhashlibとrstrで適当に生成