凌风技术分享

记录学习编程过程中遇到的问题和最后跑通的解法,主要是 Python 相关的实践笔记。

用 requests 写一个校园网自动登录脚本

Pythonrequests网络

实验室的校园网每 24 小时踢一次线,跑训练任务跑到一半断网是最烦的事。手工点登录太蠢,于是写了个脚本:开机自启,检测到掉线就自己登回去。

先抓包,别急着写代码

打开浏览器开发者工具(F12)→ Network → 勾上 Preserve log,然后手动断网重登一次,找到那个 POST 请求。需要确认三件事:

  • 登录接口的完整 URL(常见是 srun_portal / eportal/InterFace.do / login)
  • 表单里到底提交了哪些字段
  • 返回体里"成功"长什么样

最小可用的登录函数

import requests

PORTAL = "http://10.21.0.5/eportal/InterFace.do?method=login"


def login(user: str, pwd: str) -> bool:
    s = requests.Session()
    s.headers.update({
        "User-Agent": "Mozilla/5.0",
        "Referer": "http://10.21.0.5/",
    })
    # 先访问一次认证页,把 cookie 建立起来
    s.get("http://10.21.0.5/", timeout=5)

    data = {
        "userId": user,
        "password": pwd,
        "service": "",
        "queryString": "",
        "operatorPwd": "",
        "validcode": "",
    }
    r = s.post(PORTAL, data=data, timeout=8)
    return "success" in r.text.lower()

踩过的四个坑

1. 直接 POST 返回 403

因为缺 cookie 和 Referer。Session 只是帮你保持 cookie,不会替你访问首页——要显式先 GET 一次认证页,否则服务端认为你是陌生来源。

2. 密码不是明文传输

抓包时把"你输入的明文"和"实际提交的值"对比一下:如果是一串 32 位十六进制,就是 MD5;如果是带时间戳的一长串 base64,多半是 srun 的 xEncode,需要按学校前端 JS 复刻,或者干脆用 Playwright 驱动真实浏览器。

import hashlib

# 只适用于"摘要"型,具体前缀看抓包结果
password = "{MD5}" + hashlib.md5(raw_password.encode()).hexdigest()

3. "登录成功"的判定方式五花八门

有的返回 JSON {"result":"success"},有的返回 HTML 里含"认证成功",还有的干脆 302 跳转。与其猜返回体,不如直接测外网:

def online() -> bool:
    try:
        r = requests.get("http://connect.rom.miui.com/generate_204", timeout=3)
        return r.status_code == 204
    except requests.RequestException:
        return False

这个地址在联网时会返回空的 204,断网时请求直接失败,比解析业务返回可靠得多。

4. 脚本跑着跑着就停了

while True: sleep(60) 写在终端里,窗口一关就死。要交给系统托管:Linux 用 systemd,Windows 用任务计划程序并勾选"不管用户是否登录都要运行"。

import time

USER, PWD = "20230001", "your-password"

if __name__ == "__main__":
    while True:
        if not online():
            ok = login(USER, PWD)
            print("掉线,重新登录:", ok)
        time.sleep(60)

最后提醒一句

别把密码写死在脚本里。用环境变量或 keyring,尤其别把带密码的脚本推上 GitHub——公开仓库里的校园网账号被扫到只是时间问题。

用 PIL 把多张图拼成一张:横向、纵向,以及那些坑

PythonPillow图像处理

写论文附录时要把六张实验结果的截图拼成一张整图,本来以为三行代码的事,结果前后改了四五版。这篇把过程和解法记下来。

横向拼接

from PIL import Image


def merge_horizontal(paths, out, gap=8, bg=(255, 255, 255)):
    imgs = [Image.open(p).convert("RGBA") for p in paths]
    width = sum(i.width for i in imgs) + gap * (len(imgs) - 1)
    height = max(i.height for i in imgs)

    canvas = Image.new("RGBA", (width, height), bg + (255,))
    x = 0
    for im in imgs:
        # 第三个参数是 mask,透明区域才不会涂成黑块
        canvas.paste(im, (x, (height - im.height) // 2), im)
        x += im.width + gap

    canvas.convert("RGB").save(out, quality=92)

纵向把宽高互换即可:总高度是各图高度之和,宽度取最大值。

踩过的五个坑

1. 粘贴上去,透明背景变成黑块

Image.open 打开 PNG 得到的是 RGBA,如果直接 paste 到 RGB 画布并且不传 mask,透明区域会按黑色处理。两个办法:画布建 RGBA 且 paste 传第三个参数 im;或者先把每张图 convert("RGB") 铺一层白底再拼。

2. 尺寸不齐,拼出来参差不齐

先统一高度再拼,等比缩放不会变形:

def resize_to_height(im, target_h):
    w = round(im.width * target_h / im.height)
    return im.resize((w, target_h), Image.LANCZOS)

LANCZOS 是缩小时质量最稳的重采样算法,比默认的 NEAREST 好太多。

3. 手机拍的图是"横躺"的

照片的 EXIF 里有 Orientation 标记,PIL 读像素但不会自动按标记旋转,于是拼出来全部侧躺。一行解决:

from PIL import ImageOps

im = ImageOps.exif_transpose(Image.open(path))

4. JPEG 的 65535 像素硬上限

把几十张聊天截图纵向拼成一张,很容易超过 65535 px,保存时报 Maximum image size exceeded。这是 JPEG 格式本身的限制,不是 Pillow 的锅:

from PIL import Image

Image.MAX_IMAGE_PIXELS = None   # 只关掉"图片过大"的告警,不解除 65535 限制

真要拼长图,存 PNG,或者按 60000 px 分段输出再拼接。

5. 内存瞬间打满

一张图在内存里约占 宽 × 高 × 4 字节。拼一张 10000×10000 的图就是 400MB,几张叠加直接把内存吃干净。拼接前先算一下尺寸,超了就先缩小或者分批写盘。

小结

顺序永远是:先统一尺寸和色彩模式,再拼。这两步做对,后面基本不会再返工。