Python」タグアーカイブ

macOS tree command

前回 CommandLineTools 以下に「SDK 丸ごと落とされる」と書いた。
情報として何という名のファイルが「ドコにドンダケ」ってほど入ってしまうのかを公開したい。
tree というまさしくソレ用みたいなコマンドがある。

あぁ、このコマンドって使い道があったんだなぁって思った筆者であった。
けれど macOS には tree コマンドが入っていなかった。

Fedora なら使えるのでリモート接続してリダイレクトしようかなと。
てなわけで、sftp でリモート接続した Fedora から tree コマンドを打ってみた。
けど、何時間たっても処理が戻ってこなくて諦めた。
こんなに遅いはずがない、サンドボックスかな?

普通に「macos tree コマンド」と検索するとションボリ。
何故みんなコンパイラを手に入れたのにインストールしかしないの?
brew 使う人って(以下略
てか Catarina 以降だと Gatekeeper にブロックされると思うんだけーが。

Catalina時代の「GateKeeper」と付き合う方法 – 新・OS X ハッキング!(257) | マイナビニュース

spctl は Windows でいうウイルス対策アプリを無効にする手段。
慎重に活用とかアホかと、その必要があるアプリは淘汰されるべき。

で。

実はそんなことをしなくても検疫機能を回避する方法がある。
自分でビルドすればいい、もしくは自分でスクリプトを書く。

tree コマンドが無い環境で tree コマンドを実現 – Qiita

シェルスクリプトだけでがんばった人がいた。
これでもいいけど、よし筆者は Python でがんばってみよう。
macOS で動かせるように標準モジュールだけを使う。

#!/usr/bin/env python3

'''
    tree command in Python 3.7
'''

import os, sys

all_dir_num = 0
all_file_num = 0

def flist(path):
    '''
        Exclude UNIX hidden files. and sorted.
    '''
    l = os.listdir(path)
    res = []
    for f in l:
        if f.startswith('.'): continue
        if f.endswith('~'): continue
        res.append(f)
    return sorted(res)

def tree(path, tab):
    files = flist(path)
    length = len(files)
    head = '├── '
    num = 1
    for f in files:
        if num == length:
            head = '└── '
        subdir = os.path.join(path, f)
        if os.path.isdir(subdir):
            if sys.stdout.isatty():
                # stdout
                print(f'{tab}{head}\033[34m{f}\033[0m')
            else:
                # Redirect
                print(f'{tab}{head}{f}')
            global all_dir_num
            all_dir_num += 1
            # Recursion
            tree(subdir, f'{tab}│   ')
        else:
            print(f'{tab}{head}{f}')
            global all_file_num
            all_file_num += 1
        num += 1

if __name__=='__main__':
    d = '.'
    if len(sys.argv) > 1:
        d = sys.argv[1]
        if d.startswith('~'):
            d = os.path.expanduser(d)
    print(d)
    tree(d, '')
    # footer
    print(f'\n{all_dir_num} directories, {all_file_num} file')

tree

stdout で色を付けるとリダイレクトでアララとなるから振り分けしてね。
os.listdir はカレントディレクトリならドットでいいのか、へー。

中身が一つだけの時に前の縦線を消すナイスな方法は思いつかなかった。
その中身がディレクトリで更に中身が一つだった場合等でチグハグになる。
それとディレクトリを青色にしたけど tree コマンドとなんか色が違う。
とはいえ完全に同じにする必要は無いんだしコレでいいかなと。

コレを mac で自分がパスを通した場所にコピーして。
tree という拡張子の無い名前を付け +x パーミッションを付けて。

tree /Library/Developer/CommandLineTools > cltool.txt

したものを置いておきます。
十七万五千行、17MB になってしまったので zip 圧縮した。
cltool.zip

python3 は先に自分で入れたものなのかコレに含まれているのかは解らない。
まあ微々たる差だ。
それより macOS だと場所によってはサンドボックスに引っ掛るのが困る。

mp4 mov rotate

スマホ動画の回転情報を得て我が Y901x にて正しい回転表示をさせる。
と随分前に書いてずっとサボッていたけどいいかげんに本気出そうと思う。
検索ワードを mov バイナリ 回転 とか mov に決め打ちして探したら。

バイナリエディタで .mov を回転 ? マキシマ文庫

おぉ多分コレだ!
回転情報書き換えなら Mac の QuickTime だけで簡単にできるのは置いておいて。
そこのバイナリを取得すれば回転情報は得られるってことね。

バイナリ取得や書き換えなら C や Python 等で簡単にできるのに。
てか既に沢山ありそう、参考用に探してみよう。

GitHub – danielgtaylor/qtrotate: Tools for handling rotated Quicktime/MP4 files

ズバリをあっさり見つけた。
mdat や trac アトムも取得しているし他にもやることあるみたい。
mov の構造は下記に、ソースコメントのリンク先はもう古いので。

Movie Atoms

試すならサポート終了の Python2 コードなので Python3 に書き換えてね。
print に括弧を付加と割り算を二重スラッシュにするのは毎度のこととして。
atom 比較の所の文字列には全部 b を付けてバイナリ比較に変更するのを忘れずに。
よく解らない人用に筆者が書き換えしたものを一応置いておきます。

qtrotate_py3.zip

どうやら mov と mp4 は同じ回転情報を使うようで mp4 にも適用できる。
とりあえず筆者が iPhone で撮影したり編集した動画は回転情報が得られた。

qtrotate

書き換えも問題無く可能だった。
Nautilus のサムネイルには即時繁栄されないので一旦コピペ。
ただ手持ち mp4 の中には正しくない情報が出るものもあった。
ここらをもう少し調べて Y901x に適用し。。。。。

しまった、Y901x は Gjs で作っていたんだった!
JavaScript はバイナリ編集なんてできないぞ。
GLib を使っても多分 Uint8Array に変換されて劇遅になるだけだ。

ClutterImage PyGObject/Gjs | Paepoi Blog

本気で困った。
Comipoli 同様コイツも PyGObject に書き換えることになりそう。

Python3 socket

Python からの socket アクセス。
http だけなら urllib.request で充分なんですけど。
ssh 等の通信にも使えるようなので使い方は勉強しておこう。

#!/usr/bin/env python3

'''
    http:// version
'''

import socket

HOST = 'palepoli.skr.jp'
FILE = '/suzuki/katana.html'
PORT = 80 # http

output = []

#with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock:
with socket.socket() as sock:
    sock.connect((HOST, PORT))
    msg = f'GET {FILE} HTTP/1.1\r\nHost: {HOST}\r\nConnection: close\r\n\r\n'
    s.sendall(msg.encode('utf-8'))
    while True:
        res = ssock.recv(4096)
        if not res: break
        output.append(res.decode())

print(''.join(output))

よく見かけるコードを fstring で今っぽく。
AF_INET 等は python3.7 ではデフォルト引数で指定されているので不要。
コピペだけで動くようにファイルは筆者のこのサーバーに置いています。

でもコレでは https でアクセスできないことに気が付いた。
https のポートを調べると 443 番だ、そりゃ 80 番では弾かれる。
でもポートを変更しただけでは駄目、ssl モジュールを使う必要あり。

#!/usr/bin/env python3

'''
    https:// version
'''

import socket, ssl

HOST = 'palepoli.skr.jp'
FILE = '/suzuki/v-strom.html'
PORT = 443 # https

output = []

with socket.socket() as sock:
    context = ssl.SSLContext(ssl.PROTOCOL_TLS)
    with context.wrap_socket(sock, server_hostname=HOST) as ssock:
        ssock.connect((HOST, PORT))
        msg = f'GET {FILE} HTTP/1.1\r\nHost: {HOST}\r\nConnection: close\r\n\r\n'
        ssock.send(msg.encode('utf-8'))
        while True:
            res = ssock.recv(4096)
            if not res: break
            output.append(res.decode())

print(''.join(output))

できた。

ところで実は GNOME ならこんなことができる。

#!/usr/bin/env python3

'''
    GNOME only
'''
 
from gi.repository import Gio
 
f = Gio.file_new_for_uri("https://palepoli.skr.jp/suzuki/burgman.html")
ok, contents, etag_out = f.load_contents()
if ok:
    print(contents.decode());
else:
    print('File not Found')

Gio は Gvfs という仮想ファイルでアクセス可能な URI なら全部扱える。
GNOME 標準アプリは全部 Gio アクセスなので以下みたいなことも。

sasikae

sftp でアクセスしてリモート編集が可能だったりする。
解りやすいようにコマンドで書いたけど Nautilus から sftp でアクセスして W クリックすればコレと同じことになる。
自分でアプリを作る時も Gio を使うだけ、これが GNOME の魅力。
SNS くらいしか使っていないショボイ人だとこの魅力は解らないだろうな。

追記
2020.04.22 添付画像の差し替え。

GExiv2 and subprocess

前回の GExiv2 の件。
comipoli に実装しようとしてハマったので覚書。

GSubprocess から GUnixInputStream を得て GExiv2 に読ませる。
その GUnixInputStream からは GdkPixbuf を作ることができない。
順番を逆にしても駄目、Stream の再利用はできないみたい。

他の手段で GExiv2 を使おうと思ったけど上手くいかない。
他の手段で GdkPixbuf を得ることは無理。
exif 取得と画像取得で別にアクセスするしかないようだ。
遅くなるだろうけど気にならないレベルならいいかなって。

しかし GSubprocess をもう一つ使をうとするもエラー。
原因は解らない、GSubprocess の情報なんて皆無だ。

失敗をズラズラ書いても無意味なので結論。
GSubprocess と subprocess を両方使う強行手段でなんとかなった。

class ComipoliArchive:
    def __init__(self):
    	# etc...

    def _zip_escape(self, filename):
        ESCAPE = '[]*?!^-\\'
        res = ''
        for s in filename:
            if s in ESCAPE:
                res += '\\'
            res += s
        return res

    def _on_wait(self, proc, res):
        proc.wait_check_finish(res)

    def __getitem__(self, num):
        if num == self.max:
            raise IndexError
        try:
            args = ['unzip', '-pj', self.path, self._zip_escape(self.namelist[num])]
            # tag
            ori = 0
            pr = subprocess.Popen(args, encoding='UTF-8', stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            fd = pr.stdout.fileno()
            with open(fd, 'rb') as f:
                metadata = GExiv2.Metadata()
                metadata.open_buf(f.read())
                ori = metadata.get_orientation()
            pr.wait()
            # pixbuf
            sp = Gio.Subprocess.new(args, Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_MERGE)
            sp.wait_check_async(None, self._on_wait)
            stream = sp.get_stdout_pipe()
            p = GdkPixbuf.Pixbuf.new_from_stream(stream)
            if ori == GExiv2.Orientation.HFLIP:
                p = p.flip(True)
            elif ori == GExiv2.Orientation.ROT_180:
                p = p.rotate_simple(GdkPixbuf.PixbufRotation.UPSIDEDOWN)
            elif ori == GExiv2.Orientation.VFLIP:
                p = p.flip(False)
            elif ori == GExiv2.Orientation.ROT_90_HFLIP:
                p = p.rotate_simple(GdkPixbuf.PixbufRotation.CLOCKWISE).flip(True)
            elif ori == GExiv2.Orientation.ROT_90:
                p = p.rotate_simple(GdkPixbuf.PixbufRotation.CLOCKWISE)
            elif ori == GExiv2.Orientation.ROT_90_VFLIP:
                p = p.rotate_simple(GdkPixbuf.PixbufRotation.CLOCKWISE).flip(False)
            elif ori == GExiv2.Orientation.ROT_270:
                p = p.rotate_simple(GdkPixbuf.PixbufRotation.COUNTERCLOCKWISE)
            stream.close()
            sp.force_exit()
            return p
        except Exception as e:
            raise e

前回の cbz を開いてみる。

orientation

よし回転されているな。
注意点は ROT_90 は 90 度傾いているから 270 度回転させるということ。
戻す度数という意味ではない、flip は手持ちサンプルが無いのでこれで正しいか未確認。
後 Popen は wait を忘れずに。

Scaling: GDK-PixBuf Reference Manual

展開速度も別に気にならない、てか何か変わったか?のレベル。
多分 GExiv2 が凄いだけなんだろうけど。

1200px

PC での twitter の画像観覧が少し前から原寸になった。
twitter の 4 コマ漫画が大好きな筆者は少し困ったことに。

少し前まで普通にコンテキストメニューから落とすと縦 1200px 固定だった。
原寸を落とす方法も知っていたけど cbz にする場合このサイズが都合よかった。

つまりその時作った cbz が沢山ある。
それも完結していない続き物が多い。

続き物のページを cbz に追加する場合は縦 1200px に縮小しないと整合性が。
原寸は巨大なものが多いので表示に時間が掛かるというのもある。
途中のページから急に重く、なんて嫌だよ。

そういえば追加する時に以前追加した最後の名前は何だったか調べるのも面倒。
020.jpg だったら 021.jpg にして追加したい、ずっとそうしているし。

画像を縮小して勝手に cbz ファイルから名前を調べて追加。
なんて定型作業を自動化したいな。

だったら自分で作ればいいじゃないか!

#!/usr/bin/env python3

'''
    ダウンロードした続き物の twitter 4コマまんがを選択して使う
    ダイアログで選択した cbz ファイルの 001.jpg から始まる名前を取得
    名前順の最後になるよう 022.jpg 等の名前を付ける
    縦 1200px に画像をサイズダウンしキャッシュに保存
    それを cbz ファイルに追加
    までを自動化する Nautilus スクリプト
'''

import os, re, zipfile, gi
gi.require_version('Gtk', '3.0')
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import GLib, Gtk, GdkPixbuf

# move ~/.catch
cachedir = GLib.get_user_cache_dir()
os.chdir(cachedir)

dlg = Gtk.FileChooserNative(title='Open', action=Gtk.FileChooserAction.OPEN)
dlg.set_current_folder(GLib.get_home_dir())
ft = Gtk.FileFilter()
ft.set_name('Comic Book Archive')
ft.add_mime_type('application/x-cbz')
ft.add_mime_type('application/vnd.comicbook+zip')
dlg.add_filter(ft)
r = dlg.run()
dlg.destroy()
if r == Gtk.ResponseType.ACCEPT:
    arc_name = dlg.get_file().get_path()
    with zipfile.ZipFile(arc_name, 'a') as z:
        # last name
        s = z.namelist()[-1]
        num = int(os.path.splitext(s)[0])
        # loop
        path_array = os.environ['NAUTILUS_SCRIPT_SELECTED_FILE_PATHS'].split('\n')
        for filepath in path_array:
            try:
                pixbuf = GdkPixbuf.Pixbuf.new_from_file(filepath)
            except:
                continue
            # resize
            p_width = pixbuf.get_width()
            p_height = pixbuf.get_height()
            width = p_width * 1200 // p_height
            smallpix = pixbuf.scale_simple(width, 1200, GdkPixbuf.InterpType.BILINEAR)
            # create name
            ext = os.path.splitext(filepath)[1]
            num += 1
            name = f'{num:03d}{ext}'
            # create jpeg or png
            if re.search(r'\.(jpg|jpeg)$', filepath, re.I):
                smallpix.savev(name, 'jpeg', ['quality'], ['85'])
            elif re.search(r'\.png$', filepath, re.I):
                smallpix.savev(name, 'png', ['compression'], ['9'])
            z.write(name)

作ってみた。

file-roller で一旦 cbz 内のファイル名を調べる必要が無くなった。
1.jpg とか適当な名前の一時保存でも追加時に自動変名、我ながら超便利。
何故今までコレを思いつかなかったのだ俺よ。

ZipFile 作成にうっかり w 指定をして消えてしまったファイルも…
こういうのを作る時はバックアップをしてから、絶対だよ。

しかし特に Node.js 屋で見かけるんだけーが。
「コードを書くのが面倒だから npm でインストール!」
みたいなことを書く輩はいったい何故プログラミングを勉強しているのだ?
定型作業が面倒だからコードを書いて楽をするんじゃないのかと。
コードを書くのが面倒な人がそんなことするとは思えない。
定型作業はタイピング速度で解決、とか考えそう。