自动下载素材并剪辑,然后发布到短视频平台

功能

对红色APP某博主的文章列表进行遍历,针对每一篇文章自动下载图集采集标题,然后使用黑色软件的一键成片功能自动剪辑刚下载的图集,把生成的短视频发布出去。(可以扩展:下载生成的视频,发布到多个短视频平台)


开发环境

  • 电脑 Win10 or 11
  • 安卓手机 v7.0+
  • AScript v3.0.13
  • 某书 v8.26.0.dbe9ed0
  • 某音 v29.0.0 (20240311)

下载地址:https://www.123pan.com/s/QZGSVv-mr6N3.html


安装和配置

  • 某书和某音,如果是新安装,注意把一些教程或申请权限弹出点掉,让APP进入一种日常使用的状态
  • AScript,安装后给足权限,尤其是无障碍权限,开发者中找到局域网IP,在浏览器中打开这个地址,即为开发窗口,包含代码编辑器开发文档工具集合

需求分析

`循环`执行以下任务:
从手机桌面打开某书(默认打开后是首页)
从首页进入用户主页(文章列表页)
`结果` = 从用户文章列表中找一篇合适的文章
如果`结果`是一篇文章:
点击文章,进入文章详情页面
`标题` = 采集标题任务
`图片个数` = 采集图片任务(采集图片们,并返回`图片个数`)
返回某书用户文章列表页面
打开某音(默认打开后是首页)
剪辑
发布
返回某音主页
返回手机桌面
另外的情况:
退出任务循环

写代码

采集标题

def collect_title():
"""
采集标题
:return:
"""
title_node = Selector(1).type("ViewGroup").depth(13).brother(-0.1).child(1).find()
return title_node.text

————————————————————

采集图片

def collect_images():
"""
收集图片,从图集详情第一张开始(等右上角的数字消失之后再开始)
:return: 图片个数
"""
recycler_node = Selector(1).type("RecyclerView").depth(14).find()
recycler_node_rect = recycler_node.rect
jishu_node = recycler_node.parent(1).brother(-1)
zongshu = len(jishu_node.child())
counter = 1
while True:
image = screen.capture(recycler_node_rect.left, recycler_node_rect.top,
recycler_node_rect.left + recycler_node_rect.width(),
recycler_node_rect.top + recycler_node_rect.height())
screen.bitmap_to_file(R.sd(f'{counter}.jpg'), image)
# print(f'图片{counter}.jpg保存完成')
recycler_node.swipe(-1)
time.sleep(1)

if counter + 1 > zongshu:
break
else:
counter = counter + 1
return counter

————————————————————

返回某书用户文章列表页面

action.Key.back()

————————————————————

打开某音(默认打开后是首页)

system.open('抖音')

————————————————————

剪辑

def images_to_movie(images_count):
"""
将图片合成视频
:param images_count: 图片数量
:return:
"""
jiahao_node = wait_for_node(Selector(1).type("ImageView").desc("拍摄,按钮"))
action.Key.back()
time.sleep(3)
if jiahao_node:
jiahao_node.parent(4).click()

xiangce_btn = wait_for_node(Selector(1).type("Button").desc("相册"))
xiangce_btn.click()

time.sleep(3)

gouxuanzu = Selector(1).type("TextView").desc(", 未选中").find_all()

for i in range(0, images_count):
gouxuan = gouxuanzu[i]
gouxuan.parent(1).click()
time.sleep(1)
if i >= 11:
break

next_btn = Selector(1).type("TextView").text("下一步").clickable(True).find()
next_btn.click()
time.sleep(4)

yjcp_btn = Selector(1).type("Button").desc("一键成片").find()
yjcp_btn.click()
time.sleep(6)

xiaoguomen = Selector(1).type("RecyclerView").depth(16).child().find_all()
suijishu = random.randint(1, len(xiaoguomen) - 1)
xiaoguo = xiaoguomen[suijishu]
xiaoguo.click()
time.sleep(3)
print(f'选择了索引为{suijishu}的效果')

baocun_btn = Selector(1).type("TextView").text("保存").find()
baocun_btn.click()
time.sleep(3)

xiayibu = Selector(1).text("下一步").desc("下一步").type("TextView").find()
xiayibu.parent(1).click()
time.sleep(4)

————————————————————

wait_for_node: 等待某个节点出现

def wait_for_node(selector, retry_times=10, retry_interval=1):
"""
等待某个节点出现
:param selector: 选择器
:param retry_times: 重试次数
:param retry_interval: 重试间隔
:return: 节点对象 或 None
"""
counter = 1
while counter <= retry_times:
node = selector.find()
if node:
return node
counter += 1
time.sleep(retry_interval)
return None

————————————————————

发布

def post_video(title):
"""
发布视频
:return:
"""
miaoshukuang = wait_for_node(Selector(1).text("添加作品描述..").type("EditText"))
miaoshukuang.input(title)

fabu_btn = Selector(1).desc("发布").type("FrameLayout").find()
fabu_btn.click()
time.sleep(5)

————————————————————

返回某音主页

action.Key.back()
time.sleep(2)

————————————————————

返回手机桌面

action.Key.home()
time.sleep(2)

————————————————————

带循环功能完整项目代码

import os
import random
import re
# 导入系统资源模块
from ascript.android.system import R
# 导入动作模块
from ascript.android import action
# 导入节点检索模块
from ascript.android import node
# 导入图色检索模块
from ascript.android import screen
from ascript.android import system
from ascript.android.node import Selector
# from ascript.android.ui import dialog
import time


