二分法を用いて関数の根を求める

1変数連続関数の根(値が0になる点)を求める、二分法というアルゴリズムとそれをScipyで実装する方法を紹介します。

アルゴリズムの内容

二分法というのは中間値の定理をベースとした求根アルゴリズムです。アイデアは非常に単純で、連続関数$f$に対して、$f(x_1)$と$f(x_2)$の符号が異なるように、$x_1, x_2$を選びます。この時点で、中間値の定理より区間$(x_1, x_2)$に根があることがわかりますのでさらに細かくみていきます。次は、$x_1, x_2$の中点$x_M = \frac{x_1+x_2}{2}$を取り、$f(x_M)$の符号を調べます。$f(x_M)$と$f(x_1)$の符号が同じであれば、$x_1$を$x_M$で置き換え、逆に$f(x_M)$と$f(x_2)$の符号が同じであれば、$x_2$を$x_M$で置き換えると、区間の幅が半分になった$(x_1, x_2)$が得られますが根はこの中にあることがわかります。これを繰り返すと、根が存在する範囲を狭めていくことができ、$f(x_M)$の絶対値が0になるか、もしくは十分0に近づいたらその値を数値的に求めた根とします。

以上が、一般的な二分法のアルゴリズムの説明です。ただし、後に紹介するSciPyではどうやら区間が十分狭くなったかイレーション回数が上限に達したか等の基準でループを打ち切り、$f(x)$の値を確認していないようです。

二分法のメリットとデメリット

方法が単純でわかりやすい、というのが個人的に感じている1番のメリットです。

また、連続関数であれば使えるため、ニュートン法などのアルゴリズムのように元の関数の微分を必要とせず、微分が難しい関数や微分不可能な関数でも使えます。

また、根が存在しうる区間を狭めながら探索するため、最初の区間の幅と繰り返し回数により、結果の精度を保証できることも大きな利点です。

逆にデメリットとしては、ニュートン法等と比較して収束が遅いこととか、初期値として関数が異符号になる2点を探して与える必要があること、もしその区間に複数の根が存在した場合にどれに収束するか不確定なことなどが挙げられます。

ただし僕の経験上では、ある程度根の目処がついていたり、単調な関数に対して使う場面が多くこれらのデメリットを深刻に感じることは少ないです。

SciPyによる実装

SciPyではscipy.optimizeというモジュールで実装されています。bisectという専用メソッドを使うか、root_scalarという汎用的なメソッドで(method=’bisect’)を指定して使うことになります。

参考:
scipy.optimize.bisect — SciPy v1.11.4 Manual
root_scalar(method=’bisect’) — SciPy v1.11.4 Manual

試しに、$\sin$関数の 3と4の間にある根を探させて見せましょう。既知の通りそれは円周率$\pi$になるはずです。

from scipy import optimize
import numpy as np


root1 = optimize.bisect(np.sin, 3, 4)
print(root1)
# 3.1415926535901235

root_result = optimize.root_scalar(np.sin, bracket=[3, 4], method='bisect')
# 結果は複数の情報を含むRootResults形式で戻る。
print(root_result)
"""
      converged: True
           flag: 'converged'
 function_calls: 41
     iterations: 39
           root: 3.1415926535901235
"""
print(type(root_result))
# class 'scipy.optimize._zeros_py.RootResults'

# 根の値へのアクセス方法
print(root_result.root)
# 3.1415926535901235

いかにも円周率ぽい結果が得られましたね。root_scalarの方では収束したことを示すフラグや、イテレーション回数なども得られています。

失敗事例1. 区間の両端での関数の値が同符号の場合

二分法は初期設定を誤ってると失敗するのでその場合のSciPyの挙動も見ておきましょう。失敗パターンの一つは、最初に指定した区間の両端で符号が一致していた場合です。もちろん関数の形によってはその区間内に根がある可能性もあるのですが、存在は保証されなくなります。

また$\sin$関数で、その間に根が存在しない区間$(1, 2)$と、実は両端で同符号だけど根が存在する区間$(1, 7)$でやってみましょう。bisectとroot_scalarで全く同じエラーメッセージ出るのでbisectの方だけ載せます。

try:
    optimize.bisect(np.sin, 1, 2)
except Exception as e:
    print(e)
# f(a) and f(b) must have different signs

try:
    optimize.bisect(np.sin, 1, 7)
except Exception as e:
    print(e)
# f(a) and f(b) must have different signs

はい、$f(a)$と$f(b)$は違う符号にせよとのことでした。根が存在しない1個目の例はさておき、2個目の例は根は区間内に2個存在するのですが探さずにエラーになりました。

失敗事例2. 連続関数ではなかった場合

もう一つ失敗するのは、関数が連続関数ではないケースです。

例えば$\tan$の、$\frac{\pi}{2}$近辺の挙動で見てみましょう。数学的に厳密な話をすると、$\frac{\pi}{2}$では$\tan$は定義されないので、$\tan$は数学的には連続関数(定義域内のすべての点で連続)なのですが、数値計算的には不連続と考えた方が都合が良いです。

話が脇に逸れたので実例の話に移ります。実は、bisectメソッドは結果を返して来ちゃうんですよね。そしてそれが全然根ではないということも見ておきましょう。

root = optimize.bisect(np.tan, 1, 2)
# pi/2に近い結果が得られている
print(root)
# 1.5707963267941523

# 元の関数に代入した結果は全く0に近くない
np.tan(root)
# 1343445450736.3804

root_scalarの方だったら、結果のフラグ等もあるのでアラート等あげてくれるのかと期待したのですがそういう機能はなさそうです。

root_result = optimize.root_scalar(np.tan, bracket=[1, 2], method='bisect')
print(root_result)
"""
      converged: True
           flag: 'converged'
 function_calls: 41
     iterations: 39
           root: 1.5707963267941523
"""
np.tan(root_result.root)
# 1343445450736.3804

要するに、SciPyに渡す関数が本当に連続関数であるかどうかは利用者が責任を持たないといけないということです。また、結果が本当に根なのかどうかは代入して確認した方が良いでしょう。

まとめ

以上が二分法の説明とSciPyで利用する方法、その注意点でした。

MySQLでインデックスヒントを使う

※ MySQLって書いてますがその互換の Aurora で検証しています。

MySQLのレコード数が大きなテーブルからデータを検索するとき、インデックスは処理時間の短縮において重要な役割を果たしてくれます。

ただ、一個のテーブルにインデックスを何個も作成しているとSQLによっては最適なインデックスが使われないことがあります。このような場合、インデックスヒントを使うことでクエリを最適化できる可能性があります。

参考: MySQL :: MySQL 8.0 リファレンスマニュアル :: 8.9.4 インデックスヒント

自分の事例で言うと、昔仕事で扱っていたDBでインデックスが10個以上張られているテーブルがあり、きちんと指定してあげないと正しく使ってくれないものがあったことがあります。

