2025年12月21日星期日

LinuxDo任务脚本

1.购买服务器阿里云:服务器购买地址https://t.aliyun.com/U/Bg6shY若失效,可用地址

1.购买服务器

阿里云:

服务器购买地址

https://t.aliyun.com/U/Bg6shY

若失效,可用地址

https://www.aliyun.com/daily-act/ecs/activity_selection?source=5176.29345612&userCode=49hts92d

腾讯云:

https://curl.qcloud.com/wJpWmSfU

若失效,可用地址

https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=ad201ee2ef3b771157f72ee5464b1fea&from=console

华为云

https://activity.huaweicloud.com/cps.html?fromacct=64b5cf7cc11b4840bb4ed2ea0b2f4468&utm_source=V1g3MDY4NTY=&utm_medium=cps&utm_campaign=201905

2.部署教程

2024年最新青龙面板跑脚本教程(一)持续更新中

3.代码如下

"""cron: 0 */6 * * *new Env("Linux.Do 签到")"""
import osimport randomimport timeimport functoolsimport sysimport refrom loguru import loggerfrom DrissionPage import ChromiumOptions, Chromiumfrom tabulate import tabulatefrom curl_cffi import requestsfrom bs4 import BeautifulSoup

def retry_decorator(retries=3):    def decorator(func):        @functools.wraps(func)        def wrapper(*args, **kwargs):            for attempt in range(retries):                try:                    return func(*args, **kwargs)                except Exception as e:                    if attempt == retries - 1:  # 最后一次尝试                        logger.error(f"函数 {func.__name__} 最终执行失败: {str(e)}")                    logger.warning(                        f"函数 {func.__name__} 第 {attempt + 1}/{retries} 次尝试失败: {str(e)}"                    )                    time.sleep(1)            return None
        return wrapper
    return decorator

os.environ.pop("DISPLAY"None)os.environ.pop("DYLD_LIBRARY_PATH"None)
USERNAME = os.environ.get("LINUXDO_USERNAME")PASSWORD = os.environ.get("LINUXDO_PASSWORD")BROWSE_ENABLED = os.environ.get("BROWSE_ENABLED""true").strip().lower() not in [    "false",    "0",    "off",]if not USERNAME:    USERNAME = os.environ.get("USERNAME")if not PASSWORD:    PASSWORD = os.environ.get("PASSWORD")GOTIFY_URL = os.environ.get("GOTIFY_URL")  # Gotify 服务器地址GOTIFY_TOKEN = os.environ.get("GOTIFY_TOKEN")  # Gotify 应用的 API TokenSC3_PUSH_KEY = os.environ.get("SC3_PUSH_KEY")  # Server酱³ SendKey
HOME_URL = "https://linux.do/"LOGIN_URL = "https://linux.do/login"SESSION_URL = "https://linux.do/session"CSRF_URL = "https://linux.do/session/csrf"

