Android の定型操作——毎日のチェック、決まった順番のタップ、画面の監視——を PC から自動化したい。そんなときに使えるのが ADB(Android Debug Bridge) です。
ADB は Android 公式の開発ツールで、PC からコマンドを送ることでタップ・スワイプ・スクリーンショット・アプリ起動などを実行できます。相手は USB でつないだ実機でも、NOX Player や BlueStacks などのエミュレータでも OK です。
この記事では、この ADB コマンドを Python から実行できるようにクラス化したものを紹介します。最終的にこう書けるようになります。
adb.ExecTap(None, 100, 200) # 座標(100,200)をタップ
image = adb.ExecScreenCap(None) # スクリーンショットをOpenCV画像で取得
スクリーンショットが OpenCV 画像で取れるのがポイントで、画像認識と組み合わせると「画面を撮る → ボタンを探す → その座標をタップ」という自動化ボットが Python だけで作れます。
adb.exe のパスを確認する
ADB は単体の実行ファイル(adb.exe)です。入手方法は主に2つあります。
- Google 公式の Platform Tools をダウンロードして展開する(実機を操作したい場合はこれ)
- エミュレータに同梱されているものを使う(エミュレータを操作するならこちらが確実)
私は NOX Player 付属のものを使いました。以前 NOX をインストールした際、なにを思ったか E ドライブに入れていたみたいです。

私の場合は E:\nox_vm\Nox\bin\adb.exe になります。エミュレータによって場所と名前が違うので注意してください。
| 環境 | adb の場所(例) |
|---|---|
| NOX Player | <インストール先>\Nox\bin\adb.exe |
| BlueStacks | <インストール先>\HD-Adb.exe(名前が HD-Adb.exe) |
| 公式 Platform Tools | 展開したフォルダの adb.exe |
まずはコマンドを手で叩いてみる
Python に組み込む前に、コマンドプロンプトで動きを確認します。接続されている端末の一覧を出すコマンドが adb devices です。