また、最近は私用で個人的に集めているデータを溜めているテーブルにおいても、レコード数が増えるにれて正しいインデックスを使ってくれない事例が発生するようになりました。

自分の事例で言うと、どうもMySQLによってインデックスによる絞り込み効果が低いと判断されるとそれが使われないってことがあるようですね。半年分のデータを取得しようとすると正しく実行されないが、1ヶ月分のデータを取得しようとすると正しく使われる、といった事例をよく見ます。

テーブルに貼られているインデックスの確認方法

まずはどんなインデックスが使いうるのか知らないと話にならないのでその確認方法です。

SHOW CREATE TABLE {テーブル名};
でテーブル定義を丸ごと確認するか、インデックス情報取得専用の構文である、
SHOW INDEX FROM {テーブル名};
を使うことで確認できます。

僕個人が分析用に溜めている株価のテーブルだと次のようになります。code(証券コード)とdate(日付)を主キーとしていて、code, date 個別にもインデックスを貼っています。

select
SHOW INDEX FROM price
-- 結果
Table	Non_unique	Key_name	Seq_in_index	Column_name	Collation	Cardinality	Sub_part	Packed	Null	Index_type	Comment	Index_comment
0	price	0	PRIMARY	1	code	A	4561	None	None		BTREE		
1	price	0	PRIMARY	2	date	A	7336473	None	None		BTREE		
2	price	1	code	1	code	A	4383	None	None		BTREE		
3	price	1	date	1	date	A	3166	None	None		BTREE		

key_name列で、primary, code, date の3種類のインデックスがあることが確認できますね。

クエリが利用しようとしているインデックスの確認方法

おそらく処理時間とかを計測したりてこのクエリは正しくインデクス使ってないな、って気づくことになると思うのですが、実際にどのインデックスを使っているかは、そのクエリの先頭に EXPLAIN をつけて実行することで確認できます。

例えば、次のような感じです。possible_keys が利用可能なインデックスで、key が実際に使うインデックスです。

EXPLAIN SELECT
    *
FROM
    price
WHERE
    date >= '2023-01-01'

-- 結果 (これはkeyがNoneなのでインデックスを使ってない)
	id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
0	1	SIMPLE	price	None	ALL	date	None	None	None	7336473	25.53	Using where

EXPLAIN SELECT
    *
FROM
    price
WHERE
    date >= '2023-12-01'

-- 結果 (これはdateをインデックスとして使っている)
	id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
0	1	SIMPLE	price	None	range	date	date	3	None	176076	100.0	Using index condition

さて、上記の上の例でもdateをインデックスとして使って欲しい場合にインデックスヒントを使います。

インデックスヒントの使い方

インデックスヒントを使う場合、特定のインデックスを強制する USE INDEX, 特定のインデックスの利用を防ぐ IGNORE INDEX, USE INDEXよりも強力に指定したインデックスを強制する FORCE INDEX の3種類があります。

使い方は同じで、FROM句のテーブル名の後ろに、
テーブル名 USE INDEX (インデックス名)
のように書きます。カッコを忘れないように注意してください。

FORCE IDNEXでやってみます。

EXPLAIN SELECT
    *
FROM
    price FORCE INDEX(date)
WHERE
    date >= '2023-01-01'

-- 結果 (FORCEしたインデックスが使われている)

id	select_type	table	partitions	type	possible_keys	key	key_len	ref	rows	filtered	Extra
0	1	SIMPLE	price	None	range	date	date	3	None	176076	100.0	Using index condition

最後に

インデックスヒントを使うことでMySQLが利用するインデックスをある程度制御できるようにあります。ただ、実際にパフォーマスが改善するかどうかはまた別の問題です。というのもMySQLが選んだキーの方が効率的である可能性というのも十分あり得るからです。

このクエリはチューニングが必要だなと感じることがあった場合にインデックスヒントを使うことになると思いますが、処理時間が本当に改善してるかどうかはきちんと計測するようにしましょう。

2024年のご挨拶と今年の方針

新年明けましておめでとうございます。本年もよろしくお願いします。

今年のこのブログの運用方針ですが、2022年, 2023年と同じように週1回更新を目指していこうと思います。昨年後半からnoteもスタートしててそっちは月1回なのでアウトプットの総量としては少し増加するイメージです。

毎年年初の記事でこれを書くぞって宣言した内容ほど書けてない変な傾向にあるので、昨年書きたいのに書かなかった内容を少しでもまとめて行けたらいいなと思っています。それ以外だと、最近の自分の関心としては、pyMCとかStreamlit あたりを使い始めているのでこの辺で記事を書けるといいですね。SnowflakeとかReDashの記事を書いてもいいのですが仕事で使ってる系はnoteの方に回りそうです。

ブログ以外の目標

データサイエンティストとしてはせっかく新しい業界に転職したので、その業界のドメイン知識・技術を重点的に学んでいく一年にしたいと思っています。項目応答理論とかもっと幅広く教育工学全般について。今年の取り組みとしてはこれが一番重要かな。

株式の運用に使っている自作システム群の改良も続けていきます。昨年から本格的にJ-Quants APIを使っていますが今年はプランをもう一個上げて活用の幅を広げていくことを検討中です。

その自作システム群ではAmazon Aurora Serverless v1 を使っているのですが、これが2024年の12月末でサポート終了するとアナウンス がありました(2023/12/28に。年末にびっくりしました)。個人開発周りで今年一番重いのはこの移行作業ですね。v2は高いので採用したくなく、何か別のやり方を考えて移行していく必要があります。

そういった感じで、このブログ自体は昨年と変わらず細々と更新されていく形になると思いますがどうぞ本年もよろしくお願いいたします。

2023年のまとめ

早いもので今年も最後の月曜日を迎えました。この記事が今年最後の投稿になるので1年のまとめをやっていきます。

まず、このブログに関しては途中でサーバーの障害があり投稿が遅れたこともありますが、今年も無事毎週の継続更新を完遂できました。

アクセス数等も集計していきます。Google Analyticsのユニバーサルアナリティクスがサービス終了してしまったので、今年からGA4の集計です。そのため基準が変わってしまったので昨年比は参考値でですが結果は以下のようなりました。(GA4基準で言うところの総ユーザー数とpage_viewイベント数を数えています。)

– 累計記事数 618記事 (この記事含む。昨年時点566記事)
– 総ユーザー数 254,463人 (昨年実績 272,075人 UA基準)
– page_viewイベント数 461,717回 (昨年実績 476,587回 UA基準)
(これらの数値は年が変わった段階でもう一回更新します)

記事数は無事に600を超えました。アクセスの観点では昨年比でやや訪問者数落ちちゃいましたね。実は3月くらいと9月くらいにそれぞれ何かがあったようで全体的なSEOが悪化しています。この調子で行くと来年はもう一段階下がりそうです。とはいえ延べ20万人以上の方にアクセスいただいているのでめげずに更新していこうと思います。

記事のまとめ

