Java豆瓣电影爬虫——模拟登录的前世今生与验证码的爱恨情仇

前言

并不是所有的网站都能够敞开心扉让你看个透彻,它们总要给你出些难题让你觉得有些东西是来之不易的,往往,这也更加激发你的激情和斗志! 从《为了媳妇的一张号,我与百度医生杠上了》里就有网友提出可以通过获取cookie的方式来登录,不需要借助selenium这样的模拟浏览器操作了,到后来在公众号里一号友说豆瓣如何实现登录,以及近期园友都有提到想获取更多的网站数据是需要登录的……登录,一直是爬虫界躲不了也绕不开的话题。 之前已经试过通过启动浏览器,模拟人工操作填写用户名和密码并点击登录来完成登录。 这次准备由台前模式切换到幕后,研究下不用启动浏览器如何实现使用后台代码就模拟登录豆瓣电影君。

我们登录网站时发生了什么

类似这样的登录界面(movie.douban.com),我们只要填写用户名和密码,乐意的话选中“下次自动登录”,然后点击登录按钮,不出意外(如果你总是调戏人家,屡次输入错误,人家不会用小锤锤捶你胸,但是搞个验证码也够你喝一壶的了),你就登录成功了。 看着还是比较简单,但是在浏览器后面,实际上已经做了不少事儿~~~ 如果是Chrome浏览器,可以按F12,切到Network选项,在点击页面中的登录后,你可以看到唰唰唰的请求,好比这样

其中最关键的发生在第一条,即通过发送HTTP Post请求与服务器交互,请求登录,在这条请求中你可以看到很熟悉的内容

没错,你在登录网站的时候,实际上是与服务器做了一次通讯,验证成功后,服务器才让你登录网站。

模拟登录

如果网站像上面这样的情况,其实很好登录,比如在postman中填写这些对应的参数就能够成功登录,但是有情操的网站都会有验证码,一般会出现在多次登录失败或者登录网站过于频繁就会出现验证码,好比这样

则对应的post请求如下

这时候我们使用postman实施登录,结果似乎不尽如人意,之所以产生这样的效果时因为这个captcha-id在每次请求的时候都会重新生成,感觉是和验证码绑定的,所以即使你在请求参数中带上了captcha-id也无济于事,因为这次的cookie已经不能使用上次的cookie,直白说就是这次我们又有了一个新的验证码,但是使用的captcha-solution却是上一次的(一张旧船票如何登上进入的新船)。

解决思路

旧船票登不上船是因为日期不对,就是这里的captcha-solution和captcha-id不匹配,所以需要

  • 预先请求获得验证码图片对应的captcha-id
  • 下载这张验证码的图片到本地,识别验证码的内容即captcha-solution
  • 有了今天的船票captcha-solution和今天的船captcha-solution,就可以扬帆起航了

代码

package com.jackie.crawler.doubanmovie.utils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.json.JSONException;
import org.json.JSONObject;
/**
 * Created by Jackie on 2017/3/12.
 */


public class LoginTest {
    private static HttpClient httpClient=new DefaultHttpClient();

