Skip to main content

脚本爬取数据の初体验

嗯,为了给我们正在搭建的社团管理网站添加一些初始信息,我决定去学校的平台上抓点全校社团的清单信息下来。

Requests 一类的 Python 爬虫我从来没有实际用过,通过抓包也没有暴露任何 Request 参数,所以我也不晓得从何下手;因此,我选择了直接检查 AJAX 请求源码并批量构造 payload 的方法。毕竟,学校的平台是我的老朋友了,对于它的架构,还是比较了解的。(当然,有几位大佬比我了解:我还从来没有尝试绕过权限检查获取无法查看的数据过,下次搞定了再写个 write-up)

我们要“爬取”的是社团信息页,它展示了所有社团的名字与介绍,但只显示在下拉菜单中选择的社团,而手动逐个选择再复制显然不现实。为此,我们要先知道获取并刷新页面上的社团信息的请求是从何处发出的。

先定位到页面的 js 代码。看起来有三个外部脚本:init category dropdown.jsinit group dropdown.jsadd group info.js,以及一个页面内嵌的脚本:

jQuery(document).ready(function() {
initCAScategoryDropDown.init();
$('#select_category').change(function(){
var selValue=$(this).children('option:selected').val();
initCASGroupsDropDown.init(selValue);
});
$('#select_group').change(function(){
var selValue=$(this).children('option:selected').val();
addGroupInfo.init(selValue);
});
);

其中提到了两个元素:#select_category#select_group。不难发现,这是右边的两个下拉菜单。逻辑是,先在第一个菜单中选择分组(Service, Academic 等),再根据选择的项目更新第二个社团菜单。为了确定这一点,实际操作了一次:

先在第一个菜单中选择 "Service",init_groups_dropdown.php 被调用;再选择“信息化社”,add_group_info.php 被调用,符合上面的猜想。此时,就对后端的调用顺序有了大概的了解。

我们最为关心的自然是实际将社团信息注入进页面的那个请求:add_group_info。查看它的 js 代码:

var addGroupInfo = (function () {
return {
init: function (valID) {
$.ajax({
url: "php/cas_add_group_info.php",
dataType: "json",
type: "post",
data: {
groupid: valID,
},
success: function (data) {
//$("#text_groupNo").val(data.groups[i].C_GroupNo);
//$("#text_foundtime").val(data.groups[i].C_FoundTime);
$("#span_group_name").empty();
var groupName = "";
groupName += "<h4>" + data.groups[0].C_NameC;
groupName += "(" + data.groups[0].C_NameE + ")</h4>";
$("#span_group_name").append(groupName);
$("#span_descriptionC").empty();
var DescC = "";
DescC += "<p>" + data.groups[0].C_DescriptionC + "</p>";
$("#span_descriptionC").append(DescC);
$("#span_descriptionE").empty();
var DescE = "";
DescE += "<p>" + data.groups[0].C_DescriptionE + "</p>";
$("#span_descriptionE").append(DescE);
$("#d_teamleader").empty();
var TeamLeader = '<div class="row">';
//alert(data.gmember.length);
for (var i = 0; i < data.gmember.length; i++) {
if (data.gmember[i].LeaderYes > 0) {
TeamLeader += '<div class="col-md-4">';
TeamLeader += '<ul class="list-inline sidebar-tags">';
TeamLeader += '<li><a href="#"><i class="fa fa-user"></i>';
TeamLeader += data.gmember[i].S_Name + "(";
TeamLeader += data.gmember[i].S_Nickname + ")</a></li></ul>";
TeamLeader +=
'Email: <a href="#">' + data.gmember[i].S_Email + "</a>";
TeamLeader += "<p>TEL:" + data.gmember[i].S_STel + "</p>";
TeamLeader += "</div>";
}
}
TeamLeader += "</div>";
$("#d_teamleader").append(TeamLeader);
/////////////////////////////////////////////////////////
$("#d_supervisor").empty();
var Supervisor = "";
if (data.supervisor.length != 0) {
Supervisor += '<ul class="list-inline sidebar-tags" >';
Supervisor +=
'<li><a href="#"><i class="fa fa-user"></i>' +
data.supervisor[0].T_Name;
Supervisor +=
"(" + data.supervisor[0].T_Nickname + ")</a></li></ul>";
Supervisor +=
'Email: <a href="#">' + data.supervisor[0].T_Email + "</a>";
Supervisor += "<p>TEL:" + data.supervisor[0].T_MTel + "</p>";
}
$("#d_supervisor").append(Supervisor);
//////////////////////////////////////////////////////
$("#d_group_leader").empty();
var Groupleader = "";
for (var i = 0; i < data.gmember.length; i++) {
if (data.gmember[i].LeaderYes == 2) {
Groupleader += '<ul class="list-inline sidebar-tags" >';
Groupleader +=
'<li><a href="#"><i class="fa fa-user"></i>' +
data.gmember[0].S_Name;
Groupleader +=
"(" + data.gmember[0].S_Nickname + ")</a></li></ul>";
Groupleader +=
'Email: <a href="#">' + data.gmember[0].S_Email + "</a>";
Groupleader += "<p>TEL:" + data.gmember[0].S_STel + "</p>";
}
}
$("#d_group_leader").append(Groupleader);
///////////////////////////////////////////////////////
$("#s_tNumber").empty();
//alert(data.gmember.length);
var TNumber = "";
TNumber += data.gmember.length;
$("#s_tNumber").append(TNumber);
///////////////////////////////////////////////////////
$("#u_teammate").empty();
var teammate = "";
//alert(data.gmember.length);
for (var i = 0; i < data.gmember.length; i++) {
teammate += '<li><a href="#"><i class="fa fa-user"></i>';
teammate += data.gmember[i].S_Name;
teammate += "(" + data.gmember[i].S_Nickname;
teammate += ")</a></li>";
}
$("#u_teammate").append(teammate);
if (data.projectyes == 0) {
$("#d_projectyes").show();
} else {
$("#d_projectyes").hide();
}
$("#d_activity_records").empty();
var actrecord = "";
//alert(data.gmember.length);
for (var i = 0; i < data.grecord.length; i++) {
actrecord +=
'<ul class="blog-info"><li><i class="fa fa-calendar"></i>';
actrecord += data.grecord[i].C_Date.substr(0, 10) + "</li>";
//actrecord += '<li><i class="fa fa-comments"></i>' + 17 +'</li>';
actrecord += '<li><i class="fa fa-tags"></i>';
actrecord += data.grecord[i].C_Theme + "</li></ul>";
actrecord += "<p>" + data.grecord[i].C_Reflection + "</p>";
}
$("#d_activity_records").append(actrecord);
},
error: function () {
alert("Request failed!");
},
beforeSend: function () {
//alert("Loading!...");
},
});
},
// ...其他函数
};
})();

这个 init() 函数非常壮观。我们先定位到我们需要的几个信息:中英文名字和中英文简介,然后把其他无关信息(比如社员名单)都剔除。同时可以把 jQuery 操作 DOM 元素的指令也删除,改成输出到控制台。

function init(valID) {
$.ajax({
url: "php/cas_add_group_info.php",
dataType: "json",
type: "post",
data: {
groupid: valID,
},
success: function (data) {
console.log(data.groups[0].C_NameC);
console.log(data.groups[0].C_NameE);
console.log(data.groups[0].C_DescriptionC);
console.log(data.groups[0].C_DescriptionE);
},
});
}

其中 valID#select_group 的 option value,比如下面列出的这些:

在发送请求时,只要提供一个 valID 就可以得到相应的社团信息:

但是,这些 option value 并不是连续编码的,看起来非常随机。那么,为了获取每个分组对应的 option value 列表,就需要查看获取这一信息的 init group dropdown.js

var initCASGroupsDropDown = (function () {
return {
init: function (initData) {
$.ajax({
url: "php/cas_init_groups_dropdown.php",
dataType: "json",
type: "post",
data: {
categoryid: initData,
},
success: function (data) {
//alert(data[0].title);
$("#select_group").empty();
$("#select_group").append(
"<option value='0'>" + "选择分组 ..." + "</option>",
);
for (var i = 0; i < data.length; i++) {
$("#select_group").append(
"<option value='" +
data[i].C_GroupsID +
"'>" +
data[i].C_NameC +
"(" +
data[i].C_GroupNo +
")</option>",
);
}
},
error: function () {
alert("Request Initdropdow failed2!");
},
beforeSend: function () {
//alert("Loading!...");
},
});
},
};
})();

看来,每个分组对应的所有的 option value 被保存在 C_GroupsID 下。我们共有 6 个分组,分别对应了 categoryid 1~6;先写个简单的脚本,试试输出每个分组下的 valID 列表:

for (let group = 1; group < 7; group++) {
$.ajax({
url: "php/cas_init_groups_dropdown.php",
dataType: "json",
type: "post",
data: {
categoryid: group,
},
async: false,
success: function (data) {
let list = [];
for (let i = 0; i < data.length; i++) {
list.push(data[i].C_GroupsID);
}
console.log(group + ": " + list);
},
});
}

这些请求必须是同步的(AJAX 默认是异步请求),不然 grouplist 无法一一对应。输出如下:

有了这些数据,就可以逐个发送请求了。下面的代码把返回的数据输出成 json 格式:

function getInfo(valID) {
let obj = { chnName: "", engName: "", chnDesc: "", engDesc: "" };
$.ajax({
url: "php/cas_add_group_info.php",
dataType: "json",
type: "post",
data: {
groupid: valID,
},
async: false,
success: function (data) {
obj.chnName = data.groups[0].C_NameC;
obj.engName = data.groups[0].C_NameE;
obj.chnDesc = data.groups[0].C_DescriptionC;
obj.engDesc = data.groups[0].C_DescriptionE;
},
});
return obj;
}

let categoryList = [];

for (let group = 1; group < 7; group++) {
$.ajax({
url: "php/cas_init_groups_dropdown.php",
dataType: "json",
type: "post",
data: {
categoryid: group,
},
async: false,
success: function (data) {
let clubList = [];
for (let i = 0; i < data.length; i++)
clubList.push(getInfo(data[i].C_GroupsID));
categoryList.push(clubList);
},
});
}

console.log(JSON.stringify(categoryList));

看看输出:

有 89.5kB 的信息,选择拷贝到编辑器。

我们首先要把它分行并正确缩进,把其中 \r\n 的换行都替换成 \n。写几个正则解决这一问题:

/(?<=[\[,])\[/ ==> "\n  ["   // 匹配所有二级列表的前括号并换行
/(?<=\})\]/ ==> "\n ]" // 匹配所有二级列表的后括号并换行
/\{/ ==> "\n {" // 匹配所有对象的前括号并换行
/\}/ ==> "\n }" // 匹配所有对象的后括号并换行
/"chnName":/ ==> '\n "chnName": '
/"engName":/ ==> '\n "engName": '
/"chnDesc":/ ==> '\n "chnDesc": \n '
/"engDesc":/ ==> '\n "engDesc": \n '
/\\r\\n/ ==> "\\n"

有了这些信息,就可以送进自己的网站里,生成社团列表了。

还有一些挺有意义的东西可以做:比如,可以爬取所有已经消失的社团(也就是那些被跳过的编号)的信息。具体如何写脚本,应该是十分显然的,各位也可以模仿上文的过程自行尝试。几句吐槽:看来公益/志愿者类社团都活不长啊……还有,魔方社也消失了是我没想到的。


声明:本文未利用任何信息安全漏洞,请求规模也远在服务器承受能力之下。当然,该网站的安全漏洞一直都在,想获取正常手段无法查看的数据也不是那么困难,但这超过了本文的讨论范畴。