乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

プログラミング

今回はアプリをリリースしたので
そちらのご報告です。

解説動画はこちら



アプリについて


今回作成したアプリはこちらです

・チャンクde英会話

iOS
AppStore


Android
Googleplay

良かったら使ってみてください


アプリの作成方法

Flutterを用いて作成しました

Flutterにした理由は
 1.iOSとAndroid両対応であること
 2.学習コストが低い
 3.文献が豊富で有る

以上の理由からFlutterにしてみました。

それ以外にもアプリを作る方法は
いくらでもありますが、今回は
Flutterを使ってみました。

開発言語がDartになるので
1から勉強することになります。

また、コードやデータなどの大半は
ChatGPTを用いて作成していますので
実質ChatGPTに頼れば
アプリの開発は容易かと思います。

学習開発の期間で2ヶ月くらいでした。



リリースについて

コードを実装しシミュレータや
実機でのテストが終わったら
ビルドを行なってアプリを作り
リリース準備ができます。


その前にアプリストアのアカウントがないと
そもそもリリース出来ないので
アプリストアのアカウント取得が必要です。

これもそこそこ手間と時間が掛かります。

リリース登録をしたら審査が行われますが
審査に通らなければビルドからやり直し
審査に通るまでの繰り返しです。

審査に通ったらようやく
リリース、アプリ配信ができる様になります。

リリース作業を始めてから
アカウント登録とアプリリリースまでで
大体1ヶ月くらい掛かっています。



Flutterについて

動画の方では少しだけ解説していますが
FlutterはiOSとAndroidの両方のアプリを
作成する事ができる開発フレームワークです。

Flutterをインストールしたら
VSCodeなどでコードを書き進める事が
できる様になります。

テストやビルドなども
Flutterコマンドを用いて行う形になります。

この辺りも
VSCodeと合わせておくと
開発が楽になるかなと思います。

どんな感じなのかは
動画の方で解説していますので
参考にしていただければと思います。


最後に

これからアプリ開発を行いたい方にとっては
色々な選択肢があると思いますが
Flutterを使ってアプリを開発したい方が
増えていただけたら幸いです。

Python言語の解説と共に
アプリ開発の方も進めていきますので
要望などあれば是非コメントいただければと思います。

それでは。

プログラミング未経験の方のための
プログラミング学習講座を作成しました

その名も
「1時間で学べるPythonプログラミング」


講義動画はこちら




この講座は初学者の方が
短時間でPython言語を学ぶことのできる
プログラミング学習用の講座です

プログラミングが分からないない方は
Python言語を通じて
プログラミングの基礎を学習できます

講座は動画に加えてGoogle Colabを用いて
手元でコードを動かすことのできます
コードがどう動くのかを確認をしながら
進めていってください

資料はここ:
Google Colabの資料


00:00 1.はじめに
02:13 2.導入方法
02:55 3.GoogleColaboratoryの操作方法
06:19 4.Pythonの計算の基礎
27:27 5.Pythonの制御文
42:14 6.Pythonのクラス
49:11 7.Pythonのその他構文
64:30 8.まとめ

なおPythonチートシートを作成しています。

コーディングに迷った際に役に立ち

WEB検索する時間を無くして

作業時間を効率化できます。

note
Pythonチートシート


 

今回はゲーム理論にまつわる
プログラミング小噺です。

解説動画はこちら



ブライスのパラドックスとは

今回はプログラミングにまつわるものの小話です。

ドイツのルール大学の数学者
ディートリヒ・ブライスによって提唱されたもので

移動時間の短縮を目的としてネットワーク中に新たに
最短流路を作ったにもかかわらず、移動時間の短縮どころか
逆に移動時間が増加する場合があるという
交通工学におけるパラドックスのことです。


Start -  End の経路をドライバーが移動する事を考えます。
A,B2つのルートが存在します。

ブライスのパラドックス2.003


この状態ではA,Bルートともに
かかる時間は80分になっています。

ブライスのパラドックス2.004


ここで新たなルート A - B を作ります。
ここを通ると元々Start - End を80分で行けていたところが
70分に短縮されそうですが

ブライスのパラドックス2.005

みんな一番時間の掛からないルートを通ろうとして
逆に時間が掛かるようになっていまいます。

ブライスのパラドックス2.006

実際にこんな事になっていた道路が有ったようです。

この現象における重要なポイントは
ナッシュ均衡が必ずしもパレート最適ではない
ということです。

他のドライバーの行動を考えずに自身が最短経路を辿ろうとすると
逆に最適な状態にならない、というのがこのパラドックスにおける
重要なポイントです。

この考え方は、さまざまな分野で応用されています。

送電網
通信網
スポーツのチーム編成
選挙の投票 etc