今年もよく読まれた記事ランキングを見ていきましょう。GA4なので、1年間のpage_viewイベントの数でランキングします。

  1. Pythonで日付の加算、特にnヶ月後やn年後の日付を求める方法 (昨年1位)
  2. PythonでBase64エンコードとデコード (昨年8位)
  3. matplotlibでグラフ枠から見た指定の位置にテキストを挿入する (昨年3位)
  4. matplotlibのdpiとfigsizeの正確な意味を調べてみた (昨年5位)
  5. M2搭載のMacBookにPython環境構築 (2023年02月時点) (New)
  6. matplotlibのグラフを高解像度で保存する (昨年2位)
  7. Pythonのリストをn個ずつに分割する (昨年4位)
  8. PythonでMeCabを動かそうとしたらmecabrcファイルが無いというエラーが出たので原因を調べた (昨年10位)
  9. globでサブフォルダを含めて再帰的にファイルを探索する (昨年7位)
  10. Pandasで欠損のある列の文字列型の数値を数値型に変換する (昨年9位)

新規でランクインした記事が1記事だけってのがちょっと寂しいですが昨年からの人気記事がそのまま今年もアクセスを集めていました。これらの記事は比較的ChatGPT等の生成AIへの質問で代用しやすいのかなとも思っており、このブログのアクセス低下の一因ともなっていそうです。

MeCabの小ネタなどは意外とChatGPTが詳しい情報を持ってなかったりするのでこの辺でアクセス集めれるといいのですがいかんせん元々ジャンルがマイナーですからね。そして自然言語処理の各技術自体がLLMに押されて関心を向けられなくなっているような気もします。

年初の目標の振り返り

一応、年初に目標立てていたのでその振り返りもやっておきます。
参考: 2023年のご挨拶

予定していた通り、統計数理研究所の講座を受講したり、すうがくぶんかや和からのイベントを聴講したりと仕事直結するもの以外の勉強にも色々時間をつかえた年になったと思います。

ブログ記事に関しては、生存分析とか、状態空間モデルとか、因果探索の記事書くぞ、みたいなことを書いていたのにこの辺の記事は全然書けていません。大変申し訳ない。ただ、年初に想定していなかった内容で書きたい内容が多く出てきたと言うことでもあり、一概に悪いことばかりでもないなと思っています。書けなかったテーマは来年に持ち越しです。

ブログのメンテやるぞ、って目標に関してはいよいよ管理画面が動かなくなるほどの事態に追い込まれての対応になりましたがリソース増やしたりPHPとWordpressのバージョン上げたりと対応を完遂できました。GAもUAからGA4に移行できていますし。ちゃんと計測していませんがもし、以前より快適にアクセスできるよになっていたら嬉しいです。

投資ツール開発の個人プロジェクトも進み、かなりスムーズに運用が回るようになっています。もうほとんどの作業がAWS環境に乗りました。一部、Excelマクロが生き残ってるので来年こそ完全AWS化をしたいです。

それ以外の出来事について

この2023年は個人的にはイベントが盛りだくさんの1年間でした。データ分析を担当した本が新しく出版された(これが2冊目)とか、データ提供した某ビジネス誌の記事に名前を載せてもらえたとか、分析結果が日経新聞の1面に載ったとか、勤め先が新規上場したとか、マーダーミステリー始めたとか新しいコミュニティーに参加するようになったとか本当に色々ありました。

その中でも一番大きいベントは転職と引越しです。

7月末で会社を辞めて8月から新しい会社に転職しました。職種はデータサイエンティストのままですが、業界は人材系から教育系に変わっています。

転職に伴い住居も引っ越しました。ただ、転職前後の会社が両方ともフルリーモートなのであんまり引っ越した意味はなかったですね。

新会社では情報発信に力を入れており、サービス開発部の各メンバーがnoteやQiita、Zennで記事書いていますので僕もその会社の社員としてのアカウントをnoteに作ってそちらでも記事を書き始めました。まだ数の面でも内容の面でも大した記事はないですが、このブログの一番下のリンクにひっそりと追加しています。

転職があったので、業務内容も扱う技術スタックも大幅に変わり、転職後は各種ツールやサービスの使い方、社内のデータ構造の把握等にインプットのリソースを大きく割いていました。その影響か、ただでさえtips的な内容で記事数を稼いでいたこのブログが一層内容が薄くなってた部分もあるかなぁと感じています。アクセス数停滞の1番の要因はこれかも。

転職から半年近く経ってようやく、教育工学等のドメイン知識の領域へ手を伸ばせる気配が出て来たのでこれからまた徐々に活動の幅を広げられたらいいなと思っています。今はまだ社内のデータチームが立ち上げ段階でほぼダッシュボード係&データ抽出屋さんって感じなので。

来年に向けて

来年以降のこのブログをどうするかはまた今週よく考えて決めておきたいと思います。

来年はnoteも書くのでなかなかこのままのペースは厳しいような気もしていまして。ただ、書きたいと思ってるのに書けてない記事ネタの山を見るとペースは維持したいような気もします。今週よく考えて年初の記事で目標宣言できたらと思います。

それではみなさま、今年も1年間ありがとうございました。また来年もよろしくお願いいたします。

argparseで引数を受け取る

はじめに

今週の記事もPythonスクリプトで引数を受け取って使う話です。前回はsys.argvつかって受け取る方法を紹介していましたが、今回は便利な専用モジュールのargparseを紹介します。

これを使うと、引数を変数に自動的に格納したり、オプション引数やフラグを作成したり、ヘルプ機能を自動的に作ってくれたりします。

よくUnix/Linux コマンドでは -o filename みたいな感じで出力先ファイルを指定できたりしますが、これをsys.argvで実装しようとすると、配列を全部見て-oがあるかどうか確にして、その次の値をfilenameとして取得して、みたいな結構面倒な処理を自分で作る必要があります。-oが複数出てきたらどうするかとか、-oの次にファイル名がなかった場合のハンドリングとか色々考えないといけないのでとても面倒です。こういう手間を削減してくれます。

順に使い方書いていきますが、ドキュメントはこちらです。
参考: argparse — コマンドラインオプション、引数、サブコマンドのパーサー — Python 3.12.1 ドキュメント

基本的な使い方

ざっくりいうと、argparseは次の3手順で使います。

  • ArgumentParserオブジェクトの作成
  • 必要な引数をパーサーオブジェクトに追加する
  • 引数を解析して結果を取得する

一回単純なサンプル作ってやってみましょう。上記ドキュメントの例をそのまま使います。
sample.py というファイル名で次のスクリプトを作成し、実行権限を `$ chmod u+x sample.py` でつけておきます。

#!/usr/bin/env python
import argparse


# パーサーオブジェクトの作成
parser = argparse.ArgumentParser(
    prog="ProgramName",
    description="What the program does",
    epilog="Text at the bottom of help"
)

# 必要な引数の追加
parser.add_argument("filename")  # 位置引数
parser.add_argument("-c", "--count")  # 値を取るオプション 
parser.add_argument("-v", "--verbose", action="store_true")  # on/off フラグ

