我正在参与掘金创作者训练营第 4 期,点击了解活动详情,一起学习吧!
2022 年 03 月 14 日
提高性能:调用动态链接库(
.so)2022 年 03 月 10 日
- 通过
sessionid(在 cookie 中)获取 token 和uid,降低运行脚本的难度- 使用
jwt库(需安装)查找uid,完全解决补位的问题2022 年 03 月 07 日
- 采用确定性的递归算法(返回生成器,每次生成一个解法)
- 添加 token 值判断
2022 年 03 月 05 日
- 使用
re库替代regex库(使用上没有影响,但 regex 需要额外安装)2022 年 03 月 04 日
- 解决 base 64 解码时的补位问题(
binascii.Error: Incorrect padding)- 添加
JuejinError,展示错误时的 API 响应
shuzimiti.py:
from ctypes import CDLL, c_uint, POINTER
from itertools import chain
from pathlib import Path
from time import time, sleep
from typing import List, Literal, Tuple, Union
from jwt import decode
from requests import post, RequestException, get
VALID_OPERATIONS = ("+", "-", "*", "/", "&")
GET_TOKEN_URL = "https://juejin.cn/get/token"
GAME_URL = "https://juejin-game.bytedance.com/game/num-puzz/ugc/start"
SO_FILENAME = "puzzle.so"
try:
C_LIB = CDLL(str(Path(__file__).resolve().parent / SO_FILENAME))
except FileNotFoundError:
raise FileNotFoundError(f"file '{SO_FILENAME}' not found, please make sure that you have compiled the script into "
f".so file using the following command: gcc -shared -o puzzle.so -fPIC puzzle.c") from None
class JuejinError(RequestException):
pass
def brute_force(target: int, nums: List[int], syms: List[Literal[0, 1, 2, 3, 4]]) \
-> List[Tuple[int, Literal["+", "-", "*", "/", "&"], int]]:
if not all(map(lambda x: isinstance(x, int), nums)):
raise ValueError("'nums' should only be comprised of integers")
if not all(map(lambda x: 0 <= x < len(VALID_OPERATIONS), syms)):
raise ValueError(f"'syms' contains invalid value")
if not isinstance(target, int):
raise TypeError(f"'target' should be '{int.__name__}', not {type(target).__name__}")
nums_length = len(nums)
c_nums = (c_uint * nums_length)(*nums)
syms_length = len(nums) - 1
syms.extend([4] * (syms_length - len(syms)))
c_syms = (c_uint * syms_length)(*syms)
history_length = 0
c_history = (c_uint * history_length)()
C_LIB.brute_force.restype = POINTER(c_uint * (syms_length * 3))
try:
result = list(C_LIB.brute_force(target,
c_nums, nums_length,
c_syms, syms_length,
c_history, history_length).contents)
except ValueError as e:
if str(e) == "NULL pointer access":
raise ValueError("puzzle not solvable; If you think this is a bug, please report to the author") from None
raise e
formatted = []
for idx in range(0, syms_length * 3, 3):
num1, symbol, num2 = result[idx:idx + 3]
symbol = VALID_OPERATIONS[symbol]
formatted.append([num1, symbol, num2])
return formatted
def get_token_and_uid(session_id: str) -> Tuple[str, str]:
response = get(GET_TOKEN_URL, cookies={
"sessionid": session_id
}).json()
try:
token = response["data"]
except:
raise JuejinError(response["err_msg"]) from None # Suppress the context being printed
try:
uid = decode(token, options={"verify_signature": False})["userId"]
except:
raise ValueError("invalid token")
return token, uid
def fetch_data(token: str, uid: str) -> dict:
response = post(GAME_URL, headers={
"authorization": "Bearer " + token
}, params={
"uid": uid,
"time": int(time() * 1000)
}).json()
try:
return response["data"]
except KeyError:
raise JuejinError(response["message"]) from None
def resolve_map(game_map: List[List[Union[int, float]]]) -> Tuple[List[int], List[Literal[1, 2, 3, 4]]]:
nums = []
syms = []
for item in chain(*game_map):
if isinstance(item, int):
nums.append(item)
elif item == 0.3:
syms.append(0)
elif item == 0.4:
syms.append(1)
elif item == 0.5:
syms.append(2)
elif item == 0.6:
syms.append(3)
return nums, syms
if __name__ == "__main__":
# Edit here
MY_SESSION_ID = "xxx"
TOKEN, UID = get_token_and_uid(MY_SESSION_ID)
last_level = None
while True:
data = fetch_data(TOKEN, UID)
level = data["round"]
if last_level != level:
print("Level", level)
print()
for step in brute_force(data["target"], *resolve_map(data["map"])):
print(*step)
print()
last_level = level
sleep(3)
puzzle.c:
#include <math.h>
#include <string.h>
#include <stdio.h>
int calc(int num1, int sym, int num2) {
int offset = num2 != 0 ? log10(num2) + 1 : 1;
switch (sym) {
case 0:
return num1 + num2;
case 1:
return num2 > num1 ? num2 - num1 : -1;
case 2:
return num1 * num2;
case 3:
return num2 != 0 && num1 % num2 == 0 ? num1 / num2 : -1;
case 4:
return num1 * pow(10, offset) + num2;
default:
return -1;
}
}
int* brute_force(int target,
int nums[], int n_size,
int symbols[], int s_size,
int history[], int h_size) {
if (n_size == 1 && s_size == 0 && nums[0] == target) {
return history;
}
for (int symbol = 0; symbol < 5; symbol++) {
int symbols_copied[s_size - 1];
int once = 1;
int ptr = 0;
for (int idx = 0; idx < s_size; idx++) {
if (symbols[idx] == symbol && once) {
once = 0;
continue;
}
symbols_copied[ptr++] = symbols[idx];
}
if (once == 1){
continue;
}
for (int num1_idx = 0; num1_idx < n_size - 1; num1_idx++) {
for (int num2_idx = num1_idx + 1; num2_idx < n_size; num2_idx++) {
for (int idx = 0; idx < 2; idx++) {
int num1 = nums[num1_idx];
int num2 = nums[num2_idx];
int result = calc(num1, symbol, num2);
if (result != -1) {
int numbers_copied[n_size - 1];
numbers_copied[0] = result;
int ptr = 1;
for (int idx = 0; idx < n_size; idx++) {
if (idx == num1_idx || idx == num2_idx) {
continue;
}
numbers_copied[ptr++] = nums[idx];
}
int history_copied[h_size + 3];
for (int idx = 0; idx < h_size; idx++) {
history_copied[idx] = history[idx];
}
history_copied[h_size] = num1;
history_copied[h_size + 1] = symbol;
history_copied[h_size + 2] = num2;
int* ret = brute_force(target,
numbers_copied, n_size - 1,
symbols_copied, s_size - 1,
history_copied, h_size + 3);
if (ret != NULL) {
return ret;
}
}
if ((symbol == 1 || symbol == 3 || symbol == 4) && num1 != num2) {
num1_idx ^= num2_idx;
num2_idx ^= num1_idx;
num1_idx ^= num2_idx;
}
}
}
}
}
return NULL;
}
Github Gist 地址:gist.github.com/jaredliw/78…
脚本特点
- 配置简单,无需各种 session、token、UID
- 无需手动输入数字、运算符
- 支持待机,通关后自动读取下一关的数据
- 持续更新,欢迎留言反馈问题
展示
环境要求
- Python >= 3.9;
requests库和jwt库;- GCC。
使用说明
- 代码拷到本地;
- 安装依赖:
pip install requests jwt; - 编译 C 语言代码:
gcc -shared -o puzzle.so -fPIC puzzle.c(也可到上方的 Gist 地址下载); - 修改 Python 代码中的
MY_SESSION_ID的值,即 cookie 里的sessionid的值(在任意的掘金页面都能找到)。
代码解析
1. 定义运算
(对应脚本里的 calc 函数)
定义加减乘除和数字合并运算(这里用 & 表示),同时排除一些不合法的运算:
- 被减数比减数小;
- 被除数不能被除数整除;
- 除以 0;
2. 调用 API,获取关卡数据
(对应脚本里的 fetch_data 函数)
POST 请求 /game/num-puzz/ugc/start,需要三个值:
-
token:Bearer Token -
uid:掘金用户 ID个人主页网址里的那串数字。这里可以通过对
token进行 base64 解码获得,就不要求使用者提供了。 -
time:时间戳当前毫秒时间戳,13 位数字。
3. 解析返回值
(对应脚本里的 resolve_map 函数)
返回的 JSON 数据里有:
-
author:关卡作者 -
bug:bug 道具的数量 -
map:游戏地图6 x 6 的二维数组;整数代表数字,0.1 代表空白,0.2 代表障碍,0.3 至 0.6 分别代表加减乘除。
-
round:当前关卡 -
target:合成的目标数字
4. 暴力破解
(对应脚本里的 brute_force 函数)
递归地排列组合数字和符号。
最后
这个脚本只能帮你解决最烧脑的运算部分,游戏还是得手动完成的(坐等能写出自动寻路的大佬)。
这个礼拜我想前十,你们下个礼拜再卷好不?