pookz@stme commited on
Commit ·
b843170
1
Parent(s): 5525396
1. add xhs
Browse files2. change douyin third-pard(checkbox)
3. add douyin demo
4. add group-qr
- README.MD +48 -3
- conf.py +1 -0
- douyin_uploader/main.py +7 -4
- examples/upload_video_to_xhs.py +57 -0
- xhs_uploader/__init__.py +0 -0
- xhs_uploader/accounts.ini +2 -0
- xhs_uploader/cdn.jsdelivr.net_gh_requireCool_stealth.min.js_stealth.min.js +0 -0
- xhs_uploader/main.py +59 -0
- xhs_uploader/xhs_login_qrcode.py +35 -0
README.MD
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
# social-auto-upload
|
| 2 |
social-auto-upload 该项目旨在自动化发布视频到各个社交媒体平台
|
| 3 |
|
|
|
|
| 4 |
## 💡Feature
|
| 5 |
- 中国主流社交媒体平台:
|
| 6 |
- 抖音
|
|
@@ -25,9 +26,15 @@ pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
|
| 25 |
playwright install chromium
|
| 26 |
```
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
# 核心模块解释
|
| 29 |
|
| 30 |
-
## 视频文件准备
|
| 31 |
filepath 本地视频目录,目录包含
|
| 32 |
- 视频文件
|
| 33 |
- 视频meta信息txt文件
|
|
@@ -43,7 +50,9 @@ meta_file 内容:
|
|
| 43 |
#坚持不懈 #爱情执着 #奋斗使者 #短视频
|
| 44 |
```
|
| 45 |
|
| 46 |
-
### 抖音
|
|
|
|
|
|
|
| 47 |
使用playwright模拟浏览器行为
|
| 48 |
> 抖音前端实现,诸多css class id 均为随机数,故项目中locator多采用相对定位,而非固定定位
|
| 49 |
1. 准备视频目录结构
|
|
@@ -71,9 +80,45 @@ generate_schedule_time_next_day 默认从第二天开始(此举为避免选择
|
|
| 71 |
- https://github.com/lishang520/DouYin-Auto-Upload.git
|
| 72 |
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
### 其余部分(todo)
|
| 75 |
整理后上传
|
| 76 |
|
| 77 |
# 联系我
|
| 78 |
探讨自动化上传、自动制作视频
|
| 79 |
-
|
|
|
|
|
|
| 1 |
# social-auto-upload
|
| 2 |
social-auto-upload 该项目旨在自动化发布视频到各个社交媒体平台
|
| 3 |
|
| 4 |
+
|
| 5 |
## 💡Feature
|
| 6 |
- 中国主流社交媒体平台:
|
| 7 |
- 抖音
|
|
|
|
| 26 |
playwright install chromium
|
| 27 |
```
|
| 28 |
|
| 29 |
+
# 🐇 About
|
| 30 |
+
该项目为我自用项目抽离出来,我的发布策略是定时发布(提前一天发布),故发布部分采用的事件均为第二天的时间
|
| 31 |
+
|
| 32 |
+
如果你有需求立即发布,可自行研究源码或者向我提问
|
| 33 |
+
|
| 34 |
+
|
| 35 |
# 核心模块解释
|
| 36 |
|
| 37 |
+
## 1. 视频文件准备
|
| 38 |
filepath 本地视频目录,目录包含
|
| 39 |
- 视频文件
|
| 40 |
- 视频meta信息txt文件
|
|
|
|
| 50 |
#坚持不懈 #爱情执着 #奋斗使者 #短视频
|
| 51 |
```
|
| 52 |
|
| 53 |
+
### 2. 抖音
|
| 54 |
+
<img src="media/show/pdf3.gif" alt="douyin show" width="500"/>
|
| 55 |
+
|
| 56 |
使用playwright模拟浏览器行为
|
| 57 |
> 抖音前端实现,诸多css class id 均为随机数,故项目中locator多采用相对定位,而非固定定位
|
| 58 |
1. 准备视频目录结构
|
|
|
|
| 80 |
- https://github.com/lishang520/DouYin-Auto-Upload.git
|
| 81 |
|
| 82 |
|
| 83 |
+
|
| 84 |
+
### 3. 小红书
|
| 85 |
+
该实现,借助ReaJason的[xhs](https://github.com/ReaJason/xhs),再次感谢。
|
| 86 |
+
|
| 87 |
+
1. 目录结构同上
|
| 88 |
+
2. cookie获取,可使用chrome插件:EditThisCookie
|
| 89 |
+
- 设置导出格式
|
| 90 |
+

|
| 91 |
+
- 导出
|
| 92 |
+

|
| 93 |
+
3. 黏贴至 accounts.ini文件中
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
#### 解释与注意事项:
|
| 97 |
+
|
| 98 |
+
##### 上传方式
|
| 99 |
+
- 本地签名
|
| 100 |
+
- 自建签名服务
|
| 101 |
+
|
| 102 |
+
测试下来发现本地签名,在实际多账号情况下会存在问题
|
| 103 |
+
故如果你有多账号分发,建议采用自建签名服务(todo 上传docker配置)
|
| 104 |
+
|
| 105 |
+
##### 疑难杂症
|
| 106 |
+
遇到签名问题,可尝试更新cdn.jsdelivr.net_gh_requireCool_stealth.min.js_stealth.min.js文件
|
| 107 |
+
https://github.com/requireCool/stealth.min.js
|
| 108 |
+
|
| 109 |
+
参考: https://reajason.github.io/xhs/basic
|
| 110 |
+
|
| 111 |
+
##### todo
|
| 112 |
+
- 扫码登录方式(实验下来发现与浏览器获取的存在区别,会有问题,未来再研究)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
参考项目:
|
| 116 |
+
- https://github.com/ReaJason/xhs
|
| 117 |
+
|
| 118 |
### 其余部分(todo)
|
| 119 |
整理后上传
|
| 120 |
|
| 121 |
# 联系我
|
| 122 |
探讨自动化上传、自动制作视频
|
| 123 |
+
|
| 124 |
+
<img src="media/group-qr.png" alt="group-qr" width="300"/>
|
conf.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
from pathlib import Path
|
| 2 |
|
| 3 |
BASE_DIR = Path(__file__).parent.resolve()
|
|
|
|
|
|
| 1 |
from pathlib import Path
|
| 2 |
|
| 3 |
BASE_DIR = Path(__file__).parent.resolve()
|
| 4 |
+
XHS_SERVER = "http://127.0.0.1:11901"
|
douyin_uploader/main.py
CHANGED
|
@@ -162,10 +162,13 @@ class DouYinVideo(object):
|
|
| 162 |
await asyncio.sleep(1)
|
| 163 |
await page.locator('div[role="listbox"] [role="option"]').first.click()
|
| 164 |
|
| 165 |
-
# 頭條
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
if self.publish_date != 0:
|
| 171 |
await self.set_schedule_time_douyin(page, self.publish_date)
|
|
|
|
| 162 |
await asyncio.sleep(1)
|
| 163 |
await page.locator('div[role="listbox"] [role="option"]').first.click()
|
| 164 |
|
| 165 |
+
# 頭條/西瓜
|
| 166 |
+
third_part_element = '[class^="info"] > [class^="first-part"] div div.semi-switch'
|
| 167 |
+
# 定位是否有第三方平台
|
| 168 |
+
if await page.locator(third_part_element).count():
|
| 169 |
+
# 检测是否是已选中状态
|
| 170 |
+
if 'semi-switch-checked' not in await page.eval_on_selector(third_part_element, 'div => div.className'):
|
| 171 |
+
await page.locator(third_part_element).locator('input.semi-switch-native-control').click()
|
| 172 |
|
| 173 |
if self.publish_date != 0:
|
| 174 |
await self.set_schedule_time_douyin(page, self.publish_date)
|
examples/upload_video_to_xhs.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import configparser
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
from time import sleep
|
| 4 |
+
|
| 5 |
+
from xhs import XhsClient
|
| 6 |
+
|
| 7 |
+
from conf import BASE_DIR
|
| 8 |
+
from utils.files_times import generate_schedule_time_next_day, get_title_and_hashtags
|
| 9 |
+
from xhs_uploader.main import sign_local, beauty_print
|
| 10 |
+
|
| 11 |
+
config = configparser.RawConfigParser()
|
| 12 |
+
config.read(Path(BASE_DIR / "xhs_uploader" / "accounts.ini"))
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
if __name__ == '__main__':
|
| 16 |
+
filepath = Path(BASE_DIR) / "videos"
|
| 17 |
+
# 获取视频目录
|
| 18 |
+
folder_path = Path(filepath)
|
| 19 |
+
# 获取文件夹中的所有文件
|
| 20 |
+
files = list(folder_path.glob("*.mp4"))
|
| 21 |
+
file_num = len(files)
|
| 22 |
+
|
| 23 |
+
cookies = config['account1']['cookies']
|
| 24 |
+
xhs_client = XhsClient(cookies, sign=sign_local, timeout=60)
|
| 25 |
+
# auth cookie
|
| 26 |
+
# 注意:该校验cookie方式可能并没那么准确
|
| 27 |
+
try:
|
| 28 |
+
xhs_client.get_video_first_frame_image_id("3214")
|
| 29 |
+
except:
|
| 30 |
+
print("cookie 失效")
|
| 31 |
+
exit()
|
| 32 |
+
|
| 33 |
+
publish_datetimes = generate_schedule_time_next_day(file_num, 1, daily_times=[16])
|
| 34 |
+
|
| 35 |
+
for index, file in enumerate(files):
|
| 36 |
+
title, tags = get_title_and_hashtags(str(file))
|
| 37 |
+
tags_str = ' '.join(['#' + tag for tag in tags])
|
| 38 |
+
# 打印视频文件名、标题和 hashtag
|
| 39 |
+
print(f"视频文件名:{file}")
|
| 40 |
+
print(f"标题:{title}")
|
| 41 |
+
print(f"Hashtag:{tags}")
|
| 42 |
+
|
| 43 |
+
topics = []
|
| 44 |
+
# 获取hashtag
|
| 45 |
+
for i in tags[:3]:
|
| 46 |
+
topic_official = xhs_client.get_suggest_topic(i)
|
| 47 |
+
if topic_official:
|
| 48 |
+
topics.append(topic_official[0])
|
| 49 |
+
|
| 50 |
+
note = xhs_client.create_video_note(title=title[:20], video_path=str(file), desc=title + tags_str,
|
| 51 |
+
topics=topics,
|
| 52 |
+
is_private=False,
|
| 53 |
+
post_time=publish_datetimes[index].strftime("%Y-%m-%d %H:%M:%S"))
|
| 54 |
+
|
| 55 |
+
beauty_print(note)
|
| 56 |
+
# 强制休眠30s,避免风控(必要)
|
| 57 |
+
sleep(30)
|
xhs_uploader/__init__.py
ADDED
|
File without changes
|
xhs_uploader/accounts.ini
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[account1]
|
| 2 |
+
cookies = changeme
|
xhs_uploader/cdn.jsdelivr.net_gh_requireCool_stealth.min.js_stealth.min.js
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
xhs_uploader/main.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import configparser
|
| 2 |
+
import json
|
| 3 |
+
import pathlib
|
| 4 |
+
from time import sleep
|
| 5 |
+
|
| 6 |
+
import requests
|
| 7 |
+
from playwright.sync_api import sync_playwright
|
| 8 |
+
|
| 9 |
+
from conf import BASE_DIR, XHS_SERVER
|
| 10 |
+
|
| 11 |
+
config = configparser.RawConfigParser()
|
| 12 |
+
config.read('accounts.ini')
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def sign_local(uri, data=None, a1="", web_session=""):
|
| 16 |
+
for _ in range(10):
|
| 17 |
+
try:
|
| 18 |
+
with sync_playwright() as playwright:
|
| 19 |
+
stealth_js_path = pathlib.Path(
|
| 20 |
+
BASE_DIR) / "xhs_uploader" / "cdn.jsdelivr.net_gh_requireCool_stealth.min.js_stealth.min.js"
|
| 21 |
+
chromium = playwright.chromium
|
| 22 |
+
|
| 23 |
+
# 如果一直失败可尝试设置成 False 让其打开浏览器,适当添加 sleep 可查看浏览器状态
|
| 24 |
+
browser = chromium.launch(headless=True)
|
| 25 |
+
|
| 26 |
+
browser_context = browser.new_context()
|
| 27 |
+
browser_context.add_init_script(path=stealth_js_path)
|
| 28 |
+
context_page = browser_context.new_page()
|
| 29 |
+
context_page.goto("https://www.xiaohongshu.com")
|
| 30 |
+
browser_context.add_cookies([
|
| 31 |
+
{'name': 'a1', 'value': a1, 'domain': ".xiaohongshu.com", 'path': "/"}]
|
| 32 |
+
)
|
| 33 |
+
context_page.reload()
|
| 34 |
+
# 这个地方设置完浏览器 cookie 之后,如果这儿不 sleep 一下签名获取就失败了,如果经常失败请设置长一点试试
|
| 35 |
+
sleep(2)
|
| 36 |
+
encrypt_params = context_page.evaluate("([url, data]) => window._webmsxyw(url, data)", [uri, data])
|
| 37 |
+
return {
|
| 38 |
+
"x-s": encrypt_params["X-s"],
|
| 39 |
+
"x-t": str(encrypt_params["X-t"])
|
| 40 |
+
}
|
| 41 |
+
except Exception:
|
| 42 |
+
# 这儿有时会出现 window._webmsxyw is not a function 或未知跳转错误,因此加一个失败重试趴
|
| 43 |
+
pass
|
| 44 |
+
raise Exception("重试了这么多次还是无法签名成功,寄寄寄")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def sign(uri, data=None, a1="", web_session=""):
|
| 48 |
+
# 填写自己的 flask 签名服务端口地址
|
| 49 |
+
res = requests.post(f"{XHS_SERVER}/sign",
|
| 50 |
+
json={"uri": uri, "data": data, "a1": a1, "web_session": web_session})
|
| 51 |
+
signs = res.json()
|
| 52 |
+
return {
|
| 53 |
+
"x-s": signs["x-s"],
|
| 54 |
+
"x-t": signs["x-t"]
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def beauty_print(data: dict):
|
| 59 |
+
print(json.dumps(data, ensure_ascii=False, indent=2))
|
xhs_uploader/xhs_login_qrcode.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
import json
|
| 3 |
+
import qrcode
|
| 4 |
+
from time import sleep
|
| 5 |
+
|
| 6 |
+
from xhs import XhsClient
|
| 7 |
+
|
| 8 |
+
from xhs_uploader.main import sign
|
| 9 |
+
|
| 10 |
+
# pip install qrcode
|
| 11 |
+
if __name__ == '__main__':
|
| 12 |
+
xhs_client = XhsClient(sign=sign)
|
| 13 |
+
print(datetime.datetime.now())
|
| 14 |
+
qr_res = xhs_client.get_qrcode()
|
| 15 |
+
qr_id = qr_res["qr_id"]
|
| 16 |
+
qr_code = qr_res["code"]
|
| 17 |
+
|
| 18 |
+
qr = qrcode.QRCode(version=1, error_correction=qrcode.ERROR_CORRECT_L,
|
| 19 |
+
box_size=50,
|
| 20 |
+
border=1)
|
| 21 |
+
qr.add_data(qr_res["url"])
|
| 22 |
+
qr.make()
|
| 23 |
+
img = qr.make_image(fill_color="black", back_color="white")
|
| 24 |
+
img.save('qrcode.png')
|
| 25 |
+
|
| 26 |
+
while True:
|
| 27 |
+
check_qrcode = xhs_client.check_qrcode(qr_id, qr_code)
|
| 28 |
+
print(check_qrcode)
|
| 29 |
+
sleep(1)
|
| 30 |
+
if check_qrcode["code_status"] == 2:
|
| 31 |
+
print(json.dumps(check_qrcode["login_info"], indent=4))
|
| 32 |
+
print("当前 cookie:" + xhs_client.cookie)
|
| 33 |
+
break
|
| 34 |
+
|
| 35 |
+
print(json.dumps(xhs_client.get_self_info(), indent=4))
|