欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

模拟浏览器爬取网页

程序员文章站 2022-05-09 21:24:17
...

浏览器向服务器请求网页的过程:
1.浏览器发送请求
2.服务器发回响应
请求头和响应头包含了请求和响应信息,可以用谷歌浏览器在开发者工具中查看。响应代码ResponseCode反映了请求的执行情况

  • ResponseCode为302:需要重定向
  • ResponseCode为200:请求成功

1.用HttpUrlConnection或HtppsUrlConnection建立连接、进行通信。如果请求返回的ResponseCode为302,需要获取响应头中的重定向地址,带上服务器在响应头中设置的Cookie重新访问重定向地址,直到ResponseCode为200正常为止。
2.ResponseCode为200后,可以通过UrlConnection的getInputStream()方法获得输入流,从而获取html源码。
3.ResponseCode为200只表示请求成功,却不一定表示能获得真正的html网页。有些服务器发送的是带有js代码的html,用js代码再设置cookie,并且再次跳转(jump(),window.location="?")。这些js代码不一定是明文的,可能是用eval加密的。在这种情况下,我们可以解密这些js代码,分析它的行为(设置了哪些cookie,又跳转到何处)。然后在程序中模拟这些行为,从而获得真正的html源码。一般来说,一个普通站点的js加密方法和行为是固定的,分析几次后可以知道如何获取关键数据。

以下是代码。

  • getRealHtml()获得真正的html源码,对于直接发送真实数据的站点来说,不需要这一步。
  • 使用HttpsUrlConnection还是HttpURLConnection取决于访问的url地址使用Https协议还是Http协议
  • 调用URLConnection的getResponseCode()或getInputStream()方法后,服务器已经返回响应,本次连接结束
  • URLConnection一定要设置不立即重定向。因为需要在设置cookie后再重定向
import javax.net.ssl.HttpsURLConnection;
import java.io.*;
import java.net.*;
import java.text.SimpleDateFormat;
import java.util.Date;

public class HtmlCatcher {
    //带cookie访问url地址,获取ResponseCode不为302后(即重定向后)的html源码
    //对于初始cookie为空的访问,传入参数cookie为"",字符串空
    public  String getHTmlText(String url,String cookie){
        BufferedReader in = null;
        String result = "";
        try{
            HttpsURLConnection conn=getConnection(url);//获取一个UrlConnection
            int code=0;
            conn.setRequestProperty("Cookie", cookie);//设置请求头的cookie
            while ((code=conn.getResponseCode())==302){
                String redirect=conn.getHeaderField("Location");//获取重定向地址
                String[] str=conn.getHeaderField("Set-Cookie").split(";");//获取响应头设置的cookie
                for(int i=0;i<str.length-1;i++){
                    if(!cookie.contains(str[i]))
                        cookie+=str[i]+"; ";
                }
                conn=getConnection(redirect);
                conn.setRequestProperty("Cookie", cookie);//带上cookie,返回while时访问重定向地址
            }
            //获取html源码
            in = new BufferedReader(new InputStreamReader(conn.getInputStream(),"UTF-8"));
            String line=null;
            while((line = in.readLine())!=null){
                result +="\n"+line;
            }
        }catch(Exception e){
            //异常情况
            e.printStackTrace();
            if(e instanceof UnknownHostException){
                //网络断开,5分钟后重试
                Date date=new Date();
                SimpleDateFormat sf=new SimpleDateFormat("yyyy-mm-dd  HH:mm:ss");
                System.out.println(sf.format(date)+" 网络断开。将在5分钟后重试");
                try {
                    Thread.sleep(300000);
                } catch (InterruptedException interruptedException) {
                    interruptedException.printStackTrace();
                }
                return getHTmlText(url, cookie);
            }else  if(e instanceof ConnectException){
                //连接超时,立即重试
                return getHTmlText(url,cookie);
            }
            return  null;//发生其它异常时返回空
        }finally{
            try{
                if(in!=null){
                    in.close();//关闭资源
                }
            }catch(IOException ex){
                ex.printStackTrace();
            }
        }
        return result;

    }
    //获取向urlStr所示地址的连接
    public HttpsURLConnection getConnection(String urlStr){
        URL url = null;
        HttpsURLConnection conn = null;
        try {
            url = new URL(urlStr);
            conn = (HttpsURLConnection) url.openConnection();
            conn.setRequestProperty("User-Agent", "Chrome/71.0.3578.98 Safari/537.36");//设置客户端
            conn.setRequestProperty("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");//设置文件类型
            conn.setRequestProperty("Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3");//设置语言
            conn.setRequestProperty("Accept-Encoding", "UTF-8");//设置编码
            conn.setRequestProperty("Connection", "keep-alive");
            conn.setRequestProperty("Upgrade-Insecure-Requests", "1");
            conn.setDoOutput(true);
            conn.setDoInput(true);
            conn.setInstanceFollowRedirects(false);//设置不要立即重定向。非常重要!!!!!!
        } catch (IOException e) {
            e.printStackTrace();
        }
        return conn;
    }
    //获取真正的html源码,对于一些站点来说不需要
    public String getRealHtml(String url) {
        String htmlText = getHTmlText(url, "");
        //从第一次获得的htmlText分析出 redirect,cookie value,cookie key
        ...
        //带上cookie再次访问,获取真正的html
        //String html=getHTmlText(redirect,key+"="+value+";path=/;");
        return html;
    }
}