class LinuxDoBrowser:    def __init__(self) -> None:        from sys import platform
        if platform == "linux" or platform == "linux2":            platformIdentifier = "X11; Linux x86_64"        elif platform == "darwin":            platformIdentifier = "Macintosh; Intel Mac OS X 10_15_7"        elif platform == "win32":            platformIdentifier = "Windows NT 10.0; Win64; x64"
        co = (            ChromiumOptions()            .headless(True)            .incognito(True)            .set_argument("--no-sandbox")        )        co.set_user_agent(            f"Mozilla/5.0 ({platformIdentifier}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36"        )        self.browser = Chromium(co)        self.page = self.browser.new_tab()        self.session = requests.Session()        self.session.headers.update(            {                "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0",                "Accept""application/json, text/javascript, */*; q=0.01",                "Accept-Language""zh-CN,zh;q=0.9",            }        )
    def login(self):        logger.info("开始登录")        # Step 1: Get CSRF Token        logger.info("获取 CSRF token...")        headers = {            "User-Agent""Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36 Edg/142.0.0.0",            "Accept""application/json, text/javascript, */*; q=0.01",            "Accept-Language""zh-CN,zh;q=0.9",            "X-Requested-With""XMLHttpRequest",            "Referer": LOGIN_URL,        }        resp_csrf = self.session.get(CSRF_URL, headers=headers, impersonate="chrome136")        csrf_data = resp_csrf.json()        csrf_token = csrf_data.get("csrf")        logger.info(f"CSRF Token obtained: {csrf_token[:10]}...")
        # Step 2: Login        logger.info("正在登录...")        headers.update(            {                "X-CSRF-Token": csrf_token,                "Content-Type""application/x-www-form-urlencoded; charset=UTF-8",                "Origin""https://linux.do",            }        )
        data = {            "login": USERNAME,            "password": PASSWORD,            "second_factor_method""1",            "timezone""Asia/Shanghai",        }
        try:            resp_login = self.session.post(                SESSION_URL, data=data, impersonate="chrome136", headers=headers            )
            if resp_login.status_code == 200:                response_json = resp_login.json()                if response_json.get("error"):                    logger.error(f"登录失败: {response_json.get('error')}")                    return False                logger.info("登录成功!")            else:                logger.error(f"登录失败,状态码: {resp_login.status_code}")                logger.error(resp_login.text)                return False        except Exception as e:            logger.error(f"登录请求异常: {e}")            return False
        # Step 3: Pass cookies to DrissionPage        logger.info("同步 Cookie 到 DrissionPage...")
        # Convert requests cookies to DrissionPage format        # Using standard requests.utils to parse cookiejar if possible, or manual extraction        # requests.Session().cookies is a specialized object, but might support standard iteration
        # We can iterate over the cookies manually if dict_from_cookiejar doesn't work perfectly        # or convert to dict first.        # Assuming requests behaves like requests:
        cookies_dict = self.session.cookies.get_dict()
        dp_cookies = []        for name, value in cookies_dict.items():            dp_cookies.append(                {                    "name": name,                    "value": value,                    "domain"".linux.do",                    "path""/",                }            )
        self.page.set.cookies(dp_cookies)
        logger.info("Cookie 设置完成,导航至 linux.do...")        self.page.get(HOME_URL)
        time.sleep(5)        user_ele = self.page.ele("@id=current-user")        if not user_ele:            # Fallback check for avatar            if "avatar" in self.page.html:                logger.info("登录验证成功 (通过 avatar)")                return True            logger.error("登录验证失败 (未找到 current-user)")            return False        else:            logger.info("登录验证成功")            return True
    def click_topic(self):        topic_list = self.page.ele("@id=list-area").eles(".:title")        logger.info(f"发现 {len(topic_list)} 个主题帖,随机选择10个")        for topic in random.sample(topic_list, 10):            self.click_one_topic(topic.attr("href"))
    @retry_decorator()    def click_one_topic(self, topic_url):        new_page = self.browser.new_tab()        new_page.get(topic_url)        if random.random() < 0.3:  # 0.3 * 30 = 9            self.click_like(new_page)        self.browse_post(new_page)        new_page.close()
    def browse_post(self, page):        prev_url = None        # 开始自动滚动,最多滚动10次        for _ in range(10):            # 随机滚动一段距离            scroll_distance = random.randint(550650)  # 随机滚动 550-650 像素            logger.info(f"向下滚动 {scroll_distance} 像素...")            page.run_js(f"window.scrollBy(0, {scroll_distance})")            logger.info(f"已加载页面: {page.url}")
            if random.random() < 0.03:  # 33 * 4 = 132                logger.success("随机退出浏览")                break
            # 检查是否到达页面底部            at_bottom = page.run_js(                "window.scrollY + window.innerHeight >= document.body.scrollHeight"            )            current_url = page.url            if current_url != prev_url:                prev_url = current_url            elif at_bottom and prev_url == current_url:                logger.success("已到达页面底部,退出浏览")                break
            # 动态随机等待            wait_time = random.uniform(24)  # 随机等待 2-4 秒            logger.info(f"等待 {wait_time:.2f} 秒...")            time.sleep(wait_time)
    def run(self):        if not self.login():  # 登录            logger.error("登录失败,程序终止")            sys.exit(1)  # 使用非零退出码终止整个程序
        self.print_connect_info()  # 打印连接信息
        if BROWSE_ENABLED:            self.click_topic()  # 点击主题            logger.info("完成浏览任务")
        self.send_notifications(BROWSE_ENABLED)  # 发送通知        self.page.close()        self.browser.quit()
    def click_like(self, page):        try:            # 专门查找未点赞的按钮            like_button = page.ele(".discourse-reactions-reaction-button")            if like_button:                logger.info("找到未点赞的帖子,准备点赞")                like_button.click()                logger.info("点赞成功")                time.sleep(random.uniform(12))            else:                logger.info("帖子可能已经点过赞了")        except Exception as e:            logger.error(f"点赞失败: {str(e)}")
    def print_connect_info(self):        logger.info("获取连接信息")        resp = self.session.get("https://connect.linux.do/", impersonate="chrome136")        soup = BeautifulSoup(resp.text, "html.parser")        rows = soup.select("table tr")        info = []
        for row in rows:            cells = row.select("td")            if len(cells) >= 3:                project = cells[0].text.strip()                current = cells[1].text.strip() if cells[1].text.strip() else "0"                requirement = cells[2].text.strip() if cells[2].text.strip() else "0"                info.append([project, current, requirement])
        print("--------------Connect Info-----------------")        print(tabulate(info, headers=["项目""当前""要求"], tablefmt="pretty"))
    def send_notifications(self, browse_enabled):        status_msg = "✅每日登录成功"        if browse_enabled:            status_msg += " + 浏览任务完成"
        if GOTIFY_URL and GOTIFY_TOKEN:            try:                response = requests.post(                    f"{GOTIFY_URL}/message",                    params={"token": GOTIFY_TOKEN},                    json={"title""LINUX DO""message": status_msg, "priority"1},                    timeout=10,                )                response.raise_for_status()                logger.success("消息已推送至Gotify")            except Exception as e:                logger.error(f"Gotify推送失败: {str(e)}")        else:            logger.info("未配置Gotify环境变量,跳过通知发送")
        if SC3_PUSH_KEY:            match = re.match(r"sct(\d+)t", SC3_PUSH_KEY, re.I)            if not match:                logger.error(                    "❌ SC3_PUSH_KEY格式错误,未获取到UID,无法使用Server酱³推送"                )                return
            uid = match.group(1)            url = f"https://{uid}.push.ft07.com/send/{SC3_PUSH_KEY}"            params = {"title""LINUX DO""desp": status_msg}
            attempts = 5            for attempt in range(attempts):                try:                    response = requests.get(url, params=params, timeout=10)                    response.raise_for_status()                    logger.success(f"Server酱³推送成功: {response.text}")                    break                except Exception as e:                    logger.error(f"Server酱³推送失败: {str(e)}")                    if attempt < attempts - 1:                        sleep_time = random.randint(180360)                        logger.info(f"将在 {sleep_time} 秒后重试...")                        time.sleep(sleep_time)