プログラミングの分野でも表立って見えないですが
最適な状態を目指すために色々な所で使われています。

分散システムのリソース配分の最適化
レコメンドエンジンの推薦システム
広告配信のオークション
etc



ブライスのパラドックス2.014


今回はプログラミングに直接的には関わらないけど
裏ではすごく重要な考え方である
ブライスのパラドックスについて
お伝えしました。

今日はここまでです
それでは。




今回はZIPとかPDFのパスワードを
総当たりで開けに行くやつのご紹介です

解説動画はこちら




ファイルにパスワード掛かっていて開けられないよーーー
どうにかならない?

そんな時ありますよね!!!!

そんな時はプログラムで突破すればいい

今回は
ZIPやPDFのパスワードを破るプログラム
についての解説です。


 
ライブラリのインストール

Google Colab上で
ZIPやPDFを扱うのに必要な
ライブラリのインストールです。
pip install pyzipper
pip install pypdf
pip install reportlab



パスワード付きファイルの用意

パスワード付きのZIP , PDFファイルを
作成しておきます。

今回のパスワードは
a12
として
PDFファイルや
ZIP用のファイルを先に作っておきます。

lock_password = "a12"

from reportlab.pdfgen import canvas
from PIL import Image

# PDFを作成
pdf_filename = "original.pdf"
c = canvas.Canvas(pdf_filename)
c.drawString(100, 750, "Hello, this is a sample PDF file.")
c.drawString(100, 730, "This PDF was created using Python and reportlab.")
c.save()

# ZIP用の内包ファイルを用意
with open("secret.txt","w") as _w:
    _w.write("This is a secret message")

Image.new("RGB", (100, 100), (255, 255, 255)).save("image.png")


パスワード付きのZIPファイルを作るコード
import pyzipper

# パスワード付きzipファイルを作成する
def create_protected_zip(file_list, zip_name, password):
    with pyzipper.AESZipFile(zip_name, 'w', compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES) as zf:
        zf.setpassword(password.encode("utf-8"))
        for file in file_list:
            zf.write(file)
    print(f"[+] パスワード付きZIPを作成: {zip_name}")

create_protected_zip(["secret.txt", "image.png"], "protected.zip", lock_password)


PDFにパスワードをかけるコード
from pypdf import PdfReader, PdfWriter

# パスワード付きpdfファイルを作成する
def create_protected_pdf(input_pdf, output_pdf, password):
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page in reader.pages:
        writer.add_page(page)

    writer.encrypt(password)  # パスワードを設定
    with open(output_pdf, "wb") as f:
        writer.write(f)
    print(f"[+] パスワード付きPDFを作成: {output_pdf}")

create_protected_pdf("original.pdf", "protected.pdf", lock_password)




ZIPとPDFファイルのパスワードを破るコード


ZIPとPDFに対応
総当たりでパスワードを当てにいくものです
一応辞書にも対応しています。
import itertools
import string
import zipfile
import pyzipper
from pypdf import PdfReader

def extract_zip(file_path, password):
    """ ZIPファイルの解凍 (ZIPCrypto & AES-256対応) """
    password_bytes = password.encode("utf-8")  # バイト列に変換
    try:
        with zipfile.ZipFile(file_path) as zf:
            zf.extractall(pwd=password_bytes)
        print(f"[+] ZIP のパスワード発見: {password}")
        return True
    except (RuntimeError, NotImplementedError):
        pass

    try:
        with pyzipper.AESZipFile(file_path) as zf:
            zf.setpassword(password_bytes)
            zf.extractall()
        print(f"[+] PDF のパスワード発見: {password}")
        return True
    except Exception as e:
        return False

def try_password(file_path, file_type, password):
    """ ZIP または PDF のパスワードを試す """
    try:
        if file_type == "zip":
            return extract_zip(file_path, password)
        elif file_type == "pdf":
            reader = PdfReader(file_path)
            if reader.decrypt(password) == 0:
                raise ValueError("Incorrect password")
            print(f"[+] PDF のパスワード発見: {password}")
            return True
    except Exception:
        return False