解密js,分析所设置cookie和跳转地址的一个例子:

  • 原html代码,onLoad="javascript:jump()"表示页面加载时调用了jump()方法:
  • js代码原来是一行
<!doctype html><html>
	<head>
		<meta charset="utf-8">
		<meta http-equiv="pragma" content="no-cache">
		<meta http-equiv="cache-control" 
			  content="no-cache, no-store, must-revalidate">
		<meta http-equiv="expires" content="0">
		<script type="text/javascript">
//js原本是一行,可以用菜鸟工具尝试添加换行
eval(
				function(p,a,c,k,e,r){
				e=function(c){
					return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))
				};
				if(!''.replace(/^/,String)){
					while(c--)r[e(c)]=k[c]||e(c);
					k=[function(e){return r[e]}];
					e=function(){return'\\w+'};
					c=1};
				while(c--)
					if(k[c])p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c]);
				return p
			}
			(
			'c 8={2:"t",f:"r"};6 d(){c a=3.2.k(/[a-j-9]{q}(?==)/g);s(a)w(c b=a.7;b--;)3.2=a[b]+"=0;l="+(m n(0)).o()}6 p(a){i 0<3.2.7&&(4=3.2.e(a+"="),-1!=4)?(4=4+a.7+1,5=3.2.e(";",4),-1==5&&(5=3.2.7),u(3.2.v(4,5))):""}6 h(a,b){3.2=a+"="+x(b)+";y=/;"}6 z(){d();h("A",8.2);B.C=8.f};',39,39,'6886868ccdeac86886868ccdeac8cookie6886868ccdeac8document6886868ccdeac8c_start6886868ccdeac8c_end6886868ccdeac8function6886868ccdeac8length6886868ccdeac8data6886868ccdeac86886868ccdeac86886868ccdeac86886868ccdeac8var6886868ccdeac8clearCookie6886868ccdeac8indexOf6886868ccdeac8uri6886868ccdeac86886868ccdeac8setCookie6886868ccdeac8return6886868ccdeac8f06886868ccdeac8match6886868ccdeac8expires6886868ccdeac8new6886868ccdeac8Date6886868ccdeac8toUTCString6886868ccdeac8getCookie6886868ccdeac8326886868ccdeac8https://www.IDontWantToShow.com6886868ccdeac8if6886868ccdeac8dce5c8305cb3140ae798fa328de49a926886868ccdeac8unescape6886868ccdeac8substring6886868ccdeac8for6886868ccdeac8escape6886868ccdeac8path6886868ccdeac8jump6886868ccdeac853dcd339779c66aa32fec592260eda0f6886868ccdeac8window6886868ccdeac8location'.split('6886868ccdeac8'),0,{}
			 )
			)
				</script>
	</head>
	<body onLoad="javascript:jump()">
	</body></html>
  • 解密后的js:
  • 解密方法是在一个解密网页中输入待解密js代码进行解密。解密网页自行百度或见本文最后。