if __name__ == "__main__":    if not USERNAME or not PASSWORD:        print("Please set USERNAME and PASSWORD")        exit(1)    l = LinuxDoBrowser()    l.run()
解析

这是一个 Linux.Do 定时签到/活跃脚本(cron:每 6 小时一次),核心目标是:

  1. 用账号密码 模拟登录 linux.do(先取 CSRF,再提交 session 登录)

  2. 把 requests 会话拿到的 Cookie 同步到 DrissionPage 浏览器,用于后续网页行为

  3. (可选)执行"浏览任务":随机点开 10 个主题帖,滚动阅读,且有一定概率点赞

  4. 访问 https://connect.linux.do/ 抓取并打印一个 Connect Info 表格(项目/当前/要求)

  5. 最后通过 Gotify / Server酱³ 推送"登录成功/浏览完成"的通知

主要方法

1) retry_decorator(retries=3)

通用 重试装饰器:给函数包一层重试逻辑。

  • 失败会 warning,每次间隔 1 秒

  • 最后一次失败会 error

  • 用在 click_one_topic(),避免单个帖子打开/加载异常导致整体中断

LinuxDoBrowser 类

2) __init__(self)

初始化运行环境("请求端 + 浏览器端"双通道):

  • 根据系统平台拼 User-Agent

  • 创建 无头、隐身 Chromium(DrissionPage)

  • 创建 curl_cffi.requests.Session() 并设置默认 headers(更像真实浏览器,且支持 impersonate

3) login(self)

