テキスト音声読上げ
入力テキストをpyttsx3で読み上げるアプリ。速度・声質・音量の調整とテキスト読込・音声ファイル保存機能付き。
1. アプリ概要
書き上げた原稿を耳で確かめたいとき、貼り付けてボタンを押すだけで読み上げられれば画面を見続けずに済みます。このアプリは入力欄の文章を pyttsx3 で読み上げ、速度・声質・音量の調整とテキストファイルの読み込み、音声ファイルへの保存までを1つのウィンドウで扱います。
読み上げは「。」「!」「?」や改行で区切った1文ずつ進み、「読み上げ箇所をハイライト」がオンなら今読んでいる文に色が付きます(_do_speak())。
コントロール行の「💾 音声を保存」ボタン(133〜134行)は、読み上げ用の say() を呼ばずに音声ファイルを書き出します。保存先は filedialog.asksaveasfilename() で選び、拡張子は .wav が既定です(_save_audio()・247〜274行)。書き出しは pyttsx3 の save_to_file() と runAndWait() に任せているため、できあがるファイルの形式は使っている音声エンジンによって変わります(章9の冒頭に当サイトで確認できた範囲を書いています)。
2. 機能一覧
- 「▶ 読み上げ開始」ボタン(
_speak())・「⏹ 停止」ボタン(_stop())・「📂 テキストを開く」ボタン(_open_file())・「💾 音声を保存」ボタン(_save_audio())・「🗑 クリア」ボタン(lambda: self.text_input.delete('1.0', tk.END))で操作 - イベント
<<ComboboxSelected>>から_on_voice_change()を実行 filedialog.askopenfilename()によるファイル選択ダイアログfiledialog.asksaveasfilename()によるファイル保存ダイアログmessagebox.showerror()・messagebox.showwarning()によるダイアログ通知- ウィンドウはタイトル「テキスト音声読上げ」・サイズ 800x709 で起動
3. 事前準備・環境
Python 3.11 以上(Ubuntu で 3.14・3.15 の起動を確認) / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は必要なライブラリ(pip install)を入れれば動作する想定ですが、未検証です。
- Python 3.11 以上(Ubuntu で 3.14・3.15 の起動を確認)
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import threading
import os
try:
import pyttsx3
PYTTSX3_AVAILABLE = True
except ImportError:
PYTTSX3_AVAILABLE = False
class App099:
"""テキスト音声読上げ (TTS)"""
def __init__(self, root):
self.root = root
self.root.title("テキスト音声読上げ")
self.root.geometry("800x709")
self.root.minsize(800, 709)
self.root.configure(bg="#1e1e1e")
self._engine = None
self._speaking = False
self._build_ui()
if PYTTSX3_AVAILABLE:
self._init_engine()
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
def _init_engine(self):
try:
self._engine = pyttsx3.init()
voices = self._engine.getProperty("voices")
self._voices = voices
voice_names = [v.name for v in voices]
self.voice_cb["values"] = voice_names
if voice_names:
self.voice_var.set(voice_names[0])
rate = self._engine.getProperty("rate")
self.rate_var.set(rate)
vol = self._engine.getProperty("volume")
self.vol_var.set(int(vol * 100))
except Exception as e:
self.status_var.set(f"エンジン初期化エラー: {e}")
def _build_ui(self):
header = tk.Frame(self.root, bg="#252526", pady=6)
header.pack(fill=tk.X)
tk.Label(header, text="🔊 テキスト音声読上げ",
font=("Noto Sans JP", 12, "bold"),
bg="#252526", fg="#4fc3f7").pack(side=tk.LEFT, padx=12)
if not PYTTSX3_AVAILABLE:
tk.Label(self.root,
text="⚠ pyttsx3 が未インストールです (pip install pyttsx3)",
bg="#fff3cd", fg="#856404", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X)
# 設定パネル
cfg = tk.LabelFrame(self.root, text="音声設定", bg="#252526",
fg="#ccc", font=("Arial", 9), padx=8, pady=6)
cfg.pack(fill=tk.X, padx=8, pady=4)
# 音声選択
tk.Label(cfg, text="音声:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=0, column=0, sticky="w")
self.voice_var = tk.StringVar()
self.voice_cb = ttk.Combobox(cfg, textvariable=self.voice_var,
state="readonly", width=30)
self.voice_cb.grid(row=0, column=1, padx=4, sticky="w")
self.voice_cb.bind("<<ComboboxSelected>>", self._on_voice_change)
# 速度
tk.Label(cfg, text="速度:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=1, column=0, sticky="w", pady=4)
self.rate_var = tk.IntVar(value=200)
rate_frame = tk.Frame(cfg, bg="#252526")
rate_frame.grid(row=1, column=1, sticky="w", padx=4)
ttk.Scale(rate_frame, from_=50, to=400, variable=self.rate_var,
orient=tk.HORIZONTAL, length=200,
command=lambda v: self.rate_lbl.config(
text=f"{int(float(v))} wpm")
).pack(side=tk.LEFT)
self.rate_lbl = tk.Label(rate_frame, text="200 wpm",
bg="#252526", fg="#ccc", font=("Arial", 8))
self.rate_lbl.pack(side=tk.LEFT, padx=4)
# 音量
tk.Label(cfg, text="音量:", bg="#252526", fg="#ccc",
font=("Arial", 9)).grid(row=2, column=0, sticky="w")
self.vol_var = tk.IntVar(value=100)
vol_frame = tk.Frame(cfg, bg="#252526")
vol_frame.grid(row=2, column=1, sticky="w", padx=4)
ttk.Scale(vol_frame, from_=0, to=100, variable=self.vol_var,
orient=tk.HORIZONTAL, length=200,
command=lambda v: self.vol_lbl.config(
text=f"{int(float(v))}%")
).pack(side=tk.LEFT)
self.vol_lbl = tk.Label(vol_frame, text="100%",
bg="#252526", fg="#ccc", font=("Arial", 8))
self.vol_lbl.pack(side=tk.LEFT, padx=4)
# テキスト入力
tk.Label(self.root, text="読み上げテキスト", bg="#1e1e1e", fg="#888",
font=("Arial", 9)).pack(anchor="w", padx=8, pady=(4, 0))
text_f = tk.Frame(self.root, bg="#0d1117")
text_f.pack(fill=tk.BOTH, expand=True, padx=8, pady=2)
self.text_input = tk.Text(text_f, bg="#0d1117", fg="#c9d1d9",
font=("Arial", 11), relief=tk.FLAT,
insertbackground="white", wrap=tk.WORD,
undo=True)
tsb = ttk.Scrollbar(text_f, command=self.text_input.yview)
self.text_input.configure(yscrollcommand=tsb.set)
tsb.pack(side=tk.RIGHT, fill=tk.Y)
self.text_input.pack(fill=tk.BOTH, expand=True)
self.text_input.insert("1.0",
"Python は汎用プログラミング言語です。\n"
"シンプルな文法と豊富なライブラリが特長です。\n"
"データ分析、Web開発、機械学習など様々な分野で使われています。")
# コントロールボタン
ctrl = tk.Frame(self.root, bg="#1e1e1e", pady=4)
ctrl.pack(fill=tk.X, padx=8)
tk.Button(ctrl, text="▶ 読み上げ開始", command=self._speak,
bg="#1565c0", fg="white", relief=tk.FLAT,
font=("Arial", 12, "bold"), padx=20, pady=6,
activebackground="#0d47a1", bd=0).pack(side=tk.LEFT, padx=4)
tk.Button(ctrl, text="⏹ 停止", command=self._stop,
bg="#c62828", fg="white", relief=tk.FLAT,
font=("Arial", 12), padx=16, pady=6,
activebackground="#b71c1c", bd=0).pack(side=tk.LEFT, padx=4)
ttk.Button(ctrl, text="📂 テキストを開く",
command=self._open_file).pack(side=tk.LEFT, padx=4)
ttk.Button(ctrl, text="💾 音声を保存",
command=self._save_audio).pack(side=tk.LEFT, padx=4)
ttk.Button(ctrl, text="🗑 クリア",
command=lambda: self.text_input.delete("1.0", tk.END)
).pack(side=tk.LEFT, padx=2)
# 読み上げ位置ハイライトON/OFF
self.highlight_var = tk.BooleanVar(value=True)
tk.Checkbutton(ctrl, text="読み上げ箇所をハイライト",
variable=self.highlight_var,
bg="#1e1e1e", fg="#ccc", selectcolor="#3c3c3c",
activebackground="#1e1e1e").pack(side=tk.LEFT, padx=8)
self.text_input.tag_configure("current",
background="#1e4a3d",
foreground="#4fc3f7")
# プログレス
self.progress = ttk.Progressbar(self.root, mode="indeterminate")
self.progress.pack(fill=tk.X, padx=8)
self.status_var = tk.StringVar(
value="テキストを入力して「読み上げ開始」を押してください")
tk.Label(self.root, textvariable=self.status_var,
bg="#252526", fg="#858585", font=("Arial", 9),
anchor="w", padx=8).pack(fill=tk.X, side=tk.BOTTOM)
def _on_voice_change(self, _=None):
if not self._engine:
return
sel = self.voice_var.get()
for v in self._voices:
if v.name == sel:
self._engine.setProperty("voice", v.id)
break
def _speak(self):
if not PYTTSX3_AVAILABLE or not self._engine:
messagebox.showerror("エラー", "pip install pyttsx3")
return
text = self.text_input.get("1.0", tk.END).strip()
if not text:
messagebox.showwarning("警告", "テキストを入力してください")
return
if self._speaking:
return
self._speaking = True
self.progress.start()
self.status_var.set("読み上げ中...")
threading.Thread(target=self._do_speak, args=(text,),
daemon=True).start()
def _do_speak(self, text):
try:
self._engine.setProperty("rate", self.rate_var.get())
self._engine.setProperty("volume", self.vol_var.get() / 100.0)
# 文単位で読み上げ & ハイライト
import re
sentences = re.split(r"(?<=[。.!\?!?\n])", text)
pos = 0
for sent in sentences:
if not self._speaking:
break
if sent.strip():
if self.highlight_var.get():
start = f"1.0 + {pos} chars"
end = f"1.0 + {pos + len(sent)} chars"
self.root.after(0, self._highlight, start, end)
self._engine.say(sent)
self._engine.runAndWait()
pos += len(sent)
except Exception as e:
self.root.after(0, self.status_var.set, f"エラー: {e}")
finally:
self._speaking = False
self.root.after(0, self.progress.stop)
self.root.after(0, self.status_var.set, "読み上げ完了")
self.root.after(0, self.text_input.tag_remove,
"current", "1.0", tk.END)
def _highlight(self, start, end):
self.text_input.tag_remove("current", "1.0", tk.END)
self.text_input.tag_add("current", start, end)
self.text_input.see(start)
def _stop(self):
self._speaking = False
if self._engine:
try:
self._engine.stop()
except Exception:
pass
self.progress.stop()
self.text_input.tag_remove("current", "1.0", tk.END)
self.status_var.set("停止しました")
def _open_file(self):
path = filedialog.askopenfilename(
filetypes=[("テキスト", "*.txt *.md"), ("すべて", "*.*")])
if not path:
return
for enc in ("utf-8", "shift-jis", "cp932"):
try:
with open(path, encoding=enc) as f:
content = f.read()
break
except UnicodeDecodeError:
continue
self.text_input.delete("1.0", tk.END)
self.text_input.insert("1.0", content)
self.status_var.set(f"読み込み: {os.path.basename(path)}")
def _save_audio(self):
if not PYTTSX3_AVAILABLE or not self._engine:
messagebox.showerror("エラー", "pip install pyttsx3")
return
text = self.text_input.get("1.0", tk.END).strip()
if not text:
messagebox.showwarning("警告", "テキストを入力してください")
return
if self._speaking:
messagebox.showwarning("警告", "読み上げ中は保存できません")
return
path = filedialog.asksaveasfilename(
defaultextension=".wav", initialfile="speech.wav",
filetypes=[("WAV 音声", "*.wav"), ("すべて", "*.*")])
if not path:
return
self.status_var.set("音声ファイルを書き出し中...")
self.root.update_idletasks()
try:
self._engine.setProperty("rate", self.rate_var.get())
self._engine.setProperty("volume", self.vol_var.get() / 100.0)
self._engine.save_to_file(text, path)
self._engine.runAndWait()
except Exception as e:
self.status_var.set(f"保存エラー: {e}")
messagebox.showerror("エラー", f"音声ファイルを保存できませんでした:\n{e}")
return
self.status_var.set(f"保存: {os.path.basename(path)}")
def _on_close(self):
self._stop()
self.root.destroy()
if __name__ == "__main__":
root = tk.Tk()
app = App099(root)
root.mainloop()
5. コード解説
テキスト音声読上げ (TTS)のコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App099 クラスにアプリの全機能をまとめています(メソッド11個・全284行)。__init__ ではタイトル「テキスト音声読上げ」とウィンドウサイズ 800x709 を設定します。あわせて状態を保持する変数(self._engine・self._speaking)を初期化し、最後に _build_ui() で画面を組み立てます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×5・tk.Label×9・tk.LabelFrame・ttk.Combobox・ttk.Scale×2・tk.Text・ttk.Scrollbar・tk.Button×2・ttk.Button×3・tk.Checkbutton・ttk.Progressbar で構成しています。見出し付きの枠(LabelFrame)は「音声設定」のラベルでエリアを区切っています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「▶ 読み上げ開始」ボタン(_speak())・「⏹ 停止」ボタン(_stop())・「📂 テキストを開く」ボタン(_open_file())・「💾 音声を保存」ボタン(_save_audio())・「🗑 クリア」ボタン(lambda: self.text_input.delete('1.0', tk.END))を command= で接続しています。また bind("<<ComboboxSelected>>", ...) から _on_voice_change() を呼べるようにイベントも登録しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理
try-except で Exception・ImportError・UnicodeDecodeError を捕捉しています。あわせて messagebox.showerror()・messagebox.showwarning() のダイアログで、ユーザーへの通知・確認を行います。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app099.py と保存します。外部ライブラリ
pyttsx3を使います。先にpip install pyttsx3を実行してください。 -
2クラスの骨格を作る
App099クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("テキスト音声読上げ")・geometry("800x709")・configure(bg="#1e1e1e")・minsize(800, 709)をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×5・tk.Label×9・tk.LabelFrame・ttk.Combobox・ttk.Scale×2・tk.Text・ttk.Scrollbar・tk.Button×2・ttk.Button×3・tk.Checkbutton・ttk.Progressbarを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_speak()・_stop()・_open_file()・_save_audio()・lambda: self.text_input.delete('1.0', tk.END))につなぎます。bind("<<ComboboxSelected>>", ...)の登録も忘れずに。 -
6中心になるメソッドを実装する
アプリの本体である
_highlight()(4行)・_save_audio()(28行)・_speak()(15行)など を実装します。 -
7動作確認する
python app099.pyで起動し、各ボタンが反応することを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 入力したテキストを次回も呼び出せるようにする
「💾 音声を保存」で書き出せるのは音声ファイルだけで、入力欄に打ち込んだテキストそのものを残す処理はありません。アプリを閉じると入力内容は消えます。終了時に入力欄の内容をテキストファイルへ書き出し、次の起動時に読み込んで戻す機能を追加してみましょう。
💡 設定ダイアログ
フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:Noto Sans JP を指名しているのは見出しの「🔊 テキスト音声読上げ」だけで、設定欄・入力欄・読み上げと停止のボタン・ステータスバーは Arial で統一されています。読み上げの声を左右するのは「音声」欄の選択と速度・音量のスライダーです。入力欄を読みやすくしたいときは、self.text_input の font=("Arial", 11) の書体名を日本語の字形を持つフォントに差し替えます。ボタンやラベルの font 引数を消した場合は、OS 既定の書体と大きさで描かれます。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("800x709") の固定サイズで起動するためです。
解決法:geometry() と minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(800, 709) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。読み上げには pyttsx3 が必要です。入っていないときは「▶ 読み上げ開始」を押すとインストールを促すエラーが出ます(170-172行)。読み上げは OS の音声機能を使うので、音が出ても困らない場所で試してください。「💾 音声を保存」は、読み上げ用の say() を呼ばずに save_to_file() だけを使う実装です。当サイトで音が出ないことを確かめたのは Linux Mint(pyttsx3+libespeak-ng)の環境で、確認できたのは .wav ファイルが作られるところまでです。Windows(SAPI5)での保存は未確認です。macOS では拡張子に関わらず AIFF 形式で書き出されるという情報がありますが、当サイトでは確認できていません。なお、書き出した音声ファイルを配布したり仕事で使ったりする場合は、使っている音声エンジン(Windows なら OS 同梱の音声、Linux なら espeak-ng など)の利用条件を先に確認してください。掲載コードの MIT ライセンスはコードの使用条件であって、音声エンジンが作り出した音声の利用条件までは定めていません。下の課題1は、pyttsx3 が入っていなくても確かめられます。
-
課題1:文字コードを判定できないファイルで落ちないようにする
「📂 テキストを開く」は utf-8・shift-jis・cp932 の順に読み込みを試します(236-242行)。3つとも失敗したときの行き先が用意されていないので、次の行で中身の入っていない変数を使うことになります(243-244行)。しかも入力欄を空にする 243行 が先に動くため、書きかけの原稿を入力欄に置いたままファイルを開くと、その原稿だけが消えます。
期待結果:メモ帳で
こんにちは。と書き、「名前を付けて保存」の文字コードでUTF-16 LEを選んで保存する。そのファイルを「📂 テキストを開く」で開く。いまは入力欄が空になり(243行の削除だけが先に実行されるため)、本文は読み込まれないまま起動したコンソールにエラーが出る。エラー文はUnboundLocalError: cannot access local variable 'content' where it is not associated with a value。直したあとは「文字コードを判定できませんでした」のような案内が出て、入力欄の本文はそのまま残る。合格条件
- 3つとも失敗したときは案内を出すだけで、入力欄の本文が消えない
- utf-8 のテキストファイルは、これまでどおり開けて本文が入れ替わる
- 読み込めたときのステータス表示
読み込み: ファイル名(245行)は変わらない
ヒント①(どこを触るか): 触るのは1か所です。
_open_file()のfor enc in ("utf-8", "shift-jis", "cp932"):のループ(236-242行)です。
ヒント②(使うもの):forにはelse:を付けられます。breakを通らずにループが終わったときだけ、else:の中が動きます。そこでmessagebox.showerror()を出してreturnすれば、243-244行 には進みません。messagebox.showerror()の書き方は 171行 にあります。
つまずきやすい点: 最後に出るのはUnicodeDecodeErrorではありません。1つずつの失敗は 241-242行 のexcept UnicodeDecodeErrorが受け取り、次の文字コードへ進みます。最後に残るのは、変数が作られていないというUnboundLocalError(NameErrorの一種)です。except UnicodeDecodeErrorを足しても、この状態は捕まえられません。 -
課題2:選んだところだけを読み上げる
「▶ 読み上げ開始」は入力欄の全文を取り出して読み上げます(173行)。長い文章の一部だけ聞き直したいときも、先頭から読み直しになります。
期待結果:起動直後に入っている3行のうち、2行目の
シンプルな文法と豊富なライブラリが特長です。をマウスで選んでから「▶ 読み上げ開始」を押す。いまは1行目から読み始め、ハイライトも1行目に付く。直したあとは選んだ行だけを読み、ハイライトもその行に付く。合格条件
- 選択があるときは、その範囲だけを読み、ハイライトもその範囲の中だけに付く
- 選択が無いときは、これまでどおり全文を読む
- 「⏹ 停止」で途中から止められる動きと、「読み上げ箇所をハイライト」のチェックを外したときの動きは変わらない
ヒント①(どこを触るか): 触るのは2か所です。
_speak()の 173行 を、選択があればそちらを取り出す形にします。いまはtext = self.text_input.get("1.0", tk.END).strip()で全文を取っています。あわせて_do_speak()のハイライト位置(199-200行)へ、選択が始まるまでの文字数を足します。
ヒント②(使うもの): 選択範囲はself.text_input.get(tk.SEL_FIRST, tk.SEL_LAST)で取り出せます。選択が無いときはtk.TclErrorになるので、try/except tk.TclErrorで全文に戻します。選択が始まるまでの文字数はlen(self.text_input.get("1.0", tk.SEL_FIRST))で数えられます。その値をselfに持たせておけば、_do_speak()から読めます。
つまずきやすい点: ハイライトの位置は「本文の先頭から数えた文字数」で指定しています(199-200行の1.0 + {pos} chars)。posは取り出した文字列の中での位置なので、足し忘れると読んでいる場所とハイライトがずれます(起動直後の本文で2行目を選ぶと 23 文字ぶんのずれです)。選んだ範囲の先頭に空白や改行があるときは、.strip()で落としたぶんも足す必要があります。Text.count()で数える書き方もありますが、数え方を1つだけ渡したときの戻り値は1要素のタプルなので、そのまま足すとTypeErrorになります。 -
課題3:入力欄のテキストを .txt で保存する
音声ファイルの書き出しは「💾 音声を保存」で実装済みです。同じ
filedialogを使って、入力欄に打ち込んだテキストそのものを .txt として保存するボタンを追加しましょう。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。