var data={
			cookie:"dce5c8305cb3140ae798fa328de49a92",
			uri:"https://www.IDontWantToShow.com"};
			function clearCookie(){
			var a=document.cookie.match(/[a-f0-9]{32}(?==)/g);
			if(a) 
			 for(var b=a.length;b--;)
			   document.cookie=a[b]+"=0;expires="+(new Date(0)).toUTCString()
			}
			function getCookie(a){
			   return 0<document.cookie.length&&(c_start=document.cookie.indexOf(a+"="),-1!=c_start)?
			   (c_start=c_start+a.length+1,c_end=document.cookie.indexOf(";",c_start),-1==c_end
			   &&(c_end=document.cookie.length),unescape(document.cookie.substring(c_start,c_end))):""}
			function setCookie(a,b){
			   document.cookie=a+"="+escape(b)+";path=/;"
			   }
			function jump(){
			   clearCookie();
			   setCookie("53dcd339779c66aa32fec592260eda0f",data.cookie);
			   window.location=data.uri};

通过解密后的js的jump()方法,可以知道,设置了cookie并且跳转。cookie的名称和其值、以及重定向地址的位置在原js中的位置如下:

'6886868ccdeac86886868ccdeac8cookie6886868ccdeac8document6886868ccdeac8c_start6886868ccdeac8c_end6886868ccdeac8function6886868ccdeac8length6886868ccdeac8data6886868ccdeac86886868ccdeac86886868ccdeac86886868ccdeac8var6886868ccdeac8clearCookie6886868ccdeac8indexOf6886868ccdeac8uri6886868ccdeac86886868ccdeac8setCookie6886868ccdeac8return6886868ccdeac8f06886868ccdeac8match6886868ccdeac8expires6886868ccdeac8new6886868ccdeac8Date6886868ccdeac8toUTCString6886868ccdeac8getCookie6886868ccdeac8326886868ccdeac8
https://www.IDontWantToShow.com  //redirect url
6886868ccdeac8if6886868ccdeac8
dce5c8305cb3140ae798fa328de49a92 //cookie value
6886868ccdeac8unescape6886868ccdeac8substring6886868ccdeac8for6886868ccdeac8escape6886868ccdeac8path6886868ccdeac8jump6886868ccdeac8
53dcd339779c66aa32fec592260eda0f //cookie key
6886868ccdeac8window6886868ccdeac8location'.split('6886868ccdeac8')

经过分析,替换了.split()函数中的字符串后,重定向地址、cookie名称和值的位置是固定的,在程序中不需要解密js就可以获取。经验证,带cookie访问重定向地址获得了真正的、与用浏览器访问时相同的html。

  • 附:解密网页源码。新建一个html文件,粘贴内容保存后打开使用。
<html>
<body>
<script> 
a=62; 
function encode() { 
 var code = document.getElementById('code').value; 
 code = code.replace(/[\r\n]+/g, ''); 
 code = code.replace(/'/g, "\\'"); 
 var tmp = code.match(/\b(\w+)\b/g); 
 tmp.sort(); 
 var dict = []; 
 var i, t = ''; 
 for(var i=0; i<tmp.length; i++) { 
   if(tmp[i] != t) dict.push(t = tmp[i]); 
 } 
 var len = dict.length; 
 var ch; 
 for(i=0; i<len; i++) { 
   ch = num(i); 
   code = code.replace(new RegExp('\\b'+dict[i]+'\\b','g'), ch); 
   if(ch == dict[i]) dict[i] = ''; 
 } 
 document.getElementById('code').value = "eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return'\\\\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\\\\b'+e(c)+'\\\\b','g'),k[c]);return p}(" 
   + "'"+code+"',"+a+","+len+",'"+ dict.join('|')+"'.split('|'),0,{}))"; 
} 

function num(c) { 
 return(c<a?'':num(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36)); 
} 

function run() { 
 eval(document.getElementById('code').value); 
} 

function decode() { 
 var code = document.getElementById('code').value; 
 code = code.replace(/^eval/, ''); 
 document.getElementById('code').value = eval(code); 
} 
</script> 


<textarea id=code cols=80 rows=20> 

</textarea>
 
<input type=button onclick=encode() value="编码"> 
<input type=button onclick=run() value="执行"> 
<input type=button onclick=decode() value="解码">
 </body>
   </html>