• Сайт
  • Статус системы
  • API-документация
  • Блог
Telegram Icon Сообщество
RU
Русский
Português
English
中文 (中国)
Tiếng Việt
Войти Начать бесплатно
  • Сайт
  • Статус системы
  • API-документация
  • Блог
  • Сообщество








  • Russian









    Русский










    Português










    English










    中文 (中国)










    Tiếng Việt








Войти Начать бесплатно

Настраиваемые Python-скрипты

Автоматизируйте процессы с помощью Python и Multilogin API. Создавайте, управляйте и запускайте браузерные профили с полностью настраиваемыми скриптами.

search icon

Попробуйте другое ключевое слово или проверьте правильность написания. Нужна помощь? Обратитесь в поддержку.

  • Начало работы с автоматизацией
  • Базовая автоматизация с CLI
  • Автоматизация без кода в Postman
  • Script runner и готовые скрипты
  • Puppeteer, Selenium и Playwright
  • Настраиваемые Python-скрипты
  • Быстрые решения с DevTools
  • Внешние инструменты автоматизации
  • Главная
  • breadcrumb separator bar
  • Multilogin
  • breadcrumb separator bar
  • Автоматизация задач с API и скриптами
  • breadcrumb separator bar
  • Настраиваемые Python-скрипты
  • breadcrumb separator bar
  • Как автоматизировать публикацию в Pinterest на облачных телефонах с помощью Appium

Как автоматизировать публикацию в Pinterest на облачных телефонах с помощью Appium

Автор Anton L ( Обновлено 16 Июля 2026 г. )

Обновлено 16 Июля 2026 г.

 Облачные телефоны Multilogin поддерживают ADB, так что вы можете подключить их к инструментам автоматизации Android, таким как Appium.

В этом примере один скрипт на Python обрабатывает повторяющиеся задачи в Pinterest на нескольких облачных телефонах. Он подключается к каждому телефону, входит в аккаунт Pinterest, перемещает назначенные изображения, выполняет несколько разных действий с лентой и публикует изображения (которы в Pinterest называются «Пинами»).

➡️ Вам не нужно открывать каждый облачный телефон и повторять одни и те же действия вручную. Добавьте свои телефоны и аккаунты в devices.txt, запустите скрипт и позвольте ему работать с списком за вас.

Скрипт обрабатывает облачные телефоны по одному. Он завершает все действия на одном телефоне, прежде чем перейти к следующему.

Это пример скрипта. Он показывает, что вы можете создать с помощью Multilogin облачных телефонов, ADB и Appium. Автоматизация Pinterest не встроена в Multilogin.

 
Your browser does not support HTML5 video.

Что делает скрипт

Как только всё готово, скрипт:

  • Подключается к облачному телефону через 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 хранит пароли и другую конфиденциальную информацию в открытом виде. Используйте тестовые аккаунты, держите файл закрытым, никогда не загружайте его в публичный репозиторий и удалите, когда он больше не нужен.

 

Запустите скрипт

  1. Запустите каждый облачный телефон, указанный в devices.txt
  2. Включите ADB на каждом облачном телефоне
  3. Запустите сервер Appium на вашем компьютере
  4. Убедитесь, что скрипт, APK файл, devices.txt и images папка находятся в одном месте
  5. Сохраните скрипт ниже как 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()
 
 
  1. Откройте ваш IDE или терминал и выполните:
    python appium_script.py

Скрипт будет показывать свой прогресс в терминале. Он подключится к первому телефону, завершит рабочий процесс Pinterest, отключится и продолжит с следующим телефоном.

Хотите попробовать другое Android приложение? Это демонстрационный скрипт, а не готовый инструмент для каждой службы. Вы можете адаптировать его, но каждое приложение использует разные кнопки и локаторы элементов. Используйте Appium Inspector для поиска нужных локаторов и обновления скрипта для вашего приложения.

 

Эта статья была полезной?

Оставить отзыв об этой статье

В этой статье:

  • Что делает скрипт
  • Перед началом
  • Настройка devices.txt
  • Запустите скрипт
  • appium_script.py

Сообщество Multilogin

Будьте в курсе, делитесь своими мыслями и общайтесь с другими!

Telegram Icon Multilogin в Telegram
Related Article Title Icon

Похожие статьи

  • Как войти в аккаунт Multilogin с помощью Python
  • Основы веб-скрейпинга с помощью Selenium
  • Как получить токены API с помощью Python
  • Как создать шаблоны профилей с помощью Python

