2025-09-08 09:32:45 +08:00
from pathlib import Path
import sys
import os
# 添加项目根目录到 Python 路径
current_dir = Path ( __file__ ) . parent . resolve ( )
project_root = current_dir . parent . parent
sys . path . append ( str ( project_root ) )
from conf import LOCAL_CHROME_PATH , BASE_DIR
import re
from datetime import datetime
from playwright . async_api import Playwright , async_playwright
import os
import asyncio
#from uploader.tk_uploader.tk_config import Tk_Locator
from utils . base_social_media import set_init_script
from utils . files_times import get_absolute_path
from utils . log import X_logger
from utils . files_times import generate_schedule_time_next_day
2025-09-09 14:26:39 +08:00
from utils . anti_detection import create_stealth_browser , create_stealth_context , setup_stealth_page
from utils . human_typing_wrapper import create_human_typer
2025-09-08 09:32:45 +08:00
async def cookie_auth ( account_file ) :
async with async_playwright ( ) as playwright :
browser = await playwright . chromium . launch ( headless = True )
context = await browser . new_context ( storage_state = account_file )
context = await set_init_script ( context )
# 创建一个新的页面
page = await context . new_page ( )
# 访问指定的 URL
await page . goto ( " https://mp.weixin.qq.com/ " )
#await page.wait_for_load_state('networkidle')
await page . wait_for_load_state ( ' domcontentloaded ' , timeout = 30000 )
try :
# 选择所有的 select 元素
select_elements = await page . query_selector_all ( ' select ' )
for element in select_elements :
class_name = await element . get_attribute ( ' class ' )
# 使用正则表达式匹配特定模式的 class 名称
if re . match ( r ' ins-.*-SelectFormContainer.* ' , class_name ) :
X_logger . error ( " [+] cookie expired " )
return False
X_logger . success ( " [+] cookie valid " )
return True
except :
X_logger . success ( " [+] cookie valid " )
return True
async def wechat_setup ( account_file , handle = False ) :
account_file = get_absolute_path ( account_file , " https://mp.weixin.qq.com/ " )
if not os . path . exists ( account_file ) or not await cookie_auth ( account_file ) :
if not handle :
return False
X_logger . info ( ' [+] cookie file is not existed or expired. Now open the browser auto. Please login with your way(gmail phone, whatever, the cookie file will generated after login ' )
await get_wechat_cookie ( account_file )
return True
async def get_wechat_cookie ( account_file ) :
async with async_playwright ( ) as playwright :
options = {
' args ' : [
' --lang zh_CN ' ,
] ,
' headless ' : False , # Set headless option here
}
# Make sure to run headed.
browser = await playwright . chromium . launch ( * * options )
# Setup context however you like.
context = await browser . new_context ( ) # Pass any options
context = await set_init_script ( context )
# Pause the page, and start recording manually.
page = await context . new_page ( )
await page . goto ( " https://mp.weixin.qq.com/ " )
await page . pause ( )
# 点击调试器的继续, 保存cookie
await context . storage_state ( path = account_file )
# if __name__ == '__main__':
# account_file = Path(BASE_DIR / "cookies" / "wechat_uploader" / "account.json")
# account_file.parent.mkdir(exist_ok=True)
# cookie_setup = asyncio.run(wechat_setup(str(account_file), handle=True))
class WechatVideo ( object ) :
2025-09-09 14:26:39 +08:00
def __init__ ( self , title , file_path , tags , publish_date , account_file , thumbnail_path = None , headless = True ) :
2025-09-08 09:32:45 +08:00
self . title = title
self . file_path = file_path
self . tags = tags
self . publish_date = publish_date
self . thumbnail_path = thumbnail_path
self . account_file = account_file
2025-09-09 14:26:39 +08:00
self . headless = headless # 允许用户选择是否使用无头模式
2025-09-08 09:32:45 +08:00
self . local_executable_path = LOCAL_CHROME_PATH
self . locator_base = None
# click title to remove the focus.
# await self.locator_base.locator("h1:has-text('Upload video')").click()
async def set_schedule_time_wechat ( self , page , publish_date ) :
# 先点击定时发表开关,启用定时发布功能
await page . locator ( ' .mass-send__td-setting > .publish_container > .weui-desktop-form__controls > .mass-send__timer-wrp > .weui-desktop-switch ' ) . click ( )
await asyncio . sleep ( 1 )
deta = ( publish_date . date ( ) - datetime . now ( ) . date ( ) ) . days
2025-09-09 14:26:39 +08:00
print ( deta )
2025-09-08 09:32:45 +08:00
if deta == 0 :
day_str = " 今天 "
if deta == 1 :
day_str = " 明天 "
else :
day_str = publish_date . strftime ( " % #m月 %#d 日 " )
time_str = publish_date . strftime ( " % H: % M " )
await asyncio . sleep ( 1 )
# 点击日期下拉框( 选择第3个匹配的元素, 即日期选择器)
await page . locator ( ' dt.weui-desktop-form__dropdown__dt.weui-desktop-form__dropdown__inner-button ' ) . nth ( 2 ) . click ( )
await asyncio . sleep ( 1 )
# 在下拉列表中查找并点击对应的日期
dropdown_option = page . locator ( f ' span.weui-desktop-dropdown__list-ele__text:has-text( " { day_str } " ) ' )
await dropdown_option . click ( )
await asyncio . sleep ( 1 )
# 选择时间(点击第二个时间选择器容器)
await page . locator ( ' dl.weui-desktop-picker__time ' ) . nth ( 1 ) . click ( )
await page . keyboard . press ( " Control+KeyA " )
await page . keyboard . type ( str ( time_str ) )
await asyncio . sleep ( 0.5 )
# 点击页面其他地方关闭时间选择下拉栏,使发表按钮显示
await page . locator ( ' body ' ) . click ( )
await asyncio . sleep ( 1 )
2025-09-09 14:26:39 +08:00
#截图保存
await page . screenshot ( path = f " { self . title } .png " )
2025-09-08 09:32:45 +08:00
# 点击发表按钮
await page . locator ( ' button.weui-desktop-btn.weui-desktop-btn_primary:has-text( " 发表 " ) ' ) . first . click ( )
await asyncio . sleep ( 1 )
async def handle_upload_error ( self , page ) :
X_logger . info ( " 微信公众号上传错误,正在重试... " )
try :
# 重新查找文件上传元素并重试
upload_element = page . locator ( ' input[type= " file " ] ' )
await upload_element . wait_for ( state = ' visible ' , timeout = 10000 )
await upload_element . set_input_files ( self . file_path )
X_logger . info ( " 重试上传成功 " )
except Exception as e :
X_logger . error ( f " 重试上传失败: { e } " )
async def upload ( self , playwright : Playwright ) - > None :
2025-09-09 14:26:39 +08:00
# 使用统一的反检测工具
browser = await create_stealth_browser (
playwright = playwright ,
headless = self . headless ,
executable_path = self . local_executable_path
)
context = await create_stealth_context (
browser = browser ,
account_file = self . account_file ,
headless = self . headless
)
page = await setup_stealth_page ( context , " https://mp.weixin.qq.com/ " )
# 创建人类化输入包装器
human_typer = create_human_typer ( page )
2025-09-08 09:32:45 +08:00
X_logger . info ( f ' [+]正在上传到微信公众号------- { self . title } ' )
await page . wait_for_load_state ( ' domcontentloaded ' , timeout = 30000 )
try :
# 等待页面加载完成
await page . wait_for_timeout ( 3000 )
# 点击图文创作按钮,并处理可能的新页面
new_page = await self . click_image_text_creation ( page , context )
# 如果有新页面,使用新页面进行后续操作
working_page = new_page if new_page else page
# 等待创作页面加载
await working_page . wait_for_timeout ( 3000 )
# 上传图片或视频文件
await self . upload_media_file ( working_page )
# 添加标题和内容
2025-09-09 14:26:39 +08:00
await self . add_title_content ( working_page , human_typer )
# 调试: 分析页面状态
await self . debug_page_elements ( working_page )
2025-09-08 09:32:45 +08:00
# 发布内容
await self . click_publish ( working_page )
await self . set_schedule_time_wechat ( working_page , self . publish_date )
except Exception as e :
X_logger . error ( f " 上传过程中出现错误: { e } " )
# 处理上传错误
await self . handle_upload_error ( page )
await context . storage_state ( path = f " { self . account_file } " ) # save cookie
X_logger . info ( ' [-] 更新cookie成功! ' )
await asyncio . sleep ( 2 ) # close delay for look the video status
# close all
await context . close ( )
await browser . close ( )
async def click_image_text_creation ( self , page , context ) :
""" 点击图文创作按钮,跳转到创作页面,返回新页面对象(如果有) """
try :
# 根据用户提供的HTML结构, 查找图文创作按钮
image_text_button = page . locator ( ' div.new-creation__menu-item:has(div.new-creation__menu-title:has-text( " 图文 " )) ' )
await image_text_button . wait_for ( state = ' visible ' , timeout = 15000 )
# 尝试检测新页面
try :
# 监听新页面创建事件,设置短超时
async with context . expect_page ( timeout = 3000 ) as new_page_info :
# 点击图文创作按钮
await image_text_button . click ( )
X_logger . info ( " 成功点击图文创作按钮 " )
# 获取新页面
new_page = await new_page_info . value
await new_page . wait_for_load_state ( ' domcontentloaded ' , timeout = 30000 )
X_logger . info ( " 检测到新页面,将在新页面中进行后续操作 " )
return new_page
except :
# 如果没有检测到新页面,等待当前页面跳转
X_logger . info ( " 未检测到新页面,等待当前页面跳转 " )
await page . wait_for_timeout ( 2000 )
try :
await page . wait_for_load_state ( ' domcontentloaded ' , timeout = 15000 )
except :
pass # 忽略超时错误
X_logger . info ( " 在当前页面中跳转到创作页面 " )
return None
except Exception as e :
X_logger . error ( f " 点击图文创作按钮失败: { e } " )
# 备用方案: 使用SVG图标定位
try :
svg_button = page . locator ( ' div.new-creation__menu-item:has(svg) ' )
# 尝试检测新页面
try :
# 监听新页面创建事件,设置短超时
async with context . expect_page ( timeout = 3000 ) as new_page_info :
await svg_button . first . click ( )
X_logger . info ( " 使用备用方案成功点击图文创作按钮 " )
# 获取新页面
new_page = await new_page_info . value
await new_page . wait_for_load_state ( ' domcontentloaded ' , timeout = 30000 )
X_logger . info ( " 检测到新页面,将在新页面中进行后续操作 " )
return new_page
except :
# 如果没有检测到新页面,等待当前页面跳转
X_logger . info ( " 备用方案:未检测到新页面,等待当前页面跳转 " )
await page . wait_for_timeout ( 2000 )
try :
await page . wait_for_load_state ( ' domcontentloaded ' , timeout = 15000 )
except :
pass # 忽略超时错误
X_logger . info ( " 在当前页面中跳转到创作页面 " )
return None
except Exception as backup_e :
X_logger . error ( f " 备用方案也失败: { backup_e } " )
raise
2025-09-09 14:26:39 +08:00
async def add_title_content ( self , page , human_typer ) :
2025-09-08 09:32:45 +08:00
""" 添加文章标题和内容 """
try :
# 等待编辑器加载
await page . wait_for_timeout ( 2000 )
# 查找标题输入框 - 使用微信公众号的实际选择器
title_selectors = [
' textarea#title[placeholder*= " 请在这里输入标题 " ] ' , # 精确匹配您提供的标题框
' textarea#title ' , # 备用选择器
' input[placeholder*= " 标题 " ], input[placeholder*= " 请输入标题 " ] ' , # 通用备用
' textarea[placeholder*= " 标题 " ] ' # textarea类型的标题框
]
title_added = False
for selector in title_selectors :
try :
title_input = page . locator ( selector )
if await title_input . count ( ) > 0 :
2025-09-09 14:26:39 +08:00
# 先点击元素确保获得焦点
2025-09-08 09:32:45 +08:00
await title_input . first . click ( )
2025-09-09 14:26:39 +08:00
await page . wait_for_timeout ( 500 )
# 确保元素真正获得焦点 - 多次点击和聚焦
await title_input . first . focus ( )
await page . wait_for_timeout ( 200 )
# 清空现有内容
await page . keyboard . press ( " Control+A " )
await page . wait_for_timeout ( 200 )
await page . keyboard . press ( " Delete " )
await page . wait_for_timeout ( 200 )
# 使用传统的键盘输入,确保内容真正输入
await page . keyboard . type ( self . title , delay = 50 ) # 50ms延迟模拟人类输入
X_logger . info ( " 标题输入完成 " )
2025-09-08 09:32:45 +08:00
X_logger . info ( f " 成功添加标题: { self . title } (使用选择器: { selector } ) " )
title_added = True
break
except Exception as e :
X_logger . debug ( f " 标题选择器 { selector } 失败: { e } " )
continue
if not title_added :
X_logger . warning ( " 未找到标题输入框 " )
# 查找内容编辑区域 - 使用微信公众号的ProseMirror编辑器
content_selectors = [
' div.ProseMirror[contenteditable= " true " ] ' , # 精确匹配您提供的内容编辑器
' div[contenteditable= " true " ][translate= " no " ] ' , # 更具体的选择器
' div[contenteditable= " true " ] ' , # 通用备用
' textarea[placeholder*= " 内容 " ] ' # textarea类型的内容框
]
content_added = False
for selector in content_selectors :
try :
content_editor = page . locator ( selector )
if await content_editor . count ( ) > 0 :
2025-09-09 14:26:39 +08:00
# 构建内容,包含标签
content = self . title + " \n \n "
if self . tags :
content + = " \n " . join ( [ f " # { tag } " for tag in self . tags ] )
# 先点击内容编辑器确保获得焦点
2025-09-08 09:32:45 +08:00
await content_editor . first . click ( )
2025-09-09 14:26:39 +08:00
await page . wait_for_timeout ( 500 )
# 确保编辑器真正获得焦点
await content_editor . first . focus ( )
await page . wait_for_timeout ( 200 )
2025-09-08 09:32:45 +08:00
# 清除现有内容(包括占位符)
await page . keyboard . press ( " Control+A " )
2025-09-09 14:26:39 +08:00
await page . wait_for_timeout ( 200 )
2025-09-08 09:32:45 +08:00
await page . keyboard . press ( " Delete " )
2025-09-09 14:26:39 +08:00
await page . wait_for_timeout ( 200 )
2025-09-08 09:32:45 +08:00
2025-09-09 14:26:39 +08:00
# 使用传统的键盘输入,确保内容真正输入到富文本编辑器
await page . keyboard . type ( content , delay = 30 ) # 30ms延迟模拟人类输入
X_logger . info ( " 内容输入完成 " )
2025-09-08 09:32:45 +08:00
X_logger . info ( f " 成功添加文章内容和标签 (使用选择器: { selector } ) " )
content_added = True
break
except Exception as e :
X_logger . debug ( f " 内容选择器 { selector } 失败: { e } " )
continue
if not content_added :
X_logger . warning ( " 未找到内容编辑区域 " )
except Exception as e :
X_logger . error ( f " 添加标题和内容时出错: { e } " )
async def upload_media_file ( self , page ) :
""" 上传图片或视频文件 """
try :
# 查找文件上传按钮或区域, 优先使用微信公众号的透明label上传按钮
upload_buttons = [
' label[style*= " opacity: 0 " ][style*= " width: 100 % " ][style*= " height: 100 % " ][style*= " cursor: pointer " ] ' , # 精确匹配您提供的样式
' label[style*= " opacity: 0 " ][style*= " cursor: pointer " ] ' , # 微信公众号透明上传按钮
' label[style*= " opacity: 0 " ] ' , # 更通用的透明label选择器
' input[type= " file " ] ' ,
' button:has-text( " 插入图片 " ) ' ,
' button:has-text( " 上传图片 " ) ' ,
' div:has-text( " 点击上传 " ) ' ,
' .upload-btn '
]
uploaded = False
for selector in upload_buttons :
try :
upload_element = page . locator ( selector )
if await upload_element . count ( ) > 0 :
X_logger . info ( f " 找到上传元素,使用选择器: { selector } " )
if selector == ' input[type= " file " ] ' :
await upload_element . first . set_input_files ( self . file_path )
else :
# 对于透明label和其他元素, 先悬停再点击触发文件选择器
element = upload_element . first
# 确保元素可见
await element . scroll_into_view_if_needed ( )
await page . wait_for_timeout ( 300 )
# 获取元素的边界框
box = await element . bounding_box ( )
if box :
# 计算元素中心点
center_x = box [ ' x ' ] + box [ ' width ' ] / 2
center_y = box [ ' y ' ] + box [ ' height ' ] / 2
# 先移动鼠标到元素中心位置
await page . mouse . move ( center_x , center_y )
X_logger . info ( f " 鼠标移动到坐标: ( { center_x } , { center_y } ) " )
# 稍等一下让透明按钮显示
await page . wait_for_timeout ( 800 )
# 再次悬停确保状态
await element . hover ( )
await page . wait_for_timeout ( 300 )
async with page . expect_file_chooser ( ) as fc_info :
await element . click ( )
file_chooser = await fc_info . value
await file_chooser . set_files ( self . file_path )
else :
# 如果无法获取边界框,使用普通悬停
await element . hover ( )
await page . wait_for_timeout ( 500 )
async with page . expect_file_chooser ( ) as fc_info :
await element . click ( )
file_chooser = await fc_info . value
await file_chooser . set_files ( self . file_path )
X_logger . info ( " 成功上传媒体文件 " )
uploaded = True
break
except Exception as e :
X_logger . debug ( f " 选择器 { selector } 失败: { e } " )
continue
if not uploaded :
X_logger . warning ( " 未找到文件上传元素,跳过文件上传 " )
except Exception as e :
X_logger . error ( f " 上传媒体文件时出错: { e } " )
2025-09-09 14:26:39 +08:00
async def debug_page_elements ( self , page ) :
""" 调试页面元素,分析无头模式下的问题 """
try :
X_logger . info ( " 🔍 开始调试页面元素... " )
# 获取页面URL
current_url = page . url
X_logger . info ( f " 当前页面URL: { current_url } " )
# 获取页面标题
page_title = await page . title ( )
X_logger . info ( f " 页面标题: { page_title } " )
# 检查页面是否有错误信息
error_elements = await page . query_selector_all ( ' [class*= " error " ], [class*= " Error " ], .msg-error, .error-msg ' )
if error_elements :
X_logger . warning ( f " 发现 { len ( error_elements ) } 个错误元素 " )
for i , element in enumerate ( error_elements [ : 3 ] ) : # 只显示前3个
try :
text = await element . inner_text ( )
X_logger . warning ( f " 错误信息 { i + 1 } : { text } " )
except :
pass
# 检查是否有验证码或安全验证
captcha_selectors = [
' [class*= " captcha " ] ' , ' [class*= " verify " ] ' , ' [class*= " security " ] ' ,
' iframe[src*= " captcha " ] ' , ' .tc-wrap ' , ' .nc-container '
]
for selector in captcha_selectors :
elements = await page . query_selector_all ( selector )
if elements :
X_logger . warning ( f " ⚠️ 检测到可能的验证码/安全验证: { selector } " )
# 检查页面主要容器是否存在
main_containers = [
' .main-content ' , ' .content-wrap ' , ' #app ' , ' .page-container ' ,
' [class*= " main " ] ' , ' [class*= " container " ] '
]
for selector in main_containers :
elements = await page . query_selector_all ( selector )
if elements :
X_logger . info ( f " ✅ 找到主容器: { selector } ( { len ( elements ) } 个) " )
break
else :
X_logger . warning ( " ⚠️ 未找到主要页面容器 " )
# 检查是否在正确的页面
if ' mp.weixin.qq.com ' not in current_url :
X_logger . error ( f " ❌ 页面跳转异常,当前不在微信公众号页面: { current_url } " )
# 输出页面HTML长度
html_content = await page . content ( )
X_logger . info ( f " 页面HTML长度: { len ( html_content ) } 字符 " )
# 如果HTML内容太短, 可能页面没有正确加载
if len ( html_content ) < 10000 :
X_logger . warning ( " ⚠️ 页面HTML内容较短, 可能加载不完整 " )
except Exception as e :
X_logger . error ( f " 调试页面元素时出错: { e } " )
2025-09-08 09:32:45 +08:00
async def click_publish ( self , page ) :
""" 点击发布按钮发布文章 """
try :
# 等待页面稳定
await page . wait_for_timeout ( 2000 )
# 查找微信公众号的发布按钮
publish_selectors = [
' button.mass_send ' ,
' button:has-text( " 发表 " ) ' ,
' button:has-text( " 群发 " ) ' ,
' button:has-text( " 发布 " ) ' ,
' button[class*= " publish " ] ' ,
' .btn-publish '
]
published = False
for selector in publish_selectors :
try :
publish_button = page . locator ( selector )
if await publish_button . count ( ) > 0 :
await publish_button . first . click ( )
X_logger . info ( " 成功点击发布按钮 " )
published = True
break
except Exception :
continue
if not published :
X_logger . warning ( " 未找到发布按钮,可能需要手动发布 " )
# 等待发布完成
await page . wait_for_timeout ( 3000 )
X_logger . info ( " 文章发布完成 " )
except Exception as e :
X_logger . error ( f " 点击发布按钮时出错: { e } " )
async def main ( ) :
""" 用于测试微信公众号上传功能的主函数 """
from pathlib import Path
# 配置文件路径
account_file = Path ( __file__ ) . parent . parent . parent / " cookies " / " wechat_uploader " / " account.json "
account_file . parent . mkdir ( exist_ok = True , parents = True )
# 首先确保cookie有效
if not await wechat_setup ( str ( account_file ) , handle = True ) :
X_logger . error ( " Cookie设置失败 " )
return
# 创建测试图片对象(微信公众号可上传图片或视频)
image_path = Path ( __file__ ) . parent . parent . parent / " videos " / " demo.png "
if not image_path . exists ( ) :
X_logger . error ( f " 测试图片文件不存在: { image_path } " )
return
publish_datetimes = generate_schedule_time_next_day ( 1 , 1 , daily_times = [ 16 ] )
wechat_video = WechatVideo (
title = " 测试微信公众号图文发布 " ,
file_path = str ( image_path ) ,
tags = [ " 测试 " , " 微信公众号 " , " 图文 " ] ,
publish_date = publish_datetimes [ 0 ] ,
2025-09-09 14:26:39 +08:00
account_file = str ( account_file ) ,
headless = True # 建议使用有头模式,避免被检测
2025-09-08 09:32:45 +08:00
)
# 开始上传
async with async_playwright ( ) as playwright :
await wechat_video . upload ( playwright )
if __name__ == ' __main__ ' :
asyncio . run ( main ( ) )