修复转发
All checks were successful
Update Code / StopService (windows-101.36.104.175) (push) Successful in 0s
Update Code / StopService (windows-101.36.102.136) (push) Successful in 0s
Update Code / CD (windows-101.36.102.136) (push) Successful in 8s
Update Code / CD (windows-101.36.104.175) (push) Successful in 12s
All checks were successful
Update Code / StopService (windows-101.36.104.175) (push) Successful in 0s
Update Code / StopService (windows-101.36.102.136) (push) Successful in 0s
Update Code / CD (windows-101.36.102.136) (push) Successful in 8s
Update Code / CD (windows-101.36.104.175) (push) Successful in 12s
This commit is contained in:
271
spider/task.py
271
spider/task.py
@@ -585,11 +585,148 @@ def _get_latest_post_url(page):
|
|||||||
return page.url
|
return page.url
|
||||||
|
|
||||||
|
|
||||||
|
def _open_latest_profile_story(page):
|
||||||
|
post_links = page.locator('//div[@aria-posinset="1"]//a[@role="link"]')
|
||||||
|
count = post_links.count()
|
||||||
|
if count == 0:
|
||||||
|
raise OperationFailed("未找到主页最新动态链接")
|
||||||
|
|
||||||
|
preferred_indexes = [2, 1, 0]
|
||||||
|
for index in preferred_indexes:
|
||||||
|
if index >= count:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
post_links.nth(index).click(timeout=60000)
|
||||||
|
sleep(3, 5)
|
||||||
|
page.reload(timeout=180000)
|
||||||
|
return page.url
|
||||||
|
except Error as e:
|
||||||
|
logger.warning(f"打开主页最新动态失败,尝试下一个链接: index={index}, error={e}")
|
||||||
|
|
||||||
|
raise OperationFailed("打开主页最新动态失败")
|
||||||
|
|
||||||
|
|
||||||
def _is_facebook_home(page):
|
def _is_facebook_home(page):
|
||||||
current_url = (page.url or '').rstrip('/')
|
current_url = (page.url or '').rstrip('/')
|
||||||
return current_url in {'https://www.facebook.com', 'https://facebook.com'}
|
return current_url in {'https://www.facebook.com', 'https://facebook.com'}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_share_config(target_url):
|
||||||
|
if 'permalink.php?story_fbid' in target_url or '/posts/' in target_url or '/permalink/' in target_url:
|
||||||
|
return {
|
||||||
|
'share_button': '//*[@role="dialog"]//div[@data-ad-rendering-role="share_button"]',
|
||||||
|
'input_box': '//form[@method="POST" and count(@*) = 1]/div/div/div[2]',
|
||||||
|
'share_now_button': '//span[text()="Share now"]',
|
||||||
|
}
|
||||||
|
if 'watch/?v' in target_url or '/videos/' in target_url or 'watch?v' in target_url:
|
||||||
|
return {
|
||||||
|
'share_button': '//span[@dir="auto" and text()="Share"]',
|
||||||
|
'input_box': '//form[@method="POST" and count(@*) = 1]/div/div/div[2]',
|
||||||
|
'share_now_button': '//span[text()="Share now"]',
|
||||||
|
}
|
||||||
|
if '/reel/' in target_url:
|
||||||
|
return {
|
||||||
|
'share_button': '//div[@aria-label="Share"]',
|
||||||
|
'input_box': '//form[@method="POST" and count(@*) = 1]/div/div/div[2]',
|
||||||
|
'share_now_button': '//span[text()="Share now"]',
|
||||||
|
}
|
||||||
|
raise OperationFailed(f'不支持的帖子类型: {target_url}')
|
||||||
|
|
||||||
|
|
||||||
|
def _click_share_button(page, selector):
|
||||||
|
locator = page.locator(selector)
|
||||||
|
count = locator.count()
|
||||||
|
|
||||||
|
for index in range(count):
|
||||||
|
candidate = locator.nth(index)
|
||||||
|
try:
|
||||||
|
if candidate.is_visible():
|
||||||
|
candidate.scroll_into_view_if_needed(timeout=30000)
|
||||||
|
candidate.click(timeout=60000)
|
||||||
|
return
|
||||||
|
except Error:
|
||||||
|
continue
|
||||||
|
|
||||||
|
element = page.query_selector(selector)
|
||||||
|
if element is None:
|
||||||
|
raise OperationFailed(f'未找到分享按钮: {selector}')
|
||||||
|
|
||||||
|
page.evaluate(
|
||||||
|
"""(node) => {
|
||||||
|
const clickable =
|
||||||
|
node.closest('[role="button"]') ||
|
||||||
|
node.closest('[aria-label]') ||
|
||||||
|
node.parentElement ||
|
||||||
|
node;
|
||||||
|
clickable.click();
|
||||||
|
}""",
|
||||||
|
element
|
||||||
|
)
|
||||||
|
page.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_share_content(page, selector, content):
|
||||||
|
if not content:
|
||||||
|
return
|
||||||
|
container = page.locator(selector).first
|
||||||
|
container.wait_for(state='visible', timeout=60000)
|
||||||
|
|
||||||
|
candidates = [
|
||||||
|
container.locator('[contenteditable="true"]').first,
|
||||||
|
container.locator('[role="textbox"]').first,
|
||||||
|
page.locator(f'{selector}//div[@contenteditable="true"]').first,
|
||||||
|
page.locator(f'{selector}//div[@role="textbox"]').first,
|
||||||
|
container,
|
||||||
|
]
|
||||||
|
|
||||||
|
last_error = None
|
||||||
|
for candidate in candidates:
|
||||||
|
try:
|
||||||
|
if candidate.count() == 0:
|
||||||
|
continue
|
||||||
|
candidate.wait_for(state='visible', timeout=10000)
|
||||||
|
candidate.click(timeout=30000)
|
||||||
|
page.keyboard.press('Control+A')
|
||||||
|
page.keyboard.press('Backspace')
|
||||||
|
candidate.fill(content, timeout=60000)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
last_error = e
|
||||||
|
|
||||||
|
element = container.element_handle()
|
||||||
|
if element is None:
|
||||||
|
raise OperationFailed(f'未找到分享输入框: {selector}')
|
||||||
|
|
||||||
|
page.evaluate(
|
||||||
|
"""({ node, value }) => {
|
||||||
|
const editable =
|
||||||
|
node.matches?.('[contenteditable="true"], [role="textbox"]')
|
||||||
|
? node
|
||||||
|
: node.querySelector?.('[contenteditable="true"], [role="textbox"]') || node;
|
||||||
|
editable.focus();
|
||||||
|
if ('value' in editable) {
|
||||||
|
editable.value = value;
|
||||||
|
} else {
|
||||||
|
editable.textContent = value;
|
||||||
|
}
|
||||||
|
editable.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }));
|
||||||
|
editable.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
|
}""",
|
||||||
|
{"node": element, "value": content}
|
||||||
|
)
|
||||||
|
page.wait_for_timeout(500)
|
||||||
|
if last_error:
|
||||||
|
logger.warning(f"分享文案输入回退到 JS 注入: {last_error}")
|
||||||
|
|
||||||
|
|
||||||
|
def _submit_share(page, selector):
|
||||||
|
share_now_button = page.locator(selector).first
|
||||||
|
share_now_button.wait_for(state='visible', timeout=60000)
|
||||||
|
share_now_button.click(timeout=60000)
|
||||||
|
page.wait_for_selector('//span[text()="Posting..."]', state='detached', timeout=180000)
|
||||||
|
page.wait_for_selector('//span[text()="Shared to your profile"]', timeout=180000)
|
||||||
|
|
||||||
|
|
||||||
def playwright_post(cookies, content, image_key=None, dry_run=False):
|
def playwright_post(cookies, content, image_key=None, dry_run=False):
|
||||||
path = os.path.join(BASE_PATH, 'chrome', '130-0008', 'chrome.exe')
|
path = os.path.join(BASE_PATH, 'chrome', '130-0008', 'chrome.exe')
|
||||||
with lock:
|
with lock:
|
||||||
@@ -1322,78 +1459,80 @@ def playwright_share(cookies, target_url, content):
|
|||||||
with lock:
|
with lock:
|
||||||
with sync_playwright() as playwright:
|
with sync_playwright() as playwright:
|
||||||
update_windows_distinguish()
|
update_windows_distinguish()
|
||||||
|
parsed_cookies = parse_cookies(cookies)
|
||||||
|
max_browser_retries = 3
|
||||||
|
last_error = None
|
||||||
|
|
||||||
username = 'moremore_51WM1'
|
for browser_attempt in range(max_browser_retries):
|
||||||
password = 'TOv5y0nXCZH_JH+5'
|
browser = None
|
||||||
country = 'US'
|
context = None
|
||||||
|
try:
|
||||||
|
browser = playwright.chromium.launch(
|
||||||
|
headless=False, args=['--start-maximized'], executable_path=path,
|
||||||
|
# proxy={
|
||||||
|
# "server": "http://pr.oxylabs.io:7777", # 必填
|
||||||
|
# "username": f"customer-{username}-cc-{country}",
|
||||||
|
# "password": password
|
||||||
|
# }
|
||||||
|
)
|
||||||
|
|
||||||
browser = playwright.chromium.launch(
|
context = browser.new_context(no_viewport=True)
|
||||||
headless=False, args=['--start-maximized'], executable_path=path,
|
context.add_cookies(parsed_cookies)
|
||||||
# proxy={
|
page = context.new_page()
|
||||||
# "server": "http://pr.oxylabs.io:7777", # 必填
|
page.set_default_timeout(30000)
|
||||||
# "username": f"customer-{username}-cc-{country}",
|
page.set_default_navigation_timeout(180000)
|
||||||
# "password": password
|
check_account_status(page, parsed_cookies)
|
||||||
# }
|
|
||||||
)
|
|
||||||
|
|
||||||
context = browser.new_context(no_viewport=True)
|
retry_goto(page, target_url)
|
||||||
context.add_cookies(parse_cookies(cookies))
|
share_config = _get_share_config(target_url)
|
||||||
page = context.new_page()
|
_click_share_button(page, share_config['share_button'])
|
||||||
check_account_status(page, parse_cookies(cookies))
|
_fill_share_content(page, share_config['input_box'], content)
|
||||||
try:
|
_edit_privacy(page)
|
||||||
retry_goto(page, target_url)
|
_submit_share(page, share_config['share_now_button'])
|
||||||
if 'permalink.php?story_fbid' in target_url or '/posts/' in target_url or "/permalink/" in target_url:
|
|
||||||
# 文字或图片类型
|
|
||||||
share_button = page.query_selector(
|
|
||||||
'//*[@role="dialog"]//div[@data-ad-rendering-role="share_button"]')
|
|
||||||
input_box = '//form[@method="POST" and count(@*) = 1]/div/div/div[2]'
|
|
||||||
share_now_button = '//span[text()="Share now"]'
|
|
||||||
share_button.scroll_into_view_if_needed()
|
|
||||||
page.evaluate("(element) => element.click()", share_button)
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
elif 'watch/?v' in target_url or '/videos/' in target_url or 'watch?v' in target_url:
|
uid = {i['name']: i['value'] for i in parsed_cookies}['c_user']
|
||||||
# 视频类型, 视频类型,
|
retry_goto(page, f'https://facebook.com/profile.php?id={uid}')
|
||||||
share_button = '//span[@dir="auto" and text()="Share"]'
|
page.wait_for_load_state()
|
||||||
input_box = '//form[@method="POST" and count(@*) = 1]/div/div/div[2]'
|
post_url = _open_latest_profile_story(page)
|
||||||
share_now_button = '//span[text()="Share now"]'
|
screenshot_content = _full_screenshot()
|
||||||
page.locator(share_button).first.click()
|
key = f'screenshot/{uuid.uuid4()}.png'
|
||||||
elif '/reel/' in target_url:
|
put_object(key, screenshot_content)
|
||||||
# 短视频类型
|
return {'response_url': post_url, 'screenshot_key': key}
|
||||||
share_button = '//div[@aria-label="Share"]'
|
except TimeoutError as e:
|
||||||
input_box = '//form[@method="POST" and count(@*) = 1]/div/div/div[2]'
|
last_error = e
|
||||||
share_now_button = '//span[text()="Share now"]'
|
logger.warning(
|
||||||
page.locator(share_button).click()
|
f"转发任务超时,尝试重建浏览器重试: attempt {browser_attempt + 1}/{max_browser_retries}, error={e}"
|
||||||
else:
|
)
|
||||||
raise OperationFailed(f'不支持的帖子类型: {target_url}')
|
except Error as e:
|
||||||
page.locator(input_box).type(content, delay=30)
|
last_error = e
|
||||||
_edit_privacy(page)
|
if is_page_crash_error(e):
|
||||||
page.click(share_now_button)
|
logger.warning(
|
||||||
time.sleep(1)
|
f"转发任务页面崩溃,尝试重建浏览器重试: attempt {browser_attempt + 1}/{max_browser_retries}, error={e}"
|
||||||
page.wait_for_selector('//span[text()="Posting..."]', state='detached')
|
)
|
||||||
time.sleep(1)
|
else:
|
||||||
success_tag = page.wait_for_selector('//span[text()="Shared to your profile"]')
|
logger.warning(
|
||||||
if not success_tag:
|
f"转发任务 Playwright 异常,尝试重试: attempt {browser_attempt + 1}/{max_browser_retries}, error={e}"
|
||||||
raise OperationFailed('转发失败,原因未知')
|
)
|
||||||
cookies = {i['name']: i['value'] for i in parse_cookies(cookies)}
|
finally:
|
||||||
|
if context is not None:
|
||||||
|
try:
|
||||||
|
context.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if browser is not None:
|
||||||
|
try:
|
||||||
|
browser.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
uid = cookies['c_user']
|
if browser_attempt < max_browser_retries - 1:
|
||||||
retry_goto(page, f'https://facebook.com/profile.php?id={uid}')
|
time.sleep(2)
|
||||||
page.wait_for_load_state()
|
|
||||||
post_index = page.locator('//div[@aria-posinset="1"]//a[@role="link"]').nth(2)
|
|
||||||
post_index.click()
|
|
||||||
time.sleep(5)
|
|
||||||
page.reload()
|
|
||||||
post_url = page.url
|
|
||||||
screenshot_content = _full_screenshot()
|
|
||||||
except Error as e:
|
|
||||||
raise OperationFailed(f'操作超时,请重试{e}')
|
|
||||||
context.close()
|
|
||||||
browser.close()
|
|
||||||
|
|
||||||
key = f'screenshot/{uuid.uuid4()}.png'
|
if isinstance(last_error, TimeoutError):
|
||||||
put_object(key, screenshot_content)
|
raise OperationFailed(f'操作超时,请重试: {last_error}')
|
||||||
return {'response_url': post_url, 'screenshot_key': key}
|
if isinstance(last_error, Error) and is_page_crash_error(last_error):
|
||||||
|
raise OperationFailed(f'页面崩溃,请重试: {last_error}')
|
||||||
|
raise OperationFailed(f'操作失败,请重试: {last_error}')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
43
test_playwright_share.py
Normal file
43
test_playwright_share.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
import spider.task as task_module
|
||||||
|
|
||||||
|
|
||||||
|
# 直接在这里填写测试参数
|
||||||
|
COOKIES = {"c_user":"61588267419224","datr":"3D2XaYqWJ1w9rJU6X6e02or_","fr":"0k8G2UtA1NqMJqD0s.AWemansED9s7o5tmbiUwA7gqAoWOk99OEJw8_zCrRks9IgULoSk.Bplz6g..AAA.0.0.Bplz6g.AWfaScT_l9g4id9lBpHtDtHo-T4","xs":"28:izkUxLXyFn_-OA:2:1771519655:-1:-1"}
|
||||||
|
|
||||||
|
TARGET_URL = "https://www.facebook.com/permalink.php?story_fbid=pfbid023QsxMBw26HAdt3LW1Ln7GUugWYbQkzfL9Ws68XUiuaXJvFD3u1iKtVFq7hpypdFtl&id=61580561183111"
|
||||||
|
|
||||||
|
CONTENT = "You’ve earned my admiration with this quality."
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_config():
|
||||||
|
missing = [key for key, value in COOKIES.items() if not str(value).strip()]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"cookies 缺少字段: {', '.join(missing)}")
|
||||||
|
|
||||||
|
if not TARGET_URL.strip():
|
||||||
|
raise ValueError("TARGET_URL 不能为空")
|
||||||
|
|
||||||
|
if "facebook.com" not in TARGET_URL:
|
||||||
|
raise ValueError(f"TARGET_URL 不是有效的 Facebook 链接: {TARGET_URL}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
_validate_config()
|
||||||
|
|
||||||
|
logger.add("./log/test_playwright_share.log", rotation="20 MB")
|
||||||
|
|
||||||
|
result = task_module.playwright_share(
|
||||||
|
cookies=COOKIES,
|
||||||
|
target_url=TARGET_URL,
|
||||||
|
content=CONTENT,
|
||||||
|
)
|
||||||
|
logger.info("转发结果: {}", result)
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user