List of devices attached 以降が実行結果です。この場合、127.0.0.1:62001 という接続先に device(正常接続)状態の端末が1台あることになります。NOX Player を起動しながら実行したのでこうなりました。実機を USB でつなぐ場合は、端末側で「USB デバッグ」を有効にしておくと、ここにシリアル番号が並びます。
タップやスワイプも同じ要領で、adb shell input touchscreen tap 100 200 のようなコマンドで実行できます。よく使うコマンドは次のとおりです。
| やりたいこと | コマンド |
|---|---|
| 端末一覧 | adb devices |
| タップ | adb shell input touchscreen tap <x> <y> |
| スワイプ | adb shell input touchscreen swipe <x1> <y1> <x2> <y2> <ミリ秒> |
| キー送信(ホーム等) | adb shell input keyevent <キーコード> |
| テキスト入力 | adb shell input text <文字列> |
| アプリ起動 | adb shell am start -n <パッケージ/アクティビティ> |
| スクリーンショット | adb exec-out screencap -p |
つまり「Python から adb.exe を実行して結果を受け取る」仕組みを1つ作れば、Android を丸ごと Python で操れるわけです。以降はこれをクラスにまとめたものです。
ソースコード(クラス)
from enum import IntEnum
import subprocess
import os
import cv2
import time
import numpy as np
class AdbCtrl(object):
class InitParam(object):
""" 初期化パラメータ """
def __init__(self) -> None:
self.AdbRootDir: str = r"" # adb.exeがあるディレクトリパス
self.IsUseHdAdb: bool = False # BlueStacks(HD-Adb.exe)を使う場合はTrue
class Device(object):
""" 接続先端末情報モデル """
def __init__(self, deviceId: str = "", deviceName: str = "") -> None:
self.DeviceId: str = deviceId
self.DeviceName: str = deviceName
class eKeyCode(IntEnum):
Home = 3 # ホームボタン
Back = 4 # 戻るボタン
Task = 187 # タスク一覧ボタン
def __init__(self) -> None:
self.initParam = AdbCtrl.InitParam()
self.isInitialized = False
def Initialize(self, param: InitParam) -> None:
if (os.path.isdir(param.AdbRootDir)) == False:
raise ValueError(f"ADBのフォルダパス({param.AdbRootDir})が不正です")
self.initParam = param
self.isInitialized = True
def Dispose(self) -> None:
self.isInitialized = False
def IsInitialized(self) -> bool:
return self.isInitialized
def Sleep(self, msec: int) -> None:
time.sleep(msec / 1000.0)
# ---- ここからコマンド実行関数(実際に使うところ) ----
def ExecDevices(self) -> list[Device]:
""" 接続できる端末の一覧を返す """
recvs = self.Query("adb devices")
devices: list[AdbCtrl.Device] = []
for recv in recvs:
arr = recv.split('\t')
if (len(arr) != 2):
continue
devices.append(AdbCtrl.Device(arr[0], arr[1]))
return devices
def ExecTap(self, device: Device, x: int, y: int) -> None:
""" 指定座標をタップする(deviceがNoneなら1台目に接続) """
send = f"adb {self.GetOptionDeviceStr(device)}shell input touchscreen tap {int(x)} {int(y)}"
self.NonQuery(send)
def ExecSwipe(self, device: Device, x1: int, y1: int, x2: int, y2: int, timeMs: int) -> None:
""" (x1,y1)から(x2,y2)までtimeMsミリ秒かけてスワイプする """
send = f"adb {self.GetOptionDeviceStr(device)}shell input touchscreen swipe {x1} {y1} {x2} {y2} {timeMs}"
self.NonQuery(send)
def ExecScreenCap(self, device: Device) -> any:
""" スクリーンショットをOpenCV画像(BGR)で返す """
send = f"adb {self.GetOptionDeviceStr(device)}exec-out screencap -p"
png_bytes = self.QueryBin(send)
image = cv2.imdecode(np.frombuffer(png_bytes, np.uint8), cv2.IMREAD_COLOR)
return image
def ExecKeyEvent(self, device: Device, key: eKeyCode) -> None:
""" ホーム・戻るなどのキーイベントを送る """
send = f"adb {self.GetOptionDeviceStr(device)}shell input keyevent {key.value}"
self.NonQuery(send)
def ExecInputText(self, device: Device, text: str) -> None:
""" テキストを入力する """
send = f"adb {self.GetOptionDeviceStr(device)}shell input text {text}"
self.NonQuery(send)
def ExecStartN(self, device: Device, arg: str) -> None:
""" 指定したアプリ(パッケージ/アクティビティ)を起動する """
send = f"adb {self.GetOptionDeviceStr(device)}shell am start -n {arg}"
self.NonQuery(send)
# ---- ここから内部処理 ----
def GetOptionDeviceStr(self, device: Device) -> str:
""" 端末指定オプション(-s)。Noneなら省略=1台目に接続 """
if (device is None):
return ""
return f"-s {device.DeviceId} "
def NonQuery(self, send: str) -> None:
self.QueryBin(send)
def Query(self, send: str) -> list[str]:
recv = self.QueryBin(send).decode("utf-8")
return recv.split("\r\n")
def QueryBin(self, send: str) -> bytes:
# BlueStacksの場合は実行ファイル名を差し替える
if (self.initParam.IsUseHdAdb == True):
send = send.replace("adb", "HD-Adb", 1)
# パスに空白("Program Files"等)があっても動くように、exe部分を引用符で囲む
cmd_parts = send.split(' ', 1)
exe_path = f'"{self.initParam.AdbRootDir}\\{cmd_parts[0]}"'
args = cmd_parts[1] if len(cmd_parts) > 1 else ""
final_cmd = f"{exe_path} {args}"
pipe = subprocess.Popen(final_cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=True)
recv = pipe.stdout.read()
pipe.communicate()
return recv
コードのポイント
- 全コマンドが最終的に
QueryBinに集約されます。「adb.exe のフルパスを引用符で囲んで subprocess で実行し、標準出力を bytes で受け取る」だけの関数です。パスを引用符で囲んでいるのは、C:\Program Files\...のような空白入りパスだとコマンドが分断されて動かないためです(実際にハマりました) - スクリーンショットは
exec-outを使うのが重要ポイントです。adb shell screencapだと出力がテキストモード経由になり、改行コード変換でPNG のバイナリが壊れます。exec-outはバイナリをそのまま返すので、cv2.imdecodeに直行できます。ファイル保存を経由しないので速いのもうれしいところ IsUseHdAdb: BlueStacks の adb は実行ファイル名がHD-Adb.exeなので、コマンド文字列の先頭だけ差し替えていますdevice引数はNoneでも動きます。-s <デバイスID>オプションを省略すると1台目に接続されます。ただし端末を2台以上つないでいるときは指定必須です(省略するとエラーになります)
使い方
クラスの実体生成と初期化。
self.adbCtrl = AdbCtrl()
initParam = AdbCtrl.InitParam()
initParam.AdbRootDir = self.appConfig.AdbRootDir # adb.exeのあるフォルダ
self.adbCtrl.Initialize(initParam)
タップするとき(x=100, y=200 の座標)。
self.adbCtrl.ExecTap(None, 100, 200)
スワイプするとき(x=100 の位置で y=100 から y=200 まで1秒かける)。
self.adbCtrl.ExecSwipe(device, 100, 100, 100, 200, 1000)
スクリーンショットを取得するとき。
image = self.adbCtrl.ExecScreenCap(device)
解放するとき。
self.adbCtrl.Dispose()
ハマりポイント
adb devicesに端末が出てこない → 実機なら「開発者向けオプション → USB デバッグ」が ON か、接続時の許可ダイアログを承認したかを確認。エミュレータなら起動済みかを確認- スクリーンショットの画像が壊れる
→
shell screencapを使っていないか確認。前述のとおりexec-outが正解です input textに日本語やスペースが入力できない → ADB のinput textは ASCII のみで、スペースは%sに置き換える必要があります。日本語入力が必要な場合は別のアプローチ(クリップボード連携アプリなど)が要ります- 座標が端末によって違う → タップ座標は解像度依存です。解像度の違う端末でも動かしたいなら、固定座標ではなく後述の画像認識で座標を求める方式にしておくと吉です
応用:OpenCV と組み合わせて「見て押す」ボットに
ExecScreenCap が OpenCV 画像を返すので、テンプレートマッチング(cv2.matchTemplate)で「ボタンの画像」を画面から探し、見つかった座標を ExecTap に渡す——という画像認識ベースの自動化が素直につながります。
image = adb.ExecScreenCap(None) # 1. 画面を撮る
res = cv2.matchTemplate(image, buttonImg, cv2.TM_CCOEFF_NORMED)
_, maxVal, _, maxLoc = cv2.minMaxLoc(res) # 2. ボタンを探す
if maxVal > 0.9:
adb.ExecTap(None, maxLoc[0] + w // 2, maxLoc[1] + h // 2) # 3. 中心をタップ
固定座標のタップと違って画面レイアウトの変化に強く、「このボタンが出るまで待つ」といった制御もできるようになります。定型作業の自動化にどうぞ。