├── .gitignore ├── README.md ├── LICENSE ├── requirements.txt └── 抖音.py /.gitignore: -------------------------------------------------------------------------------- 1 | screenlog* 2 | venv 3 | config.yaml -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 抖音自动化 群控方案 2 | 3 | [云手机](https://www.chinac.com/) [群控方案 1](https://airtest.netease.com/) [群控方案 1](https://github.com/barry-ran/QtScrcpy) 4 | 5 | ## 抖音自动化 6 | 7 | 1. 行为关注: 设置喜欢的内容, 随机时间刷视频, 直到遇到预设的用户则关注, 或者刷直播广场, 遇到主播关注 8 | 2. 异常检测: 青少年提醒, 定位权限, 电话权限, 授权协议, 加载动画 9 | 3. 直播间真人人气: 随机事件进入房间, 一天进入N次, 不同时间段进入, 进入房间使用内置的词库进行评论, 评论 CD 随机 10 | 4. 行为仿真: 刷视频的频率为 每条视频5-20秒, 不喜欢的视频3秒, 感兴趣的视频10秒, 喜欢的视频20秒, 一次看30分钟,休息2小时 11 | 12 | ## 注意事项 13 | 14 | 1. 当前程序未完善 15 | 2. 多线程需要考虑很多问题 16 | 3. 电话卡实名问题 17 | 18 | ## 题外话 19 | 20 | 1. 如果你感兴趣的话,希望多多支持 21 | 2. 如果需要现成的抖音类的服务 可以联系 T50333 (非作者) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 XRSec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | airtest==1.2.10 2 | attrs==23.1.0 3 | bcrypt==4.0.1 4 | blinker==1.6.2 5 | cached-property==1.5.2 6 | certifi==2022.12.7 7 | cffi==1.15.1 8 | charset-normalizer==3.1.0 9 | click==8.1.3 10 | cryptography==40.0.2 11 | decorator==5.1.1 12 | Deprecated==1.2.13 13 | docker==4.4.4 14 | docker-compose==1.25.4 15 | dockerpty==0.4.1 16 | docopt==0.6.2 17 | facebook-wda==1.4.6 18 | ffmpeg-python==0.2.0 19 | filelock==3.12.0 20 | Flask==2.3.2 21 | future==0.18.3 22 | gitdb2==2.0.6 23 | GitPython==2.1.15 24 | hrpc==1.0.9 25 | idna==3.4 26 | itsdangerous==2.1.2 27 | Jinja2==3.1.2 28 | jsonschema==3.2.0 29 | MarkupSafe==2.1.2 30 | mss==6.1.0 31 | nose==1.3.7 32 | numpy==1.24.3 33 | opencv-contrib-python==4.6.0.66 34 | packaging==21.3 35 | paramiko==3.1.0 36 | Pillow==9.5.0 37 | poco==0.99.1 38 | pocoui==1.0.89 39 | py==1.11.0 40 | pyaml==19.12.0 41 | pycparser==2.21 42 | PyGithub==1.45 43 | PyJWT==2.6.0 44 | PyNaCl==1.5.0 45 | pyparsing==3.0.9 46 | pyrsistent==0.19.3 47 | python-dateutil==2.8.2 48 | python-gitlab==1.15.0 49 | python-xlib==0.33 50 | pywinauto==0.6.3 51 | PyYAML==5.3 52 | requests==2.30.0 53 | retry==0.9.2 54 | six==1.16.0 55 | smmap==5.0.0 56 | smmap2==3.0.1 57 | svn==1.0.1 58 | texttable==1.6.7 59 | urllib3==2.0.2 60 | websocket-client==0.48.0 61 | Werkzeug==2.3.4 62 | wrapt==1.15.0 63 | -------------------------------------------------------------------------------- /抖音.py: -------------------------------------------------------------------------------- 1 | # -*- encoding=utf8 -*- 2 | 3 | import socket 4 | 5 | import yaml 6 | import logging 7 | import random 8 | import signal 9 | 10 | from airtest.core.api import * 11 | from multiprocessing import Process 12 | from poco.drivers.android.uiautomation import AndroidUiautomationPoco 13 | 14 | logging.getLogger("airtest").setLevel(logging.ERROR) 15 | logging.getLogger("poco").setLevel(logging.ERROR) 16 | logging.getLogger("adb").setLevel(logging.ERROR) 17 | global devices, go_live_room, api_server_process 18 | devices = {} 19 | go_live_room = False 20 | api_server_process = Process() 21 | douYinAppID = "com.ss.android.ugc.aweme" 22 | auto_setup(__file__) 23 | 24 | 25 | def init_douyin(): 26 | # 停止抖音 27 | stop_app(douYinAppID) 28 | print("设备{}: 启动抖音".format(num)) 29 | start_app(douYinAppID) 30 | 31 | for _ in range(5): 32 | # 个人信息保护指引 33 | if poco(text="个人信息保护指引").exists(): 34 | poco(text="同意").click() 35 | print("设备{}: 个人信息保护指引 -> 同意".format(num)) 36 | if poco(text="允许抖音获取此设备的位置信息吗?").exists(): 37 | poco(text="不允许").click() 38 | print("设备{} 获取此设备的位置信息 -> 不允许".format(num)) 39 | if poco(text="允许“抖音”拨打电话和管理通话吗?").exists(): 40 | poco(text="不允许").click() 41 | print("设备{} 拨打电话和管理通话 -> 不允许".format(num)) 42 | # TODO 青少年模式 43 | if poco(text="青少年模式").exists(): 44 | poco(text="我知道了").click() 45 | print("设备{} 青少年模式 -> 我知道了".format(num)) 46 | if poco(text="抖音登录").exists(): 47 | poco(desc="关闭,按钮").click() 48 | poco.swipe([0.52, 0.62], [0.42, 0.12], duration=0.7) 49 | 50 | # 提示登录账号 51 | # def login(): 52 | # sleep(1) 53 | # poco(text="密码登录").click() 54 | # sleep(1) 55 | # poco(text="请输入手机号").click() 56 | # poco(text="请输入手机号").set_text("18888888888") 57 | # poco(text="请输入密码").click() 58 | # poco(text="请输入密码").set_text("18888888888") 59 | # # 用户协议 勾选 60 | # poco("com.ss.android.ugc.aweme:id/o7g").click() 61 | # poco(text="登录").click() 62 | # if poco(text="系统繁忙,请稍后再试").exists(): 63 | # print("系统繁忙,请稍后再试") 64 | 65 | # if poco(text="抖音登录").exists(): 66 | # poco(text="立即登录").click() 67 | # login() 68 | # if poco(text="我", desc="我,按钮"): 69 | # poco(text="我", desc="我,按钮").click() 70 | # login() 71 | watch_time = random.randint(1800, 3000) 72 | print("设备{} 看{}分钟".format(num, round(watch_time / 60), 2)) 73 | for i in range(watch_time): # 看30-50 分钟休息一会 74 | if poco(name="com.ss.android.ugc.aweme:id/ct").exists(): 75 | print("设备{}: 视频底部加载动画".format(num)) # TODO 这里 有一个视频底部加载动画,需要注意 76 | return 77 | wait_time = random.randint(5, 10) 78 | poco.swipe([0.52, 0.62], [0.42, 0.12], duration=0.7) 79 | video_user_name = poco(name="com.ss.android.ugc.aweme:id/user_avatar") 80 | ''' 81 | 姓名: 82 | 过滤: 83 | 剧,剧场 84 | 停留: 85 | 美女,穿搭,变装,兄弟,跳舞,舞蹈,御姐,女人味,身材,美腿,美臀,美胸,裙,喜欢,实名观看,可以吗,心动,心动推荐 86 | ''' 87 | if video_user_name.exists(): 88 | video_user_name = video_user_name.attr("desc") 89 | else: 90 | video_user_name = "未知" 91 | video_desc = poco(name="com.ss.android.ugc.aweme:id/desc") 92 | if video_desc.exists(): 93 | video_desc = video_desc.attr("text") 94 | else: 95 | video_desc = "未知" 96 | # guan_zhu_bottom = poco(name="com.ss.android.ugc.aweme:id/f9j", desc="关注") 97 | # if guan_zhu_bottom.exists(): 98 | # print("设备{}: 用户名: {} {}".format(num, user_name, "未关注")) 99 | # else: 100 | # print("设备{}: 用户名: {} {}".format(num, user_name, "已关注")) 101 | # wait_time = 10 # 自己人多看一会 102 | print("设备{}: 描述: {} 用户名: {} 观看{}秒".format(num, video_desc, video_user_name, wait_time)) 103 | sleep(wait_time) 104 | sleep_time = random.randint(2000, 6000) 105 | print("设备{}: 看完了,休息{}分钟".format(num, round(sleep_time / 60), 2)) 106 | sleep(sleep_time) # 休息一会 107 | 108 | 109 | def live_room(): 110 | print("设备{} 进入直播间".format(num)) 111 | handler(signal.SIGTERM, None) 112 | 113 | 114 | def handler(signum, frame): 115 | for n in devices: 116 | os.kill(devices[n]["process"].pid, signal.SIGUSR1) 117 | sleep(1) 118 | devices[n]["process"].terminate() 119 | devices[n]["process"].join() 120 | # 关闭 API服务 121 | api_server_process.terminate() 122 | api_server_process.join() 123 | print("程序退出") 124 | os.kill(os.getpid(), signal.SIGTERM) 125 | 126 | 127 | def handler_sub_process(signum, frame): 128 | print("设备{}: 子线程退出".format(num)) 129 | stop_app(douYinAppID) 130 | 131 | 132 | def douyin_sub_process(id, ipaddress, port): 133 | global poco, times, num 134 | times = 0 135 | num = id 136 | device_info = "android:///{}:{}".format(ipaddress, port) 137 | print("设备{}: 连接设备 {}".format(num, device_info)) 138 | connect_device(device_info) 139 | poco = AndroidUiautomationPoco(use_airtest_input=True, screenshot_each_action=False) 140 | signal.signal(signal.SIGUSR1, handler_sub_process) # TODO 这你需要做异常拦截,防止子线程 崩溃 141 | # TODO 继续任务,不要浪费时间 重复观看 142 | while True: 143 | if go_live_room: 144 | live_room() 145 | else: 146 | init_douyin() 147 | 148 | 149 | def api_server(): 150 | # TODO go_live_room 全局共享变量 151 | host, port = '0.0.0.0', 9999 152 | listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 153 | listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 154 | listen_socket.bind((host, port)) 155 | listen_socket.listen(1) 156 | print('服务启动中 端口: %s ...' % port) 157 | 158 | while True: 159 | client_connection, client_address = listen_socket.accept() 160 | try: 161 | url_path = client_connection.recv(1024).decode("utf-8").split("\r\n")[0].split(' ')[1] 162 | except UnicodeDecodeError: 163 | print("api_server UnicodeDecodeError") 164 | url_path = "Error" 165 | 166 | if url_path == '/api': 167 | client_connection.send(b"""HTTP/1.1 200 OK\n\n{"code": "200"}""") 168 | go_live_room = True # 进入直播间信号 // TODO 修改外部变量 169 | print("接收到请求 -> 重启子进程, 即将进入直播间") 170 | print(devices, go_live_room) 171 | # TODO 此处的 devices 非 daemon 内部的 devices,即 daemon 内部的 devices 与 外部的 devices 不是同一个对象 172 | for n in devices: 173 | devices[n]["process"].terminate() 174 | sleep(1) 175 | for n in devices: 176 | devices[n]["process"].start() 177 | else: 178 | client_connection.send(b"""HTTP/1.1 404 NOT FOUND\n\n{"code": "404"}""") 179 | client_connection.close() 180 | 181 | 182 | def daemon(): 183 | global devices, api_server_process 184 | # os.system("adb kill-server > /dev/null 2>&1") 185 | # os.system("adb devices > /dev/null 2>&1") 186 | sleep(3) 187 | f = open('config.yaml', 'r') 188 | devices = yaml.load(f, Loader=yaml.FullLoader) 189 | 190 | # douyin_sub_process(0, devices[0]["ipaddress"], devices[0]["port"]) 191 | # exit(0) 192 | for n in devices: 193 | devices[n]["process"] = Process(target=douyin_sub_process, args=(n, devices[n]["ipaddress"], devices[n]["port"])) 194 | devices[n]["process"].start() 195 | 196 | api_server_process = Process(target=api_server) 197 | api_server_process.start() 198 | 199 | 200 | if __name__ == '__main__': 201 | signal.signal(signal.SIGINT, handler) 202 | daemon() 203 | --------------------------------------------------------------------------------