美文网首页测试开发栈程序员
Java爬虫实战—爬取某网盘技术类PDF电子书

Java爬虫实战—爬取某网盘技术类PDF电子书

作者: 测试开发栈 | 来源:发表于2018-06-28 09:15 被阅读164次

    背景

    背景是这样的:前2天在网上搜技术类电子书,结果发现CSDN某博客更新了大量技术类PDF电子书(链接在这里程序员成长思路-电子书),考虑到他这个应该是为网盘导流,文件有可能是临时存储的,所以保险起见得下到自己本地来,常规下载如下图,感觉操作和跳转步骤太多,懒筋作祟,于是想怎么不写个爬虫把它全搞下来!

    分析页面

    在CSDN博客页面,查看跳转及网络请求,没有发现什么可利用的点,于是转到网盘下载页面,看下页面HTML结构如下图:



    对于分析页面我就关注下面两个点:

    1. 下载链接的本身,发现是通过一个点击事件去处理下载逻辑的,应该是通过传入的参数生成了对应的文件下载链接以及权限验证等,搜了一下那个free_down()函数,在所有暴露的js里面都木有找到(js被混淆压缩处理了),所以破解free_down函数的方法只能暂时放弃了。
    2. 页面其他可利用的链接,发现有导航链接,期望可以通过导航链接能找到一个汇总的下面home页面,在尝试到第二个导航链接的时候,终于发现有用的东西了,这是用户在网盘上的上传文件汇总页面,如下图所示:



      看上图,发现有批量下载和打包下载两个按钮,点进去发现都是要注册改网盘用户后才可以使用的,其实正常情况下注册后打包下载就结束了,可是今天是要将技术实现的,所以不想整这个注册,到这个页面后,我只需要拿到这些文件的下载链接就行。可是链接点进去还是回到前面的选择下载页面,看来还是得读到那个free_down()函数才行,下面给大家介绍一个简单的前端调试方法,使用chrome浏览器,按下F12进入开发者控制台,进入sources页签,找到这个页面,找到你要调试的位置,在位置对应的代码左侧的行号上点一下即可(打上debug标识的行会有一个蓝色的标志):



      然后按F9一步步调试,然后就会跳到相关js中执行,调试发现那个js文件默认是blackboxed in debugger,难怪前端找不到,哈哈,这样就发现这个函数了,就这么简单,把那个函数提取出来:
    function free_down(file_id, folder_id, file_chk, mb, app, verifycode) {
        verifycode = typeof verifycode !== 'undefined' ? verifycode: "";
        $.getJSON("/get_file_url.php?uid=" + userid + "&fid=" + file_id + "&folder_id=" + folder_id + "&fid=" + file_id + "&file_chk=" + file_chk + "&mb=" + mb + "&app=" + app + "&verifycode=" + verifycode + '&rd=' + Math.random(),
        function(data) {
            if (data.code == 503) {
                if (mb) {
                    window.location = "/iajax_guest.php?item=file_act&action=verifycode&uid=" + userid + "&fid=" + file_id + "&folder_id=" + folder_id + "&fid=" + file_id + "&file_chk=" + file_chk + "&mb=" + mb + "&app=" + app;
                } else {
                    $(ctmodal).load("/iajax_guest.php?item=file_act&action=verifycode&uid=" + userid + "&fid=" + file_id + "&folder_id=" + folder_id + "&fid=" + file_id + "&file_chk=" + file_chk + "&mb=" + mb + "&app=" + app).modal().draggable();
                }
            }
            if (data.code == 200) {
                if ($("#clickcount_log").size() > 0 && $("#clickcount_log").attr("src").length > 50) {
                    $("#clickcount_log").attr("src", data.confirm_url);
                }
                if (app) {
                    if (!mb) {
                        $(ctmodal).load("/iajax_guest.php?item=file_act&action=xt_downlink&xtlink=" + data.xt_link).modal().draggable();
                    } else {
                        window.location.href = "ctfile://inapp.ctfile.com/app.php?||xt||" + data.xt_link;
                        $("#xtlink_input").val(data.xt_link);
                        $("#xtlink_copy_alert").show();
                    }
                } else {
                    if (!mb) {
                        setTimeout(function() {
                            free_vip_upgrade(data.file_size)
                        },
                        1000);
                        window.location.href = data.downurl + "&mtd=1";
                    } else {
                        window.location.href = data.downurl;
                    }
                }
            }
        });
    }
    

    先读懂它:根据请求参数请求get_file_url接口,会返回一个json数据,我们只需要关注返回code=200的情况,其他都属于异常情况,在code=200的逻辑里面看到了最后就是按downurl跳转的,那么我们拿到json里面的downurl即可……下面再把参数理一下:
    file_id 文件id,页面上能拿到, folder_id 文件夹id调试发现传0就可以了, app很好理解吧,是不是在网盘客户端发起的请求,我们用httpclient来请求肯定不是了,所以app=0(注:0=false,1=true),同理,mb表示mobile,是否移动端,很明显也是0,verifycode应该是登录信息,忽视它,最后说一下file_chk,这个应该是一个文件检查码,表面看可能是md5可是验证并不是,怀疑应该是按一定规则生成的,在选择下载页面就已经生成了,估计是页面渲染的时候就在js中生成了,这不太好调试,又来一个坑。。。。暂时只能再请求一次,再请求一次到下载页面拿到这个file_chk。

    实现步骤:

    根据上面的分析,实现步骤渐渐明晰了:

    1. 解析用户的上传文件汇总页面,拿到所有的电子书下载页url;
    2. 请求下载页url,拿到对应的file_chk;
    3. 根据参数组合最终的下载url;
    4. 请求url,将返回数据流写入磁盘文件系统;
      思路有了,下面编码实现,使用HttpClinet来处理请求、Jsoup来解析页面、FastJson来解析json,先贴上对应Maven的dependency:
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpclient</artifactId>
        <version>4.5.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.httpcomponents</groupId>
        <artifactId>httpmime</artifactId>
        <version>4.5.2</version>
        <exclusions>
            <exclusion>
                <groupId>org.apache.httpcomponents</groupId>
                <artifactId>httpclient</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.4</version>
    </dependency>
    
    <dependency>
        <groupId>org.jsoup</groupId>
        <artifactId>jsoup</artifactId>
        <version>1.10.3</version>
    </dependency>
    

    实现第一个步骤:
    解析用户的上传文件汇总页面(这里用了界面上的一个数据分页接口),拿到所有的电子书下载页url,并存入Map中。

    private Map<String, String> getDownloadPageUrls() throws Exception {
            Map<String, String> urlMap = new HashMap<String, String>();
            String urlFormat = "https://page74.ctfile.com/iajax_guest.php?item=file_act&action=file_list&task=file_list&folder_id=21645009&uid=" + UID + "&display_subfolders=1&t=1529909656&k=3de0d7f0ad5df1e6d6b71c4b43ce10d6&sEcho=%d&iColumns=4&sColumns=&iDisplayStart=%d&iDisplayLength=%d";
            int page = 1;
            int pageSize = 100;
            int pageStart = 1;
            int pageCount = pageSize;
            while (true) {
                String url = String.format(urlFormat, page, pageStart, pageSize);
                HttpResponse response = new HttpRequest(url).doGet();
                String responseBody = EntityUtils.toString(response.getEntity());
                JSONObject data = JSONObject.parseObject(responseBody);
                pageCount = data.getInteger("iTotalRecords");
    
                JSONArray dataList = data.getJSONArray("aaData");
                Document doc = null;
                for (int i = 0; i < dataList.size(); i++) {
                    JSONArray item = dataList.getJSONArray(i);
                    doc = Jsoup.parse(item.get(0).toString());
                    Element tdId = doc.select("#file_ids").first();
                    String id = tdId.attr("value");
    
                    doc = Jsoup.parse(item.get(1).toString());
                    Element tdUrl = doc.select("a[href]").first();
                    String url1 = tdUrl.attr("href");
    
                    if (StringUtils.isNotEmpty(id) && StringUtils.isNotEmpty(url1)) {
                        url1 = MAIN_HOST + url1.replace("//", "/5a8841/");
                        urlMap.put(id, url1);
                    }
                }
                if (pageSize + pageStart > pageCount) {
                    break;
                }
                page++;
                pageStart += pageSize;
            }
            System.out.println("have fetched download page url size :" + urlMap.size());
            return urlMap;
        }
    

    实现第二个步骤:
    请求下载页url,拿到对应的file_chk,这里请求的时候要加上header,不然会被当做非法请求。

    private String getFileChk(String url) throws IOException {
            Map<String, Object> headers = new HashMap<String, Object>();
            headers.put("Host", new URL(url).getHost());
            headers.put("Cookie", "PHPSESSID=eb2vqs2s7pgjkuuvqrq2aslov4; clicktopay=" + System.currentTimeMillis() + "; unique_id=5a8841; ua_checkmutilogin=OoDN6nCiRd;");
            headers.put("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36");
            HttpResponse response = new HttpRequest(url).setHeaders(headers).doGet();
            String responseBody = EntityUtils.toString(response.getEntity());
    
            String chkStr = "";
            Document doc = Jsoup.parse(responseBody);
            Element link = doc.select("#free_down_link").first();
            if (link != null) {
                String text = link.attr("onclick");
                if (StringUtils.isNotEmpty(text)) {
                    text = text.substring(text.indexOf("("), text.indexOf(")"));
                    String[] arr = text.split(",");
                    chkStr = arr[2];
                    chkStr = chkStr.replaceAll("'", "").trim();
                }
            }
            return chkStr;
        }
    

    实现第三个步骤:
    根据参数组合最终的下载url:

    private List<String> getDownloadUrls() throws Exception {
            List<String> urls = new ArrayList<String>();
            Map<String, String> urlMap = getDownloadPageUrls();
            int rndCode = new Random(1000).nextInt() + 100;
            String urlFormat = "https://page74.ctfile.com/get_file_url.php?uid=" + UID + "&fid=%s&folder_id=0&file_chk=%s&mb=0&app=0&verifycode=&rd=" + rndCode;
            for (Map.Entry<String, String> entry : urlMap.entrySet()) {
                String fid = entry.getKey();
                String file_chk = getFileChk(entry.getValue());
    
                if (StringUtils.isNotEmpty(file_chk)) {
                    String url = String.format(urlFormat, fid, file_chk);
                    HttpResponse response = new HttpRequest(url).doGet();
                    String responseBody = EntityUtils.toString(response.getEntity());
    
                    JSONObject data = JSONObject.parseObject(responseBody);
                    String downurl = data.getString("downurl");
                    if (StringUtils.isNotEmpty(downurl)) {
                        urls.add(downurl);
                    }
                }
            }
            return urls;
        }
    

    实现第四个步骤:
    请求url,并将返回的数据流写入磁盘文件系统,刚开始我是采用多线程的处理方式,因为毕竟下载一个文件要几分钟时间,可是跑起来发现,普通下载只给支持单线程,网盘服务端限制了,暂时改成单线程处理咯(花时间钻研下,应该可以破了它)

    @Test
    public void crawlAll1() throws Exception {
        List<String> urls = getDownloadUrls();
        for (String url : urls) {
            String name = getFileName(url);
            try {
                System.out.println("[Begin] task is begin to run with download file: " + name);
                HttpRequest.doDownload(DATA_PATH, url);
                System.out.println("[End] task have finished to run with download file: " + name);
            } catch (Exception e) {
                System.err.println("[Error] task meet error when process url: " + url);
                e.printStackTrace();
            }
        }
    }
    

    最后还要贴一下doDownload()方法:

    public static void doDownload(String path, String url) throws Exception {
        HttpClient httpClient = getSSLHttpClient();
        URL  Url = new URL(url);
        String paths = Url.getPath();
        String fileName = "";
        for (String param : paths.split("/")) {
            if (param.toLowerCase().endsWith(".pdf")) {
                fileName = URLDecoder.decode(param, "UTF-8");
                break;
            }
        }
        fileName = path + fileName;
    
        File file = new File(fileName);
        if(file.exists()) {
            file.delete();
        }
        try {
            //使用file来写入本地数据
            file.createNewFile();
            FileOutputStream outStream = new FileOutputStream(fileName);
    
            //执行请求,获得响应
            HttpResponse httpResponse = httpClient.execute(new HttpGet(url), new BasicHttpContext());
            int code = httpResponse.getStatusLine().getStatusCode();
            System.out.println("[DOWNLOADING STATUS] get response status [" + httpResponse.getStatusLine() + "] for file :" + file.getName());
            if (code == 200) {
                HttpEntity httpEntity = httpResponse.getEntity();
                InputStream inStream = httpEntity.getContent();
                while (true) {//这个循环读取网络数据,写入本地文件
                    byte[] bytes = new byte[1024 * 1024]; //1M
                    int k = inStream.read(bytes);
                    if (k >= 0) {
                        outStream.write(bytes, 0, k);
                        outStream.flush();
                    } else break;
                }
                inStream.close();
                outStream.close();
            }
        } catch (IOException e){
            e.printStackTrace();
        }
    }
    

    跑起来,效果如下(没有多线程下载,就是慢了点点):



    PS:完整代码(包含多线程下载的实现),已上传到github的demo项目下:https://github.com/AlanYangs/demo/tree/master/pdf-spider,欢迎大家start~

    原文来自下方公众号,转载请联系作者,并务必保留出处。
    想第一时间看到更多原创技术好文和资料,请关注公众号:测试开发栈

    相关文章

      网友评论

        本文标题:Java爬虫实战—爬取某网盘技术类PDF电子书

        本文链接:https://www.haomeiwen.com/subject/bbieyftx.html