commit 20171bd31a78d038fdd04e2ab0b767c4a002dec5 Author: 徐微 <1134129891@qq.com> Date: Mon Dec 8 15:27:00 2025 +0800 init commit diff --git a/__pycache__/scan_qrcode.cpython-310.pyc b/__pycache__/scan_qrcode.cpython-310.pyc new file mode 100644 index 0000000..45a31b9 Binary files /dev/null and b/__pycache__/scan_qrcode.cpython-310.pyc differ diff --git a/__pycache__/scan_qrcode.cpython-312.pyc b/__pycache__/scan_qrcode.cpython-312.pyc new file mode 100644 index 0000000..de0daea Binary files /dev/null and b/__pycache__/scan_qrcode.cpython-312.pyc differ diff --git a/__pycache__/use_cookie.cpython-310.pyc b/__pycache__/use_cookie.cpython-310.pyc new file mode 100644 index 0000000..6e07c80 Binary files /dev/null and b/__pycache__/use_cookie.cpython-310.pyc differ diff --git a/__pycache__/use_cookie.cpython-312.pyc b/__pycache__/use_cookie.cpython-312.pyc new file mode 100644 index 0000000..e35cebc Binary files /dev/null and b/__pycache__/use_cookie.cpython-312.pyc differ diff --git a/__pycache__/util.cpython-310.pyc b/__pycache__/util.cpython-310.pyc new file mode 100644 index 0000000..562f327 Binary files /dev/null and b/__pycache__/util.cpython-310.pyc differ diff --git a/__pycache__/util.cpython-312.pyc b/__pycache__/util.cpython-312.pyc new file mode 100644 index 0000000..f6b85cf Binary files /dev/null and b/__pycache__/util.cpython-312.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..10acb22 --- /dev/null +++ b/app.py @@ -0,0 +1,55 @@ +from flask import Flask, render_template, request, session +from playwright.sync_api import sync_playwright +import os +import time +app = Flask(__name__) +app.secret_key = 'your_secret_key' + +@app.route('/') +def index(): + return render_template('login.html') + +@app.route('/send_code', methods=['POST']) +def send_code(): + phone = request.form['phone'] + session['phone'] = phone + # 用 Playwright 打开页面并点击发送验证码 + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + context = browser.new_context() + page = context.new_page() + page.goto("https://creator.xiaohongshu.com/login?source=&redirectReason=401&lastUrl=%252Fnew%252Fhome") + page.get_by_role("textbox", name="手机号").click() + page.get_by_role("textbox", name="手机号").fill(phone) + page.wait_for_timeout(15000) # 等待用户手动输入验证码 + page.get_by_text("发送验证码").click() + # 保存 context 到磁盘,后续用 + time.sleep(20) # 确保操作完成 + context.storage_state(path="state.json") + # browser.close() + return "验证码已发送,请查收手机短信!返回" + +@app.route('/login', methods=['POST']) +def login(): + code = request.form['code'] + phone = session.get('phone') + # 用 Playwright继续登录 + with sync_playwright() as p: + browser = p.chromium.launch(headless=True) + context = p.chromium.launch_persistent_context(user_data_dir=".", storage_state="state.json") + page = context.pages[0] if context.pages else context.new_page() + page.goto("https://creator.xiaohongshu.com/login?source=&redirectReason=401&lastUrl=%252Fnew%252Fhome") + # 填写验证码并登录 + page.wait_for_selector('input[placeholder*="验证码"]', timeout=15000) + vcode_box = page.locator('input[placeholder*="验证码"]') + vcode_box.fill(code) + page.get_by_role("button", name="登 录").click() + # 等待跳转 + page.wait_for_url("**/new/home", timeout=20000) + # 保存 cookies + context.storage_state(path="cookies.json") + browser.close() + return "登录成功,cookie已保存!返回" + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000) diff --git a/cookies.json b/cookies.json new file mode 100644 index 0000000..5561357 --- /dev/null +++ b/cookies.json @@ -0,0 +1,162 @@ +[ + { + "name": "acw_tc", + "value": "0a0d0ad817647684166947926e745d544464c3eaa8c03b5c1daaf7727ce7ff", + "domain": "creator.xiaohongshu.com", + "path": "/", + "expires": 1764770219.611397, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "xsecappid", + "value": "ugc", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1796304431, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "a1", + "value": "19ae46510a243heinye4i1n8fbktbb3sjw9vp3uz950000223112", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1796304420, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "webId", + "value": "374bfec4808d4b49918e4f704bf9fde6", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1796304420, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "acw_tc", + "value": "0a4a65a917647684172526611e22e34304a2cc5b000e71927ba9fa334c4ca7", + "domain": "edith.xiaohongshu.com", + "path": "/", + "expires": 1764770220.163317, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "acw_tc", + "value": "0a00074117647684176224664e82e886141fe61870fff4ae1ea35c0940faa6", + "domain": "customer.xiaohongshu.com", + "path": "/", + "expires": 1764770220.557782, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "websectiga", + "value": "59d3ef1e60c4aa37a7df3c23467bd46d7f1da0c1918cf335ee7f2e9e52ac04cf", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1765027620, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "sec_poison_id", + "value": "b47ab11d-6876-4ed6-a53d-2192a935782b", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1764769025, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "gid", + "value": "yj0d4K2yJSVSyj0d4K2y8Eq70J4qxd3Ivd43jujyIYiDTh289DDqMU888JJqyyJ880ySdS84", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1799328434.79468, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "customer-sso-sid", + "value": "68c517579622683438383105kvqpbrqnjhxcuozw", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1765373230.597087, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "x-user-id-creator.xiaohongshu.com", + "value": "68763bd9000000001b019a5c", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1799328431.597156, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "customerClientId", + "value": "091245717510800", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1799328431.597244, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "access-token-creator.xiaohongshu.com", + "value": "customer.creator.AT-68c517579622683438383106wtbhklklncen9nms", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1767360430.59727, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "galaxy_creator_session_id", + "value": "jxq51QjFn05XJyW41Uz1ASvOSreHs5r2ImYX", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1767360431.597285, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "galaxy.creator.beaker.session.id", + "value": "1764768428620029392110", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1767360431.597294, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "loadts", + "value": "1764768431906", + "domain": ".xiaohongshu.com", + "path": "/", + "expires": 1796304431, + "httpOnly": false, + "secure": false, + "sameSite": "Lax" + } +] \ No newline at end of file diff --git a/demo_ip.py b/demo_ip.py new file mode 100644 index 0000000..02609ba --- /dev/null +++ b/demo_ip.py @@ -0,0 +1,45 @@ +import requests +import time +if __name__ == '__main__': + # 客户ip提取链接,每次提取1个,提取链接可以换成自己购买的 + url = 'http://api.tianqiip.com/getip?secret=6o09i8io&num=2&type=txt&port=1&mr=1&sign=5451e454a54b9f1f06222606c418e12f' + # 访问的目标地址 + targeturl = 'http://47.99.184.230/info.php' + response = requests.get(url) + content =response.content.decode("utf-8").strip() + print('提取IP:' + content) + nowtime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) + print('提取IP时间:' + nowtime) + sj = content.strip().split(":", 1) + sj1 = sj[0] + print("IP:", sj1) + sj2 = sj[1] + print("端口:", sj2) + try: + #proxyMeta = "http://nfd0p2:bHQAp5iW@%(host)s:%(port)s" % { # 账密验证,需要购买的代理套餐开通才可使用账密验证,此种情况无需加白名单 + proxyMeta = "http://%(host)s:%(port)s" % {#白名单验证 + "host": sj1, + "port": sj2, + } + print("代理1:", proxyMeta) + proxysdata = { + 'http': proxyMeta, + 'https': proxyMeta + } + print("代理2:", proxysdata) + headers = { + "user-agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.' + } + start = int(round(time.time() * 1000)) + resp = requests.get(targeturl, proxies=proxysdata, headers=headers, timeout=20) + costTime = int(round(time.time() * 1000)) - start + print("耗时:" + str(costTime) + "ms") + print("返回:",resp.text) + s = requests.session() + s.keep_alive = False + except Exception as e: + print(e) + + + + diff --git a/main.py b/main.py new file mode 100644 index 0000000..e956a0b --- /dev/null +++ b/main.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +""" +main.py - 支持扫码登录和 cookie 登录模式选择 +""" +import os + +def main(): + print("请选择登录模式:") + print("1. 扫码登录(首次登录/刷新 cookie)") + print("2. 使用已保存 cookie 自动发文") + choice = input("请输入模式编号(1或2):").strip() + if choice == '1': + from scan_qrcode import scan_qrcode + scan_qrcode() + elif choice == '2': + cookie_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cookies.json') + if not os.path.exists(cookie_path): + print("未检测到 cookie 文件,请先扫码登录!") + return + from use_cookie import get_test_content, auto_publish_xhs + title, desc, image_list = get_test_content() + if not image_list: + print('未找到图片文件,请将图片放入test文件夹') + else: + auto_publish_xhs(title, desc, image_list) + else: + print("输入无效,请输入 1 或 2!") + +if __name__ == "__main__": + main() diff --git a/phone_num.py b/phone_num.py new file mode 100644 index 0000000..e4ae035 --- /dev/null +++ b/phone_num.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +手机号登录小红书创作者中心 +功能: +- 自动打开登录页,等待用户手机号登录 +- 登录成功后可获取 cookies 或提示 +""" +import time +from playwright.sync_api import sync_playwright + +def scan_qrcode(): + with sync_playwright() as p: + import getpass + browser = p.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 1600, "height": 900}) + page = context.new_page() + # 1. 让用户输入手机号和验证码 + phone = input("请输入手机号: ") + page.goto("https://creator.xiaohongshu.com/login?source=&redirectReason=401&lastUrl=%252Fnew%252Fhome") + page.get_by_role("textbox", name="手机号").click() + page.get_by_role("textbox", name="手机号").fill(phone) + page.get_by_text("发送验证码").click() + code = input("请输入收到的验证码: ") + # 等待验证码输入框出现,兼容多种写法 + try: + # 先尝试常规定位 + page.wait_for_selector('input[placeholder*="验证码"], input[name*="code"], input[type="text"]', timeout=15000) + # 优先用 placeholder 包含“验证码” + if page.query_selector('input[placeholder*="验证码"]'): + vcode_box = page.locator('input[placeholder*="验证码"]') + elif page.query_selector('input[name*="code"]'): + vcode_box = page.locator('input[name*="code"]') + else: + # 兜底用第2个文本框 + vcode_box = page.locator('input[type="text"]').nth(1) + vcode_box.click() + vcode_box.fill(code) + except Exception as e: + print("验证码输入框定位失败,请检查页面结构或手动调整定位方式。", e) + browser.close() + return + page.get_by_role("button", name="登 录").click() + # 等待跳转到主页 + for i in range(30): + time.sleep(2) + if "new/home" in page.url: + print("手机号登录成功!") + break + else: + print("登录失败,请检查手机号和验证码。") + browser.close() + return + # 只有手机号登录成功后才执行发文流程 + try: + import os + # 读取 test 文件夹内容 + base_dir = os.path.dirname(os.path.abspath(__file__)) + test_dir = os.path.join(base_dir, 'test') + title_path = os.path.join(test_dir, 'title.txt') + desc_path = os.path.join(test_dir, 'desc.txt') + # 查找所有图片,支持多图上传 + exts = ['.jpg', '.jpeg', '.png', '.bmp', '.gif'] + image_list = [] + for fname in os.listdir(test_dir): + if any(fname.lower().endswith(ext) for ext in exts): + image_list.append(os.path.join(test_dir, fname)) + if not image_list: + raise Exception("未找到图片文件,请在 test 文件夹中放入图片。") + with open(title_path, 'r', encoding='utf-8') as f: + title = f.read().strip() + with open(desc_path, 'r', encoding='utf-8') as f: + desc = f.read().strip() + page.get_by_text("发布图文笔记").click() + time.sleep(1) + # 上传多张图片,定位 input[type='file'] + file_input = page.locator("input[type='file']") + file_input.set_input_files(image_list) + time.sleep(1) + # 填写标题 + page.get_by_role("textbox", name="填写标题会有更多赞哦~").click() + page.get_by_role("textbox", name="填写标题会有更多赞哦~").fill(title) + time.sleep(1) + # 填写正文 + page.get_by_role("textbox").nth(1).click() + page.get_by_role("textbox").nth(1).fill(desc) + time.sleep(1) + # 点击发布 + page.get_by_role("button", name="发布").click() + print("已点击发布(多图模式)") + # 发完图文后保存 cookie + try: + from util import save_cookies + cookie_path = os.path.join(base_dir, "cookies.json") + save_cookies(context, cookie_path) + print(f"登录 cookie 已保存到 {cookie_path}") + except Exception as e: + print("保存 cookie 失败:", e) + except Exception as e: + print("自动发文流程异常:", e) + browser.close() + +if __name__ == "__main__": + scan_qrcode() diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..54d0ec8 --- /dev/null +++ b/readme.md @@ -0,0 +1,30 @@ +1. 安装依赖 + pip install -r requirements.txt + +2. 安装 Playwright 浏览器驱动 + playwright install chromium + +3. 文件结构说明 + ├── main.py # 主入口,选择登录模式 + ├── scan_qrcode.py # 扫码登录并自动发文,首次登录/刷新 cookie 用 + ├── use_cookie.py # 使用已保存 cookie 自动发文 + ├── util.py # cookie 保存与加载 工具 + ├── requirements.txt # 依赖包列表 + ├── test/ # 发文内容文件夹 + │ ├── title.txt # 标题 + │ ├── desc.txt # 正文 + │ └── *.jpg/png/... # 图片(支持多图) + +4. 使用方法 + - 首次登录或 cookie 失效时,运行 main.py,选择【1 扫码登录】,扫码后自动保存 cookie 并发文。 + - 后续可直接选择【2 使用已保存 cookie 自动发文】,无需再次扫码。 + - 发文内容请放在 test 文件夹,支持多张图片。 + +5. 常见问题 + - cookies.json 文件丢失或无效时,必须重新扫码登录。 + - Playwright 导入失败请检查依赖安装。 + +6. 运行示例 + python main.py + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e4ce746 Binary files /dev/null and b/requirements.txt differ diff --git a/scan_qrcode.py b/scan_qrcode.py new file mode 100644 index 0000000..d4cd179 --- /dev/null +++ b/scan_qrcode.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +""" +扫码登录小红书创作者中心 +功能: +- 自动打开登录页,展示二维码,等待用户扫码登录 +- 登录成功后可获取 cookies 或提示 +""" +import time +from playwright.sync_api import sync_playwright + +def scan_qrcode(): + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 1600, "height": 900}) + page = context.new_page() + + login_success = False + for i in range(120): + page.goto("https://creator.xiaohongshu.com/new/home") + if "new/home" in page.url: + print("扫码登录成功!") + login_success = True + break + ## 等待10s的扫码时间 + time.sleep(10) + if not login_success: + print("扫码超时,请重试。") + browser.close() + return + # 只有扫码登录成功后才执行发文流程 + try: + import os + # 读取 test 文件夹内容 + base_dir = os.path.dirname(os.path.abspath(__file__)) + test_dir = os.path.join(base_dir, 'test') + title_path = os.path.join(test_dir, 'title.txt') + desc_path = os.path.join(test_dir, 'desc.txt') + # 查找所有图片,支持多图上传 + exts = ['.jpg', '.jpeg', '.png', '.bmp', '.gif'] + image_list = [] + for fname in os.listdir(test_dir): + if any(fname.lower().endswith(ext) for ext in exts): + image_list.append(os.path.join(test_dir, fname)) + if not image_list: + raise Exception("未找到图片文件,请在 test 文件夹中放入图片。") + with open(title_path, 'r', encoding='utf-8') as f: + title = f.read().strip() + with open(desc_path, 'r', encoding='utf-8') as f: + desc = f.read().strip() + page.get_by_text("发布图文笔记").click() + time.sleep(1) + # 上传多张图片,定位 input[type='file'] + file_input = page.locator("input[type='file']") + file_input.set_input_files(image_list) + time.sleep(1) + # 填写标题 + page.get_by_role("textbox", name="填写标题会有更多赞哦~").click() + page.get_by_role("textbox", name="填写标题会有更多赞哦~").fill(title) + time.sleep(1) + # 填写正文 + page.get_by_role("textbox").nth(1).click() + page.get_by_role("textbox").nth(1).fill(desc) + time.sleep(1) + # 点击发布 + page.get_by_role("button", name="发布").click() + print("已点击发布(多图模式)") + # 发完图文后保存 cookie + try: + from util import save_cookies + cookie_path = os.path.join(base_dir, "cookies.json") + save_cookies(context, cookie_path) + print(f"登录 cookie 已保存到 {cookie_path}") + except Exception as e: + print("保存 cookie 失败:", e) + except Exception as e: + print("自动发文流程异常:", e) + browser.close() + +if __name__ == "__main__": + scan_qrcode() diff --git a/scrapy_proxy.py b/scrapy_proxy.py new file mode 100644 index 0000000..0c6e85e --- /dev/null +++ b/scrapy_proxy.py @@ -0,0 +1,32 @@ +import scrapy + +class MimvpSpider(scrapy.spiders.Spider): + name = "mimvp" + allowed_domains = ["mimvp.com"] + start_urls = [ + "http://proxy.mimvp.com/exist.php", + "https://proxy.mimvp.com/exist.php", + ] + + ## 代理设置方式1:直接在代理里设置 + def start_requests(self): + urls = [ + "http://proxy.mimvp.com/exist.php", + "https://proxy.mimvp.com/exist.php", + ] + for url in urls: + meta_proxy = "" + if url.startswith("http://"): + meta_proxy = "http://180.96.27.12:88" # http代理 + elif url.startswith("https://"): + meta_proxy = "http://109.108.87.136:53281" # https代理 + + yield scrapy.Request(url=url, callback=self.parse, meta={'proxy': meta_proxy}) + + + def parse(self, response): + mimvp_url = response.url # 爬取时请求的url + body = response.body # 返回网页内容 + + print("mimvp_url : " + str(mimvp_url)) + print("body : " + str(body)) \ No newline at end of file diff --git a/state.json b/state.json new file mode 100644 index 0000000..07adc5f --- /dev/null +++ b/state.json @@ -0,0 +1 @@ +{"cookies": [{"name": "acw_tc", "value": "0a00073f17648186622332701e43566b95b602a93cd549a88b024eee608633", "domain": "creator.xiaohongshu.com", "path": "/", "expires": 1764820461.74781, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "xsecappid", "value": "ugc", "domain": ".xiaohongshu.com", "path": "/", "expires": 1796354663, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "loadts", "value": "1764818663108", "domain": ".xiaohongshu.com", "path": "/", "expires": 1796354663, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "a1", "value": "19ae763b6c588mpg6z68nwm5jhc3ndw0r6snpywrx50000382215", "domain": ".xiaohongshu.com", "path": "/", "expires": 1796354663, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "webId", "value": "634abc526217352bc577c6c427da9e75", "domain": ".xiaohongshu.com", "path": "/", "expires": 1796354663, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "acw_tc", "value": "0a50896717648186651941329eb01af570a20066b520e2ff1034dffa32d0b3", "domain": "edith.xiaohongshu.com", "path": "/", "expires": 1764820464.69803, "httpOnly": true, "secure": false, "sameSite": "Lax"}, {"name": "websectiga", "value": "88%3B6be45f388a1ee7bf611a69f3e174cae48f1ea02c0f8ec3256031b8be9c7ee", "domain": ".xiaohongshu.com", "path": "/", "expires": 1765077866, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "sec_poison_id", "value": "ecf5ae6b-e74a-438b-8798-f95c3add5334", "domain": ".xiaohongshu.com", "path": "/", "expires": 1764819271, "httpOnly": false, "secure": false, "sameSite": "Lax"}, {"name": "acw_tc", "value": "0a00071517648186671184918e7d081a899a9aa951214f73a355f65e3832f0", "domain": "customer.xiaohongshu.com", "path": "/", "expires": 1764820466.772548, "httpOnly": true, "secure": false, "sameSite": "Lax"}], "origins": [{"origin": "https://creator.xiaohongshu.com", "localStorage": [{"name": "last_tiga_update_time", "value": "1764818666580"}, {"name": "sdt_source_storage_key", "value": "{\"extraInfo\":{},\"reportUrl\":\"/api/sec/v1/shield/webprofile\",\"xhsTokenUrl\":\"https://fe-static.xhscdn.com/as/v1/3e44/public/bf7d4e32677698655a5cadc581fd09b3.js\",\"validate\":false,\"commonPatch\":[\"/fe_api/burdock/v2/note/post\",\"/api/sns/web/v1/comment/post\",\"/api/sns/web/v1/note/like\",\"/api/sns/web/v1/note/collect\",\"/api/sns/web/v1/user/follow\",\"/api/sns/web/v1/feed\",\"/api/sns/web/v1/login/activate\",\"/api/sns/web/v1/note/metrics_report\",\"/api/redcaptcha\",\"/api/store/jpd/main\",\"/phoenix/api/strategy/getAppStrategy\",\"/web_api/sns/v2/note\"],\"signUrl\":\"https://fe-static.xhscdn.com/as/v1/f218/a15/public/04b29480233f4def5c875875b6bdc3b1.js\",\"signVersion\":\"1\",\"url\":\"https://fe-static.xhscdn.com/as/v2/fp/962356ead351e7f2422eb57edff6982d.js\",\"desVersion\":\"2\"}"}]}]} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..63172ae --- /dev/null +++ b/templates/login.html @@ -0,0 +1,37 @@ + + + + + 小红书创作者中心登录 + + + +
+

