はじめに
最近CI/CD用に書いていたシェルスクリプトをちまちまとPythonに移植しています。なんでかと言うと単純にシェルスクリプトで扱うにはいささか複雑な処理やデータが増えてきたからですね。
じゃあなんでPythonなの?って話なんですが
- 標準ライブラリだけでわりと色々できる
- OSの機能にアクセスしやすい
- 型を扱えつつ雑にもできる
- 開発環境の構築が楽
- アレルギー持ちが少ない
この辺りが理由です。あとAmazon LinuxやGitHub-hosted runnerにプリインストールなのもベネ。
ただし全てのシェルスクリプトをPythonに移植することが正しいかと言われるとそれは場合によるとしか言いようがないのでここでは事の是非については触れません。
この記事はシェルでよく書くアレをPythonではこう書きますよという言わば互換表のようなものだと思ってください。
環境構築
本筋ではありませんが冒頭で環境構築について触れたので軽くご紹介しておきます。
とりあえずPythonをスクリプトとして書き始めたい場合に必要な工程は3つです。
- pyenv経由でPythonをインストール
- VS Codeに拡張機能を入れる
- VS Codeの設定を書く
Pythonインストール
pyenvはPythonのバージョンマネージャーです。特にこだわりがなければ開発環境にはpyenv経由でPythonをインストールすることをおすすめします。
pyenvインストール
Invoke-WebRequest -UseBasicParsing -Uri "https://raw.githubusercontent.com/pyenv-win/pyenv-win/master/pyenv-win/install-pyenv-win.ps1" -OutFile "./install-pyenv-win.ps1"; &"./install-pyenv-win.ps1"
参考: https://github.com/pyenv-win/pyenv-win#quick-start
curl https://pyenv.run | bash
参考: https://github.com/pyenv/pyenv#automatic-installer
Pythonインストール
pyenv instlal 3.11.0
pyenv install -l
でインストール可能なバージョンの一覧が表示されるのでお好みのものをインストールしてください。
VS Codeの拡張機能
インストール
基本機能(型検査含む)
リンター
フォーマッター
フォーマッターその2
設定
setting.jsonに以下の設定を入れてください。
{
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.organizeImports": true
},
"editor.formatOnType": true
},
"flake8.args": ["--max-line-length=120"],
"black-formatter.args": ["--line-length=120"],
"isort.args": ["--profile", "black"],
}
これでどのディレクトリで書いても大体のことはできる/自動でやってくれるようになりました。
記法あれこれ
ディレクトリ/ファイル操作
Pythonにおけるディレクトリやファイルの操作は基本的にpathlib.Path
を介して行います。
from pathlib import Path
# 相対パスでも絶対パスでも
r = Path("piyo.txt")
a = Path("/home/hoge/fuga/piyo.txt")
# 相互変換も可能
r.absolute()
>>> PosixPath('/home/hoge/fuga/piyo.txt')
# カレントディレクトリ
Path.cwd()
>>> PosixPath('/home/hoge/fuga')
Path()
>>> PosixPath('.')
# ホームディレクトリ
Path.home()
>>> PosixPath('/home/hoge')
# スクリプト自身のパス
Path(__file__)
>>> PosixPath('/home/hoge/fuga/piyo.py')
移動/検索
from pathlib import Path
cwd = Path()
# 移動
child = cwd / "hoge"
child
>>> PosixPath('./hoge')
child.parent
>>> PosixPath('.')
child.joinpath("fuga", "piyo")
>>> PosixPath('./hoge/fuga/piyo')
# 検索
[*cwd.glob("*.py")]
>>> [PosixPath('./fuga.py'), PosixPath('./hoge.py')]
# サブディレクトリ内も含めて検索
[*cwd.glob("**/*.py")]
>>> [PosixPath('./fuga.py'), PosixPath('./hoge.py'), PosixPath('./piyo/piyo.py')]
# ls
[*cwd.glob("*")]
>>> [PosixPath('./fuga.py'), PosixPath('./hoge.py'), PosixPath('./piyo')]
属性
from pathlib import Path
hoge = Path("hoge.txt")
# ステータス(ls -lとかstatで見るようなやつ)
hoge.stat()
>>> os.stat_result(st_mode=33206, st_ino=20829148276990953, st_dev=105550640, st_nlink=1, st_uid=0, st_gid=0, st_size=3039, st_atime=1680457867, st_mtime=1679661130, st_ctime=1680457867)
# 表示されるもの以外も見れるので詳しくは↓
# https://docs.python.org/ja/3/library/os.html#os.stat_result
# モード(パーミッション)変更
hoge.chmod(0o664) # プレフィックス"0o"のついた数値は8進数と見なされます
oct(hoge.stat().st_mode)
>>> 0o100664
# 存在確認
hoge.exists()
>>> True
# 名前系
hoge.name
>>> hoge.txt
hoge.suffix
>>> .txt
hoge.stem
>>> hoge
# is_hoge()
hoge.is_dir()
>>> False
hoge.is_file()
>>> True
# 他にも色々
glob
と属性の取得を組み合わせてls
にオプションを指定したような感じにすることもできます。
[*Path().glob("*")]
>>> [PosixPath('./fuga.py'), PosixPath('./hoge.py'), PosixPath('./piyo')]
[p for p in Path().glob("*") if p.is_dir()]
>>> [PosixPath('./piyo')]
読み書き
from pathlib import Path
hoge = Path("hoge.txt")
hoge.write_text("hoge!!")
hoge.read_text()
>>> 'hoge!!'
なおテキストのエンコード/デコードにはシステムロケールが適用されるのでWindows環境で開発する場合などはencoding="utf-8"
を忘れないようにしましょう1。
from pathlib import Path
hoge = Path("hoge.txt")
hoge.write_text("ほげ!!", encoding="utf-8")
hoge.read_text(encoding="utf-8")
>>> 'ほげ!!'
移動/リネーム
from pathlib import Path
hoge = Path("hoge/hoge.txt")
# 移動
hoge.replace(f"fuga/{hoge.name}")
# リネーム
hoge.replace(hoge.with_name("piyo.txt"))
移動/リネーム後のパスに既にディレクトリ/ファイルが存在する場合は権限があれば上書き、権限がなければ怒られます。
リンク
from pathlib import Path
# シンボリックリンク
Path("./fuga").symlink_to("./hoge/fuga")
# "./hoge/fuga"へのリンクが"./fuga"に作られる
# ハードリンク
Path("./piyo").hardlink_to("./hoge/piyo")
作成/削除
from pathlib import Path
# ディレクトリ
d = Path("hoge/fuga")
d.mkdir(parents=True, exist_ok=True) # 途中のディレクトリがない場合全部作る、既に存在する場合もエラーにならない
d.rmdir()
# ファイル
f = Path("piyo.txt")
f.touch() # mkdir()はexist_okがデフォルトFalseだがtouch()はTrue
f.unlink()
コピー及びディレクトリの削除はshutil
モジュールを使う
上記Path.rmdir()
には3.11現在オプションが存在しません。そのため削除したくとも空のディレクトリにしか使えないというなんともな性能をしています。俺がやりたいのはrm -rf
なんだよ!という方は併せてshutil
モジュールをご利用ください。
またPath
にはコピー系のメソッドが存在しないので(なんで?)こちらもshutil
で解決します。
from pathlib import Path
import shutil
# ディレクトリコピー
hoge = Path("./hoge")
hoge_ = shutil.copytree(hoge, hoge.with_name("hoge_"))
hoge_
>>> PosixPath('./hoge_')
# ディレクトリ削除
shutil.rmtree(hoge_)
# ファイルコピー
fuga = Path("./fuga.txt")
fuga_ = shutil.copy2(fuga, fuga.with_stem("fuga_"))
fuga_
>>> PosixPath('./fuga_.txt')
ちなみにshutil
の各メソッドに渡すsrc
, dst
引数にはパスを表す文字列でもPath
オブジェクトでもどちらを使用してもいいのですが渡したものと同じ型のオブジェクトが返ってくるため個人的にはPath
を使った方がおすすめです。
一時ディレクトリ/ファイル
使用モジュール: tempfile
from pathlib import Path
import tempfile
# 一時ディレクトリ
with tempfile.TemporaryDirectory() as tempdir_name:
tempdir = Path(tempdir_name)
...
# 一時ファイル
with tempfile.TemporaryFile() as fp:
fp.write(b'hoge!')
...
TemporaryDirectory()
は作成された一時ディレクトリの名前を返しますがTemporaryFile()
から返ってくるものはopenされたfile-like objectそのものなので注意が必要です。
環境変数
使用モジュール: os
import os
# 取得
hoge = os.environ["HOGE"]
# 更新
os.environ["HOGE"] = "hoge!"
# 削除
del os.environ["HOGE"]
Assignments to items in os.environ are automatically translated into corresponding calls to putenv(); however, calls to putenv() don't update os.environ, so it is actually preferable to assign to items of os.environ. This also applies to getenv() and getenvb(), which respectively use os.environ and os.environb in their implementations.
となんだかよく分からないことを言っているので使わない方がよさそうです。
正規表現
使用モジュール: re
import re
# 基本の使い方
commit_msg_ptn = re.compile(r"^(?P<prefix>[a-z]+): (?P<description>.*) #(?P<issue>[0-9]+)$")
if m := commit_msg_ptn.search("update: hoge to fuga #0000"):
m["prefix"]
m["description"]
m["issue"]
>>> 'update'
>>> 'hoge to fuga'
>>> '0000'
# 置換
re.sub(r"\s", "", "hoge fuga piyo\n")
>>> 'hogefugapiyo'
# マッチした部分文字列を参照して置換
re.sub(r"(?P<desc>[a-z]+)(?P<num>[0-9]+)", r"\g<desc>_\g<num>", "hoge2.png")
>>> 'hoge_2.png'
参考: 正規表現のシンタックス
HTTP通信/JSON
@hoto17296 さんのこの記事が全てです。
日時
使用モジュール: datetime
from datetime import datetime, date, timedelta
# 取得
datetime.fromisoformat("2020-01-01T01:01:01+09:00")
>>> 2020-01-01 01:01:01+09:00
datetime.fromtimestamp(1680546010)
>>> 2023-04-04 03:20:10
datetime.today()
>>> 2023-04-04 03:26:35.380565
date.today()
>>> 2023-04-04
# 出力
today = datetime.today()
today.strftime("%Y年%m月%d日")
>>> '2023年04月04日'
today.timestamp()
>>> 1680547503.322209
# 計算
today + timedelta(days=1)
>>> 2023-04-05 03:45:03.322209
アーカイブ
使用モジュール: shutil
デフォルトでzip、tar、gztar、bztar、xztarが扱えます。
地味にとても助かる。
import shutil
from pathlib import Path
Path.cwd()
>>> PosixPath('/home/hoge')
# "./fuga/fuga"ディレクトリの中身を"./fuga.tar.gz"として圧縮
fuga_tar = Path(shutil.make_archive("./fuga", "gztar", "./fuga/fuga"))
fuga_tar
>>> PosixPath('/home/hoge/fuga.tar.gz')
# "fuga.tar.gz"を"./piyo"ディレクトリの中に解凍
piyo = Path("./piyo")
shutil.unpack_archive(fuga_tar, piyo)
他のプログラムを動かす
使用モジュール: subprocess
from subprocess import run, Popen, PIPE
# 基本の使い方
cpl_proc = run(["git", "log", "-1"], capture_output=True, text=True) # Windowsはencoding="utf-8"も必要
cpl_proc.returncode
>>> 0
print(cpl_proc.stdout)
>>> commit aaaaaaaaa...
>>> Author: nicco_mirai <hoge@hoge.com>
>>> Date: Sun Apr 2 15:35:29 2023 +0900
>>>
>>> hoge!
>>>
cpl_proc.stderr
>>>
# 通常のコマンドも(OS依存)
cpl_proc = run(["ps", "aux"], capture_output=True, text=True)
print(cpl_proc.stdout)
>>> USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
>>> ec2-user 258807 0.1 1.0 230460 10168 pts/0 S+ 01:47 0:00 python3
>>> ec2-user 258809 0.0 0.2 232520 2712 pts/0 R+ 01:48 0:00 ps aux
# バックグラウンドで実行
proc = Popen(["npx", "nuxt", "build"], stdout=PIPE, text=True, cwd="nuxt_project_dir")
do_something()
proc.wait() # 終了するまで待機
終了
使用モジュール: sys
from sys import exit
# 正常終了
exit()
# 異常終了
exit("エラーメッセージ") # 終了コードは1になる
おわりに
一通り私が使うものは紹介しきれたと思います。
が、ここで紹介したものはPythonの標準ライブラリ全体から見れば1/100にも満たない程度ですので気になる方はぜひ公式ドキュメントにも目を通してみてください。
Python 標準ライブラリ
今後また使ってみて便利だったものがあれば随時更新していきます。
-
UTF-8 Modeという手もあります。 ↩