Как автоматизировать публикацию в Pinterest на облачных телефонах с помощью Appium
Облачные телефоны Multilogin поддерживают ADB, так что вы можете подключить их к инструментам автоматизации Android, таким как Appium.
В этом примере один скрипт на Python обрабатывает повторяющиеся задачи в Pinterest на нескольких облачных телефонах. Он подключается к каждому телефону, входит в аккаунт Pinterest, перемещает назначенные изображения, выполняет несколько разных действий с лентой и публикует изображения (которы в Pinterest называются «Пинами»).
➡️ Вам не нужно открывать каждый облачный телефон и повторять одни и те же действия вручную. Добавьте свои телефоны и аккаунты в devices.txt, запустите скрипт и позвольте ему работать с списком за вас.
Скрипт обрабатывает облачные телефоны по одному. Он завершает все действия на одном телефоне, прежде чем перейти к следующему.
Это пример скрипта. Он показывает, что вы можете создать с помощью Multilogin облачных телефонов, ADB и Appium. Автоматизация Pinterest не встроена в Multilogin.
Что делает скрипт
Как только всё готово, скрипт:
- Подключается к облачному телефону через ADB
- Устанавливает Pinterest из APK файла
- Перемещает изображения с вашего компьютера на облачный телефон
- Входит в предоставленный вами аккаунт Pinterest
- Выполняет несколько образцов взаимодействия с лентой, чтобы продемонстрировать автоматизированную навигацию в приложении
- Публикует назначенные изображения
- Переходит к следующему облачному телефону в списке
Каждый облачный телефон может использовать свой собственный аккаунт Pinterest и диапазон изображений. Это упрощает организацию контента для нескольких учетных записей.
Примеры взаимодействия с лентой также можно адаптировать для процесса разогрева аккаунта. Проверьте Как разогреть профиль в Multilogin для получения дополнительной информации.
Имейте в виду, что несколько автоматизированных взаимодействий не гарантируют доверие к аккаунту или предотвращают ограничения платформы. Всегда следуйте правилам Pinterest и автоматизируйте только те аккаунты, которыми вам разрешено пользоватьтся.
Перед началом
Вам потребуется:
- Установленный Appium и UiAutomator2 на вашем компьютере: Документация Appium
- Запущенный Multilogin облачный телефон с включенным ADB: Как использовать ADB в облачных телефонах
- Тестовый аккаунт Pinterest и его данные для входа
- Файл APK Pinterest
-
Папка с именем
imagesс изображениями, которые вы хотите опубликовать -
Текстовый файл с именем
devices.txt
Оставьте скрипт, APK файл, devices.txt и images папку в одной папке. Это поможет скрипту автоматически находить всё.
Настройка devices.txt
Добавьте один облачный телефон в строку, используя этот формат:
IP:PORT ADB_PASSWORD PINTEREST_EMAIL PINTEREST_PASSWORD IMAGE_RANGEПример:
199.190.44.226:25922 j3fYxT email@email.com password 1-3Вот что означает каждое значение:
-
IP:PORT: адрес ADB облачного телефона -
ADB_PASSWORD: пароль, используемый для подключения через ADB -
PINTEREST_EMAIL: электронная почта аккаунта Pinterest -
PINTEREST_PASSWORD: пароль аккаунта Pinterest -
IMAGE_RANGE: изображения, назначенные этому аккаунту
Например, 1-3 указывает скрипту использовать первые три изображения из папки images, отсортированной по алфавиту.
Вы можете разделить изображения между несколькими облачными телефонами:
199.190.44.226:25922 j3fYxT user1@email.com password1 1-3
199.190.44.227:25922 k9pQrS user2@email.com password2 4-5Первый телефон получает изображения 1–3. Второй телефон получает изображения 4–5.
Оставьте диапазон изображений пустым, если хотите, чтобы телефон использовал каждое изображение в папке.
Храните свои данные для входа в безопасности. devices.txt хранит пароли и другую конфиденциальную информацию в открытом виде. Используйте тестовые аккаунты, держите файл закрытым, никогда не загружайте его в публичный репозиторий и удалите, когда он больше не нужен.
Запустите скрипт
-
Запустите каждый облачный телефон, указанный в
devices.txt - Включите ADB на каждом облачном телефоне
- Запустите сервер Appium на вашем компьютере
-
Убедитесь, что скрипт, APK файл,
devices.txtиimagesпапка находятся в одном месте -
Сохраните скрипт ниже как
appium_script.py
appium_script.py
"""
Как использовать
-----
Запустите этот скрипт и включите его в папку с двумя файлами: devices.txt и папкой images devices.txt -- один телефон в строке: IP:PORT TOKEN USERNAME PASSWORD [IMAGE_RANGE]
IMAGE_RANGE это количество изображений которые будут использованы для определенного аккаунта, например, "1-3"
в алфавитно-отсортированном списке изображений в images/
Пропустите это чтобы дать этому телефону каждое изображение в папке. Пример, разделение 5 изображений между 2
телефонами (телефон 1 получает изображения 1-3, телефон 2 получает изображения 4-5):
199.190.44.226:25922 j3fYxT user1 pass1 1-3
199.190.44.227:25922 k9pQrS user2 pass2 4-5
images/ -- папка изображений (jpg/png), или один файл изображения.
Требуется: pip install Appium-Python-Client
Сервер Appium работает локально (appium) и ANDROID adb на PATH.
Скрипт устанавливает APK и изображения на телефон, открывает Pinterest, вводит данные аккаунта, выполняет несколько образцов взаимодействия с лентой, и публикует назначенные изображения.
"""
import argparse
import random
import re
import subprocess
import sys
import time
from pathlib import Path
from urllib.parse import quote as urlquote
from appium import webdriver
from appium.options.android import UiAutomator2Options
from appium.webdriver.common.appiumby import AppiumBy
APPIUM_SERVER_URL = "http://localhost:4723"
PINTEREST_PACKAGE = "com.pinterest"
CONNECT_RETRIES = 6
CONNECT_RETRY_SLEEP = 2
INSTALL_RETRIES = 3
# Лимит разогрева
WARM_UP_MIN_ACTIONS = 3
WARM_UP_MAX_ACTIONS = 5
# Папка в которую мы загружаем изображения REMOTE_IMAGE_DIR = "/sdcard/Pictures/bulk_publisher"
# Функция подключения adb
def adb(*args, serial=None, timeout=60):
cmd = ["adb"] + (["-s", serial] if serial else []) + list(args)
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
def connect_and_login(serial: str, token: str) -> bool:
"""ПодключениекADB, использует имя/адрес телефона и glogin."""
print(f" [ПОДКЛЮЧЕНИЕ] {serial}: подключение через adb...")
for attempt in range(1, CONNECT_RETRIES + 1):
adb("connect", serial)
state = adb("get-state", serial=serial).stdout.strip()
if state == "device":
break
print(f" [ПОДКЛЮЧЕНИЕ] {serial}: еще не онлайн (попытка {attempt}/{CONNECT_RETRIES}, состояние={state или 'неизвестное'}), повторнаяпопытка...")
adb("disconnect", serial)
time.sleep(CONNECT_RETRY_SLEEP)
else:
print(f" [ОШИБКА_ПОДКЛЮЧЕНИЯ] {serial}")
return False
print(f" [ПОДКЛЮЧЕНО] {serial}: онлайн, вход в систему (glogin)...")
result = adb("shell", "glogin", token, serial=serial)
output = (result.stdout + result.stderr).lower()
if "success" in output or "уже вошел" в output:
print(f" [ОК] {serial}: вошел в систему")
return True
print(f" [ОШИБКА_GLOGIN] {serial}: {result.stdout или result.stderr}")
return False
def install_apk(serial: str, apk_path: str) -> bool:
for attempt in range(INSTALL_RETRIES):
result = adb("install", "-r", apk_path, serial=serial, timeout=180)
если "success" в result.stdout.lower():
вернуть True
time.sleep(3)
print(f" [INSTALL_FAIL] {serial}: {result.stdout или result.stderr}")
вернуть False
def grant_full_media_access(serial: str):
"""Предоставляет полный доступ к фото/видео библиотеке через ADB """
adb("shell", "pm", "grant", PINTEREST_PACKAGE, "android.permission.READ_MEDIA_IMAGES", serial=serial)
adb("shell", "pm", "grant", PINTEREST_PACKAGE, "android.permission.READ_MEDIA_VIDEO", serial=serial)
def sanitize_filename(name: str) -> str:
"""Сопоставление имен (если скрипт не может найти изображение по умолчанию)"""
stem, suffix = Path(name).stem, Path(name).suffix
вернуть re.sub(r"[^A-Za-z0-9._-]", "_", stem) + suffix
def push_images(serial: str, image_paths, remote_dir=REMOTE_IMAGE_DIR) -> list:
"""Отправляет локальные изображения на телефон и запускает сканирование медиа"""
adb("shell", "mkdir", "-p", remote_dir, serial=serial)
remote_paths = []
used_names = set()
total = len(image_paths)
для i, local_path в enumerate(image_paths, start=1):
safe_name = sanitize_filename(Path(local_path).name)
candidate, n = safe_name, 1
пока candidate в used_names: # два оригинала очищены до одинакового имени
candidate = f"{Path(safe_name).stem}_{n}{Path(safe_name).suffix}"
n += 1
used_names.add(candidate)
remote_path = f"{remote_dir}/{candidate}"
print(f" [UPLOADING] {serial}: отправка изображения {i}/{total}: {Path(local_path).name}")
push_result = adb("push", local_path, remote_path, serial=serial)
если push_result.returncode != 0 или "error" в push_result.stdout.lower():
print(f" [PUSH_FAIL] {serial}: {Path(local_path).name}: "
f"{push_result.stdout.strip() или push_result.stderr.strip()}")
продолжить
scan_uri = "file://" + urlquote(remote_path)
scan_result = adb(
"shell", "am", "broadcast", "-a",
"android.intent.action.MEDIA_SCANNER_SCAN_FILE",
"-d", scan_uri,
serial=serial,
)
если scan_result.returncode != 0:
print(f" [SCAN_FAIL] {serial}: {Path(local_path).name}: "
f"{scan_result.stdout.strip() или scan_result.stderr.strip()}")
remote_paths.append(remote_path)
вернуть remote_paths
def disconnect(serial: str):
adb("disconnect", serial)
def build_driver(serial: str):
"""Запускает сессию Appium, затем запускает Pinterest. """
capabilities = dict(
platformName="Android",
automationName="uiautomator2",
udid=serial,
newCommandTimeout=180,
noReset=True,
language="en",
locale="US",
)
driver = webdriver.Remote(APPIUM_SERVER_URL, options=UiAutomator2Options().load_capabilities(capabilities))
driver.activate_app(PINTEREST_PACKAGE)
вернуть driver
def find_any(driver, candidates, timeout=20, poll=0.5, require_enabled=False):
"""Попробуйте каждый (по, значению) локатор кандидата в порядке, пока один не разрешится,
опрос до истечения времени, вместо того, чтобы провалиться при первом промахе. """
deadline = time.time() + timeout
last_err = None
пока time.time() < deadline:
для by, value в candidates:
попробовать:
el = driver.find_element(by=by, value=value)
если el.is_displayed() и (не require_enabled или el.get_attribute("enabled") == "true"):
вернуть el
кроме Exception как exc: # noqa: BLE001 - попробовать несколько стратегий на целенаправленно
last_err = exc
time.sleep(poll)
raise TimeoutError(f"Не найдено соответствующего локатора в {timeout}s: {candidates}") из last_err
def tap_any(driver, candidates, timeout=20, require_enabled=False):
find_any(driver, candidates, timeout=timeout, require_enabled=require_enabled).click()
def tap_if_present(driver, candidates, timeout=3) -> bool:
"""Как tap_any, но возвращает False вместо того, чтобы вызывать исключение, когда ничего не соответствует
-- для всплывающих окен, которые иногда появляются."""
попробовать:
tap_any(driver, candidates, timeout=timeout)
return True
except TimeoutError:
return False
# Этап 1: Первый экран входа только email.
EMAIL_ONLY_FIELD = [
(AppiumBy.XPATH, "//*[@resource-id='com.pinterest:id/email_address']//android.widget.EditText"),
]
CONTINUE_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/continue_email_bt"),
]
# Этап 2: комбинированный экран email + пароль, затем "Войти".
LOGIN_EMAIL_FIELD = [
(AppiumBy.XPATH, "//*[@resource-id='com.pinterest:id/email']//android.widget.EditText"),
]
LOGIN_PASSWORD_FIELD = [
(AppiumBy.XPATH, "//*[@resource-id='com.pinterest:id/password']//android.widget.EditText"),
]
LOGIN_SUBMIT_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/login_bt"),
]
# Всплывающие окна, которые могут появляться в непредсказуемые моменты после входа.
EMAIL_UPDATE_DISMISS = [
(AppiumBy.ID, "com.pinterest:id/actionPromptDismissButton"), # "Это ваш email?" -> закрыть
]
NOTIFICATIONS_PERMISSION_ALLOW = [
(AppiumBy.ID, "com.android.permissioncontroller:id/permission_allow_button"),
]
PHOTOS_PERMISSION_ALLOW_ALL = [
(AppiumBy.ID, "com.android.permissioncontroller:id/permission_allow_all_button"),
]
# Открывает "Начать создание сейчас" нижний лист. Скрипты ищут селекторы взависимости от разрешения и размера
CREATE_ICON_BUTTON = [
(AppiumBy.XPATH,
'//android.widget.Button[@content-desc="Create"]'
'/android.widget.FrameLayout[@resource-id="com.pinterest:id/icon_button_container"]'
'/android.widget.Button[@resource-id="com.pinterest:id/icon_button"]'),
(AppiumBy.XPATH,
'//android.widget.FrameLayout[@content-desc="Create"]'
'/android.widget.LinearLayout[@resource-id="com.pinterest:id/tab_content"]'),
]
# Нижняя панель навигации "Главная" вкладка
HOME_TAB_BUTTON = [
(AppiumBy.ACCESSIBILITY_ID, "Home"),
(AppiumBy.ID, "com.pinterest:id/bottom_nav_home_icon"),
]
# "Начать создание сейчас" лист который появляется после нажатия Создать -- выберите "Закрепить"
PIN_ACTION_BUTTON = [
(AppiumBy.XPATH, '//android.widget.Button[@content-desc="Pin"]'),
]
# Медиа выбор "Далее" кнопка (включена после выбора фото).
GALLERY_NEXT_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/end_container_text_button"),
]
# Финальная отправка на экране деталей создания пина -- обозначенная "Создать".
CREATE_PIN_SUBMIT_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/create_gestalt_button"),
]
# --- Разогрев локаторов (просмотр ленты домашней: открыть пин, нравится/сохранить/комментировать) ---
# Общий пин карточка. Скрипт находит любой пин на экране для этого телефона/аккаунта/сессии.
PIN_CARD = [
# Находит нажимаемую совпадение. Переходите к любому совпадению, если ниодно нажимаемое не найдено, в случае если
(AppiumBy.XPATH, '//*[contains(@content-desc, "Pin from") and @clickable="true"]'),
(AppiumBy.XPATH, '//*[contains(@content-desc, "Pin from")]'),
]
REACT_BUTTON = [
(AppiumBy.XPATH, '//android.widget.ImageView[@content-desc="React to this Pin"]'),
]
SAVE_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/save_pinit_bt"),
(AppiumBy.XPATH, '//android.widget.Button[@content-desc="Save"]'),
]
COMMENTS_MODULE = [
(AppiumBy.ID, "com.pinterest:id/new_comments_module_container"),
]
CLOSEUP_BACK_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/closeup_back_button"),
(AppiumBy.ACCESSIBILITY_ID, "Back"),
]
def dismiss_popups(driver):
"""Удаляет 'Это ваш email?' подсказку и оба Android разрешения
диалоги (уведомления, доступ к фото) если они появляются на экране."""
tap_if_present(driver, EMAIL_UPDATE_DISMISS, timeout=2)
tap_if_present(driver, NOTIFICATIONS_PERMISSION_ALLOW, timeout=2)
tap_if_present(driver, PHOTOS_PERMISSION_ALLOW_ALL, timeout=2)
def ensure_on_home_feed(driver, max_attempts=6, settle_seconds=1.5):
"""Подтверждает видимость значка Создать (домашняя лента) и активно возвращается
назад, если не так."""
for _ in range(max_attempts):
try:
find_any(driver, CREATE_ICON_BUTTON, timeout=4)
return
except TimeoutError:
pass
if not tap_if_present(driver, HOME_TAB_BUTTON, timeout=3):
try:
driver.back()
except Exception: # noqa: BLE001 - восстановление по максимуму, продолжайте попытки
pass
time.sleep(settle_seconds)
# Проверка на проверку если приложение свернуто
driver.activate_app(PINTEREST_PACKAGE)
try:
find_any(driver, CREATE_ICON_BUTTON, timeout=10)
return
except TimeoutError:
pass
raise TimeoutError("Не удалось вернуться к лента домашней Pinterest (значок Создать так и не появился)")
def login(driver, username: str, password: str):
try:
find_any(driver, EMAIL_ONLY_FIELD, timeout=15).send_keys(username)
tap_any(driver, CONTINUE_BUTTON)
except TimeoutError:
pass # уже на комбинированном экране email+пароль
email_field = find_any(driver, LOGIN_EMAIL_FIELD, timeout=20)
email_field.clear()
email_field.send_keys(username)
find_any(driver, LOGIN_PASSWORD_FIELD).send_keys(password)
tap_any(driver, LOGIN_SUBMIT_BUTTON)
dismiss_popups(driver)
ensure_on_home_feed(driver)
dismiss_popups(driver)
def publish_pin(driver, remote_image_path: str):
ensure_on_home_feed(driver)
dismiss_popups(driver)
tap_any(driver, CREATE_ICON_BUTTON, timeout=10)
dismiss_popups(driver)
# Некоторые сессии могут пропустить лист «Начать создание сейчас» и перейти сразу
# к выбору фото, поэтому отсутствующий лист здесь не является фатальным.
tap_if_present(driver, PIN_ACTION_BUTTON, timeout=6)
dismiss_popups(driver) # диалог разрешения доступа к фото обычно появляется здесь
# Сопоставьте на «<папка загрузки>/<имя_файла>», а не только имя_файла -- телефон, у
# которого уже есть файл с темже именем в другом месте (например, Загрузки/).
unique_suffix = f"{Path(REMOTE_IMAGE_DIR).name}/{Path(remote_image_path).name}"
thumbnail = [(AppiumBy.XPATH, f"//*[contains(@content-desc, '{unique_suffix}')]")]
tap_any(driver, thumbnail, timeout=25) # сетку галереи может занять время для заполнения
# «Далее» начинает быть недоступным до тех пор, пока выбор не будет зафиксирован -- подождите, пока enabled=true,
# не просто присутствует, иначе клик втихую ничего не сделает на недоступной кнопке.
tap_any(driver, GALLERY_NEXT_BUTTON, timeout=15, require_enabled=True)
# 45с здесь потому что Pinterest должен завершить загрузку/обработку
# изображения прежде чем эта кнопка отобразится.
tap_any(driver, CREATE_PIN_SUBMIT_BUTTON, timeout=45)
ensure_on_home_feed(driver)
dismiss_popups(driver)
def _open_random_pin(driver) -> bool:
"""Нажимает на случайную видимую карту пина на главной ленте."""
for by, value in PIN_CARD:
try:
cards = [c for c in driver.find_elements(by=by, value=value) if c.is_displayed()]
except Exception: # noqa: BLE001 - попробуйте следующего кандидата
continue
if not cards:
continue
try:
random.choice(cards).click()
return True
except Exception: # noqa: BLE001 - пропущенный тап просто означает «попробуйте что-то другое»
continue
return False
def _close_pin(driver):
"""Закрывает пин в ближайшем виде."""
try:
if find_any(driver, CREATE_ICON_BUTTON, timeout=1).is_displayed():
return
except TimeoutError:
pass
if not tap_if_present(driver, CLOSEUP_BACK_BUTTON, timeout=3):
try:
driver.back()
except Exception: # noqa: BLE001 - восстановление с наилучшим образом
pass
def _like_current_pin(driver) -> bool:
return tap_if_present(driver, REACT_BUTTON, timeout=4)
def _save_current_pin(driver) -> bool:
return tap_if_present(driver, SAVE_BUTTON, timeout=4)
def _view_comments(driver) -> bool:
if not tap_if_present(driver, COMMENTS_MODULE, timeout=4):
return False
time.sleep(random.uniform(1, 2))
try:
driver.back()
except Exception: # noqa: BLE001 - восстановление с наилучшим образом
pass
return True
def _scroll_feed(driver):
"""Проводит вверх/вниз по ленте на случайное количество, имитируя бездействующее прокручивание, а не только нажимая на первый пин, который попадется на глаз."""
size = driver.get_window_size()
x = int(size["width"] * 0.5)
start_y = int(size["height"] * random.uniform(0.55, 0.7))
end_y = int(size["height"] * random.uniform(0.2, 0.35))
driver.swipe(x, start_y, x, end_y, random.randint(400, 900))
WARM_UP_PIN_ACTIONS = [_like_current_pin, _save_current_pin, _view_comments]
def warm_up(driver, min_actions=WARM_UP_MIN_ACTIONS, max_actions=WARM_UP_MAX_ACTIONS):
"""Выполняет случайную группу действий по просмотру главной ленты (открыть пин,
затем понравиться/сохранить/посмотреть комментарии, затем назад) сразу после входа и перед
публикацией любого пина"""
ensure_on_home_feed(driver)
dismiss_popups(driver)
action_count = random.randint(min_actions, max_actions)
print(f" [WARMUP] выполняется {action_count} разогрев действий)")
done = 0
attempts = 0
while done < action_count and attempts < action_count * 3:
attempts += 1
if random.random() < 0.3:
_scroll_feed(driver)
time.sleep(random.uniform(1, 2))
continue
if not _open_random_pin(driver):
_scroll_feed(driver) # ничего нельзя нажать пока -- загрузить больше из ленты
time.sleep(1)
continue
time.sleep(random.uniform(1.5, 2.5)) # позволить ближайшему виду закончить загрузку
dismiss_popups(driver)
random.choice(WARM_UP_PIN_ACTIONS)(driver)
time.sleep(random.uniform(1, 2))
_close_pin(driver)
dismiss_popups(driver)
# Проверяйте после каждой итерации.
try:
ensure_on_home_feed(driver)
except TimeoutError as exc:
print(f" [WARMUP] потерян домашний фид в середине разогрева, остановка раньше: {exc}")
break
time.sleep(random.uniform(0.5, 1.5))
done += 1
ensure_on_home_feed(driver)
dismiss_popups(driver)
# Загрузка изображений
def parse_image_range(range_str: str, line_no: int):
"""Парсит диапазон как '1-3' или один индекс как '4'."""
try:
if "-" in range_str:
start_str, end_str = range_str.split("-", 1)
start, end = int(start_str), int(end_str)
else:
start = end = int(range_str)
except ValueError:
raise ValueError(f"devices.txt строка {line_no}: недопустимый диапазон изображений {range_str!r} (ожидается например '1-3' или '4')")
if start < 1 or end < start:
raise ValueError(f"devices.txt строка {line_no}: недопустимый диапазон изображений {range_str!r} (ожидается например '1-3' или '4')")
return start, end
def parse_devices(path: str):
devices = []
for line_no, raw_line in enumerate(Path(path).read_text().splitlines(), start=1):
line = raw_line.strip()
if not line or line.startswith("#"):
continue
parts = line.split()
if len(parts) == 4:
serial, token, username, password = parts
image_range = None # -> получает каждое изображение в папке, см. main()
elif len(parts) == 5:
serial, token, username, password, range_str = parts
image_range = parse_image_range(range_str, line_no)
else:
raise ValueError(
f"devices.txt строка {line_no}: ожидалось 4 или 5 полей "
f"(IP:PORT TOKEN USERNAME PASSWORD [IMAGE_RANGE]), получено {len(parts)}: {raw_line!r}"
)
devices.append(dict(serial=serial, token=token, username=username, password=password, image_range=image_range))
return devices
def resolve_images(images_arg: str):
"""Возвращает каждое изображение, найденное, отсортированное в алфавитном порядке. Поля IMAGE_RANGE
в devices.txt индексируются в этот же отсортированный список."""
p = Path(images_arg)
if p.is_file():
return [str(p)]
return sorted([str(f) for f in p.iterdir() if f.suffix.lower() in (".jpg", ".jpeg", ".png")])
# Автообнаружение: если аргумент опущен, ищите его рядом со скриптом
def autodiscover_devices_file(base: Path) -> Path:
candidate = base / "devices.txt"
if candidate.is_file():
return candidate
raise FileNotFoundError(f"Не найден devices.txt в {base}")
def autodiscover_apk(base: Path) -> Path:
apks = sorted(base.glob("*.apk"))
if not apks:
raise FileNotFoundError(f"Не найден .apk файл в {base}")
if len(apks) > 1:
print(f">> найдено несколько .apk файлов в {base}, используется {apks[0].name}")
return apks[0]
def autodiscover_images(base: Path) -> Path:
images_dir = base / "images"
if images_dir.is_dir():
return images_dir
loose = [f for f in base.iterdir() if f.suffix.lower() in (".jpg", ".jpeg", ".png")]
if loose:
return base
raise FileNotFoundError(f"Не найдена папка images/ или файлы .jpg/.png в {base}")
def run_device(device: dict, apk_path: str, image_paths: list):
serial = device["serial"]
print(f"\n=== {serial} ===")
if not connect_and_login(serial, device["token"]):
return
if not install_apk(serial, apk_path):
disconnect(serial)
return
grant_full_media_access(serial)
remote_images = push_images(serial, image_paths)
driver = None
try:
driver = build_driver(serial)
login(driver, device["username"], device["password"])
try:
warm_up(driver)
except Exception as exc: # noqa: BLE001 - warm-up является лучшим-усилием, публикация всё равно имеет значение
print(f" [WARMUP_FAILED] {serial}: {exc}")
published = 0
total = len(remote_images)
for i, remote_image in enumerate(remote_images, start=1):
print(f" [PUBLISHING] {serial}: pin {i}/{total}: {Path(remote_image).name}")
try:
publish_pin(driver, remote_image)
published += 1
except Exception as exc: # noqa: BLE001 - один медленный/неудачный pin не должен стоить остальных
print(f" [PIN_FAILED] {serial}: {Path(remote_image).name}: {exc}")
time.sleep(2)
print(f" [DONE] {serial}: published {published}/{len(remote_images)} pin(ов)")
except Exception as exc: # noqa: BLE001 - продолжить к следующему телефону влюбом случае
print(f" [ERROR] {serial}: {exc}")
finally:
if driver:
driver.quit()
disconnect(serial)
def main():
script_dir = Path(__file__).resolve().parent
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("devices_file", nargs="?", default=None,
help=f"IP:PORT TOKEN USERNAME PASSWORD [IMAGE_RANGE], один на строку "
f"(поумолчанию: devices.txt рядом с этим скриптом, т.е. {script_dir})")
parser.add_argument("apk_path", nargs="?", default=None,
help="Путь к APK для установки Pinterest (поумолчанию: the .apk найденный рядом с этим скриптом)")
parser.add_argument("images", nargs="?", default=None,
help="Папка с изображениями, или один файл изображения "
"(поумолчанию: папка images/ или отдельные .jpg/.png файлы, рядом с этим скриптом)")
args = parser.parse_args()
try:
devices_file = Path(args.devices_file) if args.devices_file else autodiscover_devices_file(script_dir)
apk_path = Path(args.apk_path) if args.apk_path else autodiscover_apk(script_dir)
images_arg = Path(args.images) if args.images else autodiscover_images(script_dir)
except FileNotFoundError as exc:
sys.exit(f"{exc}\nУкажите devices_file / apk_path / images явно, или разместите их рядом с скриптом.")
print(f">> devices file: {devices_file}")
print(f">> apk: {apk_path}")
print(f">> images: {images_arg}")
try:
devices = parse_devices(devices_file)
except ValueError as exc:
sys.exit(str(exc))
all_images = resolve_images(images_arg)
if not all_images:
sys.exit(f"Не найдены изображения по {images_arg}")
print(f">> {len(devices)} телефон(ов) вочереди, {len(all_images)} изображение(й) доступно:")
for i, image_path in enumerate(all_images, start=1):
print(f" {i}: {Path(image_path).name}")
for device in devices:
start, end = device["image_range"] or (1, len(all_images))
if end > len(all_images):
print(f" [SKIP] {device['serial']}: диапазон изображений {start}-{end} превышает "
f"из{len(all_images)} изображение(й) доступно")
continue
image_subset = all_images[start - 1:end]
run_device(device, str(apk_path), image_subset)
if __name__ == "__main__":
main()- Откройте ваш IDE или терминал и выполните:
python appium_script.py
Скрипт будет показывать свой прогресс в терминале. Он подключится к первому телефону, завершит рабочий процесс Pinterest, отключится и продолжит с следующим телефоном.
Хотите попробовать другое Android приложение? Это демонстрационный скрипт, а не готовый инструмент для каждой службы. Вы можете адаптировать его, но каждое приложение использует разные кнопки и локаторы элементов. Используйте Appium Inspector для поиска нужных локаторов и обновления скрипта для вашего приложения.