├── .gitignore ├── requirements.txt ├── README.md ├── .github └── workflows │ └── soushuba.yml ├── discuz-login.py └── soushuba.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.venv/ 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.5 2 | beautifulsoup4==4.13.5 3 | lxml==6.0.1 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 搜书吧自动签到 2 | 3 | 使用 GitHub Actions 每天凌晨自动签到搜书吧。 4 | 5 | ## 配置 6 | 7 | 为了能够自动签到,需要设置几个 Actions secrets,如下: 8 | 9 | | secrets | 说明 | 例子 | 10 | | ----------------- | -------------- | -------------------- | 11 | | SOUSHUBA_HOSTNAME | 搜书吧永久地址 | `www.soushu2030.com` | 12 | | SOUSHUBA_USERNAME | 搜书吧用户名 | | 13 | | SOUSHUBA_PASSWORD | 搜书吧密码 | | 14 | -------------------------------------------------------------------------------- /.github/workflows/soushuba.yml: -------------------------------------------------------------------------------- 1 | name: Run soushuba qiandao 2 | on: 3 | push: 4 | # Allows you to run this workflow manually from the Actions tab 5 | workflow_dispatch: 6 | schedule: 7 | - cron: '0 21 * * *' 8 | 9 | jobs: 10 | run: 11 | runs-on: ubuntu-latest 12 | env: 13 | SOUSHUBA_HOSTNAME: ${{ secrets.SOUSHUBA_HOSTNAME }} 14 | SOUSHUBA_USERNAME: ${{ secrets.SOUSHUBA_USERNAME }} 15 | SOUSHUBA_PASSWORD: ${{ secrets.SOUSHUBA_PASSWORD }} 16 | 17 | steps: 18 | - name: Check out repository code 19 | uses: actions/checkout@v3 20 | - name: Set up Python 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: '3.13' 24 | cache: 'pip' 25 | - run: pip install -r requirements.txt 26 | - run: python soushuba.py 27 | -------------------------------------------------------------------------------- /discuz-login.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | 4 | 5 | class DiscuzLogin: 6 | proxies = { 7 | 'http': 'http://127.0.0.1:1080', 8 | 'https': 'https://127.0.0.1:1080' 9 | } 10 | 11 | def __init__(self, hostname, username, password, questionid='0', answer=None, proxies=None): 12 | self.session = requests.session() 13 | self.hostname = hostname 14 | self.username = username 15 | self.password = password 16 | self.questionid = questionid 17 | self.answer = answer 18 | if proxies: 19 | self.proxies = proxies 20 | 21 | @classmethod 22 | def user_login(cls, hostname, username, password, questionid='0', answer=None, proxies=None): 23 | user = DiscuzLogin(hostname, username, password, questionid, answer, proxies) 24 | user.login() 25 | 26 | def form_hash(self): 27 | rst = self.session.get(f'https://{self.hostname}/member.php?mod=logging&action=login').text 28 | loginhash = re.search(r'
', rst).group(1) 29 | formhash = re.search(r'', rst).group(1) 30 | return loginhash, formhash 31 | 32 | def login(self): 33 | loginhash, formhash = self.form_hash() 34 | login_url = f'https://{self.hostname}/member.php?mod=logging&action=login&loginsubmit=yes&loginhash={loginhash}&inajax=1' 35 | form_data = { 36 | 'formhash': formhash, 37 | 'referer': f'https://{self.hostname}/', 38 | 'loginfield': self.username, 39 | 'username': self.username, 40 | 'password': self.password, 41 | 'questionid': self.questionid, 42 | 'answer': self.answer, 43 | 'cookietime': 2592000 44 | } 45 | login_rst = self.session.post(login_url, proxies=self.proxies, data=form_data) 46 | if self.session.cookies.get('xxzo_2132_auth'): 47 | print(f'Welcome {self.username}!') 48 | else: 49 | raise ValueError('Verify Failed! Check your username and password!') 50 | 51 | 52 | if __name__ == '__main__': 53 | DiscuzLogin.user_login('hostname', 'username', 'password') -------------------------------------------------------------------------------- /soushuba.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | 实现搜书吧论坛登入和发布空间动态 4 | """ 5 | import os 6 | import re 7 | import sys 8 | from copy import copy 9 | 10 | import requests 11 | from bs4 import BeautifulSoup 12 | from urllib.parse import urlparse 13 | import xml.etree.ElementTree as ET 14 | import time 15 | import logging 16 | import urllib3 17 | 18 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 19 | 20 | logger = logging.getLogger(__name__) 21 | logger.setLevel(logging.DEBUG) 22 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 23 | 24 | ch = logging.StreamHandler(stream=sys.stdout) 25 | ch.setLevel(logging.INFO) 26 | ch.setFormatter(formatter) 27 | logger.addHandler(ch) 28 | 29 | def get_refresh_url(url: str): 30 | try: 31 | response = requests.get(url) 32 | if response.status_code != 403: 33 | response.raise_for_status() 34 | 35 | soup = BeautifulSoup(response.text, 'html.parser') 36 | meta_tags = soup.find_all('meta', {'http-equiv': 'refresh'}) 37 | 38 | if meta_tags: 39 | content = meta_tags[0].get('content', '') 40 | if 'url=' in content: 41 | redirect_url = content.split('url=')[1].strip() 42 | logger.info(f"Redirecting to: {redirect_url}") 43 | return redirect_url 44 | else: 45 | logger.error("No meta refresh tag found.") 46 | return None 47 | except Exception as e: 48 | logger.exception(f'An unexpected error occurred: {e}') 49 | return None 50 | 51 | def get_url(url: str): 52 | resp = requests.get(url) 53 | soup = BeautifulSoup(resp.content, 'html.parser') 54 | 55 | links = soup.find_all('a', href=True) 56 | for link in links: 57 | if link.text == "搜书吧": 58 | return link['href'] 59 | return None 60 | 61 | class SouShuBaClient: 62 | 63 | def __init__(self, hostname: str, username: str, password: str, questionid: str = '0', answer: str = None, 64 | proxies: dict | None = None): 65 | self.session: requests.Session = requests.Session() 66 | self.hostname = hostname 67 | self.username = username 68 | self.password = password 69 | self.questionid = questionid 70 | self.answer = answer 71 | self._common_headers = { 72 | "Host": f"{ hostname }", 73 | "Connection": "keep-alive", 74 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 75 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", 76 | "Accept-Language": "zh-CN,cn;q=0.9", 77 | "Content-Type": "application/x-www-form-urlencoded", 78 | } 79 | self.proxies = proxies 80 | 81 | def login_form_hash(self): 82 | rst = self.session.get(f'https://{self.hostname}/member.php?mod=logging&action=login', verify=False).text 83 | loginhash = re.search(r'
', rst).group(1) 84 | formhash = re.search(r'', rst).group(1) 85 | return loginhash, formhash 86 | 87 | def login(self): 88 | """Login with username and password""" 89 | loginhash, formhash = self.login_form_hash() 90 | login_url = f'https://{self.hostname}/member.php?mod=logging&action=login&loginsubmit=yes' \ 91 | f'&handlekey=register&loginhash={loginhash}&inajax=1' 92 | 93 | 94 | headers = copy(self._common_headers) 95 | headers["origin"] = f'https://{self.hostname}' 96 | headers["referer"] = f'https://{self.hostname}/' 97 | payload = { 98 | 'formhash': formhash, 99 | 'referer': f'https://{self.hostname}/', 100 | 'username': self.username, 101 | 'password': self.password, 102 | 'questionid': self.questionid, 103 | 'answer': self.answer 104 | } 105 | 106 | resp = self.session.post(login_url, proxies=self.proxies, data=payload, headers=headers, verify=False) 107 | if resp.status_code == 200: 108 | logger.info(f'Welcome {self.username}!') 109 | else: 110 | raise ValueError('Verify Failed! Check your username and password!') 111 | 112 | def credit(self): 113 | credit_url = f"https://{self.hostname}/home.php?mod=spacecp&ac=credit&showcredit=1&inajax=1&ajaxtarget=extcreditmenu_menu" 114 | credit_rst = self.session.get(credit_url, verify=False).text 115 | 116 | # 解析 XML,提取 CDATA 117 | root = ET.fromstring(str(credit_rst)) 118 | cdata_content = root.text 119 | 120 | # 使用 BeautifulSoup 解析 CDATA 内容 121 | cdata_soup = BeautifulSoup(cdata_content, features="lxml") 122 | hcredit_2 = cdata_soup.find("span", id="hcredit_2").string 123 | 124 | return hcredit_2 125 | 126 | def space_form_hash(self): 127 | rst = self.session.get(f'https://{self.hostname}/home.php', verify=False).text 128 | formhash = re.search(r'', rst).group(1) 129 | return formhash 130 | 131 | def space(self): 132 | formhash = self.space_form_hash() 133 | space_url = f"https://{self.hostname}/home.php?mod=spacecp&ac=doing&handlekey=doing&inajax=1" 134 | 135 | headers = copy(self._common_headers) 136 | headers["origin"] = f'https://{self.hostname}' 137 | headers["referer"] = f'https://{self.hostname}/home.php' 138 | 139 | for x in range(5): 140 | payload = { 141 | "message": "开心赚银币 {0} 次".format(x + 1).encode("GBK"), 142 | "addsubmit": "true", 143 | "spacenote": "true", 144 | "referer": "home.php", 145 | "formhash": formhash 146 | } 147 | resp = self.session.post(space_url, proxies=self.proxies, data=payload, headers=headers, verify=False) 148 | if re.search("操作成功", resp.text): 149 | logger.info(f'{self.username} post {x + 1}nd successfully!') 150 | time.sleep(120) 151 | else: 152 | logger.warning(f'{self.username} post {x + 1}nd failed!') 153 | 154 | 155 | if __name__ == '__main__': 156 | try: 157 | redirect_url = get_refresh_url('http://' + os.environ.get('SOUSHUBA_HOSTNAME', 'www.soushu2035.com')) 158 | time.sleep(2) 159 | redirect_url2 = get_refresh_url(redirect_url) 160 | url = get_url(redirect_url2) 161 | logger.info(f'{url}') 162 | client = SouShuBaClient(urlparse(url).hostname, 163 | os.environ.get('SOUSHUBA_USERNAME', "USERNAME"), 164 | os.environ.get('SOUSHUBA_PASSWORD', "PASSWORD")) 165 | client.login() 166 | client.space() 167 | credit = client.credit() 168 | logger.info(f'{client.username} have {credit} coins!') 169 | except Exception as e: 170 | logger.error(e) 171 | sys.exit(1) 172 | --------------------------------------------------------------------------------