Edited at

ワンライナーにかける情熱

More than 3 years have passed since last update.

こんにちは、toyohamaです。

今回はおそらく誰ともかぶらないであろう「ワンライナー」についてお話したいと思います。

各スクリプトの細かい解説については長くなりすぎるんで後日、別記事にてアップします。


使用環境

ひらたくいうと普通のMacです。


  • OS : Mac OS X 10.10.5

  • シェル : GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin14)

  • Perl : perl 5, version 18, subversion 2 (v5.18.2) built for darwin-thread-multi-2level


ある日の午後、依頼がきた(※フィクションです)

上「toyohamaくん、sample.netのページがちゃんと表示されているか確認してほしいんだよね」

私「もちろんです、どのページですか?」

上「このエクセルに記載されてるURL、全てだ」

sample.xlsx

私「承知しました、いつまでですか?」

上「3時間後までだ」

私「(URL拾うだけで3時間はかかるぞこれ)」

上「とりあえずサーバエラーとかタイムアウトせずにページさえちゃんと返ってくるのがわかればそれでいいから。表示崩れとかは気にしなくていいので。」

私「は、はぁ」

上「あ、できればそのページのHTML、どこかに一覧にしておける?あとで構文チェックかけたいんで」

こんなとき、手作業の効率を100倍にしてくれる。

そう、ワンライナーならね。


ルール

とまあ、こんなシチュエーションはまずありえませんが(笑)、全てワンライナーでやってみたいと思います。


  • エンターキーを押下するまでがワンライナー

  • 入出力するファイルは対象外とする

  • 多少汚くても(速さではなく)早さ優先

  • 1回きりの作業なので、環境依存は目を伏せる

(記事中のコードは見づらいので改行入れています)

なんか言い訳がましく聞こえますが、さいごにまとめも書いてあるので後で読んでみてください。


まずはエクセルからURLを拾ってみる

Office 2007からのエクセルファイルは「Office オープン XML」形式と呼ばれる、xmlファイルをzipで固めた仕様になっています。つまり、unzipしてxmlを探せばワークシートの内容にアクセスできます。便利な世の中になったものです。

(数値ではない)文字列のデータはシートに依らずxl/sharedStrings.xmlに一元格納されているので、

 bash $ unzip test.xlsx && for i in `cat xl/sharedStrings.xml` ; do echo $i | 

perl -0ne 'while ( m|(http://www\.sample[^/]+/[^<]+)|g ){ print $1."\n";}' ; done

こんな感じでとってこれます。&&は前方のコマンドが成功したときに次のコマンドを実行するものですね。逆は||です。


出力結果

http://www.sample.net/a/doc/rc1/order.html

http://www.sample.net/sample/html.html
http://www.sample.net/toyohama/
http://www.sample.net/msoffice/
http://www.sample.net/aarrg/
http://www.sample.net/uxnsw/
http://www.sample.net/abcde/
:


ページを取得してみる

上の出力結果をurllist.txtに保存してある前提で、HTMLファイルを取ってきましょう。さくっとcurlで……と思ったのですがindex.htmlがたくさんあり同じ階層には保存できません。今回は一括して渡すときのことを考慮し、パスをファイル名に変換して同一ディレクトリに保存します。

 bash $ for i in `cat urllist.txt` ; 

do OF=`echo $i | sed "s|^http://www\.sample\.net/||" | sed "s|[/]|_|g" | sed "s|_$|_index.html|"` ;
curl -o $OF $i ; done

うまく取得できたでしょうか。


結果

 bash $ ls -1

a_doc_rc1_order.html
aarrg_index.html
abcde_index.html
msoffice_index.html
sample_html.html
toyohama_index.html
urllist.txt
uxnsw_index.html
:

大丈夫そうですね。


取得に失敗したurlを保存する

おっと、本来の目的を忘れるところでした。

失敗したものをロギングするために、curlのfオプションを使い、2>&1で標準エラー出力を標準出力に流し、ファイルに落としましょう。

 bash $ for i in `cat urllist.txt | head -5` ; 

do OF=`echo $i | sed "s|^http://www\.sample\.net/||" | sed "s|[/]|_|g" | sed "s|_$|_index.html|"` ;
echo "$i > $OF" ; curl -f -o $OF $i ; done > log.txt 2>&1


log.txt

http://www.sample.net/xxxxxxx > xxxxxxx

% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
^M 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (22) The requested URL returned error: 404 Not Found
http://www.sample.net/a/doc/rc1/order.html > a_doc_rc1_order.html
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
^M 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0^M100 19978 100 19978 0 0 345k 0 --:--:-- --:--:-- --:--:-- 348k
:

テストのためわざと存在しないURLを仕込んでいますが、うまく404が取れました。


目次を作る

目次も用意しておきましょう。

 bash $ (TOC="toc.html"; echo "<html><body>" > $TOC; 

for i in `ls *.html | grep -v "$TOC"` ; do echo "<a href=$i>$i</a><p>" >> $TOC ;
done ; echo "</body></html>" >> $TOC)

