Content Summary: This script automates check-in for Tiantongyuan community, including server setup tutorials, code implementation with login and check-in functions, and detailed technical instructions for deployment.
阿里云:
服务器购买地址
https://t.aliyun.com/U/siC86h若失效,可用地址
https://www.aliyun.com/benefit?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.代码如下
# -*- coding: utf-8 -*-import reimport sysimport timeimport randomfrom dataclasses import dataclassfrom typing import Optional, Tuple, Listimport requestsfrom bs4 import BeautifulSoupBASE_URL = "https://www.ttysq.com"class SignResult:ok: boolmessage: strused_url: str = ""class TtysSignIn:def __init__(self, username: str, password: str, timeout: int = 20):self.username = usernameself.password = passwordself.timeout = timeoutself.sess = requests.Session()# 伪装常见浏览器 UA,尽量降低被挡概率self.sess.headers.update({"User-Agent": ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) ""AppleWebKit/537.36 (KHTML, like Gecko) ""Chrome/122.0.0.0 Safari/537.36"),"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Accept-Language": "zh-CN,zh;q=0.9","Connection": "keep-alive",})# ------------------------- 基础工具方法 -------------------------def _sleep_jitter(self, a: float = 0.8, b: float = 1.8):time.sleep(random.uniform(a, b))def _abs(self, path: str) -> str:if path.startswith("http"):return pathif not path.startswith("/"):path = "/" + pathreturn BASE_URL + pathdef _get(self, url: str, **kwargs) -> requests.Response:return self.sess.get(url, timeout=self.timeout, allow_redirects=True, **kwargs)def _post(self, url: str, data=None, **kwargs) -> requests.Response:return self.sess.post(url, data=data, timeout=self.timeout, allow_redirects=True, **kwargs)def _extract_formhash(self, html: str) -> Optional[str]:"""Discuz 常见:formhash=xxxxx可能出现在 hidden input 或 JS 变量里"""m = re.search(r'name="formhash"\s+value="([^"]+)"', html)if m:return m.group(1)m = re.search(r"formhash=([0-9a-zA-Z]+)", html)if m:return m.group(1)return Nonedef _is_logged_in(self, html: str) -> bool:"""粗略判断:Discuz 登录后常见有"退出/设置/消息/space-uid"等痕迹"""keys = ["退出", "设置", "space-uid", "pwderr", "欢迎您回来"]return any(k in html for k in keys)# ------------------------- 核心:登录 -------------------------def login(self) -> Tuple[bool, str]:"""走 Discuz 常见登录流程:1) 访问登录页拿 formhash2) 提交 member.php?mod=logging&action=login&loginsubmit=yes..."""# Discuz 常见登录页login_page = self._abs("/member.php?mod=logging&action=login")try:r1 = self._get(login_page)except Exception as e:return False, f"访问登录页失败:{e}"if r1.status_code in (403, 429):return False, f"站点拒绝访问({r1.status_code}),可能有风控/反爬,需要更换网络或加白"formhash = self._extract_formhash(r1.text)if not formhash:# 有些站点 formhash 在首页self._sleep_jitter()home = self._abs("/")r_home = self._get(home)formhash = self._extract_formhash(r_home.text)if not formhash:return False, "未获取到 formhash(可能不是 Discuz / 或页面结构不同)"self._sleep_jitter()# Discuz 常见登录提交地址(loginsubmit=yes)login_submit = self._abs("/member.php?mod=logging&action=login&loginsubmit=yes&inajax=1")payload = {"formhash": formhash,"referer": BASE_URL + "/","username": self.username,"password": self.password,"questionid": "0","answer": "",}# 某些站点要求 X-Requested-Withheaders = {"X-Requested-With": "XMLHttpRequest", "Referer": login_page}try:r2 = self._post(login_submit, data=payload, headers=headers)except Exception as e:return False, f"提交登录失败:{e}"# Discuz inajax=1 往往返回 XML/HTML 片段# 这里再访问一次首页验证是否登录态生效self._sleep_jitter()r3 = self._get(self._abs("/"))if self._is_logged_in(r3.text):return True, "登录成功"else:# 尝试从返回内容里挖错误提示msg = self._extract_discuz_msg(r2.text) or "登录失败(账号/密码/验证码/风控)"return False, msgdef _extract_discuz_msg(self, text: str) -> Optional[str]:"""Discuz 常见 message:<p>xxx</p> 或者 XML 中的 <![CDATA[xxx]]>"""m = re.search(r"<!\[CDATA\[(.*?)\]\]>", text, re.S)if m:return re.sub(r"\s+", " ", m.group(1)).strip()# 粗略提取 p 标签文本soup = BeautifulSoup(text, "html.parser")p = soup.find("p")if p and p.get_text(strip=True):return p.get_text(strip=True)return None# ------------------------- 核心:签到 -------------------------def sign_in(self) -> SignResult:"""自动尝试多个常见 Discuz 签到插件路径:- dsu_paulsign(最常见)- k_misign(另一个常见)- 以及部分站点自定义 action"""# 先拿最新 formhash(很多签到接口需要)r_home = self._get(self._abs("/"))formhash = self._extract_formhash(r_home.text) or ""# 候选签到 URL(按常见程度排序)candidates = self._build_sign_candidates(formhash)last_err = ""for url, method, data in candidates:self._sleep_jitter(0.6, 1.2)try:if method == "GET":resp = self._get(url, headers={"Referer": BASE_URL + "/"})else:resp = self._post(url, data=data, headers={"Referer": BASE_URL + "/"})except Exception as e:last_err = f"请求异常:{e}"continue# 403/风控直接记下if resp.status_code in (403, 429):last_err = f"请求被拒绝:HTTP {resp.status_code}"continueok, msg = self._parse_sign_response(resp.text)if ok:return SignResult(True, msg, used_url=url)else:# 记录最后一次失败原因继续尝试下一个last_err = msg or "签到失败(未知原因)"return SignResult(False, f"所有候选签到接口均失败:{last_err}", used_url="")def _build_sign_candidates(self, formhash: str) -> List[Tuple[str, str, dict]]:"""组装候选请求:返回 (url, method, data)"""fh = formhash or ""return [# 1) dsu_paulsign 常见:operation=qiandao(self._abs(f"/plugin.php?id=dsu_paulsign:sign&operation=qiandao&inajax=1&formhash={fh}"), "GET", {}),(self._abs("/plugin.php?id=dsu_paulsign:sign&operation=qiandao&inajax=1"), "POST", {"formhash": fh, "qdmode": "1"}),# 2) k_misign 常见:operation=qiandao(self._abs(f"/plugin.php?id=k_misign:sign&operation=qiandao&inajax=1&formhash={fh}"), "GET", {}),(self._abs("/plugin.php?id=k_misign:sign&operation=qiandao&inajax=1"), "POST", {"formhash": fh}),# 3) 少数站点:直接 sign(self._abs(f"/plugin.php?id=dsu_paulsign:sign&inajax=1&formhash={fh}"), "GET", {}),(self._abs(f"/plugin.php?id=k_misign:sign&inajax=1&formhash={fh}"), "GET", {}),]def _parse_sign_response(self, text: str) -> Tuple[bool, str]:"""尝试从返回内容判断是否签到成功/已签到/失败原因"""# 先做通用清洗raw = re.sub(r"\s+", " ", text).strip()# 常见成功/已签到关键词(不同插件返回差异很大,这里做模糊判断)success_keywords = ["签到成功", "打卡成功", "已签到", "您已签到", "success", "Success", "恭喜"]fail_keywords = ["失败", "error", "Error", "需要先登录", "请先登录", "验证码", "权限", "拒绝访问"]if any(k in raw for k in success_keywords):# 尽量提取一句话msg = self._extract_discuz_msg(text) or "签到成功/已签到"return True, msgif any(k in raw for k in fail_keywords):msg = self._extract_discuz_msg(text) or "签到失败(可能未登录/验证码/权限/风控)"return False, msg# 如果返回非常短,可能是 inajax 的提示if len(raw) < 200 and raw:return False, rawreturn False, "签到接口返回无法识别(可能不是这些插件/需要额外参数)"# ------------------------- 统一入口 -------------------------def run(self) -> int:ok, msg = self.login()print(f"[LOGIN] {msg}")if not ok:return 1sign_res = self.sign_in()if sign_res.ok:print(f"[SIGN ] ✅ {sign_res.message}")print(f"[URL ] {sign_res.used_url}")return 0else:print(f"[SIGN ] ❌ {sign_res.message}")if sign_res.used_url:print(f"[URL ] {sign_res.used_url}")return 2if __name__ == "__main__":# 也可以改成从环境变量读取TTY_USERNAME = os.getenv("TTYSQ_USERNAME", "").strip()TTY_PASSWORD = os.getenv("TTYSQ_PASSWORD", "").strip()if not TTY_USERNAME or not TTY_PASSWORD:print("请先设置账号密码:")print(" Windows(PowerShell):")print(" setx TTYSQ_USERNAME \"你的账号\"")print(" setx TTYSQ_PASSWORD \"你的密码\"")print(" Linux/macOS:")print(" export TTYSQ_USERNAME='你的账号'")print(" export TTYSQ_PASSWORD='你的密码'")sys.exit(1)bot = TtysSignIn(TTY_USERNAME, TTY_PASSWORD)sys.exit(bot.run())
使用
requests.Session()保持登录态(Cookie)自动从页面提取
formhash(Discuz 常用 CSRF 参数)按"候选签到插件 URL 列表"逐个尝试签到(兼容常见
dsu_paulsign/k_misign)输出"签到成功 / 已签到 / 失败原因",方便快速定位需要替换的真实签到接口
主要方法
run→ 脚本统一入口:先登录,再签到,最后返回退出码login→ 执行 Discuz 常见登录流程(获取 formhash → 提交登录 → 首页校验登录态)_extract_formhash→ 从 HTML 中提取formhash(用于登录/签到等请求的关键参数)_is_logged_in→ 通过页面关键字粗略判断当前是否已登录_extract_discuz_msg→ 从 Discuz 的 inajax 返回内容里提取人类可读的提示文本(成功/失败原因)sign_in→ 执行签到:先获取最新 formhash,然后遍历候选签到 URL 逐个尝试_build_sign_candidates→ 构建"候选签到接口列表"(常见插件路径 + GET/POST 两种形态)_parse_sign_response→ 解析签到返回文本,判断成功/已签到/失败,并提取提示信息_get/_post→ 对requests的 GET/POST 做统一封装(带 timeout、redirect 等)_sleep_jitter→ 随机抖动等待,降低触发风控的概率_abs→ 拼接相对路径为完整 URL
注意:
本文部分变量已做脱敏处理,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。技术层面需要提供帮助,可以通过打赏的方式进行探讨。
没有评论:
发表评论