def password_cracker(file_path, mode="brute", password_list=None, max_length=4):
    """ ZIP/PDFファイルのパスワードを解析 """
    if file_path.endswith(".zip"):
        file_type = "zip"
    elif file_path.endswith(".pdf"):
        file_type = "pdf"
    else:
        print("[-] サポートされていないファイル形式です")
        return

    print(f"[*] {file_path} のパスワード解析開始 (モード: {mode})")

    count = 0

    # **辞書攻撃**
    if mode == "dictionary" and password_list:
        with open(password_list, "r", encoding="utf-8") as file:
            for password in file:
                count += 1
                if try_password(file_path, file_type, password.strip()):
                    print(f"[+] 試行回数: {count}")
                    return

    # **総当たり攻撃**
    elif mode == "brute":
        characters = string.ascii_lowercase + string.digits  # "abcdefghijklmnopqrstuvwxyz0123456789"
        for length in range(1, max_length + 1):
            for password in itertools.product(characters, repeat=length):
                count += 1
                if try_password(file_path, file_type, "".join(password)):
                    print(f"[+] 総当たり攻撃試行回数: {count}")
                    return

    print("[-] パスワードが見つかりませんでした")

使い方は
# 実行例(ZIP)
password_cracker("protected.zip", mode="brute", max_length=3)
[*] protected.zip のパスワード解析開始 (モード: brute)
[+] PDF のパスワード発見: a12
[+] 総当たり攻撃試行回数: 2333

こんな感じで、開けることができます。

max_length
を増やせば、大きな桁数のパスワードにも対応

文字種を増やしたい場合は
コード上の変数 characters
これに文字列を追加してください。




おまけ


文字種と文字数による
パスワードの強度についてです。

今回は文字種の数が 36種類
パスワード文字数が3桁でした

文字種36個の時の
パスワード桁数による最大試行回数は
以下のようになります。


パスワードの長さによる最大試行回数 : 文字数 01 :  0000000000000036
パスワードの長さによる最大試行回数 : 文字数 02 :  0000000000001296
パスワードの長さによる最大試行回数 : 文字数 03 :  0000000000046656
パスワードの長さによる最大試行回数 : 文字数 04 :  0000000001679616
パスワードの長さによる最大試行回数 : 文字数 05 :  0000000060466176
パスワードの長さによる最大試行回数 : 文字数 06 :  0000002176782336
パスワードの長さによる最大試行回数 : 文字数 07 :  0000078364164096
パスワードの長さによる最大試行回数 : 文字数 08 :  0002821109907456
パスワードの長さによる最大試行回数 : 文字数 09 :  0101559956668416
パスワードの長さによる最大試行回数 : 文字数 10 :  3656158440062976



一般的なPCであれば7-8桁くらいまでなら
開けられそうですね


これ以上となると
少し工夫が必要になってきます。

まあ、会社で使うパスワードは
6桁くらいまでに抑えておいた方が
いざというとき開けられなくなるんで
いいかもしれません


今回はパスワードが開けられなくて困った際の
パスワード開けプログラムについてでした

今日はここまでです
それでは。

今回はPythonの闇挙動についてです。


解説動画はこちら


 
Pythonの闇挙動10選


Pythonにはその独特の仕様があり
コードの書き方で思わぬ挙動を引き起こします。

そんな
変てこりんな挙動を10選んでみました。



1.is演算子の罠
(256 == 256 はTrueでも 257 == 257 はFalse!?)
a = 256
b = 256
print(a is b)  # True

c = 257
d = 257
print(c is d)  # True or False !?!?
True
False


Pythonは 小さい整数(-5~256)のオブジェクトを
キャッシュ する最適化を行っています。

そのため、a と b は同じオブジェクトを参照しており
is が True になります。

しかし、257 はキャッシュ対象外なので
c と d は別のオブジェクトとなり is が False になります。

対策:

値の比較には is ではなく == を使うべき





2. ミュータブルなデフォルト引数の罠

ミュータブル :
作成後にもその状態を変えることの出来るオブジェクトのこと

イミュータブル :
作成後にその状態を変えることのできないオブジェクトのこと

デフォルト引数に空のリストを設定した
関数を作ります。

これを実行してみると
# デフォルト引数に空のリストを設定した関数
def append_to_list(value, my_list=[]):
    my_list.append(value)
    return my_list

print(append_to_list(1))  # [1]
print(append_to_list(2))  # [1, 2] (???)
print(append_to_list(3))  # [1, 2, 3] (!!!)
[1]
[1, 2]
[1, 2, 3]


Pythonの関数のデフォルト引数は
関数が定義されたときに評価 され
一度作られたオブジェクトが 再利用 されます。

そのため my_list は毎回新しくなるわけではなく
前回の呼び出し時の変更が次の呼び出しに影響を与えます。

対策:

デフォルト引数にはミュータブルな値
(list, dict など)を使わず、None を使うようにする




3. += と + の違い(リストの参照問題)

リストを代入して新しいリストを作って
元のリストに要素を加えると...
# a のリストをコピーして新しいリスト b を作ってみよう
a = [1, 2, 3]
b = a
a += [4, 5]
print(a)  # [1, 2, 3, 4, 5]
print(b)  # [1, 2, 3] or [1, 2, 3, 4, 5] ?!?
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]