# 引数の解析
args = parser.parse_args()
print(args.filename, args.count, args.verbose)

だいたいイメージできると思うのですが、./sample.py を実行する時、最初の引数が filename に格納されて、 -c か –count で指定した値が count変数に格納され、 -v を選択したかどうかがTrue/False で verbos に入ります。 色々やってみましょう。

$ ./sample.py test.txt
test.txt None False

$ ./sample.py test.txt -c 4 -v
test.txt 4 True

$ ./sample.py -v --count abc test.txt
test.txt abc True

$ ./sample.py test1.txt test2.txt 
usage: ProgramName [-h] [-c COUNT] [-v] filename
ProgramName: error: unrecognized arguments: test2.txt

$ ./sample.py                    
usage: ProgramName [-h] [-c COUNT] [-v] filename
ProgramName: error: the following arguments are required: filename

$ ./sample.py --help
usage: ProgramName [-h] [-c COUNT] [-v] filename

What the program does

positional arguments:
  filename

options:
  -h, --help            show this help message and exit
  -c COUNT, --count COUNT
  -v, --verbose

Text at the bottom of help

はい、最初の3例が正しくコマンドを打ったケースでしたが、だいたいイメージ通りに引数を受け取れていることが確認できると思います。
4つ目は位置引数を過剰に設定、5つ目は逆に指定しませんでしたが、それぞれちゃんとエラー文を出してくれていますね。
6個目の例は–helpをつけていますが、なんと自動的にヘルプメッセージを作成して表示してくれています。

コマンド名が ProgramName になっていますが、これはパーサーを作成したときのprog 引数をプログラム名として使っているからです。progを省略すると、ファイル名が使われます。

これは大事なことなのですが、プログラム名=ファイル名のことが多いと思うので、基本的に省略した方がいいと思います。(さっきの例は公式ドキュメントをただ真似しただけ。)

descriptionでプログラム中身の説明、epilogでヘルプの最後に表示するメッセージを指定できますが、これらもどちらも省略可能です。ただ、descriptionは何か書いていておいた方がいいと思います。

ここから細かく仕様を見ていきます。

引数の種類

引数の種類としては、コマンドの後に何番目に渡されたかどうかで扱いが決まる位置引数と、-(ハイフン)付きの名前で始まるオプション引数があります。

argparseは接頭辞の”-“を特別な文字として扱って、これによって挙動を変えています。

上の例でもわかりますが、次のように複数の名前を指定することもできますし、1種類だけの名前でも良いです。このとき注意しないといけないのは、参照するときの変数名です。

-c みたいな短い名前だけの時は – をとって c として参照しますが、-c, –count という2種類の名前を指定した場合は、一番最初に登場する長い名前、が採用されます。長い名前というのは文字列の長さの話ではなく、 – ではなく、 — で始まる引数ということです。
つまり、次のように3つの名前をつけたら、長い名前の中で最初に登場した countが採用されるということです。

#!/usr/bin/env python
import argparse


# パーサーオブジェクトの作成
parser = argparse.ArgumentParser()

# 必要な引数の追加
parser.add_argument("-b")
parser.add_argument("-c", "--count", "--cnt")

# 引数の解析
args = parser.parse_args()
# 短い名前 -b しかないのでbでアクセス
print(args.b)

# --count と --cnt が長い名前だが、先に登場したcountの方が優先
print(args.count)

add_argument の引数

add_argument には様々な引数を指定でき、各種の設定を行うことができます。
全部紹介するのも大変なので一部抜粋して紹介しますが、公式ドキュメントの該当欄の一読をお勧めします。
参考: add_argument() メソッド

default ・・・ コマンドラインに対応する引数が存在せず、さらに namespace オブジェクトにも存在しない場合に利用されるデフォルト値。
type ・・・ データ型。 int や float、ユーザー定義の型など色々指定できる。省略すると文字列(str)。
choices – 引数として許される値のシーケンス。
help ・・・ 引数の説明。-h や –help を使用した時に使われる。
nargs ・・・ 受け取れるコマンドライン引数の数。後で説明します。
action ・・・ コマンドラインにこの引数があったときの動作。後で説明します。

だいたいはイメージ通りの挙動をしてくれるのですが、nargsとactionについてはこの後説明します。

受け取れるコマンドライン引数の数について

nargs という値を使って、受け取れる引数の数を指定できます。

nargs の指定は正規表現風になっています。 整数Nを指定すればその個数、?なら1個か0個で、0だったらdefalut値が使われます。*とすると任意の数受け取れます。また、+だとこちらも任意の数受け取れますが、0個だった場合にエラーが起きます。

例えば、任意の数のファイルのデータを入力として、1個のファイルに結果を書き出すようなコマンドがあったとしましょう。(というより、tar コマンドでアーカイブ作る時はそういう指定しますよね。 )

次のような形です。

#!/usr/bin/env python
import argparse


# パーサーオブジェクトの作成
parser = argparse.ArgumentParser()

# 必要な引数の追加
parser.add_argument("-o", "--out_file")  # nargsを省略しているので1
parser.add_argument("-i", "--in_file", nargs="+")

# 引数の解析
args = parser.parse_args()
print(args.in_file)
print(args.out_file)

# 以下実行結果
$ ./sample.py -o out.txt  -i in1.txt in2.txt in3.txt
['in1.txt', 'in2.txt', 'in3.txt']
out.txt

— in_file の方は複数の結果を受け取れるようにしたので、Python上は配列で結果が来るようになりましたね。

actionによる動作の指定について

actionを使って、オプション引数が存在したときの挙動を指定できます。

デフォルトは store でこれは要するに変数を値に格納するという挙動です。さっきまで見てるのがこれですね。

ただし、Linux/Unixコマンドではこのような値を受け取る引数ばかりではありません。皆さんがよく使う ls コマンドの -l とか -a は別に何か引数を受け取ったりせず、その存在の有無だけが重要ですよね。

この記事の冒頭のコードの `parser.add_argument(“-v”, “–verbose”, action=”store_true”) # on/off フラグ`
もまさにそうで、 -v の有無だけが問題になります。これを実現しているのが、action=”store_true”の部分です。

要するに -v が見つかったら verboseにTrueを格納するよ、という挙動になります。
逆に見つからなかったらFalseが格納されます。

これと逆にオプションがあったらFalseでなかったらTrueになるのが、”store_false”です。

このほか、キーワード引数の登場回数を数えて格納する”count”とか、複数回登場したら結果を都度配列に追加していく”append”などもあります。

これらも一通り公式ドキュメントの一読をお勧めします。
参考: action

ヘルプの作成について

最後にヘルプ機能についてです。自動的に、-h と –helpがヘルプ機能として実装されます。

これはもう実際に試していただくのが一番早いのですが、description等で指定されたプログラムの説明や、受け取れるコマンドライン引数の情報などが表示でき大変便利です。