    //主登录入口
    public static void loginDouban(){
        String redir="https://www.douban.com/people/your_home_page/";    // 输入你登录成功后要跳转的网页
        String login_src="https://accounts.douban.com/login";
        String form_email="your_username";    // 你的用户名
        String form_password="your_password";    // 你的密码
        String captcha_id=getImgID();
        String login="登录";
        String captcha_solution="";
        System.out.println("请输入验证码:");
        BufferedReader buff=new BufferedReader(new InputStreamReader(System.in));
        try {
            captcha_solution=buff.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }

        //构建参数
        List<NameValuePair> list=new ArrayList<NameValuePair>();
        list.add(new BasicNameValuePair("redir", redir));
        list.add(new BasicNameValuePair("form_email", form_email));
        list.add(new BasicNameValuePair("form_password", form_password));
        list.add(new BasicNameValuePair("captcha-solution", captcha_solution));
        list.add(new BasicNameValuePair("captcha-id", captcha_id));
        list.add(new BasicNameValuePair("login", login));
        HttpPost httpPost = new HttpPost(login_src);
        try {
            httpPost.setEntity(new UrlEncodedFormEntity(list));
            HttpResponse response=httpClient.execute(httpPost);
            HttpEntity entity=response.getEntity();
            String result=EntityUtils.toString(entity,"utf-8");

            HttpGet httpGet=new HttpGet(redir);
            HttpResponse response1=httpClient.execute(httpGet);
            HttpEntity entity1=response1.getEntity();
            String result1=EntityUtils.toString(entity1,"utf-8");
            System.out.println(result1);

        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * 获取验证码图片“token”值
     * @return token
     */
    private static String getImgID(){
        String src="https://www.douban.com/j/misc/captcha";
        HttpGet httpGet=new HttpGet(src);
        String token="";
        try {
            HttpResponse response=httpClient.execute(httpGet);
            HttpEntity entity=response.getEntity();
            String content=EntityUtils.toString(entity,"utf-8");
            Map<String,String> mapList=getResultList(content);
            token=mapList.get("token");
            String url="https:"+mapList.get("url");
            downImg(url);
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return token;
    }
    /**
     * 用JSON 把数据格式化,并生成迭代器,放入Map中返回
     * @param content 请求验证码时服务器返回的数据
     * @return Map集合
     */
    public static Map getResultList(String content){
        Map maplist=new HashMap();

        try {
            JSONObject jo=new JSONObject(content);
            Iterator it = jo.keys();
            String key="";
            String value="";
            while(it.hasNext()){
                key=(String) it.next();
                if ("r".equals(key)) {
                    value = jo.getBoolean(key) + "";
                }else {
                    value=jo.getString(key);
                }
                maplist.put(key, value);
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return maplist;
    }
    /**
     * 此方法是下载验证码图片到本地
     * @param src  给个验证图片完整的地址
     */
    private static void downImg(String src){
        File fileDir=new File("D://爬虫测试/jackie");
        if(!fileDir.exists()){
            fileDir.mkdirs();
        }
        File file=new File("D://爬虫测试/jackie/jackie.png");
        if(file.exists()){
            file.delete();
        }
        InputStream input = null;
        FileOutputStream out= null;
        HttpGet httpGet=new HttpGet(src);
        try {
            HttpResponse response=httpClient.execute(httpGet);
            HttpEntity entity = response.getEntity();
            input = entity.getContent();
            int i=-1;
            byte[] byt=new byte[1024];
            out=new FileOutputStream(file);
            while((i=input.read(byt))!=-1){
                out.write(byt);
            }
            System.out.println("图片下载成功!");
        } catch (ClientProtocolException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        loginDouban();
    }
}

以上代码参考自网友。 执行的结果如下:

由此可见,已经通过模拟登录的方式成功登录网站。

补充

上面的操作还是不够自动化, 主要是验证码这个环节, 需要人工输入下载到本地图片的验证码内容。本来想通过Tesseract-OCR技术来自动识别验证码,但是在分析验证码需要针对性的用到二值化、中值过滤等都遇到了困难,最终还是没能通过这种方式,实现全自动化模拟登录。 但是可以大概介绍下在这方面做的尝试:

  • 下载Tesseract-OCR
  • 安装完成后,可以到安装目录下执行类似tesseract 1.jpg result这样的命令将1.jpg输入通过tesseract处理将识别的讲过存入名为result的txt文件中。
  • 有关验证码的相关代码已经放在项目的ocr目录下,又需要的可以前往GitHub

代码已更新到GitHub

如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!如果您想持续关注我的文章,请扫描二维码,关注JackieZheng的微信公众号,我会将我的文章推送给您,并和您一起分享我日常阅读过的优质文章。(有些闲言碎语我可能会写在公众号)

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏智能大石头

【SmartOS】轻量级多任务调度系统

SmartOS是一个完全由新生命团队设计的嵌入式操作系统,主要应用于智能家居、物联网、工业自动化控制等领域。 ARM Cortex-M系列微处理器几乎全都做成...

431110
来自专栏木宛城主

SharePoint下利用DocX组件导出Word

平常开发时,或多或少都需要和Word打交道,特变是编辑、导出Word。 利用DocX,开源的读写Word组件,可以快速帮助我们进行对Word的操作。 Do...

25750
来自专栏跟着阿笨一起玩NET

一步一步学Linq to sql(七):并发与事务

为了看起来清晰,我已经事先把所有分类为1产品的价格和库存修改为相同值了。然后执行下面的程序:

7120
来自专栏大内老A

事件(Event),绝大多数内存泄漏(Memory Leak)的元凶[上篇]

最近这两天一直在忙着为一个项目检查内存泄漏(Memory Leak)的问题,对相关的知识进行了一下简单的学习和探索,其间也有了一些粗浅的经验积累,今天特意写一篇...

25460
来自专栏码农阿宇

.Net Core中利用TPL(任务并行库)构建Pipeline处理Dataflow

在学习的过程中,看一些一线的技术文档很吃力,而且考虑到国内那些技术牛人英语都不差的,要向他们看齐,所以每天下班都在疯狂地背单词,博客有些日子没有更新了,见谅见谅...

25810
来自专栏魏琼东

一步一步教你使用AgileEAS.NET基础类库进行应用开发-基础篇-UDA中处理事务

前文回顾         在之前的文章一步一步教你使用AgileEAS.NET基础类库进行应用开发-基础篇-使用UDA操纵SQL语句和一步一步教你使用Agile...

241100
来自专栏jeremy的技术点滴

开发小技巧备忘

34570
来自专栏NetCore

利用反射自己写的一个ModelHelper类

开发中 很多人都会使用BLL Model这种开发,我也是,虽然现在有很多的自动生成工具,能在几秒内生成cs的模板,但我个人还不是很喜欢,我还是喜欢自己一个一个去...

29260
来自专栏跟着阿笨一起玩NET

Linq to sql并发与事务

       为了看起来清晰,我已经事先把所有分类为1产品的价格和库存修改为相同值了。然后执行下面的程序:

9120
来自专栏知识分享

51采集PCF8591数据通过ESP8266上传C#上位机android 之TCP客户端编程ESP8266使用详解NodeMCU初探ESP8266刷AT固件与nodemcu固件ESP8266使用详解-

这两天测试程序还发现一个bug就是如果客户端断开了,应该检测一下哪个断开了,数据就不应该发向那个连接,,,否则就会报错,然后模块会复位重启 所以加上这段代码 c...

59650

扫码关注云+社区

领取腾讯云代金券