a += [4, 5] は a.extend([4, 5]) のように
リスト自体を変更 するため
b も同じオブジェクトを参照しているので
b にも変更が反映される

対策:

新しいリストを作成したい場合は + を使う
a = [1, 2, 3]
b = a
a = a + [4, 5]  # 新しいリストが作られる
print(a)  # [1, 2, 3, 4, 5]
print(b)  # [1, 2, 3] (bは変更されない)
[[1, 2, 3, 4, 5]
[1, 2, 3]



4. sorted() と sort() の違いに注意

リストの並び替えを行う方法は大きく2種類あります
a = [3, 1, 2]
print(sorted(a))  # [1, 2, 3]
print(a)  # [1, 2, 3] or [3, 1, 2] (???)

a.sort()
print(a)  # [1, 2, 3]
[1, 2, 3]
[3, 1, 2]
[1, 2, 3]



sorted(a) は 新しいリストを返す ため
a 自体は変更されない

a.sort() は リストを直接変更する ため
a の内容が書き換わる

対策:

リストをそのまま並び替えたい場合は リスト.sort()
元のリストを残したい場合は sorted(リスト) を使う




5. 0.1 + 0.2 == 0.3 が False になる!?

Pythonで小数点の値を比較すると...
print(0.1 + 0.2 == 0.3)  # False (???)
print(0.1 + 0.2)  # 0.3 ?!?!
False
0.30000000000000004


浮動小数点の計算誤差が原因。
0.1 や 0.2 は 2進数で正確に表現できないため
足すと誤差が生じる。

対策:

誤差を考慮して math.isclose(値 , 比較値) を使う
import math
print(math.isclose(0.1 + 0.2, 0.3))  # True
True



6."" or "Hello" は "Hello" なのに "0" or "Hello" は "0" になる!?

print("" or "Hello")  # Hello
print("0" or "Hello")  # 0 or "Hello" (???)
Hello
0



or は 最初に「真」と評価された値を返すため
""(空文字)は False なので "Hello" が返る

しかし "0" は 非空の文字列であり True と評価される ため
そのまま "0" が返る。

対策:

文字列の比較をする場合は bool(value) を明示的に使う




7.sum() で文字列を合計するとエラーになるのに max() は動く!?

print(max(["a", "b", "c"]))  # c
c
print(sum(["a", "b", "c"]))  # TypeError (???)
TypeError


sum() は 数値の合計を計算する関数 なので
文字列を足そうとするとエラーになる

max() は 「大きい方を返す」関数 なので
辞書順で "c" を返す

対策:

文字列の連結には sum() ではなく
"".join() を使う





8.range() の「スタート」には 0 が入るのに slice() には入らない!?

print(list(range(5)))  # [0, 1, 2, 3, 4]

print("hello"[slice(5)])  # hello (???)
print("hello"[slice(None, 5)])  # hello (???)
print("hello"[slice(5, None)])  # (空文字)
[0, 1, 2, 3, 4]
hello
hello



range(5) は デフォルトの開始値が 0 になるので
[0, 1, 2, 3, 4] になる

slice(5) は デフォルトの開始値が None になり
slice(None, 5) と解釈される

slice(5, None) は 5 以降の文字を取得しようとするが
範囲外なので空文字になる




9.set の順番がランダムに見える!?

文字列のデータをSET型のデータにしてみると
s = set("hello world")
print(s)
{'r', 'd', 'o', 'l', 'h', 'e', 'w', ' '}

set は 順序を保持しないデータ構造 のため
出力される順番は内部のハッシュ値によって変わる

対策:

順番を維持したいなら set ではなく
OrderedDict や list を使う

from collections import OrderedDict
ordered_set = "".join(OrderedDict.fromkeys("hello world"))
print(ordered_set)
helo wrd



10. dict.keys() の結果は list じゃない!?

d = {"a": 1, "b": 2}

print(d.keys())  # dict_keys(['a', 'b'])
print(type(d.keys()))  # 
print(list(d.keys()))  # ['a', 'b'] (明示的にリスト化)
dict_keys(['a', 'b'])
< cla ss 'dict_keys'>
['a', 'b']

d.keys() は 「ビューオブジェクト」 であり
リストではない
そのため、リストと同じように扱えないことがある

対策:

リストとして扱いたい場合は
list(d.keys()) を使う




まとめ

リストの操作や比較演算子周りには
意外と知られていない挙動が多い

1文字違うだけで別の挙動になったり
操作の順番で意図しない結果になったりする

細かい仕様を把握する必要ありますねー

ということで
今回はバグを生みやすい
Pythonの変な挙動10選についてでした。

それでは。

このページのトップヘ