気をつけないといけないのは、 -h と –help を上書きしないようにすることですね。もちろんどうしてもこれらの引数名を別用途で使いたいとか、自作のヘルプメッセージを実装したいとか事情があれば話は別ですが、普通はデフォルトのヘルプを使った方が良いと思います。

argparseをコマンドライン引数以外の文字列のパースに使う

最後にちょっとマニアックな使い方を紹介します。
このargparseですが、何も指定せずに、 parser.parse_args() すると コマンドライン引数をパースしにいきますが、ここで配列を渡すとその配列をパースします。

sample_str = “-i filename -c 5” みたいな文字列があった時にsample_str.split()して配列に分解して、 parser.parse_args(sample_str.split())と渡すとそれをコマンドライン引数と見立ててパースしてくれるのです。

そんな技術いつ使うねん、と思われるかもしれませんが、僕はマジックコマンドを作る時などに使ってます。
参考: Snowflakeに手軽にSQLを打てるJupyterマジックコマンドを作ってみた|ホンディー | ライフイズテック  (このブログ書いてる人のnote記事です。)

これをやると、parse_args は渡された配列をパースしてるのでコマンドラインから渡した引数は全部無視する点には注意してください

まとめ

長くなりましたが、以上がargparseの説明になります。argparseはシンプルに利用することもできますし、多くの引数を活用して細かいカスタマイズもでき、大変柔軟にツールを作ることができます。 自前ツールを作成する際の大変有益な武器になりますので是非触ってみてください。

Pythonファイルをコマンドラインで実行したときの引数の受け取り方。(sys.argvを使う場合)

久々にコマンドラインツールを作った時に、引数の扱い方をド忘れしてしまっていたのでそのメモです。

ちなみに、argpaseっていう大変便利なライブラリもあるのでそれの使い方も紹介したいのですが、それは別記事に回して今回はもっとシンプルな方について書きます。

記事タイトルにも書いていますが、基本的には、sys.argvというのを使います。これは関数ではなく配列型のデータで、argv[0]にそのスクリプトのファイル名、argv[1]以降に、コマンドライン引数が入ります。

参考: sys — システムパラメータと関数 — Python 3.12.1 ドキュメント

また、Pythonのバージョン3.10 以降では、sys.orig_argv ってのも追加されています。

動かしてみるのが一番確認しやすいので、やってみましょう。
次のようなファイルを作ります。ファイル名は sample.py としました。

import sys


for i in range(len(sys.argv)):
    print(f"sys.argv[{i}]=", sys.argv[i])

for i in range(len(sys.orig_argv)):
    print(f"sys.orig_argv[{i}]=", sys.orig_argv[i])

ではこれを実行してみましょう。一旦実行権限とかつけてないので、明示的にpythonコマンドとして実行します。

引数はただ表示するだけで何も処理に影響はないので、デタラメにつけてみました。

% python sample.py file.txt -l aaa.txt "bbb" --name=xyz
sys.argv[0]= sample.py
sys.argv[1]= file.txt
sys.argv[2]= -l
sys.argv[3]= aaa.txt
sys.argv[4]= bbb
sys.argv[5]= --name=xyz
sys.orig_argv[0]= /Users/{macのユーザー名}/.pyenv/versions/3.11.1/bin/python
sys.orig_argv[1]= sample.py
sys.orig_argv[2]= file.txt
sys.orig_argv[3]= -l
sys.orig_argv[4]= aaa.txt
sys.orig_argv[5]= bbb
sys.orig_argv[6]= --name=xyz

はい、argvの方は、[0]番目にスクリプト名が表示され、[1]以降に引数が格納されていましたね。”bbb”のところに注目ですが、ダブルクオーテーションは外されています。

orig_argv は、ファイル名の前の、pythonパスまで入っています。the original command line arguments というから、コマンドに打った python がそのまま入ってるかと思ったらフルパスに展開されています。

ちなみに、sys.argv[0]= sample.py と sys.orig_argv[1]= sample.py のスクリプトパスですが、これはコマンドラインで打ったファイルパスがそのまま表示されいます。どういうことかというと、現在はカレントディレクトリで実行したからこのように表示されているだけで、違う場所から相対パスや絶対パスで指定して実行したらこの中身は変わります。フルパスで指定したらフルパスが入ります。

続いて、このファイルに実行権限をつけて頭のpythonを外してみましょう。
ファイルの先頭に、 #!/usr/bin/env python のシバンを挿入して、chmod で実行権限つけときます。

これで実行すると次のようになります。(引数少し減らしました)

% ./sample.py file.txt -l aaa.txt
sys.argv[0]= ./sample.py
sys.argv[1]= file.txt
sys.argv[2]= -l
sys.argv[3]= aaa.txt
sys.orig_argv[0]= /Users/{macのユーザー名}/.pyenv/versions/3.11.1/bin/python
sys.orig_argv[1]= ./sample.py
sys.orig_argv[2]= file.txt
sys.orig_argv[3]= -l
sys.orig_argv[4]= aaa.txt

sys.argv の方はさっきと大して変わらないですね。カレントディレクトリにパスが通ってないので、スクリプトの指定が./ファイル名 になったのが反映されています。

orig_argv の方も先の結果とほぼ変わりませんが、これはちょっと意外でした。the original command line arguments というから、コマンドラインで python 省略したらここからも省略されると思ってたら相変わらずpython本体のパスが登場しています。

orig_argv が何を意図して追加されたのかがよくわからないのですが、とりあえず argv の方を使っておけば良さそうです。

[1]以降の引数たちは冒頭でも書いたargpaseってライブラリで取ることが多く、sys.argvで取得するのはよほど単純なスクリプトの場合に限られるかなとも思います。となると、もしかしたらargv[0]のファイル名の方がよく使うかもしれないですね。

おまけ

記事の本題と逸れるのですが、argv[0]でスクリプト名が取れるとしたら、スクリプトのフルパスやカレントディレクトリの取得方法が気になるかもしれないのでその撮り方もメモしておきます。

結論を言うと、ファイル名は __file__ って特別な変数に格納されています。
また、カレントディレクトリは os.getcwd() で取れます。

macでダブルクリックでシェルスクリプトやPythonファイルを実行する

知っている人にとっては常識だったのかもしれませんが、タイトルの通りMacOSのパソコンで、シェルスクリプトファイルをダブルクリックで実行する方法を紹介します。

MacではWindowsのバッチファイル(.bat等)と違って、.sh とか .py ファイルを作ってもダブルクリックでは実行できないと最近まで勘違いしていました。

ただ、どうやら .command という拡張子でファイルを作って実行権限を付与しておくと、ダブルクリックで実行できるようです。

ちょっとデスクトップにサンプルディレクトリ作ってやってみます。
シェルスクリプトの中身は、文字列の表示(echo)、カレントディレクトリの表示、ファイルリストの表示、シェルの種類の表示、くらいにしておきましょかね。

$ mkdri sample_dir
$ cd sample_dir
$ vim test.command
# 以下の内容を記入して保存
echo "Hello world!"
pwd
ls
echo $SHELL