小红书创作者中心登录

+ {% with messages = get_flashed_messages() %} + {% if messages %} +
{{ messages[0] }}
+ {% endif %} + {% endwith %} +
+ + + +
+
+ + + +
+

请先在小红书页面点击“发送验证码”后再输入收到的验证码

+
+ + diff --git a/test.py b/test.py new file mode 100644 index 0000000..e4ae035 --- /dev/null +++ b/test.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- +""" +手机号登录小红书创作者中心 +功能: +- 自动打开登录页,等待用户手机号登录 +- 登录成功后可获取 cookies 或提示 +""" +import time +from playwright.sync_api import sync_playwright + +def scan_qrcode(): + with sync_playwright() as p: + import getpass + browser = p.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 1600, "height": 900}) + page = context.new_page() + # 1. 让用户输入手机号和验证码 + phone = input("请输入手机号: ") + page.goto("https://creator.xiaohongshu.com/login?source=&redirectReason=401&lastUrl=%252Fnew%252Fhome") + page.get_by_role("textbox", name="手机号").click() + page.get_by_role("textbox", name="手机号").fill(phone) + page.get_by_text("发送验证码").click() + code = input("请输入收到的验证码: ") + # 等待验证码输入框出现,兼容多种写法 + try: + # 先尝试常规定位 + page.wait_for_selector('input[placeholder*="验证码"], input[name*="code"], input[type="text"]', timeout=15000) + # 优先用 placeholder 包含“验证码” + if page.query_selector('input[placeholder*="验证码"]'): + vcode_box = page.locator('input[placeholder*="验证码"]') + elif page.query_selector('input[name*="code"]'): + vcode_box = page.locator('input[name*="code"]') + else: + # 兜底用第2个文本框 + vcode_box = page.locator('input[type="text"]').nth(1) + vcode_box.click() + vcode_box.fill(code) + except Exception as e: + print("验证码输入框定位失败,请检查页面结构或手动调整定位方式。", e) + browser.close() + return + page.get_by_role("button", name="登 录").click() + # 等待跳转到主页 + for i in range(30): + time.sleep(2) + if "new/home" in page.url: + print("手机号登录成功!") + break + else: + print("登录失败,请检查手机号和验证码。") + browser.close() + return + # 只有手机号登录成功后才执行发文流程 + try: + import os + # 读取 test 文件夹内容 + base_dir = os.path.dirname(os.path.abspath(__file__)) + test_dir = os.path.join(base_dir, 'test') + title_path = os.path.join(test_dir, 'title.txt') + desc_path = os.path.join(test_dir, 'desc.txt') + # 查找所有图片,支持多图上传 + exts = ['.jpg', '.jpeg', '.png', '.bmp', '.gif'] + image_list = [] + for fname in os.listdir(test_dir): + if any(fname.lower().endswith(ext) for ext in exts): + image_list.append(os.path.join(test_dir, fname)) + if not image_list: + raise Exception("未找到图片文件,请在 test 文件夹中放入图片。") + with open(title_path, 'r', encoding='utf-8') as f: + title = f.read().strip() + with open(desc_path, 'r', encoding='utf-8') as f: + desc = f.read().strip() + page.get_by_text("发布图文笔记").click() + time.sleep(1) + # 上传多张图片,定位 input[type='file'] + file_input = page.locator("input[type='file']") + file_input.set_input_files(image_list) + time.sleep(1) + # 填写标题 + page.get_by_role("textbox", name="填写标题会有更多赞哦~").click() + page.get_by_role("textbox", name="填写标题会有更多赞哦~").fill(title) + time.sleep(1) + # 填写正文 + page.get_by_role("textbox").nth(1).click() + page.get_by_role("textbox").nth(1).fill(desc) + time.sleep(1) + # 点击发布 + page.get_by_role("button", name="发布").click() + print("已点击发布(多图模式)") + # 发完图文后保存 cookie + try: + from util import save_cookies + cookie_path = os.path.join(base_dir, "cookies.json") + save_cookies(context, cookie_path) + print(f"登录 cookie 已保存到 {cookie_path}") + except Exception as e: + print("保存 cookie 失败:", e) + except Exception as e: + print("自动发文流程异常:", e) + browser.close() + +if __name__ == "__main__": + scan_qrcode() diff --git a/test/desc.txt b/test/desc.txt new file mode 100644 index 0000000..1906305 --- /dev/null +++ b/test/desc.txt @@ -0,0 +1,11 @@ +从 17 岁的天才少年到 29 岁的六冠王,Faker 的名字早就刻在了《英雄联盟》的神坛上🏆​ +谁能想到,这个 1996 年出生的韩国少年,从韩服路人王出道,一脚踏入职业赛场就打出了 “双劫大战” 的封神操作,残血反杀 Ryu 的画面至今仍是电竞史上的名场面。2013 年第一次站在 S 赛舞台,他就带着 SKT 拿下 S3 冠军,出道即巅峰说的就是他吧!​ +✨三冠王的统治时代​ +2015-2016 年是 Faker 的绝对统治期,连续拿下 S5、S6 全球总决赛冠军,成为 LOL 史上第一个三冠王。那时候的他就像中路的 “大魔王”,瑞兹、发条、阿狸这些英雄在他手里仿佛有了灵魂,对手连兵线都不敢轻易靠近。​ +🌧️低谷时的坚守最动人​ +2017 年后 SKT 经历重组,Faker 也曾跌入低谷,甚至坐过替补席。但他从没离开 T1,这个从出道就效力的战队,成了他唯一的执念。2022 年重返 S 赛决赛,2023 年 27 岁的他带着新生代队友拿下 S13 冠军,四冠王的成就已经前无古人。​ +🏆六冠王!传奇还在继续​ +2024 年拿下 S14 冠军成为五冠王,2025 年在成都东安湖体育馆,他又带领 T1 打满五局战胜 KT,拿下史无前例的六冠王和三连冠!当《Silver Scrapes》的战歌响起,看着他站在领奖台上举起奖杯,真的忍不住泪目 ——12 年了,他还在守着中路的塔,守着属于自己的传奇。​ +从 S3 到 S15,队友换了一批又一批,挑战者来了又走,只有 Faker 还在赛场。他是首位入选英雄联盟名人堂的选手,是拿下 10 次 LCK 冠军、500 次 S 赛击杀的传奇,更是把 “电竞精神” 刻进骨子里的李相赫。​ +有人说他是电竞乔丹,可我觉得,Faker 就是 Faker,是独一份的英雄联盟之神🙌​ +#Faker #T1 #英雄联盟 S15 #电竞传奇 #六冠王 #李相赫​ diff --git a/test/faker.png b/test/faker.png new file mode 100644 index 0000000..eb3a64e Binary files /dev/null and b/test/faker.png differ diff --git a/test/title.txt b/test/title.txt new file mode 100644 index 0000000..47d7921 --- /dev/null +++ b/test/title.txt @@ -0,0 +1 @@ +Faker 六冠王封神!12 年守塔的电竞传奇✨ \ No newline at end of file diff --git a/use_cookie.py b/use_cookie.py new file mode 100644 index 0000000..2ae6dc0 --- /dev/null +++ b/use_cookie.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +""" +自动发文脚本:test.py +功能: +""" +import os +import json +from playwright.sync_api import sync_playwright + +def get_test_content(): + base_dir = os.path.dirname(os.path.abspath(__file__)) + test_dir = os.path.join(base_dir, 'test') + title_path = os.path.join(test_dir, 'title.txt') + desc_path = os.path.join(test_dir, 'desc.txt') + with open(title_path, 'r', encoding='utf-8') as f: + title = f.read().strip() + with open(desc_path, 'r', encoding='utf-8') as f: + desc = f.read().strip() + # 查找所有图片,支持多图上传 + exts = ['.jpg', '.jpeg', '.png', '.bmp', '.gif'] + image_list = [] + for fname in os.listdir(test_dir): + if any(fname.lower().endswith(ext) for ext in exts): + image_list.append(os.path.join(test_dir, fname)) + return title, desc, image_list + +def auto_publish_xhs(title, desc, image_path): + # 自动读取扫码登录保存的 cookie + base_dir = os.path.dirname(os.path.abspath(__file__)) + cookie_path = os.path.join(base_dir, "cookies.json") + if not os.path.exists(cookie_path): + print("未找到 cookies.json,请先扫码登录生成 cookie") + return + with open(cookie_path, 'r', encoding='utf-8') as f: + cookies = json.load(f) + + import time + with sync_playwright() as p: + browser = p.chromium.launch(headless=False) + context = browser.new_context(viewport={"width": 1600, "height": 900}) + context.add_cookies(cookies) + page = context.new_page() + # 进入主页,cookie生效后自动登录 + page.goto("https://creator.xiaohongshu.com/new/home") + time.sleep(2) + # 进入图文发布页面 + page.get_by_text("发布图文笔记").click() + time.sleep(1) + # 上传多张图片,定位 input[type='file'] + try: + file_input = page.locator("input[type='file']") + file_input.set_input_files(image_path) + print('图片已上传') + except Exception as e: + print('图片上传控件异常:', e) + time.sleep(1) + # 填写标题 + try: + page.get_by_role("textbox", name="填写标题会有更多赞哦~").click() + page.get_by_role("textbox", name="填写标题会有更多赞哦~").fill(title) + except Exception as e: + print('标题控件异常:', e) + time.sleep(1) + # 填写正文 + try: + page.get_by_role("textbox").nth(1).click() + page.get_by_role("textbox").nth(1).fill(desc) + except Exception as e: + print('正文控件异常:', e) + time.sleep(1) + # 点击发布 + try: + page.get_by_role("button", name="发布").click() + print('已点击发布(多图模式)') + except Exception as e: + print('发布按钮异常:', e) + time.sleep(2) + print('发文流程结束,请检查小红书后台是否发布成功') + browser.close() + +if __name__ == '__main__': + title, desc, image_list = get_test_content() + if not image_list: + print('未找到图片文件,请将图片放入test文件夹') + else: + auto_publish_xhs(title, desc, image_list) diff --git a/util.py b/util.py new file mode 100644 index 0000000..c31a683 --- /dev/null +++ b/util.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +""" +util.py - 用于保存和读取 cookie 的工具函数 +""" +import json +import os + +def save_cookies(context, save_path): + """ + 保存 Playwright 浏览器上下文中的 cookies 到指定路径 + :param context: Playwright browser context + :param save_path: 保存 cookie 的文件路径 + """ + cookies = context.cookies() + with open(save_path, 'w', encoding='utf-8') as f: + json.dump(cookies, f, ensure_ascii=False, indent=2) + +def load_cookies(context, load_path): + """ + 从指定路径读取 cookies 并设置到 Playwright 浏览器上下文 + :param context: Playwright browser context + :param load_path: cookie 文件路径 + """ + if not os.path.exists(load_path): + return + with open(load_path, 'r', encoding='utf-8') as f: + cookies = json.load(f) + context.add_cookies(cookies) diff --git a/web_login.py b/web_login.py new file mode 100644 index 0000000..d1cac70 --- /dev/null +++ b/web_login.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- +""" +Flask Web 登录小红书创作者中心 +- 前端输入手机号和验证码 +- 后端用 Playwright 自动登录 +""" + +from flask import Flask, render_template, request, redirect, url_for, flash, session + + +import time +from playwright.sync_api import sync_playwright +import requests + +def get_working_proxy(test_url="https://creator.xiaohongshu.com"): + proxy_url = 'http://api.tianqiip.com/getip?secret=ew9mj7j3yplbk3xb&num=1&type=txt&port=1&time=3&mr=1&sign=5451e454a54b9f1f06222606c418e12f' + try: + resp = requests.get(proxy_url, timeout=10) + proxy_content = resp.content.decode("utf-8").strip() + proxy_ip, proxy_port = proxy_content.split(":", 1) + proxy_server = f"http://{proxy_ip}:{proxy_port}" + proxies = {"http": proxy_server, "https": proxy_server} + # 检查代理可用性 + try: + test = requests.get(test_url, proxies=proxies, timeout=8) + if test.status_code == 200: + print(f"代理可用: {proxy_server}") + return proxy_server + else: + print(f"代理返回非200: {proxy_server}") + except Exception as e: + print(f"代理不可用: {proxy_server}, {e}") + except Exception as e: + print("获取代理失败", e) + return None + +app = Flask(__name__) +app.secret_key = 'xhs_secret_key' + +# 步骤1:输入手机号,自动打开浏览器并填手机号,等待用户手动点击发送验证码 +@app.route('/', methods=['GET']) +def index(): + return render_template('login.html') + +@app.route('/send_code', methods=['POST']) +def send_code(): + phone = request.form.get('phone') + if not phone: + flash('请输入手机号') + return redirect(url_for('index')) + session['phone'] = phone + # 获取可用代理 + proxy_server = get_working_proxy() + if proxy_server: + print(f"使用代理: {proxy_server}") + else: + print("未获取到可用代理,使用本地IP") + + with sync_playwright() as p: + launch_args = {"headless": False} + if proxy_server: + launch_args["proxy"] = {"server": proxy_server} + browser = p.chromium.launch(**launch_args) + context = browser.new_context() + page = context.new_page() + page.goto("https://creator.xiaohongshu.com/login?source=&redirectReason=401&lastUrl=%252Fnew%252Fhome") + page.get_by_role("textbox", name="手机号").click() + page.get_by_role("textbox", name="手机号").fill(phone) + page.get_by_text("发送验证码").click() + # 等待用户收到验证码 + input("验证码已自动发送,请查收手机短信并输入验证码,操作完成后回到命令行按回车...") + # 保存 context 到磁盘,后续用 + context.storage_state(path="state.json") + browser.close() + flash('验证码已发送,请查收手机短信并输入验证码') + return redirect(url_for('index')) + +# 步骤2:输入验证码,自动登录 +@app.route('/login', methods=['POST']) +def login(): + code = request.form.get('code') + phone = session.get('phone') + if not code or not phone: + flash('请先输入手机号并发送验证码') + return redirect(url_for('index')) + # 获取可用代理 + proxy_server = get_working_proxy() + if proxy_server: + print(f"使用代理: {proxy_server}") + else: + print("未获取到可用代理,使用本地IP") + + with sync_playwright() as p: + launch_args = {"headless": False} + if proxy_server: + launch_args["proxy"] = {"server": proxy_server} + context = p.chromium.launch_persistent_context(user_data_dir=".", storage_state="state.json", **launch_args) + page = context.pages[0] if context.pages else context.new_page() + page.goto("https://creator.xiaohongshu.com/login?source=&redirectReason=401&lastUrl=%252Fnew%252Fhome") + # 填写验证码并登录 + page.wait_for_selector('input[placeholder*="验证码"]', timeout=15000) + vcode_box = page.locator('input[placeholder*="验证码"]') + vcode_box.fill(code) + page.get_by_role("button", name="登 录").click() + # 等待跳转 + try: + page.wait_for_url("**/new/home", timeout=20000) + flash('登录成功!') + # 保存 cookies + context.storage_state(path="cookies.json") + except Exception as e: + flash('登录失败,请检查验证码') + context.close() + return redirect(url_for('index')) + +if __name__ == '__main__': + app.run(debug=True)