登录的核心流程,分三步:

  • Step1:获取 CSRF

    • GET https://linux.do/session/csrf

    • 解析 JSON 得到 csrf,用于后续登录请求头 X-CSRF-Token

  • Step2:提交登录

    • POST https://linux.do/session

    • body:login/password/second_factor_method/timezone

    • 判断是否返回 error,决定登录成功与否

  • Step3:同步 Cookie 到浏览器

    • self.session.cookies.get_dict() 转成 DrissionPage 的 cookie 格式

    • self.page.set.cookies(...) 后打开首页 https://linux.do/

    • 通过查找 #current-user 或页面是否出现 avatar 来验证登录是否生效

这一段的关键点:先用接口登录拿 Cookie,再把 Cookie 注入浏览器,比纯 UI 自动化更稳定也更快。

4) run(self)

主控制器(脚本入口):

  1. 调 login(),失败直接 sys.exit(1)

  2. print_connect_info() 打印 connect 信息表

  3. 如果 BROWSE_ENABLED 开启:执行 click_topic() 浏览/点赞任务

  4. send_notifications() 推送结果

  5. 清理资源:关闭页面、退出浏览器

5) print_connect_info(self)

抓取并展示 connect.linux.do 的表格信息

  • GET https://connect.linux.do/

  • BeautifulSoup 解析 <table>

  • 抽取每行的:项目名 / 当前值 / 要求值

  • 用 tabulate 打印为表格

6) click_topic(self)

执行"浏览任务"的入口:

  • 从 #list-area 下抓取帖子链接(. :title

  • 随机抽 10 个帖子

  • 对每个帖子调用 click_one_topic(url)

7) click_one_topic(self, topic_url)(带重试)

打开单个帖子并模拟行为:

  • 新开 tab → 打开帖子

  • 30% 概率 click_like() 点赞

  • browse_post() 进行滚动浏览

  • 关闭该 tab

8) browse_post(self, page)

模拟"像真人一样浏览":

  • 最多滚动 10 次,每次随机滚动 550~650px

  • 每次滚动后随机等待 2~4 秒

  • 3% 概率随机提前退出

  • 若检测到到底部且 URL 不再变化 → 退出

9) click_like(self, page)

给帖子点赞(概率触发):

  • 找 .discourse-reactions-reaction-button 按钮并点击

  • 若找不到就认为可能已点赞或结构变化

10) send_notifications(self, browse_enabled)

推送"执行结果"到通知渠道:

  • Gotify:POST {GOTIFY_URL}/message?token=...

  • Server酱³:GET https://{uid}.push.ft07.com/send/{SC3_PUSH_KEY}

    • 若失败,最多重试 5 次,每次等待 180~360 秒再重试

  • 必需环境变量:

    • LINUXDO_USERNAME / LINUXDO_PASSWORD(或退化用 USERNAME/PASSWORD

  • 可选开关:

    • BROWSE_ENABLED=false 可关闭浏览任务,只登录+抓 connect 信息+推送

  • 可选通知:

    • GOTIFY_URL + GOTIFY_TOKEN

    • SC3_PUSH_KEY



注意

本文部分变量已做脱敏处理,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。技术层面需要提供帮助,可以通过打赏的方式进行探讨。


历史脚本txt文件获取>>
服务器搭建,人工服务咨询>>

没有评论:

发表评论

在家做YouTube的第44天

今天没有做视频。是的,今天没有做视频。为什么没有做,还是老样子,视频没有播放,直接导致我今天一点都不想做。     今天没有做视频。是的,今天没有做视频。为什么没有做,还是老样子,视频没有播放,直接导致我今天一点都不想做。但是今天也没有闲着,找赛道,分析视频。其实对于这种赛道...