この時点でできてるファイルをダブルクリックしても、 「ファイル“test.command”は、適切なアクセス権限がないために実行できません。」 ってメッセージが表示されます。 chmod +x とか chmod 744 等して実行権限を付与します。

# 元の権限(見るだけ)
$ ls -l test.command
-rw-r--r--@ 1 yutaro  staff  39 12  4 00:22 test.command
# 所有者に実行権限付与
$ chmod u+x test.command
# 確認
$ ls -l test.command
-rwxr--r--@ 1 yutaro  staff  39 12  4 00:22 test.command

こうすると、このファイルをファインダーでダブルクリックするとターミナルが開いて以下の内容が表示されます。

/Users/yutaro/Desktop/sample_dir/test.command ; exit;
~ % /Users/yutaro/Desktop/sample_dir/test.command ; exit;
Hello world!
/Users/yutaro
# {色々散らかってるのでlsの結果は省略}
/bin/zsh

Saving session...
...copying shared history...
...saving history...truncating history files...
...completed.

[プロセスが完了しました]

zshで動きましたね。 exit; は勝手につけて動かしてくれるようです。
また、ユーザーのホームディレクトリで動いていることも確認できました。

ホームディレクトリで動くって点は注意が必要ですね。その.commandファイルが配置されているディレクトリで動いて欲しいことも多いと思います。ファイルが配置されているフォルダで動作させたい場合、スクリプトの先頭でcdして対応します。具体的には以下の1行をスクリプトの先頭に追記します。

cd "$(dirname "$0")"

次はスクリプトが動いた後、[プロセスが完了しました]の表示のまま、ターミナルが開きっぱなしになる点が気になるかもしれません。これはターミナルの設定を修正します。

iTerm2等を入れてる人もこれは標準ターミナルが動作してるので設定を変更する対象を間違えないようにしましょう。 ターミナルの設定のプロファイル> シェル のなかに、シェルの終了時: というオプションがあります。 これがウィンドウを閉じないになってると自動では閉じませんので、「シェルが正常に終了した場合は閉じる」にしておくと自動的に閉じることができます。

「ターミナル本体」の設定で挙動が変わってしまうので、.commandファイルごとに変えられないのが難点です。少々面倒ですが見逃せないエラーメッセージ等あるかもしれないので手動で閉じるようにした方がいいかもしれません。

さて、これでダブルクリックで動くシェルスクリプトファイルが作れましたが、同様にPythonのコードを動かすこともできます。 Pythonファイルと.commandファイルを個別に作成して、その.commandファイルの中でPythonファイルをキックしても良いのですが、.commandファイルの先頭に、シバンライン(#!で始まる実行するスクリプトを指定する文字列)を追加する方が1ファイルで済むのでシンプルです。 例えば次のように書きます。(ファイルの中身だけ書いています。chmod等は先ほどのファイルと同じように実行しておいてください)

#!/usr/bin/env python
for i in range(5):
    print(i)

(2行目以降はただのサンプルのPythonコードです。)

これで、拡張子を .py ではなく .command にして実行権限を付与しておけばダブルクリックで動くPythonファイルの完成です。

Pythonで100%積み上げ棒グラフを描く

個人的な好みの話ですが、僕は割合の可視化は円グラフより100%積み上げ棒グラフの方が好みです。前職時代はこの種の可視化はもっぱらTableauで描いていたのですが転職して使えなくなってしまったので、Pythonでサクッと描ける方法をメモしておきます。

最初、matplotlibのbarやbarhでbottomやleftを逐一指定してカテゴリごとに描いていく方法を紹介しようと思っていたのですが、pandas.DataFrameのメソッドを使った方が簡単だったのでそちらを先に紹介します。

データの準備

サンプルデータが必要なので適当にDataFrameを作ります。地域ごとに、どの製品が売れているか、みたいなイメージのデータです。

import pandas as pd


data = {
    '製品A': [20, 30, 50],
    '製品B': [60, 70, 40],
    '製品C': [20, 0, 10]
}
df = pd.DataFrame(data, index=["地域1", "地域2", "地域3"])

print(df)
"""
     製品A  製品B  製品C
地域1   20   60   20
地域2   30   70    0
地域3   50   40   10
"""

こちらを地域別に、売れている製品の内訳を可視化していきます。
これも個人的な好みの話ですが、棒グラフは縦向きではなく横向きにします。(ラベルが日本語だとその方が自然な結果になりやすいからです。)

シンプルにやるには、jupyter環境であれば以下のコードだけで目的のグラフが表示されます。

# データの正規化
df_normalized = df.div(df.sum(axis=1), axis=0)
# 横向きの積み上げ棒グラフの描画
df_normalized.plot(kind='barh', stacked=True)

少し脇道に逸れますが、みなさん、この行ごとに正規化するdf.div(df.sum(axis=1), axis=0) ってやり方ご存知でした?昔、「Pandasのデータを割合に変換する」って記事を書いたときは知らなかった方法でした。このときは転置して列ごとに正規化してもう一回転置するという手順を踏んでましたね。

記事の本題に戻ると、plotメソッドのstackedっていう引数をTrueにしておくと棒グラフを積み上げて表示してくれるため目的のグラフになります。
参考: pandas.DataFrame.plot.barh — pandas 2.1.3 documentation

ただ、これで出力すると、グラフの棒の並び順が上から順に地域3, 地域2, 地域1 となり、ちょっと嫌なのと、あとラベル等のカスタマイズも行えた方が良いと思うのでもう少し丁寧なコードも紹介しておきます。(こだわりなければさっきの2行で十分です。)

import matplotlib.pyplot as plt


# データの正規化
df_normalized = df.div(df.sum(axis=1), axis=0)
# 行の順序を逆にする
df_normalized = df_normalized.iloc[::-1]

# 横向きの積み上げ棒グラフの描画
fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)
df_normalized.plot(kind="barh", stacked=True, ax=ax)

# タイトルとラベルの設定(任意)
ax.set_title("地域別の販売製品割合")
ax.set_ylabel("地域")
ax.set_xlabel("割合")
plt.show()

出来上がりがこちら。

はい、サクッとできましたね。

ちなみに、pandasのplotメソッドを使わない場合は棒グラフの各色の部分の左端の位置を指定しながら順番に描くため、次のコードになります。

import numpy as np


# 積み上げ棒グラフを作成するための基準位置
left = np.zeros(len(df_normalized))

fig = plt.figure(facecolor="w")
ax = fig.add_subplot(111)


# 各カテゴリごとにバーを追加
for column in df_normalized.columns:
    ax.barh(
        df_normalized.index,
        df_normalized[column],
        left=left,
        label=column,
        height=0.5
    )
    left += df_normalized[column]

# タイトルとラベルの設定
ax.set_title("地域別の販売製品割合")
ax.set_ylabel('地域')
ax.set_xlabel('割合')

# 凡例の表示
ax.legend()

