模拟登录+cookie保持+数据爬取——中国铁塔的爬虫之旅

        先简单交代一下背景:我表哥在铁塔公司做维护工人,一旦铁塔出现故障就会有专人派单然后被派到单的人就需要在一个小时之内接单,否则就算工作失误,会扣钱,但是有一点让人非常不理解,铁塔公司的电脑端虽然有一个单子提醒的功能,但是目前看来就是一个摆设,设置了也不会起到任何作用,换句话说,如果你想知道自己有没有单子可以接,就必须自己手动定时刷新,坐在电脑旁边天天点鼠标玩,这是一件非常痛苦的事情,而且有很多单子会在晚上出来,明摆着就是想让你接不到单子扣工资嘛。所以这就找上我了。。。

        那么我所需要做的就是模拟登录进入系统,然后查询有没有单子可以接,有的话提醒表哥,需求听着很简单,但是毕竟中国铁塔,爬这个网站还是费了一番功夫。

        话不多说,进入正题,首先我要到了网站的登陆地址:中国铁塔维护系统 ,用户名以及密码

        然后进入登录页面,开启f12,如下

        

        比较不幸,有验证码,不过问题不大,先看看验证码的验证机制是怎么样的,边输验证码边查看network

        

        额,很明显了,验证码的输入框绑定了onchange事件,每次发生变化都会请求后台,后来我查看了response响应体,发现验证码输入正确会返回1,然后出现 中国铁塔,欢迎您 的字样,错误会返回0。至于验证码获取就是一个固定的地址,返回不同的图片,查看页面元素可以看的很清楚,在这里就不多说了。

        到了这一步,已经明了了自己第一步需要完成的工作 获取验证码图片-》解析验证码-》检查验证码-》获取响应结果

        话不多说,上代码,代码写的比较赶,没怎么注意规范。。。在这里是直接把验证码下载到桌面肉眼识别然后手输的,也可以使用tess4j,识别率还可以

        HttpGet getCheckCode = new HttpGet(getCheckCodeUrl);
		CloseableHttpResponse responseGet = null;

		String desktopDir =               FileSystemView.getFileSystemView().getHomeDirectory().getAbsolutePath();
		File imageFile = new File(desktopDir, "checkCode.jpg");

		try {

			responseGet = httpClient.execute(getCheckCode);

			FileOutputStream outputStream = new FileOutputStream(imageFile);
			HttpEntity entity = responseGet.getEntity();
			InputStream inputStream = entity.getContent();
			byte[] b = new byte[1024];
			int i = 0;
			while ((i = inputStream.read(b)) != -1) {
				outputStream.write(b, 0, i);
			}

			outputStream.flush();
			outputStream.close();
			EntityUtils.consume(entity);
			System.out.println("验证码获取成功,已经下载到桌面,请查看并输入验证码:");

		} catch (Exception e) {

			System.out.println("获取验证码失败!请重新运行程序!");
			return;

		} finally {
			try {
				responseGet.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		Scanner scanner = new Scanner(System.in);

		String checkCode = scanner.next();

		HttpGet checkCodeGet = new HttpGet(checkCodeUrl + checkCode);
		checkCodeGet.addHeader("Accept", "text/plain, */*; q=0.01");
		checkCodeGet.addHeader("Referer", loginUrl);
		checkCodeGet.addHeader("User-Agent",
				"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36");
		checkCodeGet.addHeader("X-Requested-With", "XMLHttpRequest");
		CloseableHttpResponse responseCheckCode = null;

		try {
			responseCheckCode = httpClient.execute(checkCodeGet);

			HttpEntity entity = responseCheckCode.getEntity();

			String codeStatus = EntityUtils.toString(entity);

			// 如果为1 则说明验证码正确,否则错误
			if (!"1".equals(codeStatus)) {
				System.out.println("验证码解析失败!请重新运行");
				return;
			}
			System.out.println("验证码解析成功!");

			EntityUtils.consume(entity);

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				responseCheckCode.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

运行效果

j经过测试,没有问题,现在可以进行模拟登录了,首先在网站进行登录,查看network的请求

好吧,居然是302重定向,猜想应该是请求带了问号后面的东西导致的重定向,去掉之后再次登录,发现状态码变成了200,ok,就用它了,如下

查看一下请求体,发现是form-data,此外发现除了用户名和密码两个参数之外,还有其他三个参数

最后经过调试,发现这几个参数在访问登录页面的时候在页面上会动态生成到隐藏域中一起传到后台,那我们也要首先访问一下获取这几个动态参数,否则登录一直是失败的。

上代码

        // 全局请求设置
		RequestConfig globalConfig = RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).setSocketTimeout(50000)
				.setConnectTimeout(50000).setConnectionRequestTimeout(50000).build();
		// 创建cookie store的本地实例
		CookieStore cookieStore = new BasicCookieStore();

		// 创建HttpClient上下文
		HttpClientContext context = HttpClientContext.create();
		context.setCookieStore(cookieStore);

		// 创建一个HttpClient
		CloseableHttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(globalConfig)
				.setDefaultCookieStore(cookieStore).build();

		CloseableHttpResponse response = null;

		String lt = "";
		String execution = "";
		String _eventId = "";

		// 先访问一下登录页面
		HttpGet getLoginPage = new HttpGet(loginUrl);
		try {
			response = httpClient.execute(getLoginPage);
			HttpEntity entity = response.getEntity();

			System.out.println("获取登录所需参数中...");
			String str = EntityUtils.toString(entity);
			lt = regex("\"lt\" value=\"([^\"]*)\"", str)[0];
			execution = regex("\"execution\" value=\"([^\"]*)\"", str)[0];
			_eventId = regex("\"_eventId\" value=\"([^\"]*)\"", str)[0];

			EntityUtils.consume(entity);
		} catch (Exception e1) {
			e1.printStackTrace();
		} finally {
			try {
				response.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

    /**
	 * 通过正则表达式获取内容
	 * 
	 * @param regex 正则表达式
	 * @param from  原字符串
	 * @return
	 */
	public static String[] regex(String regex, String from) {
		Pattern pattern = Pattern.compile(regex);
		Matcher matcher = pattern.matcher(from);
		List<String> results = new ArrayList<String>();
		while (matcher.find()) {
			for (int i = 0; i < matcher.groupCount(); i++) {
				results.add(matcher.group(i + 1));
			}
		}
		return results.toArray(new String[] {});
	}

regex方法使用正则表达式可以帮助解析出页面对应参数的值,然后进行模拟登录,上代码

		CloseableHttpResponse responseLogin = null;
		HttpPost httppost = new HttpPost(loginUrl); // 登录地址
		List<NameValuePair> nvps = new ArrayList<NameValuePair>();
		nvps.add(new BasicNameValuePair("username", "用户名"));
		nvps.add(new BasicNameValuePair("password", "密码"));
		nvps.add(new BasicNameValuePair("lt", lt));
		nvps.add(new BasicNameValuePair("execution", execution));
		nvps.add(new BasicNameValuePair("_eventId", _eventId));
		nvps.add(new BasicNameValuePair("submit", "登录"));
		httppost.addHeader("Accept",
				"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3");
		httppost.addHeader("Accept-Encoding", "gzip, deflate");
		httppost.addHeader("Connection", "keep-alive");
		httppost.addHeader("Host", "180.153.49.81:18989");
		httppost.addHeader("Origin", "http://180.153.49.81:18989");
		httppost.addHeader("Content-Type", "application/x-www-form-urlencoded");
		httppost.addHeader("Referer", loginUrl);
		httppost.addHeader("Upgrade-Insecure-Requests", "1");
		httppost.addHeader("User-Agent",
				"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36");
		HttpEntity reqEntity = new UrlEncodedFormEntity(nvps, Consts.UTF_8);
		httppost.setEntity(reqEntity);
		try {
			responseLogin = httpClient.execute(httppost);
			// 设置响应码,后面用
			int statusCode = responseLogin.getStatusLine().getStatusCode();

			if (statusCode != 200) {
				System.out.println("登录失败!请重新运行程序,如多次失败,请联系作者!");
				return;
			}
			System.out.println("登录成功,即将自动监控工单...");
			System.out.println(EntityUtils.toString(responseLogin.getEntity()));
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				responseLogin.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

运行,查看结果

非常感动,模拟登录成功!接下来访问查询界面,查询界面的url试了好一会,找了大半天,发现访问查询界面cookie会发生变化,所以直接访问不可取,查看其他请求,发现有一个请求是这样的:http://180.153.49.81:18989/SSO/login?service=http%3A%2F%2F180.153.49.216%3A9000%2Flayout%2Findex.xhtml

原来必须在请求路径后面加上查询的url才可以获取最新的cookie

shit,原来跨域了。。。访问之后获取到最新的cookie,然后带着最新的cookie去访问真正的查询界面,最后终于成功了

上代码,两分钟一刷,在这里是判断页面包不包含字符串来判断有没有单子,实际换上表哥的名字就行了,没有对页面进行深层解析,如果解析页面,可以使用jsoup,可以像原生js一样解析html,非常容易上手。值得一提的是,获取到最新的cookie之后,发现原来的cookie还在,需要去除之前的cookie,并且gc回收的时候cookiestore中的cookie有被回收的风险,需要在本地维护一份变量才行。

        List<Cookie> cookies = context.getCookieStore().getCookies();

		/*
		 * for (Cookie c : cookies) {
		 * 
		 * cookie += c.getName() + "=" + c.getValue() + "; "; }
		 */

		// System.out.println(cookie);

		CloseableHttpResponse responseQueryWork = null;
		HttpGet httpGetQuery = new HttpGet(queryWorkUrl); // 查询地址

		String viewState = "";
		try {

			responseQueryWork = httpClient.execute(httpGetQuery);

			HttpEntity entity = responseQueryWork.getEntity();
			String str = EntityUtils.toString(entity);
			// System.out.println(str);
			viewState = regex("\"javax.faces.ViewState\" value=\"([^\"]*)\"", str)[0];
			// System.out.println(viewState);

            //获取现在的cookie
			List<Cookie> lastcookies = context.getCookieStore().getCookies();
            //移除之前的cookie
			for (Cookie c : cookies) {
				lastcookies.remove(c);
			}

			// System.out.println(lastcookies);

            //清空cookie
			context.getCookieStore().clear();
            //设置最新的cookie
			for (Cookie c : lastcookies) {
				context.getCookieStore().addCookie(c);

				// 保存到当前的cookie 避免gc引起cookie丢失
                //savedCookies 为全局静态变量
				savedCookies.add(c);
			}

			/*
			 * String lastCookie = ""; for (Cookie c :
			 * context.getCookieStore().getCookies()) {
			 * 
			 * lastCookie += c.getName() + "=" + c.getValue() + "; "; }
			 */

			// System.out.println(lastCookie);

			EntityUtils.consume(entity);

		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				responseQueryWork.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}

		String lastViewState = viewState;

		Timer checkTimer = new Timer();

		System.out.println("==开始监控工单,请一定不要关闭窗口,如有工单会有音乐提醒!!两分钟一刷,尽量把电脑音量调大,以免听不见!!==");
		// 两分钟一刷
		checkTimer.schedule(new TimerTask() {

			@Override
			public void run() {

				// 判断当前的cookie是否被回收 如果被回收,把之前储存的cookie加入
				if (context.getCookieStore().getCookies().size() == 0) {
					for (Cookie c : savedCookies) {
						context.getCookieStore().addCookie(c);
					}
				}

				CloseableHttpResponse lastQueryResponse = null;
				HttpPost lastPost = new HttpPost(lastQueryUrl); // 查询地址
				List<NameValuePair> lastnvps = new ArrayList<NameValuePair>();
				lastnvps.add(new BasicNameValuePair("AJAXREQUEST", "_viewRoot"));
				lastnvps.add(new BasicNameValuePair("queryForm", "queryForm"));
				lastnvps.add(new BasicNameValuePair("queryForm:msg", "0"));
				lastnvps.add(new BasicNameValuePair("queryForm:queryBillId", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:queryBillSn", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:isQueryHis", "N"));
				lastnvps.add(new BasicNameValuePair("queryForm:queryStationId", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:deviceidText", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:addOrEditAreaNameId", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:aid", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:queryUnitId", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:j_id48", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:queryDWCompany", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:queryDWCompanyName", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:queryAlarmId", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:queryAlarmName", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:j_id58", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:starttimeInputDate", "2019-06-20 15:00"));
				lastnvps.add(new BasicNameValuePair("queryForm:starttimeInputCurrentDate", "06/2019"));
				lastnvps.add(new BasicNameValuePair("queryForm:endtimeInputDate", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:endtimeInputCurrentDate", "06/2019"));
				lastnvps.add(new BasicNameValuePair("queryForm:revertstarttimeInputDate", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:revertstarttimeInputCurrentDate", "06/2019"));
				lastnvps.add(new BasicNameValuePair("queryForm:revertendtimeInputDate", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:revertendtimeInputCurrentDate", "06/2019"));
				lastnvps.add(new BasicNameValuePair("queryForm:dealstarttimeInputDate", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:dealstarttimeInputCurrentDate", "06/2019"));
				lastnvps.add(new BasicNameValuePair("queryForm:dealendtimeInputDate", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:dealendtimeInputCurrentDate", "06/2019"));
				lastnvps.add(new BasicNameValuePair("queryForm:sitesource_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:querystationstatus_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:billStatus_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:faultSrc_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:isHasten_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:alarmlevel_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:faultDevType_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:isOverTime_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:isReplyOver_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:subOperatorHid_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:operatorLevel_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:turnSend_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:sortSelect_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:faultTypeId_hiddenValue", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:queryCrewVillageId", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:hideFlag", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:queryCrewVillageName", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:refreshTime", ""));
				lastnvps.add(new BasicNameValuePair("queryForm:panelOpenedState", ""));
				lastnvps.add(new BasicNameValuePair("javax.faces.ViewState", lastViewState));
				lastnvps.add(new BasicNameValuePair("queryForm:j_id133", "queryForm:j_id133"));
				lastnvps.add(new BasicNameValuePair("AJAX:EVENTS_COUNT", "1"));
				lastPost.addHeader("Content-Type", "application/x-www-form-urlencoded");
				lastPost.addHeader("Referer", lastQueryUrl);
				HttpEntity reqEntityQuery = new UrlEncodedFormEntity(lastnvps, Consts.UTF_8);
				lastPost.setEntity(reqEntityQuery);

				try {
					lastQueryResponse = httpClient.execute(lastPost);

					// System.out.println(lastQueryResponse.getStatusLine().getStatusCode());

					HttpEntity lastEntity = lastQueryResponse.getEntity();

					// System.out.println(EntityUtils.toString(lastEntity));

					String lastStr = EntityUtils.toString(lastEntity);

                    //如果查询成功,页面必然有包站人这几个字
					if (lastStr.contains("包站人")) {

						System.out.println("有单子来了!!快去接单吧!!音乐连续放三次会自动停止。。进入下轮检查!");
						// 连续放3次
						/*
						 * for (int i = 0; i < 3; i++) { playMusic(); }
						 */

					}


					EntityUtils.consume(lastEntity);

				} catch (IOException e) {
					e.printStackTrace();
				} finally {
					try {
						lastQueryResponse.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}

			}
		}, 0, 120000);

public static void playMusic() {
		try {
			String desktopDir = FileSystemView.getFileSystemView().getHomeDirectory().getAbsolutePath();
			FileInputStream inputStream = new FileInputStream(desktopDir + "\\tip.mp3");
			Player player = new Player(new BufferedInputStream(inputStream));
			player.play();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

总体运行效果:

大功告成,其实可以做成图形界面,但是有点麻烦,这样已经足够了,哈哈,打个jar包写个bat双击运行即可。

发布了26 篇原创文章 · 获赞 99 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/m0_37719874/article/details/93461347
今日推荐