Облачный телефон

  • Облачный телефон
  • Виртуальный телефон
  • Удалённый телефон
  • Ферма из телефонов
  • Облачный мобильный телефон
  • Облачный Андройд эмулятор
  • AI автоматизация

Мультиаккаунтинг

  • Мультиаккаунтинг в Instagram
  • Мультиаккаунтинг в TikTok
  • Мультиаккаунтинг в Reddit
  • Мультиаккаунтинг в Telegram
  • Мультиаккаунтинг в Facebook
  • Мультиаккаунтинг в Youtube
  • Мультиаккаунтинг в LinkedIn

сравнение

  • Multilogin vs. MoreLogin
  • Multilogin vs. FlashID
  • Multilogin vs. DuoPlus
  • Multilogin vs. VMOS cloud
  • Multilogin vs. Octo Browser
  • Multilogin vs. AdsPower
  • Multilogin vs. GoLogin

ПРОКСИ ДЛЯ платформ

  • Мобильные прокси
  • Прокси для Reddit
  • Прокси для TikTok
  • Прокси для Instagram
  • Прокси для Facebook
  • Прокси для YouTube
  • Прокси для LinkedIn

Применения

  • Облачный телефон для TikTok
  • Облачный телефон для Instagram
  • Облачный телефон для Reddit
  • Облачный телефон для Facebook
  • Облачный телефон для YouTube

ресурсы

  • База знаний
  • API документация
  • Глоссарий
  • Блог
  • Статус системы
  • Журнал изменений

БЕСПЛАТНЫЕ УТИЛИТЫ

  • Калькулятор дохода на YouTube
  • Калькулятор дохода в Instagram
  • URL конвертер в текст
  • Проверка локального Google SERP
  • Генератор случайных адресов

Связаться с нами

  • Служба поддержки 24/7
    support@multilogin.com
  • Реферальная программа
  • Партнерская программа
  • Цены
  • Вакансии
GDPR Compliant

© 2026 Multilogin. All rights reserved.

  • Privacy policy
  • Terms of service
  • Cookie policy
Multilogin watermark
  • ОБЛАЧНЫЙ ТЕЛЕФОН

    • Облачный телефон
    • Виртуальный телефон
    • Удалённый телефон
    • Ферма из телефонов
    • Облачный мобильный телефон
    • Облачный Андройд эмулятор
    • AI автоматизация
  • МУЛЬТИАККАУНТИНГ

    • Мультиаккаунтинг в Instagram
    • Мультиаккаунтинг в TikTok
    • Мультиаккаунтинг в Reddit
    • Мультиаккаунтинг в Telegram
    • Мультиаккаунтинг в Facebook
    • Мультиаккаунтинг в Youtube
    • Мультиаккаунтинг в LinkedIn
  • СРАВНЕНИЕ

    • Multilogin vs. MoreLogin
    • Multilogin vs. FlashID
    • Multilogin vs. DuoPlus
    • Multilogin vs. VMOS cloud
    • Multilogin vs. Octo Browser
    • Multilogin vs. AdsPower
    • Multilogin vs. GoLogin
  • ПРОКСИ ДЛЯ ПЛАТФОРМ

    • Мобильные прокси
    • Прокси для Reddit
    • Прокси для TikTok
    • Прокси для Instagram
    • Прокси для Facebook
    • Прокси для YouTube
    • Прокси для LinkedIn
  • ПРИМЕНЕНИЯ

    • Облачный телефон для TikTok
    • Облачный телефон для Instagram
    • Облачный телефон для Reddit
    • Облачный телефон для Facebook
    • Облачный телефон для YouTube
  • РЕСУРСЫ

    • База знаний
    • API документация
    • Глоссарий
    • Блог
    • Статус системы
    • Журнал изменений
  • БЕСПЛАТНЫЕ УТИЛИТЫ

    • Калькулятор дохода на YouTube
    • Калькулятор дохода в Instagram
    • URL конвертер в текст
    • Проверка локального Google SERP
    • Генератор случайных адресов
  • СВЯЗАТЬСЯ С НАМИ

    • Служба поддержки 24/7
      support@multilogin.com
    • Реферальная программа
    • Партнерская программа
    • Цены
    • Вакансии
GDPR Compliant
  • Privacy policy
  • Terms of service
  • Cookie policy

© 2026 Multilogin. All rights reserved.

Expand