plt.show()

明らかに面倒なコードでしたね。pandasがいい感じに調整してくれていた、棒の太さ(横向きなので今回の例では縦幅)なども自分で調整する必要があります。
Pythonでコーディングしてるのであれば大抵の場合は元データはDataFrameになっているでしょうし、もしそうでなくても変換は容易なので、Pandasに頼った方を採用しましょう。

Lightsail(WordPress)の2023年11月時点のディレクトリ構造

前回の記事で書いた通り、このブログが稼働しているサーバーを新しいインスタンスに移行しました。その中で各作業をやっていてディレクトリ構成が昔と変わっているのが目についたので色々メモっておきます。

自分都合で申し訳ないのですが、今の構造自体の話よりも5年前の先代のインスタンスとの差分に着目して記録しています。自分の記事や手元に残したメモと対比するためです。

バージョンの違い

いつのバージョンと比較するのか明確にするために自分の新旧インスタンスの除法を載せておきます。このブログは2019年の1月から運用していますがサーバーを立てたのはその直前の2018年12月です。両インスタンスにSSHでログインして気づいたのですが、そもそもOSから違いますね。UbuntsだったのがDebianになってます。またアプリも「the Bitnami WordPress」が 「the WordPress packaged by Bitnami」に変わったようです。これは色々変更されていても納得です。

旧インスタンス

Welcome to Ubuntu 16.04.5 LTS (GNU/Linux 4.4.0-1128-aws x86_64)
       ___ _ _                   _
      | _ |_) |_ _ _  __ _ _ __ (_)
      | _ \ |  _| ' \/ _` | '  \| |
      |___/_|\__|_|_|\__,_|_|_|_|_|

  *** Welcome to the Bitnami WordPress 4.9.8-0 ***
  *** Documentation:  https://docs.bitnami.com/aws/apps/wordpress/ ***
  ***                 https://docs.bitnami.com/aws/ ***
  *** Bitnami Forums: https://community.bitnami.com/ ***

新インスタンス

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
       ___ _ _                   _
      | _ |_) |_ _ _  __ _ _ __ (_)
      | _ \ |  _| ' \/ _` | '  \| |
      |___/_|\__|_|_|\__,_|_|_|_|_|

  *** Welcome to the WordPress packaged by Bitnami 6.3.1-23        ***
  *** Documentation:  https://docs.bitnami.com/aws/apps/wordpress/ ***
  ***                 https://docs.bitnami.com/aws/                ***
  *** Bitnami Forums: https://github.com/bitnami/vms/              ***

WordPressのインストール先の変更

WordPressの利用のために使っているインスタンスですからこれが一番重要です。

旧インスタンスでは、Homeディレクトリにappsってディレクトリがあってその下にwordpressもありました。その配下にある、
/home/bitnami/apps/wordpress/htdocs
がドキュメントルートです。wp-config.php などのファイルもここにありました。

新環境では ~/apps はなくなり、wordpressは、/opt/bitnami の配下に移動しています。
/opt/bitnami/wordpress
がドキュメントルートで、wp-config.phpなどはこちらにも移動しています。

Google アドセンスのads.txtの置き場所もこちらへ変更です。

他のブログを見てると、/opt/bitnami/apps ってディレクトリに言及してる人もいるので、もしかしたら時代によっては /opt/bitnami の前に、そこが使われた頃もあったのかもしれません。

DB関係のディレクトリパスの変更

そもそもとして、インストールされているDBがMySQLからMariaDBに変わりました。

# 旧インスタンス
$ which mysql
/opt/bitnami/mysql/bin/mysql

# 新インスタンス
$ which mysql
/opt/bitnami/mariadb/bin/mysql

結果設定ファイルのmy.confの場所も、
/opt/bitnami/mysql/my.conf
から
/opt/bitnami/mariadb/conf/my.conf
に変わっています。

このmy.confの中を見ると、datadir って変数でDBのデータの実態ファイルの書くのディレクトリが指定されていますが、
/opt/bitnami/mysql/data
から
/bitnami/mariadb/data
へと変更されています。

/bitnami ってディレクトリ自体も新設されたものですね。

アップロードした画像等の配置場所

実はアップロードした画像なども先ほど書いた、/bitnami 配下に格納されています。データ関連の実態ファイルを一箇所にまとまったイメージでしょうか。

/opt/bitnami/wordpress で ls -la すると、
wp-content から /bitnami/wordpress/wp-content へシンボリックリンクが貼られていることがわかります。

実は、 wp-config.php も実態のファイルはここにあって、ドキュメントルートにあるのはシンボリックリンクです。

なんか、こう見るとads.txtも正しい置き場所はここで、ドキュメントルートにはシンボリックリンクで対応するべきだったのかなという気がしてきます。

apacheのディレクトリ名変更

これはめっちゃ細かい話ですが、/opt/bitnami/apache2 だったのが、2が取れて /opt/bitnami/apache になりました。ご丁寧にシンボリックリンクが貼ってあって旧パスでもたどり着けるようになっています。

$ ls -la | grep apache
drwxr-xr-x 16 root    root    4096 Oct 10 05:24 apache
lrwxrwxrwx  1 root    root       6 Oct 10 05:24 apache2 -> apache

この下に logs があってそこにアクセスログ等があるのは変わらずです。
/opt/bitnami/apache/logs
に access_log / error_log の名前で入ってます。

LightsailのWordPressを新しいインスタンスに移行する手順

更新遅延について

いつも月曜日更新ですが今週は更新が遅れました。といいうのも、このWordpressが稼働しているLightsailのインスタンスがメモリ不足になり、記事の一覧や新規作成ページが開けなくなったからです。
PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 20480 bytes)
みたいなエラーがエラーログに出ていました。

また、前々からPHPのバージョンが古い(7.0.3.1)ことで管理画面で警告が出ておりいつか対応しないといけないという状況でもありました。

LightsailのwordpressはBitnamiというソフトで稼働しているのですが、実はこれはPHPのバージョンアップができません。PHPのバージョンを上げたければインスタンスを作り直さないといけないという罠があります。

以上の二つの理由で、PHPのバージョンを上げないといけないし、メモリも増やさないといけないしとなったので、Wordpressの移行作業を行いました。

やり方はいろいろあると思うのですが、今回は「All-in-One WP Migration」というプラグインを使いました。

失敗した方法と正しい順序

実は一回しくじったのでそれについて先に書きます。

ブログにアクセスできない時間を最小限にするために実は最初、次の手順でやろうとしていました。

  1. 新しいWordpressのインスタンスを立てる。
  2. 新しいインスタンスに記事やメディアのデータを移行する。
  3. IPアドレスやドメインを新しいインスタンスに移す。
  4. https化などの追加作業を行う。

こうすれば、切り替えた瞬間から訪問者の方が記事を読めると思ってやったのですが、All-in-One WP Migration のプラグインが気が利きすぎていて困ったことが起きました。