カッコをつけなくても問題なく動作しますが、その場合シェルの変数として$TOCがセットされてしまいます。たまたま元からセットしてあった変数を上書きすることのないように、つけておいたほうが無難です。


ついでにwebサーバもつくっちゃいましょう

さて、ここまでですでに求められている仕事は終了した気がするんですが、これで終わると記事にならないノってきました。webサーバもワンライナーで作ってみましょう。

 bash $ sudo perl -MIO::Socket -e 

"\$s=new IO::Socket::INET
(LocalAddr=>'localhost:80',Listen=>1);
while(\$sock=\$s->accept()){
while(<\$sock>=~/^GET\s+(\S+)\s+.*\$/){
\$p=\$1;\$path=(\$p=~/^(.*\/)\$/?\$1.'index.html':\$1);}
\$cn='';if(open(FI,'.'.\$path)){while(<FI>){\$cn.=\$_;}
}else{\$cn='NotFound';}
print \$sock \"HTTP/1.0 200 OK\nContent-Type: text/html\nContent-Length: \".length(\$cn).\"\n\n\$cn\";\$sock->close;}
\$s->close;"

これでブラウザからhttp://localhost/(ファイル名)でアクセスが可能になります。

さっきの目次を表示してみましょう。

http://localhost/toc.html

perlIO::Socket::INETは非常に取り回しのいいモジュールで、ホストやポートを設定してaccept()するだけであっという間に通信ができちゃいます。


次はクライアントが必要ですよね?

さっきブラウザから見てたじゃん……ってツッコミはなしで。

サーバと同じくperlIO::Socketを使ってもいいんですが、せっかくなんで別の方法を使ってみましょう。expectを利用して対話型コマンドを自動化してみます。

 bash $ while read A; 

do ARR=(`echo $A | sed -e "s|^\(http://\)\([^/]*\)\(/.*\)\$|\2 \3|g"`);
echo "spawn telnet ${ARR[0]} 80; expect \"Escape character is '^]'.\r\" ;
send \"GET ${ARR[1]} HTTP/1.0\rHost: ${ARR[0]}\r\r\"; expect eof;" |
expect | less ; done

readコマンドによってシェルが入力待ちになるので、http://localhost/toc.htmlと入力し、エンターを押下します。返ってきた結果がlessに渡されます。lessを抜けると再度シェルの入力待ちに戻ります。

spawn telnet localhost 80

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /toc.html HTTP/1.0
Host: localhost

HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 3910

<html><body>
<a href=a_doc_rc1_order.html>a_doc_rc1_order.html</a><p>
<a href=aarrg_index.html>aarrg_index.html</a><p>
<a href=abcde_index.html>abcde_index.html</a><p>
<a href=msoffice_index.html>msoffice_index.html</a><p>
:


いま「curlでやったほうが早くね?」って言った?

それはベストな選択肢ですが、面白くなかったんで……。書くまでもないと思いますが書いておきます。

 bash $ while read A; do curl -o - $A | less ; done

断然楽ですねw


まとめ

と、最後の方は「それ一行でやる意味あるの?」というものまでご紹介しましたが、最後に少しお話をしておきたいと思います。


物事をシンプルにする

「ワンライナーで解決する」ということは無駄なこと・余計なものを排除するという思考に繋がると自分は考えています。いかに楽に、かつ速く解決できるか、を養えると思ってます。


1回きりの作業に多大な労力を費やさない

フィクションで挙げたような状況とは逆に、とりあえず昨日のリファラログを集計してほしい、というためだけにかっちりとしたコードを綺麗に書く、という意味でも労力を費やさないほうがいいはずです(もちろんそういうコーディングの練習なら話は別ですが)。

きちんとロジック組んで綺麗に書かなきゃいけないコードは他にあるはずです。そちらに時間をかけるべきだと思います。


手作業のほうが速ければそうすべき

もし、今回のシチュエーションが3個程度のURLだったらどうでしょうか。おそらくワンライナーでコーディングするよりもエクセルでテキスト検索してブラウザで3回みて手でログ書いたほうが速いはずです。そういった場合はこだわりを捨て、迷わず手作業で実行すべきだと考えます。

というわけで、情熱を傾けすぎてめっさ長くなってしまいましたが……最後までお付き合いいただきありがとうございました!


おまけ

一番最初に出てきたエクセル(sample.xlsx)も、元データになるtsvをワンライナーで作っています。参考まで。

 bash $ for i in `curl -s -o - http://www.sample.net/ | grep 'href="/' | 

egrep '(.html|/)"' | perl -pe 's!^.*href="([^"]+)".*$!http://www.sample.net$1!g;'` ;
do R=`expr $RANDOM % 30`; for j in `seq 1 30`; do if [ $j -eq $R ];
then echo -n $i ; else echo -n `expr $RANDOM % 30000 | md5` ; fi ;
echo -ne "\t"; done ; echo ; done > test.tsv