def go_to_user_home(user_id):
"""
从首页进入用户主页
:param user_id: 用户id
:return:
"""
search_btn = Selector(1).desc("搜索").type("Button").depth(15).find()
search_btn.click()
time.sleep(2)

search_input = Selector(1).text("搜索, ").type("EditText").find()
search_input.input(user_id)
time.sleep(1)

right_search_button = Selector(1).text("搜索").type("Button").find()
right_search_button.click()
time.sleep(3)

user_btn = Selector(1).text("用户").type("TextView").find()
user_btn.parent(1).click()
time.sleep(2)

hong_shu_hao_2p = Selector(1).text("小红书号").type("TextView").depth(13).parent(2).find()
hong_shu_hao_2p.click()
time.sleep(3)


def select_a_right_article():
"""
选择一篇合适的文章
:return: 找到的文章 或 None
"""
previous_all_desc = ''
while True:
this_all_desc = ''
articles = Selector(1).type("RecyclerView").depth(18).child().find_all()
# print(len(articles))
for article in articles:
desc = article.desc
# 使用正则表达式删除倒数第2个逗号及其后的内容
# print(article)
modified_desc = re.sub(r',[^,]*(,[^,]*)$', '', desc)
this_all_desc = this_all_desc + modified_desc
if modified_desc in handled_articles:
continue
else:
handled_articles.append(modified_desc)
return article

if this_all_desc == previous_all_desc:
return None
else:
previous_all_desc = this_all_desc
recycler = Selector(1).type("RecyclerView").depth(18).find()
recycler.swipe(-1)
time.sleep(2)


def collect_title():
"""
采集标题
:return:
"""
title_node = Selector(1).type("ViewGroup").depth(13).brother(-0.1).child(1).find()
return title_node.text


def collect_images():
"""
收集图片,从图集详情第一张开始(等右上角的数字消失之后再开始)
:return: 图片个数
"""
recycler_node = Selector(1).type("RecyclerView").depth(14).find()
recycler_node_rect = recycler_node.rect
jishu_node = recycler_node.parent(1).brother(-1)
zongshu = len(jishu_node.child())
counter = 1
while True:
image = screen.capture(recycler_node_rect.left, recycler_node_rect.top,
recycler_node_rect.left + recycler_node_rect.width(),
recycler_node_rect.top + recycler_node_rect.height())
screen.bitmap_to_file(R.sd(f'{counter}.jpg'), image)
# print(f'图片{counter}.jpg保存完成')
recycler_node.swipe(-1)
time.sleep(1)

if counter + 1 > zongshu:
break
else:
counter = counter + 1
return counter


def wait_for_node(selector, retry_times=10, retry_interval=1):
"""
等待某个节点出现
:param selector: 选择器
:param retry_times: 重试次数
:param retry_interval: 重试间隔
:return: 节点对象 或 None
"""
counter = 1
while counter <= retry_times:
node = selector.find()
if node:
return node
counter += 1
time.sleep(retry_interval)
return None


def images_to_movie(images_count):
"""
将图片合成视频
:param images_count: 图片数量
:return:
"""
jiahao_node = wait_for_node(Selector(1).type("ImageView").desc("拍摄,按钮"))
action.Key.back()
time.sleep(3)
if jiahao_node:
jiahao_node.parent(4).click()

xiangce_btn = wait_for_node(Selector(1).type("Button").desc("相册"))
xiangce_btn.click()

time.sleep(3)

gouxuanzu = Selector(1).type("TextView").desc(", 未选中").find_all()

for i in range(0, images_count):
gouxuan = gouxuanzu[i]
gouxuan.parent(1).click()
time.sleep(1)
if i >= 11:
break

next_btn = Selector(1).type("TextView").text("下一步").clickable(True).find()
next_btn.click()
time.sleep(4)

yjcp_btn = Selector(1).type("Button").desc("一键成片").find()
yjcp_btn.click()
time.sleep(6)

xiaoguomen = Selector(1).type("RecyclerView").depth(16).child().find_all()
suijishu = random.randint(1, len(xiaoguomen) - 1)
xiaoguo = xiaoguomen[suijishu]
xiaoguo.click()
time.sleep(3)
print(f'选择了索引为{suijishu}的效果')

baocun_btn = Selector(1).type("TextView").text("保存").find()
baocun_btn.click()
time.sleep(3)

xiayibu = Selector(1).text("下一步").desc("下一步").type("TextView").find()
xiayibu.parent(1).click()
time.sleep(4)


def post_video(title):
"""
发布视频
:return:
"""
miaoshukuang = wait_for_node(Selector(1).text("添加作品描述..").type("EditText"))
miaoshukuang.input(title)

fabu_btn = Selector(1).desc("发布").type("FrameLayout").find()
fabu_btn.click()
time.sleep(5)


# 已处理文章列表
handled_articles = []

while True:
system.open('小红书')
time.sleep(5)
go_to_user_home('3915813063')
article = select_a_right_article()
if article:
article.click()
time.sleep(6)
title = collect_title()
images_count = collect_images()
action.Key.back()
time.sleep(1)
action.Key.back()
time.sleep(1)
action.Key.back()
time.sleep(1)
action.Key.back()
time.sleep(2)
system.open('抖音')
images_to_movie(images_count)
post_video(title)
action.Key.back()
time.sleep(2)
action.Key.home()
time.sleep(2)
else:
break


心得体会

脚本的稳定性是需要打磨的,很多特殊情况在你开发脚本流程的时候你是遇不到的,但实际应用中特殊情况随时可能出现,一个稳定的脚本是需要反复测试反复打磨才能投入使用的。