それは記事中のテキストや、リンク等に含まれるこのブログのURLが、移行先のインスタンスのURLに勝手に書き換えられてしまったことです。要するに、https://analytics-note.xyz/ って文字列があったら全部、 http://{IPアドレス}/ になってしまいました。

そして、IPアドレスやドメインを設定しても置換されたテキストは元に戻りませんでした。
これを全部治すのは面倒だったので、そのインスタンスは破棄してやり直しました。記事を読めない時間が発生するのは諦めて、次の手順で行い、今のところ正常に動いているようです。

  1. 新しいWordpressのインスタンスを立てる。
  2. IPアドレスやドメインを新しいインスタンスに移す。
  3. https化などの追加作業を行う。
  4. 新しいインスタンスに記事やメディアのデータを移行する。
  5. 残作業の実施。

それでは、細かく手順を説明していきますね。

旧インスタンス側の作業

データのエクスポート

移行するためにデータをローカルに取り出します。先述の通り「All-in-One WP Migration」ってプラグインを使いました。プラグインの一覧から有効化すると左部メニューに登場しますので、エクスポートを選択します。エクスポート先が選べるので、「ファイル」を選ぶとそのままダウンロードが始まります。僕のブログは画像とか少ないのですがそれでも250MBくらいになりました。ちなみに、オプションがいくつかあって僕は次の二つ選びました。これはあまり重要ではないと思います。

  • スパムコメントをエクスポートしない
  • 投稿リビジョンをエクスポートしない

リダイレクト処理の停止

これは、httpやIPアドレスでのアクセスをリダイレクトしてる人だけの作業なので必須ではありません。

旧環境からはドメインを剥奪するので今後はIPアドレス直打ちでしかアクセスできなくなります。僕は普通にドメインでアクセスされなかったらリダイレクトするように設定していたのでそれを戻します。要するに次の記事の逆の手順を行いました。

参考: httpのアクセスをhttpsにリダイレクトする

以上で旧環境での作業は終了です。

新環境作業

インスタンス作成

続いて新環境側の作業ですが、最初にやるのは当然ですが新環境の作成です。以下の記事を参考に新しいインスタンスを立てます。静的IPアドレスは旧環境で使ってたのを使い回すのでインスタンスだけ立てたら大丈夫です。

参考記事: Amazon Lightsail で WordPressサイトを作成する手順

今回はメモリ不足の解消を兼ねていたので、$5の、1GB/2vCPU/40GB SSD を選択しました。

ここでちょっとLightsailの宣伝なのですが、最近$3.5や$5のインスタンスでもvCPUが2個になってます。以前は1個だったので同じ値段で倍のCPUが使える計算になりますね。とはいえ、僕が不足したのはCPUではなくメモリだったので結局以前より高いプランに変えたのですが。これは、特に何か理由がない人も移行するメリットがありそうです。

静的IPの付け替え

失敗した方法のところで説明しましたが、インスタンス作ったらもう早速IPアドレスとそれに紐づいているドメインを新インスタンスに渡します。

インスタンスができたらSSH接続してパスワードを確認します。pemキーはデフォルトでは同じのが使えるのでIPアドレスだけ変えたら繋がります。

GUIで簡単に作業できます。

Lightsail の ネットワーキングにアクセスし、静的IPアドレスを選択。
管理からデタッチし、インスタンスへのアタッチでプルダウンから新しいWordpressを選んでアタッチ、と画面に沿って作業していけば完了です。

これでもう、URL叩いたら新しいWordpress(記事とか何もない初期画面)が開くようになります。

今回、PHPのバージョンアップも目的の一つだったので、SSHで繋いで $php –version してみておきましょう。無事に上がっていますね・

$ php --version
PHP 8.2.11 (cli) (built: Oct  6 2023 09:57:45) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.2.11, Copyright (c) Zend Technologies
    with Zend OPcache v8.2.11, Copyright (c), by Zend Technologies

https化

Let’s Encryptを使ってhttps化します。

最近は昔と手順が変わっていて、ssh接続して以下のコマンドを叩いてプロンプトに従っていくだけでできるようです。ドメイン名とかメールアドレスを途中で聞かれます。

$ sudo /opt/bitnami/bncert-tool

パスワードの確認と初回ログイン

https化したらデータの移行作業へと入っていきます。wordpressの管理画面にログインしないといけないので初期パスワードを確認します。

SSH接続したホームディレクトリの中に、bitnami_application_password
ってファイルがあり、その中に書かれています。

これを使ってログインします。

データのインポート

All-in-One WP Migrationを使ってインポートするので、最初にやるのはこのプラグインの有効化です。ついでに最新化もしておきましょう。

左メニューにImportが追加されるのでそれを選びます。すると、実はこの時点ではアップロードできるファイルの上限が設定されているのでそれを確認します。おそらくインスタンスサイズによって異なります。

僕の場合は、 Maximum upload file size: 80 MB. と表示されていて、保存していたファイルがそのままだと上げられませんでした。

そこで、PHPの設定を変更してアップロードできるファイルサイズを一時的に増やします。

以下のファイルを開きます。
/opt/bitnami/php/etc/php.ini

その中に、post_max_size と、 upload_max_filesize って設定があるので、それぞれ80Mを512Mとかに書き換えます。(修正前にファイルはバックアップ取りましょう。)

書き換えたら、次のコマンドで再起動します。

$ sudo /opt/bitnami/ctlscript.sh restart

これでアップロードできるファイルサイズが上がるので、エクスポートしていたファイルをアップロードします。

PHP7からPHP8への移行だったので色々警告とか出ましたが仕方ないので全て認めて作業進めます。

この時点で、記事や画像等のメディア、テーマ、小テーマ、プラグイン情報などが移行されほぼほぼ元通り見れるサイトになります。

/opt/bitnami/php/etc/php.ini の中身は元に戻して、もう一回再起動しておきましょう。

再ログインとDB更新

データをインポートすると、wordpressのユーザー情報、要するにパスワードなども復元されるので一回セッションが切れてログアウトします。

次にログインするときは、旧環境のパスワードでログインするので間違えないようにしてください。

ただし、パスワードが戻るのはアプリ(wordpress)のユーザーだけで、MySQLコマンド等で接続するときに使う、DB自体のパスワードは新しい環境のパスワードそのままです。あくまでも書き換わるのはDBの中身だけってことですね。

wordpressの管理画面に再ログインすると、「データベースの更新が必要です」ってメッセージが出るので更新します。

これでほぼ移行完了です。

残作業

残りは細かい残作業やっていきます。

例えばこれとかやる必要があります。
WordPressの一般設定にあるURLをhttpsに変える
LightsailのWordPressにads.txtを設置する
– 不要になったインスタンスの削除

ads.txtの設置場所は
/opt/bitnami/wordpress
に変わってました。

また、旧インスタンスは即消すのではなくしばらく並行しておいておく方がお勧めです。ぼくもしばらくは放置しておこうと思っています。

この後も何か残作業見つかったら順次対応していこうと思います。