引言:一个奇怪的现象
最近我在使用一个在线题库网站准备考试时,发现了一个有趣的现象:点击"下一题"按钮时,页面内容更新了,但浏览器的地址栏URL完全没有变化 ,也没有看到常见的?page=2这样的页码参数。 这让我产生了好奇:网站是怎么知道我现在应该看第几页的呢?
作为一名技术爱好者,我决定像侦探一样,一步步揭开这个谜题。
第一步:查看网络请求
首先,我打开了浏览器的开发者工具(按F12),切换到"网络(Network)"标签页。当我点击"下一题"按钮时,我看到了一条POST请求:
arduino
请求地址:https://www.freecram.com/.../exam-questions.html
请求方法:POST
关键发现1 :请求中并没有page=2或pagenum=2这样的参数,而是只有三个字段:
recaptcha:我输入的验证码do:值为+csrftoken:一长串加密字符串
这就奇怪了!没有页码信息,服务器怎么知道我要看第几页呢?
第二步:查看页面代码
我回到页面,右键点击"查看网页源代码",找到了翻页相关的HTML代码:
html
<form method="post">
<!-- 验证码部分 -->
<input type="text" name="recaptcha" placeholder="验证码">
<!-- 提交按钮 -->
<input type="submit" class="btn btn-sm" value="提交">
<!-- 隐藏字段 -->
<input type="hidden" name="do" value="+">
<input type="hidden" name="csrftoken" value="...">
</form>
关键发现2 :表单中没有action属性,这意味着表单会提交到当前页面。但问题是,这个表单看起来只是用来提交验证码的,并没有页码信息。
接着我找到了控制翻页按钮的JavaScript代码:
javascript
$('.prevqa').bind('click', function() {
$('input[name=recaptcha]').focus();
});
关键发现3:翻页按钮只是让光标聚焦到验证码输入框,并没有直接提交表单或传递页码。
第三步:Cookie里的秘密
既然页面代码里没有明显线索,我转向了另一个方向:Cookie。在网络请求中,我看到了浏览器自动发送的一长串Cookie:
ini
ASP.NET_SessionId=l2ojr0ozb0uyxmqvixoakslu
SessionID=d4d8e029-10fd-40e1-906c-7cfe5af848b4
MemberIntID=103215
...
这时我突然明白了!网站可能像餐厅给顾客安排桌号一样,通过一个"身份证"来识别每位用户。
第四步:会话(Session)的工作原理
让我用一个生活中的比喻来解释这个机制:
场景一:图书馆借书(传统分页方式)
- 你告诉图书管理员:"我要借《三体》第二册"
- 管理员直接给你第二册
- 这里明确指定了"第二册"(就像
page=2)
场景二:图书馆连续阅读(当前网站的方式)
- 第一次来图书馆,管理员给你一个借阅卡号(SessionID)
- 你在登记本上写下:"看到《三体》第二册"
- 下次来时,你只需要说:"请给我下一册"
- 管理员查看登记本,知道你看完了第二册,就给你第三册
网站的工作方式就是第二种场景:
- 首次访问:网站给你一个唯一的"借阅卡号"(存储在Cookie中的SessionID)
- 服务器记账:在服务器上记录"用户A看到第1页"
- 点击"下一页" :你告诉服务器"给我下一页"(通过
do=+参数) - 服务器查账:通过SessionID找到你的记录,知道你在看第1页,于是给你第2页
- 更新记录:在服务器上更新为"用户A看到第2页"
第五步:实验验证
为了验证这个理论,我做了几个实验:
实验一:清除Cookie测试
- 清除浏览器所有Cookie
- 重新访问同一套题链接
- 结果:从第1页重新开始!
结论:清除Cookie就像丢了"借阅卡",管理员不认识你了,只能从头开始。
实验二:隐身模式测试
- 用Chrome的隐身模式访问
- 做几道题,翻到第3页
- 新开一个隐身窗口访问同一链接
- 结果:新窗口从第1页开始!
结论:每个隐身窗口就像一个新顾客,有独立的"借阅卡"和阅读进度。
第六步:技术原理解析
现在让我们从技术角度看看这是如何实现的:
服务器端代码(伪代码)
py
# 当用户首次访问时
def handle_first_visit(request):
session_id = generate_unique_id() # 生成唯一ID
session_data = {
'current_page': 1,
'user_id': 103215,
'start_time': '2026-03-09 00:41:29'
}
save_to_session_store(session_id, session_data) # 保存到服务器
set_cookie('ASP.NET_SessionId', session_id) # 发给浏览器
return render_page(1) # 返回第1页
# 当用户点击"下一页"时
def handle_next_page(request):
session_id = request.cookies.get('ASP.NET_SessionId') # 获取SessionID
session_data = load_from_session_store(session_id) # 从服务器读取数据
current_page = session_data['current_page'] # 获取当前页码
new_page = current_page + 1 # 计算新页码
session_data['current_page'] = new_page # 更新页码
save_to_session_store(session_id, session_data) # 保存新状态
return render_page(new_page) # 返回新页面
数据传输流程
scss
浏览器点击"下一页" → 发送Cookie(SessionID) + 表单数据(do=+) → 服务器
↑ ↓
← 返回第N页HTML ← 服务器查找Session对应状态,计算N ←
第七步:为什么这样设计?
这种设计有多个优点:
1. 安全性
- 用户无法通过修改URL直接跳转到任意页
- 防止付费内容被随意访问
- 考试时不能直接跳到最后一题看答案
2. 用户体验
- URL简洁美观
- 前进/后退按钮工作正常
- 可以防止意外刷新导致进度丢失
3. 灵活性
- 网站可以随时调整分页逻辑
- 支持复杂的进度跟踪(答题对错、用时等)
- 便于实现"断点续做"功能
第八步:其他可能的分页方式
除了这种"服务器记忆"的方式,网站还可以用其他方法实现分页:
方法A:URL参数(传统方式)
ini
https://example.com/exam?page=2
优点:简单直接,可分享链接
缺点:暴露结构,可能被篡改
方法B:前端存储
使用浏览器的LocalStorage存储进度:
javascript
// 存储
localStorage.setItem('current_page', 2);
// 读取
let page = localStorage.getItem('current_page');
优点:减轻服务器压力
缺点:清理浏览器数据会丢失进度
方法C:混合模式
结合多种方式,根据场景选择最适合的。
针对这种基于服务器端会话(Session) 的爬取策略
方案一:使用Selenium自动化(最稳定)
- 完全模拟真实浏览器行为
- 自动处理Cookie和Session
- 可以处理JavaScript渲染的内容
- 可以截图验证码并使用OCR
方案三:逆向分析API接口(最高效)
- 尝试找到底层的数据接口,绕过页面渲染。
策略
尝试找到底层的数据接口,绕过页面渲染。
结语:技术的艺术
通过这次探索,我发现了一个看似简单的"下一页"按钮背后,隐藏着一个精心设计的系统。就像魔术表演一样,观众只看到结果(页面切换),而魔术师(开发者)在幕后通过巧妙的机制(Session管理)实现了这个效果。
这种设计体现了Web开发中的一个重要原则:将状态管理放在最合适的地方。对于需要安全性、一致性和复杂状态跟踪的应用(如在线考试、购物车),服务器端Session管理是最佳选择。
下次当你使用网站时,不妨想想:这个简单的点击动作背后,有多少"看不见"的机制在默默工作?技术的美妙,往往就隐藏在这些看不见的细节中。