Cách tự động đăng bài trên Pinterest bằng điện thoại đám mây với Appium
Multilogin điện thoại đám mây hỗ trợ ADB, vì vậy bạn có thể kết nối chúng với Android như Appium.
Trong ví dụ này, một script Python xử lý công việc lặp đi lặp lại trên Pinterest qua nhiều điện thoại đám mây. Nó kết nối với từng điện thoại, đăng nhập vào tài khoản Pinterest của nó, di chuyển các hình ảnh được chỉ định, thực hiện một vài hành động mẫu trên nguồn cấp, và xuất bản các hình ảnh dưới dạng Pins.
➡️ Bạn không cần phải mở từng điện thoại đám mây và lặp lại các hành động giống nhau một cách thủ công. Thêm điện thoại và tài khoản của bạn vào devices.txt, khởi động script, và để nó làm việc qua danh sách cho bạn.
Script xử lý các điện thoại đám mây từng cái một. Nó hoàn thành toàn bộ quy trình trên một điện thoại trước khi chuyển sang cái tiếp theo.
Đây là một script minh họa. Nó cho thấy những gì bạn có thể xây dựng với Multilogin điện thoại đám mây, ADB và Appium. Tự động hóa Pinterest không được tích hợp trong Multilogin.
Script làm gì
Khi mọi thứ đã sẵn sàng, script:
- Kết nối với một điện thoại đám mây qua ADB
- Cài đặt Pinterest từ một tệp APK
- Di chuyển hình ảnh từ máy tính của bạn đến điện thoại đám mây
- Đăng nhập vào tài khoản Pinterest mà bạn cung cấp
- Chạy một vài tương tác mẫu trên nguồn cấp để cho thấy việc điều hướng ứng dụng tự động
- Xuất bản các hình ảnh được chỉ định
- Chuyển sang điện thoại đám mây tiếp theo trong danh sách
Mỗi điện thoại đám mây có thể sử dụng tài khoản Pinterest và phạm vi hình ảnh riêng. Điều này giúp dễ dàng tổ chức nội dung cho nhiều hồ sơ xã hội từ một thiết lập.
Các tương tác mẫu trên nguồn cấp cũng có thể được điều chỉnh cho quy trình làm ấm tài khoản. Kiểm tra Cách làm ấm một hồ sơ trong Multilogin để biết thêm chi tiết.
Hãy nhớ rằng một vài tương tác tự động không đảm bảo độ tin cậy của tài khoản hoặc ngăn chặn các hạn chế của nền tảng. Luôn tuân theo quy tắc của Pinterest và chỉ tự động hóa các tài khoản mà bạn được phép quản lý.
Trước khi bắt đầu
Bạn sẽ cần:
- Appium và UiAutomator2 được cài đặt trên máy tính của bạn: Tài liệu Appium
- Một điện thoại đám mây Multilogin đã khởi động với ADB được bật: Cách sử dụng ADB trong điện thoại đám mây
- Một tài khoản thử nghiệm Pinterest và thông tin đăng nhập của nó
- Tệp APK của Pinterest
-
Một thư mục tên là
imagesvới các hình ảnh bạn muốn xuất bản -
Một tệp văn bản tên là
devices.txt
Giữ script, tệp APK, devices.txt, và images trong cùng một thư mục dự án. Điều này giúp script tìm mọi thứ tự động.
Thiết lập devices.txt
Thêm một điện thoại đám mây mỗi dòng theo định dạng này:
IP:PORT ADB_PASSWORD PINTEREST_EMAIL PINTEREST_PASSWORD IMAGE_RANGEVí dụ:
199.190.44.226:25922 j3fYxT email@email.com password 1-3Dưới đây là ý nghĩa của từng giá trị:
-
IP:PORT: địa chỉ ADB của điện thoại đám mây -
ADB_PASSWORD: mật khẩu được sử dụng để kết nối qua ADB -
PINTEREST_EMAIL: email tài khoản Pinterest -
PINTEREST_PASSWORD: mật khẩu tài khoản Pinterest -
IMAGE_RANGE: các hình ảnh được gán cho tài khoản đó
Ví dụ, 1-3 cho script biết sử dụng ba hình ảnh đầu tiên từ thư mục images đã được sắp xếp theo thứ tự chữ cái.
Bạn có thể chia hình ảnh giữa nhiều điện thoại đám mây:
199.190.44.226:25922 j3fYxT user1@email.com password1 1-3
199.190.44.227:25922 k9pQrS user2@email.com password2 4-5Điện thoại đầu tiên nhận các hình ảnh 1–3. Điện thoại thứ hai nhận các hình ảnh 4–5.
Bỏ qua phạm vi hình ảnh nếu bạn muốn một điện thoại sử dụng mọi hình ảnh trong thư mục.
Giữ thông tin đăng nhập của bạn an toàn. devices.txt lưu trữ mật khẩu và thông tin nhạy cảm khác dưới dạng văn bản thuần túy. Sử dụng tài khoản thử nghiệm, giữ tệp riêng tư, không bao giờ tải nó lên kho công khai, và xóa nó khi bạn không còn cần nữa.
Chạy script
-
Bắt đầu mọi điện thoại đám mây được bao gồm trong
devices.txt - Bật ADB trên mỗi điện thoại đám mây
- Bắt đầu máy chủ Appium trên máy tính của bạn
-
Kiểm tra rằng script, tệp APK,
devices.txt, vàimagesthư mục nằm ở cùng một vị trí -
Lưu script dưới đây với tên
appium_script.py
appium_script.py
"""
Cách sửdụng
-----
Chạy script này và bao gồm nó trong thư mục với hai tệp: devices.txt và thư mục images .
devices.txt -- một điện thoại mỗi dòng: IP:PORT TOKEN USERNAME PASSWORD [IMAGE_RANGE]
IMAGE_RANGE là một số hình ảnh được sử dụng cho tài khoản cụ thể, ví dụng "1-3"
vào danh sách sắp xếp theo thứ tự chữ của các hình ảnh trong images/
Bỏ qua nó để cho điện thoại sử dụng tất cả hình ảnh trong thư mục. Ví dụ, chia 5 hình ảnh cho 2
điện thoại (điện thoại 1 nhận các hình ảnh 1-3, điện thoại 2 nhận các hình ảnh 4-5):
199.190.44.226:25922 j3fYxT user1 pass1 1-3
199.190.44.227:25922 k9pQrS user2 pass2 4-5
images/ -- một thư mục hình ảnh (jpg/png), hoặc một tệp hình ảnh đơn.
Cần: pip install Appium-Python-Client
Máy chủ Appium đang chạy cục bộ (appium) và ANDROID adb trên PATH.
Script cài đặt APK và các hình ảnh trên điện thoại, mở Pinterest, nhập thông tin đăng nhập của tài khoản, chạy một vài tương tác mẫu trên nguồn cấp, và xuất bản các hình ảnh được chỉ định.
"""
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
# Giới hạn làm ấm
WARM_UP_MIN_ACTIONS = 3
WARM_UP_MAX_ACTIONS = 5
# Thư mục chúng tôi đẩy hình ảnh vào
REMOTE_IMAGE_DIR = "/sdcard/Pictures/bulk_publisher"
# chức năng kết nối và đăng nhập
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:
"""Kết nối ADB, sử dụng tên/ địa chỉ điện thoại và glogin."""
print(f" [KẾTNỐI] {serial}: đang kết nối qua 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" [KẾTNỐI] {serial}: chưa trực tuyến chưa (attempt {attempt}/{CONNECT_RETRIES}, state={state or 'unknown'}), đang thửlại...")
adb("disconnect", serial)
time.sleep(CONNECT_RETRY_SLEEP)
else:
print(f" [KẾTNỐITHẤTBẠI] {serial}")
return False
print(f" [KẾTNỐITHÀNHCÔNG] {serial}: trực tuyến, đang đăng nhập (glogin)...")
result = adb("shell", "glogin", token, serial=serial)
output = (result.stdout + result.stderr).lower()
if "success" in output or "already logged" in output:
print(f" [OK] {serial}: đã đăng nhập")
return True
print(f" [GLOGINTHẤTBẠI] {serial}: {result.stdout or 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)
nếu "thànhcông" trong result.stdout.lower():
trảvề True
time.sleep(3)
print(f" [INSTALL_FAIL] {serial}: {result.stdout hoặc result.stderr}")
trảvề False
def grant_full_media_access(serial: str):
"""Cấp quyền truycập thư viện hình ảnh/video hoànchỉnh qua 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:
"""Bản đồ tên (nếu kịchbản không tìm thấy hình ảnh mặc định)"""
stem, suffix = Path(name).stem, Path(name).suffix
trảvề re.sub(r"[^A-Za-z0-9._-]", "_", stem) + suffix
def push_images(serial: str, image_paths, remote_dir=REMOTE_IMAGE_DIR) -> list:
"""Đẩy hình ảnh cụcbộ lên điện thoại và kích hoạt quét media"""
adb("shell", "mkdir", "-p", remote_dir, serial=serial)
remote_paths = []
used_names = set()
total = len(image_paths)
cho i, local_path trong enumerate(image_paths, start=1):
safe_name = sanitize_filename(Path(local_path).name)
candidate, n = safe_name, 1
trongkhi candidate trong used_names: # hai tên gốc đã được chuẩnhóa thành cùng một tên
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}: đang đẩy hình ảnh {i}/{total}: {Path(local_path).name}")
push_result = adb("push", local_path, remote_path, serial=serial)
nếu push_result.returncode != 0 hoặc "lỗi" trong push_result.stdout.lower():
print(f" [PUSH_FAIL] {serial}: {Path(local_path).name}: "
f"{push_result.stdout.strip() hoặc push_result.stderr.strip()}")
tiếptục
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,
)
nếu scan_result.returncode != 0:
print(f" [SCAN_FAIL] {serial}: {Path(local_path).name}: "
f"{scan_result.stdout.strip() hoặc scan_result.stderr.strip()}")
remote_paths.append(remote_path)
trảvề remote_paths
def disconnect(serial: str):
adb("disconnect", serial)
def build_driver(serial: str):
"""Khởi động phiên Appium sau đó khởi động 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)
trảvề driver
def find_any(driver, candidates, timeout=20, poll=0.5, require_enabled=False):
"""Thử mỗi (theo, giátrị) định vị các ứng cử trong thứtự cho đến khi một cái được giảiquyết,
quét cho đến hết thờigian thay vì thấtbại trên cái đầutiên không trúng."""
deadline = time.time() + timeout
last_err = None
trongkhi time.time() < deadline:
cho by, value trong candidates:
cốgắng:
el = driver.find_element(by=by, value=value)
nếu el.is_displayed() và (không require_enabled hoặc el.get_attribute("enabled") == "true"):
trảvề el
ngoạitrừ Exception như exc: # noqa: BLE001 - cố gắng nhiều chiếnlược có mụcđích
last_err = exc
time.sleep(poll)
raise TimeoutError(f"Không có định vị nào khớp trong {timeout}s: {candidates}") từ 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:
"""Giống tap_any, nhưng trả về False thay vì nâng khi không có gì khớp
-- cho các popups chỉ thỉnh thoảng xuấthiện."""
cốgắng:
tap_any(driver, candidates, timeout=timeout)
return True
except TimeoutError:
return False
# Giai đoạn 1: Màn hình đăng nhập chỉ 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"),
]
# Giai đoạn 2: Màn hình nhập email + mật khẩu, sau đó "Đăng nhập".
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"),
]
# Các popup có thể xuất hiện tại các điểm không lường sau khi đăng nhập.
EMAIL_UPDATE_DISMISS = [
(AppiumBy.ID, "com.pinterest:id/actionPromptDismissButton"), # "Đây là email của bạn?" -> đóng
]
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"),
]
# Mở "Bắt đầu tạo ngay" sheet cuối. Các script tìm các selector tùy thuộc vào độ phân giải và kích thước
CREATE_ICON_BUTTON = [
(AppiumBy.XPATH,
'//android.widget.Button[@content-desc="Tạo"]'
'/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="Tạo"]'
'/android.widget.LinearLayout[@resource-id="com.pinterest:id/tab_content"]'),
]
# Tab nav "Trang chủ"
HOME_TAB_BUTTON = [
(AppiumBy.ACCESSIBILITY_ID, "Trangchủ"),
(AppiumBy.ID, "com.pinterest:id/bottom_nav_home_icon"),
]
# "Bắt đầu tạo ngay" sheet xuất hiện sau khi chạm Tạo -- chọn "Ghim"
PIN_ACTION_BUTTON = [
(AppiumBy.XPATH, '//android.widget.Button[@content-desc="Ghim"]'),
]
# Trình chọn medias "Tiếp theo" button (được kích hoạt sau khi chọn một hình ảnh).
GALLERY_NEXT_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/end_container_text_button"),
]
# Gửi cuối trên màn hình chitiết Ghim -- được đánh dấu "Tạo".
CREATE_PIN_SUBMIT_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/create_gestalt_button"),
]
# --- Khởi động các locator (duyệt feed trang chủ: mở một ghim, thích/lưu/bình luận) ---
# Generic home-feed pin card. Script tìm các ghim trên màn hình cho điện thoại/tài khoản/session.
PIN_CARD = [
# Tìm một match có thể nhấn. Fallback đến các match nếu không có match nhấn nào được tìm, trong trường hợp một
(AppiumBy.XPATH, '//*[contains(@content-desc, "Ghim từ") và @clickable="true"]'),
(AppiumBy.XPATH, '//*[contains(@content-desc, "Ghim từ")]'),
]
REACT_BUTTON = [
(AppiumBy.XPATH, '//android.widget.ImageView[@content-desc="Phản ứng với ghim này"]'),
]
SAVE_BUTTON = [
(AppiumBy.ID, "com.pinterest:id/save_pinit_bt"),
(AppiumBy.XPATH, '//android.widget.Button[@content-desc="Lưu"]'),
]
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, "Quaylại"),
]
def dismiss_popups(driver):
"""Xóa prompt 'Đây là email của bạn?' và các hộp thông báo Android (thông báo, quyền truy cập ảnh) nếu bất kỳ cái nào trên màn hình."""
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):
"""Xác nhận biểu tượng Tạo (trang chủ) đang hiện, chủ động đi
lùi nếu không."""
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 - tái cố nỗ lực, tiếp tục thử
pass
time.sleep(settle_seconds)
# Kiểm tra tính hợp lý nếu ứng dụng đang bị thu nhỏ
driver.activate_app(PINTEREST_PACKAGE)
try:
find_any(driver, CREATE_ICON_BUTTON, timeout=10)
return
except TimeoutError:
pass
raise TimeoutError("Không thể quay trở về feed trang chủ Pinterest (biểu tượng Tạo không xuất hiện)")
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 # đã ở trên màn hình nhập email+password kếthợp trước
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)
# Các phiên làm việc có thể bỏ qua tờ "Bắt đầu tạo bây giờ" và trực tiếp
# đến trình chọn ảnh, vì vậy việc thiếu tờ này không phải là thảm họa.
tap_if_present(driver, PIN_ACTION_BUTTON, timeout=6)
dismiss_popups(driver) # hộp thông báo quyền truy cập ảnh thường xuất tại đây
# Khớp với "<thư mục tải>/<tên_tập_tin>", không chỉ tên_tập_tin -- một điện thoại đã
# có file cùng tên ở nơi khác (vídụ. Tải xuống/).
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) # lưới thư viện có thể mất một thời gian để điền
# "Tiếp" bắt đầu vô hiệu cho đến khi việc chọn được đăng ký -- chờ cho enabled=true,
# không chỉ hiện, hoặc nhấn im trên một nút bị vô hiệu.
tap_any(driver, GALLERY_NEXT_BUTTON, timeout=15, require_enabled=True)
# 45s ở đây bởi Pinterest phải hoàn tất việc tải/xử lý
# hình ảnh trước khi nút này hiện.
tap_any(driver, CREATE_PIN_SUBMIT_BUTTON, timeout=45)
ensure_on_home_feed(driver)
dismiss_popups(driver)
def _open_random_pin(driver) -> bool:
"""Nhấn một pin ngẫu nhiên trên thời gian chính ."""
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 - thử các ứng viên tiếp
continue
if not cards:
continue
try:
random.choice(cards).click()
return True
except Exception: # noqa: BLE001 - một nhấn bị bỏ qua chỉ có nghĩa "thử cái khác"
continue
return False
def _close_pin(driver):
"""Đóng pin góc nhìn."""
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 - cố gắng khôi phục
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 - cố gắng khôi phục
pass
return True
def _scroll_feed(driver):
"""Kéo thời gian trong thời gian ngẫu nhiên, giả lập cuộn nhàn thay vì chỉ nhấn vào pin đầu tiên hiện ra."""
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):
"""Chạy một số hành động duy nhất trong thời gian chính (mở một pin,
rồi thích/lưu/xem bình luận, rồi quay lại) ngay sau khi đăng nhập và trước
khi có pin nào được xuất bản"""
ensure_on_home_feed(driver)
dismiss_popups(driver)
action_count = random.randint(min_actions, max_actions)
print(f" [WARMUP] đang chạy {action_count} hành động khởi động(s)")
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) # không có gì để nhấn đến -- tải thêm nhiều nội dung của luồng
time.sleep(1)
continue
time.sleep(random.uniform(1.5, 2.5)) # để cho góc nhìn hoàn tất tải
dismiss_popups(driver)
random.choice(WARM_UP_PIN_ACTIONS)(driver)
time.sleep(random.uniform(1, 2))
_close_pin(driver)
dismiss_popups(driver)
# Kiểmtralại sau mỗi lầnlặp.
try:
ensure_on_home_feed(driver)
except TimeoutError as exc:
print(f" [WARMUP] đãmất nềntảng trangchủ giữa khởiđộng, dừng sớm: {exc}")
break
time.sleep(random.uniform(0.5, 1.5))
done += 1
ensure_on_home_feed(driver)
dismiss_popups(driver)
# Tải hình
def parse_image_range(range_str: str, line_no: int):
"""Phântích một phạmvi như '1-3' hoặc một chỉsố đơn như '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 dòng {line_no}: phạmvi hình khônghợplệ {range_str!r} (mongđợi vídụ: '1-3' hoặc '4')")
if start < 1 or end < start:
raise ValueError(f"devices.txt dòng {line_no}: phạmvi hình khônghợplệ {range_str!r} (mongđợi vídụ: '1-3' hoặc '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 # -> lấy tấtcả hình trong thưmục, xem 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 dòng {line_no}: mongđợi 4 hoặc 5 trường "
f"(IP:PORT TOKEN USERNAME PASSWORD [IMAGE_RANGE]), nhận {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):
"""Trảvề tấtcả hình đượctìmthấy, sắpxếp theo chữcái. Mỗithiếtbị IMAGE_RANGE
trường trong devices.txt chỉmục vào cùng danhsách sắpxếpnày."""
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")])
# Tựđộngpháthiện: nếu một thamsố được bỏqua, tìm nó tiếptheo tới kịchbản
def autodiscover_devices_file(base: Path) -> Path:
candidate = base / "devices.txt"
if candidate.is_file():
return candidate
raise FileNotFoundError(f"Không tìmthấy devices.txt trong {base}")
def autodiscover_apk(base: Path) -> Path:
apks = sorted(base.glob("*.apk"))
if not apks:
raise FileNotFoundError(f"Không tìmthấy tậptin .apk trong {base}")
if len(apks) > 1:
print(f">> nhiều .apk tậptin đượctìmthấy trong {base}, đangsửdụng {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"Không tìmthấy thưmụcimages/ hoặc tậptin.jpg/.png trong {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 là nỗlựctốtnhất, xuấtbản vẫn quantrọng
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 - một pin chậm/thấtbại không nên làm tốn thời gian
print(f" [PIN_FAILED] {serial}: {Path(remote_image).name}: {exc}")
time.sleep(2)
print(f" [DONE] {serial}: đãxuấtbản {published}/{len(remote_images)} pin(s)")
except Exception as exc: # noqa: BLE001 - tiếp tục đến điệnthoại tiếp theo cách nào
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], một đường trên mỗi hàng "
f"(mặcđịnh: devices.txt tiếp theo kịchbản này, tứclà {script_dir})")
parser.add_argument("apk_path", nargs="?", default=None,
help="Đườngdẫn đến APK Pinterest để cài đặt (mặcđịnh: file .apk tìm thấy tiếp theo kịchbản này)")
parser.add_argument("images", nargs="?", default=None,
help="Thưmục hình ảnh, hoặc một file hình ảnh đơn "
"(mặcđịnh: thư hình ảnh/ hoặc các file jpg/.png không có chủ đề, tiếp theo kịchbản này)")
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}\nVuilòng điền devices_file / apk_path / images một cách rõràng, hoặc đặt chúng tiếp theo kịchbản.")
print(f">> file devices: {devices_file}")
print(f">> apk: {apk_path}")
print(f">> hìnhảnh: {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"Không tìm thấy hình ảnh nào tại {images_arg}")
print(f">> {len(devices)} điệnthoại(s) đãđược xếphàng, {len(all_images)} hìnhảnh(s) cósẵn:")
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']}: phạmvi hìnhảnh {start}-{end} vượtquá "
f"các {len(all_images)} hìnhảnh(s) cósẵn")
continue
image_subset = all_images[start - 1:end]
run_device(device, str(apk_path), image_subset)
if __name__ == "__main__":
main()- MởIDE hoặc terminal và chạy:
python appium_script.py
Kịch bản sẽ hiển thị tiến trình của nó trong terminal. Nó sẽ kết nối với điện thoại đầu tiên, hoàn thành quy trình làm việc của Pinterest, ngắt kết nối, và sau đó tiếp tục với điện thoại tiếp theo.
Bạn muốn thử một ứng dụng Android khác? Đây là một kịch bản trình diễn, không phải là công cụ hoàn chỉnh cho mọi dịch vụ. Bạn có thể điều chỉnh nó, nhưng mỗi ứng dụng sử dụng các nút và bộ định vị phần tử khác nhau. Sử dụng Appium Inspector để tìm các bộ định vị đúng và cập nhật kịch bản cho ứng dụng của bạn.