1.购买服务器阿里云:服务器购买地址https://t.aliyun.com/U/Bg6shY若失效,可用地址
阿里云:
服务器购买地址
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=2019052.部署教程
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 BeautifulSoupdef 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 Nonereturn wrapperreturn decoratoros.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酱³ SendKeyHOME_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 platformif 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 Tokenlogger.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: Loginlogger.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 Falselogger.info("登录成功!")else:logger.error(f"登录失败,状态码: {resp_login.status_code}")logger.error(resp_login.text)return Falseexcept Exception as e:logger.error(f"登录请求异常: {e}")return False# Step 3: Pass cookies to DrissionPagelogger.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 avatarif "avatar" in self.page.html:logger.info("登录验证成功 (通过 avatar)")return Truelogger.error("登录验证失败 (未找到 current-user)")return Falseelse:logger.info("登录验证成功")return Truedef 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 = 9self.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(550, 650) # 随机滚动 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 = 132logger.success("随机退出浏览")break# 检查是否到达页面底部at_bottom = page.run_js("window.scrollY + window.innerHeight >= document.body.scrollHeight")current_url = page.urlif current_url != prev_url:prev_url = current_urlelif at_bottom and prev_url == current_url:logger.success("已到达页面底部,退出浏览")break# 动态随机等待wait_time = random.uniform(2, 4) # 随机等待 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(1, 2))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酱³推送")returnuid = match.group(1)url = f"https://{uid}.push.ft07.com/send/{SC3_PUSH_KEY}"params = {"title": "LINUX DO", "desp": status_msg}attempts = 5for attempt in range(attempts):try:response = requests.get(url, params=params, timeout=10)response.raise_for_status()logger.success(f"Server酱³推送成功: {response.text}")breakexcept Exception as e:logger.error(f"Server酱³推送失败: {str(e)}")if attempt < attempts - 1:sleep_time = random.randint(180, 360)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 小时一次),核心目标是:
用账号密码 模拟登录 linux.do(先取 CSRF,再提交 session 登录)
把
requests会话拿到的 Cookie 同步到 DrissionPage 浏览器,用于后续网页行为(可选)执行"浏览任务":随机点开 10 个主题帖,滚动阅读,且有一定概率点赞
访问
https://connect.linux.do/抓取并打印一个 Connect Info 表格(项目/当前/要求)最后通过 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-TokenStep2:提交登录
POST
https://linux.do/sessionbody:
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)
主控制器(脚本入口):
调
login(),失败直接sys.exit(1)print_connect_info()打印 connect 信息表如果
BROWSE_ENABLED开启:执行click_topic()浏览/点赞任务send_notifications()推送结果清理资源:关闭页面、退出浏览器
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_TOKENSC3_PUSH_KEY
注意:
本文部分变量已做脱敏处理,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。技术层面需要提供帮助,可以通过打赏的方式进行探讨。
没有评论:
发表评论