基于selenium
、browsermob-proxy
和httpclient
的爬取个人的大学教务数据(Java)
由于大部分靠现有知识直接做的,所以很多东西说的会不太严谨还有些错误,如果有错误请指出
这里只能爬取个人登录后的数据
这里我gitee上有完整的功能代码,但是仅限于我自己的学校
教务官网例子是正方的
目标功能:
想实现类似大学小程序那样的功能,可以更方便快捷的获取自己想要的数据
用户在我们的页面的表单中输入账号密码和验证码后提交,就能关联到大学的数据
分析:
有的人会问,为什么不直接使用爬虫呢?
因为它并不像一些固定的图片啊,音乐啊,百科啊,那些东西每个人访问的东西都是固定的,你用哪个请求就会响应对应的固定数据。但是大部分需要登录的界面,往往需要个人数据,但是这些数据是比较敏感的,大部分情况这些接口都不会对你直接开放,并且每个人响应的的数据都是不同的,因此没法直接采用爬虫的方式。
就是你学过一点web知识的话会知道,大部分界面登录成功后后端会响应一个cookie|token表示你登录成功,同时保存你个人的一些账号信息。也就理论上你只要拥有这些信息,就能模拟出你的各种请求
具体情况具体分析下,这里测试过是行的,具体就是将这个令牌复制到api测试中测试一下,如果能够返回成功登录的东西一般是可以的
初步的步骤:
- 获取当前会话的cookie/token(主要也是难点)
- 使用这个cookie就能发起不同的请求,想应到诸如课表、成绩之类的数据
获取cookie方法
首先肯定到官网上,对登录的网络请求进行分析,认真看看它的js代码和各种网络请求
这里我只想到了两种方案:
利用网络请求直接获取到对应的cookie
如果是古早一点,确实直接这样整就行了,网上大部分教程也是这样的,但是时代变了,直接这样做其实是不行的。
你直接去观察我们学校的官网会发现的现在的登录往往都会有动态验证码或者一些七七八八的动态码,同时在看他的网络请求会发现它发起的登录请求对很多东西加密了,并且感觉还加了一些其他的会话技术,直接这样网络请求难度是特别大的,以现在的技术水平一时半会也解决不了。
通过模拟浏览器登录
上面的方案通过评估后,发现不太好走,只能接着去翻各种博客了
然后翻着翻着,发现了通过计算机模拟人在浏览器各种操作的方法,也就是selenium(这个就是自动化测试的东西了,网上的资料还是与python为主,java的会比较少)
后面做着做着发现单单有这个还是不够,还是因为那个该死的动态二维码
点开会发现相同的链接,但是却是两张不同的图片。也是因为这个码其实并不是真正的网络链接(大部分图片其实都是通过链接来访问,直接就能获取),但是这里并不是存在云端,然后又要回去分析源码了,会发现他直接存在本地浏览器中。这个其实是不太好直接获取的,也不太好直接爬。因为它应该是直接和当前页面的网络请求绑定的,如果你直接发起相同的请求,并不是在同一个会话下,会生成不同的验证码。这里又卡住了
然后又回去翻博客了,发现抓包这东西,就是它能抓取网络请求,抓取响应的结果,这个就是能把网络请求的各种响应的结果都抓取下来,刚好验证码返回的数据会在这里响应回来,这里用的是
browsermob-proxy
代理服务来抓包,将访问的界面的响应的数据中的验证码,给拦截下来
总结下:selenium+proxy+一些网络请求的类
获取cookie后发起网络请求
这个你拿apifox或者postman测试一下你就懂了
主要还是回去官网分析下那个表单,看看请求头和请求体中应该要带什么
操作步骤:
- 在后端服务器中,打开首先先教务系统的网页后,抓取验证码的信息
- 把这个验证码信息
- 用户输入账号密码和验证码,返回给后端
- 后端自动填入这些信息,得到cookie
- 用这个cookie,获取到当前用户在教务系统的各种信息
1. 获取cookie相关:
模块的准备:
像一些web基础的东西我也不讲了
调用这些可能还是需要一点英语基础的,不然有些东西不好懂
selenium的一些api
java使用selenium实现模拟浏览器操作API大全 模拟登录_java 无头浏览器模拟登录-优快云博客
这篇博客各种api写的挺详细的,但是还是有点不够,具体还需要去查
具体的功能就是:
selenium元素定位、控制浏览器操作、WebDriver常用方法、模拟鼠标操作、模拟键盘操作、获取断言信息、设置元素等待、定位一组元素、多表单切换、多窗口切换、下拉框选择、警告框处理、文件上传、浏览器cookie操作、调用JavaScript代码、获取窗口截屏;
最关键的就是组件元素的定位,然后就是对元素的各种操作,像表单提交之类的,还有像获取cookie之类的东西
这里我用的xpath定位,也不用学,开发者模式自动生成
部分参考代码:
Selenium-java,进行web自动化测试(二)模拟登录操作 - 知乎 (zhihu.com)
browsermob-proxy
主要参考:
【教程】browsermob-proxy 基于Java的代理服务 配合selenium使用-优快云博客
java使用selenium和browsermob简易爬虫_browsermob selenium-优快云博客
lightbody/browsermob-proxy: A free utility to help web developers watch and manipulate network traffic from their AJAX applications. (github.com):快速入门文档,往下翻有java+maven的
这里主要就是要学配置这个代理服务各种东西,和获取抓下来的请求的各种东西,像什么请求头请求体之类的。
Selenium爬虫-获取浏览器Network请求和响应-腾讯云开发者社区-腾讯云 (tencent.com)(python)
图片响应的二进制需要进行解码
试试各种图片编码,发现是Base64编码在线Base64转图片 (lddgo.net)
这里主要就是要获取当前会话的验证码图片,这里响应体里的图片一般是以二进制字节流的形式传输,然后这些二进制数据还可能编码成字符串传输(像base64编码),然后你需要解码成二进制,在保存成图片
搞清楚这些接下来又是调库了
模拟浏览器操作具体实现:
这里不说具体代码实现,像这种api调用的gpt很擅长,但是一定不要太迷信它了,这里就被坑了很多次,最好就是配各种博客一起看,不过这里可能会有很多的细节问题,这时候就看你改bug的能力了。
这里主要就是讲讲逻辑而已
一开始我就是简单的实现出基本功能,还没用类封装起来,用类封装的感觉主要目的就是增加复用性,避免写太多重复代码
一开始我是想用结构化的程序
有些格式需要转换,不然网络请求是会报错的
@Test
public void TestSeleniumAndProxy(){
// 设置 Microsoft Edge WebDriver 的路径
System.setProperty("webdriver.edge.driver", "src\\test\\java\\com\\wawu\\edgedriver_win64\\msedgedriver.exe");
// 创建 BrowserMob Proxy 实例
BrowserMobProxy proxy = new BrowserMobProxyServer();
proxy.start();//运行在随机端口
// proxy.enableHarCaptureTypes(CaptureType.RESPONSE_CONTENT);
/*// 获取代理服务器地址
String proxyHost="localhost";//代理服务器的主机名
int proxyPort=proxy.getPort();//代理服务器的端口号*/
//创建一个Selenium代理对象
Proxy seleniumProxy= ClientUtil.createSeleniumProxy(proxy);//别导错包了
//配置代理浏览器
//在Selenium中,所需的能力是一种可选配置选项,用于自定义和控制不同浏览器的行为。
/*一堆我还没有接触到的功能:随便举几个感兴趣的:
1.网络请求、
2.IP伪装和地理位置模拟
还有一堆*/
DesiredCapabilities capabilities=new DesiredCapabilities();
capabilities.setCapability(CapabilityType.PROXY,seleniumProxy);
capabilities.setAcceptInsecureCerts(true);//一定要设置这个不然会警告
// capabilities.setJavascriptEnabled(true);
//开启一个浏览器,来模拟
WebDriver driver=new EdgeDriver(capabilities);
// 如果需要,启用更详细的 HAR 捕获(有关完整列表,请参阅 CaptureType)
//HAR(HTTP Archive)是一种用于记录和存储HTTP会话的格式
//就是你看到网络请求中的各种信息
// proxy.enableHarCaptureTypes(CaptureType.REQUEST_CONTENT,CaptureType.RESPONSE_CONTENT);
proxy.setHarCaptureTypes(CaptureType.RESPONSE_CONTENT);
proxy.enableHarCaptureTypes(CaptureType.RESPONSE_BINARY_CONTENT);
//创建一个HAR,会给捕获的东西创建一个名字
proxy.newHar("com.大学官网");
// try {
// Thread.sleep(10000);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
// driver.manage().window().maximize();
// driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
//打开一个网站
driver.get("https://jwxt.webvpn.大学官网.edu.cn/jwglxt/xtgl/index_initMenu.html?jsdm=&_t=1711540059835");
// String pageSource = driver.getPageSource();//获取页面数据
//har=proxy.newHar("com.大学官网");//proxy.newHar();//不知道为什么要放这里,才能抓取到请求
Har har=proxy.getHar();
/*//遍历请求条目请输出请求
for(HarEntry entry:har.getLog().getEntries()){
String resquestUrl=entry.getRequest().getUrl();
System.out.println("URL:"+resquestUrl);
System.out.println("Method:"+entry.getRequest().getMethod());
HarResponse response = entry.getResponse();
System.out.println(response.getContent().getText());
System.out.println();
}*/
// 遍历请求条目请输出请求
har.getLog().getEntries().forEach(entry -> {
String requestUrl = entry.getRequest().getUrl();
System.out.println(requestUrl);
if (requestUrl.startsWith("https://cas-443.webvpn.大学官网.edu.cn/authserver/getCaptcha.htl")) {//验证码的请求开头
try {
// System.out.println(entry.getResponse().getContent().getText());
//直接用二进制的不行,还需要进行解码
// byte[] responseData = entry.getResponse().getContent().getText().getBytes();
// Files.write(Paths.get("captcha.png"), responseData);
String chaptchaBase64String=entry.getResponse().getContent().getText();
// 解码Base64字符串为字节数组
byte[] imageBytes = Base64.getDecoder().decode(chaptchaBase64String);
// 将字节数组写入文件
String filePath="captcha.jpg";
//这里一定要用流的形式才能成功
FileOutputStream fos = new FileOutputStream(filePath);
fos.write(imageBytes);
fos.close();
System.out.println("Image saved successfully.");
} catch (IOException e) {
e.printStackTrace();
}
}
});
WebElement captcha=driver.findElement((new By.ByXPath("//*[@id=\"captcha\"]")));
Scanner sc=new Scanner(System.in);
String captchaInput=sc.next();
captcha.sendKeys(captchaInput);
WebElement submit=driver.findElement(new By.ByXPath("//*[@id=\"login_submit\"]"));
//填写账号密码
WebElement username = driver.findElement(new By.ByXPath("//*[@id=\"username\"]"));
WebElement password = driver.findElement(new By.ByXPath("//*[@id=\"password\"]"));
username.sendKeys("学号");
password.sendKeys("密码");
submit.click();
try {
Thread.sleep(10000);//程序结束端口结束
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 获取页面的 cookie
Set<Cookie> cookies = driver.manage().getCookies();
System.out.println();
String getCookies="";
for(Cookie cookie:cookies){
getCookies+=cookie.getName();
getCookies+='=';
getCookies+=cookie.getValue();
getCookies+=";";
}
System.out.println(getCookies);
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Test
void formSubmitExample() throws Exception {
HttpClient client = HttpClients.createDefault();
// HttpPost httpPost = new HttpPost("https://cas-443.webvpn.大学官网.edu.cn/authserver/login?service=https%3A%2F%2Fwebvpn.大学官网.edu.cn%2Fusers%2Fauth%2Fcas%2Fcallback%3Furl"); // 表单提交的地址
HttpPost httpPost = new HttpPost("https://jwxt.webvpn.大学官网.edu.cn/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151");
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("xnm","2023"));
params.add(new BasicNameValuePair("xqm","12"));
params.add(new BasicNameValuePair("kzlx","ck"));
params.add(new BasicNameValuePair("xsdm",""));
Scanner sc=new Scanner(System.in);
String cookies=sc.next();
httpPost.setHeader("cookie",cookies);
// 将参数编码为表单实体
httpPost.setEntity(new UrlEncodedFormEntity(params));
// 执行 HTTP Post 请求
HttpResponse response = client.execute(httpPost);
// 处理响应
HttpEntity entity = response.getEntity();
BufferedReader rd = new BufferedReader(new InputStreamReader(entity.getContent()));
// String line;
// while ((line = rd.readLine()) != null) {
// System.out.println(line);
// }
System.out.println(rd.readLine());
}
面向对象封装(代码写完,但是没整理)
目标:我是想直接封装成一个获取cookie的工具类
发现有些类还能分解出来:像登录信息的这个实体类,base64图片类
到时候cookie工具类调用这些类就行
cookie工具类的一些操作:
- 获取到当前的验证码:
- 跳转到教务系统的登录界面
- 获取到当前会话的验证码
- 等待用户填完表单,把登录信息的封装到实体类中,传到获取cookie的方法中
2. 网络请求到响应的数据
难点就是要去官网找到对应的请求接口,分离出必要的传的东西,以及各种参数,把那些没有必要的给去掉就行了
去apifox或postman中慢慢试,就能试出来了
设计一些敏感数据不放了,自己测试就行
模块准备
这个能调的api就很多了,学下就行,不说了
Java使用代码调用接口(HttpClient详细使用示例) (youkuaiyun.com)
这里还有阿里巴巴的那个json工具包
结合着看就行
这个包不要认错了,很容易报错
认准那个
apache
那个包
debug
httpclient连接多次后,发生了阻塞
下面这些是手动释放
关于httpclient中多次执行execute阻塞问题,卡住不动了解决方式。_发送post请求 ,卡死在 excute-优快云博客
使用for循环调用HttpClient只执行了前三次就不再执行_代码多次执行后就不再执行-优快云博客
这个是自动释放:
by-gpt
要确保连接释放良好,可以使用
try-with-resources
语句自动关闭HttpResponse
和HttpEntity
,而不需要手动调用releaseConnection()
。这会确保所有资源在使用完后都被正确释放。
/**
* (通过泛型)提取出喵喵大学公共的httpclient请求代码
* * 条件:
* * 1.请求方式为post
* * 2.返回的json为:{..,"items":[{},{},{},..]},需要的数据在items中,为一个数组
* @param :对应的请求网址
* @param 大学官网Cookie:对应学生获取到的cookie
* @param params:请求体中的内容
* @param clazz
* @return : 返回的是T这个类型的结果数组
* @param <T>
* @throws IOException
*/
public <T> List<T> get大学官网JsonConverterObject(String 大学官网Url,String 大学官网Cookie,List<NameValuePair> params,Class<T> clazz) throws IOException{
//通过httpclient发起请求
HttpPost httpPost = new HttpPost(大学官网Url);
httpPost.setHeader(大学官网RequestHeaderConstants.COOKIE_HEADER,大学官网Cookie);
// 将参数编码为表单实体
httpPost.setEntity(new UrlEncodedFormEntity(params));
// 使用 try-with-resources 确保资源自动关闭,防止出现阻塞状态
// 执行 HTTP Post 请求
try(CloseableHttpResponse response = httpClient.execute(httpPost)){
// 获取响应状态码
int statusCode = response.getStatusLine().getStatusCode();
// System.out.println("Response Code: " + statusCode);
if(statusCode != 200){
throw new 大学官网NotConnectException(MessageConstant.大学官网_NOT_CONNECT_ERROR);
}
// 处理响应
HttpEntity entity = response.getEntity();
BufferedReader rd = new BufferedReader(new InputStreamReader(entity.getContent()));
String responseJsonString=rd.readLine();
// //必须要释放连接,否则会出现httpclient阻塞
// rd.close();
// entity.getContent().close();
// httpPost.releaseConnection();
//DONE 测试
// System.out.println(responseJsonString);
//通过fastjson转化为实体类
JSONObject jsonObject = JSON.parseObject(responseJsonString);
JSONArray itemsArray = jsonObject.getJSONArray(大学官网ResponseJsonConstant.RESULT_NAME);
List<T> jsonConverterObejctList = itemsArray.toJavaList(clazz);
return jsonConverterObejctList;
}
}