利用Selenium实现交互式WebService接口 --处理验证码

OverView


写于2017年7月25日,仅从 我的其他博客转移至此,不做维护,如有不当之处,请包含!

  作者现在需要爬取一个某政务型 查询网站,实现WebService接口。本文为了方便叙述,把接口和调用接口的业务系统合并---统称为系统,如果不懂,忽略此句
  简要概括就是:

  1. 系统填一个表单、并将验证码交给用户识别
  2. 用户填完验证码提交后系统继续填写页面表单
  3. 最后将抓取的结果返回给用户。

为了方便大家理解,画流程图如下:

flowchat
st=>start: 客户:扫一扫
op=>operation: 客户:数据JSON上传
op1=>operation: 系统:接收并填写号码
cond1=>condition: 网站:判断号码
cond2=>condition: 系统:表单提交
op2=>operation: 网站:显示验证码
op3=>operation: 系统:验证码发送客户端
op4=>operation: 客户:填写验证码
e=>end: 用户:获取结果
st->op->op1(right)->cond1(yes)->op2
op2(right)->op3->op4->cond2(yes)->e
cond1(no)->op
cond2(no)->op4

一、准备

1、网站交互分析(F12)

  大致了解需求后就是开发的过程,首先你做的就是网页交互分析,请按F12,在Network(网络)那一栏观察分析,务必!!!

如果能直接找到URL接口的,千万不要用Selenium方案!

F12

以此政务网站为例,第一、第三行就系统交互:先填写发票代码,网页JS验证代码并Ajax获取验证码。这个验证码是不同颜色字符组成的,填写时会让你选择其中部分颜色的字,提交表单时这里的加密也需要提交。(PS:所以这里我们基本可以判定,必须Selenium)。填写完验证码和其他数据后,我们便可以点击查验。

flowchat
op1=>operation: 填写发票代码
op2=>operation: 显示验证码
op3=>operation: 填写其他数据+验证码
op4=>operation: 查验
op1(right)->op2(right)->op3(right)->op4

为了加速填写,表单填写采用了复制粘贴。可恰巧的是这个网站表单的个别box有限制粘贴,故采用粘贴与键盘输入结合。

2、系统分析

  结合一开始的需求分析,WebService设计如下:


网站结构

  相比较以往抓取,WebService最大的问题就是将原本线性过程转变成分步的交互过程。所以必须暂存drive,暂存你的操作过程。**

  • 线性的抓取过程:写好代码逻辑,一步一步执行抓取并返回结果。
  • Http 交互过程 :一个http请求完成一次逻辑并返回结果,每一次请求都是一次执行。

所以,我们必须将原来的过程断开,在每一次请求时分步执行。

Tips:为了提高响应速度,当验证码发送给用户填写时,后台已经在异步执行其他数据的填写操作了。当用户填好验证码发送至系统时,系统只需补充验证码便可以确认查验了。

二、系统搭建

环境准备

  技术实现采用的Selenium+Java Servlet,目录如下:Tips:Springboot好像不能操作系统的剪切板,故而简单的来。

目录结构

  • 关于selenium+Java基本准备,可以参照此篇文档:Selenium+Java基础 --此处默认大家都熟悉Selenium+Java环境搭建。

三、Coding

1、工具类PageUtil

/**
 * 
 * @author minzhou<mingxuan.zhou@outlook.com>
 * @version 1.0
 * Time 20170703
 */
public class PageUtils {
    /**
     * 获取IE的WebDriver
     *
     * @param page
     * @return
     */
    public static WebDriver getWebDriver(String url) {
        System.setProperty("webdriver.ie.driver", "D:/Develop/WebCrawler/IEDriverServer_x64_3.4.0/IEDriverServer.exe");
        DesiredCapabilities ieCapabilities = DesiredCapabilities.internetExplorer();
        ieCapabilities.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS,true);
        WebDriver driver = new InternetExplorerDriver();
        
        driver.get(url);
        return driver;
    }
    /**
     * 复制粘贴的两种方案,推荐第一个
     * @param driver
     * @param str
     */
    public static void CtrlC_V(WebDriver driver,String str) {
        StringSelection stringSelection = new StringSelection(str);
        Toolkit.getDefaultToolkit().getSystemClipboard()
                .setContents(stringSelection, null);
        new Actions(driver).sendKeys(Keys.chord(Keys.CONTROL+"v")).perform();
    }
    public static void keyOperation(String str){
        StringSelection stringSelection = new StringSelection(str);
        Toolkit.getDefaultToolkit().getSystemClipboard()
                .setContents(stringSelection, null);
        //系统级粘贴
        Robot robot = null;
        try {
            robot = new Robot();
        } catch (AWTException e1) {
            e1.printStackTrace();
        }
        robot.keyPress(KeyEvent.VK_CONTROL);
        robot.keyPress(KeyEvent.VK_V);
        robot.keyRelease(KeyEvent.VK_V);
        robot.keyRelease(KeyEvent.VK_CONTROL);
    }
}

2、获取验证码ImageServlet

  原始图片的静态jpg文件,刷新后的图片为base64,依此,我们可以根据src的变化判断是否出现验证码,并在验证码出现后获取src,截取相应部分并将base64编码的图片的String以及提示信息发送到客户端即可。

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        HttpSession session=request.getSession();
        WebDriver driver = null;
        
        Invoice invoice=new Invoice();
        invoice.setInvoiceCode(request.getParameter("invoiceCode"));
        invoice.setInvoiceCheckSum(request.getParameter("invoiceCheckSum"));
        invoice.setInvoiceDate(request.getParameter("invoiceDate"));
        invoice.setInvoiceNum(request.getParameter("invoiceNum"));
        
        String url="https://inv-veri.chinatax.gov.cn/";
        //Use session storage drive
        if (session.getAttribute("driver") == null) {
            driver = PageUtils.getWebDriver(url);
            session.setAttribute("driver", driver);//首次访问时,新建dirve对象
        }else {
            driver =(WebDriver) session.getAttribute("driver");//非首次打开时,获取对象
            if (driver.getCurrentUrl()==url) {
                driver.navigate().refresh();//URL为目标页,刷新
            } else {
                driver.navigate().to(url);//不是目标页导航至目标页
            }
        }
        //InvoiceCode==发票代码
        WebElement invoiceCodeInput = driver.findElement(By.id("fpdm"));
        new Actions(driver).click(invoiceCodeInput).perform();
        //new Actions(driver).moveToElement(invoiceCodeInput).click().perform();
        //PageUtils.keyOperation(invoice.getInvoiceCode());
        PageUtils.CtrlC_V(driver, invoice.getInvoiceCode());
        
        //waiting for verification information
        try {
            new WebDriverWait(driver, 500).until(new ExpectedCondition<Boolean>(){
                            public Boolean apply(WebDriver d){
                                return d.findElement(By.id("yzm_img"))
                                        .getAttribute("src")
                                        .contains("base64");
                            }
                        });
        } catch (Exception e) {
        }
        WebElement codeImage = driver.findElement(By.id("yzm_img"));
        WebElement codeTips = driver.findElement(By.id("yzminfo"));
        
        
        try {
            request.setAttribute("codeImage", codeImage);
            request.setAttribute("codeTips", codeTips);
            /*
            // convert to json
            Gson gson=new Gson();
            Map<String,String> map=new HashMap<>();
            map.put("codeImage", codeImage.getAttribute("src")+"");
            map.put("codeTips", codeTips.getText()+"");
            String json = gson.toJson(map);
            //json信息返回
            PrintWriter out = response.getWriter();
            out.println(json);
            out.flush();
            out.close();*/
        } catch (JSONException e) {
            e.printStackTrace();
        }
        request.getRequestDispatcher("/views/image.jsp").forward(request, response);
    }

3、填写验证码并解析

从session中取出drive并继续执行操作,并解析最终页面。

作者在操作时候发现不同浏览器对于selenium命令执行的结果是不一致的。比如IE,根据id定位对象的时候居然可以跨iframe,但是同样的代码chrome就不行。由于页面复杂,并没有注意到iframe存在,故而检查了好久。所以在此给大家提示:确认定位正确而找不到元素,可以利用F12在源码中搜索是否有iframe存在**

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        HttpSession session = request.getSession();
        WebDriver driver = (WebDriver) session.getAttribute("driver");
        Invoice invoice = (Invoice) session.getAttribute("invoice");
        
        //获取前台传来的验证码
        invoice.setCode(request.getParameter("checkNum").trim());
        
        //selenium操作
        WebElement CodeInput = driver.findElement(By.id("yzm"));
        new Actions(driver).moveToElement(CodeInput).click().perform();
        PageUtils.CtrlC_V(driver,invoice.getCode());
        WebElement InvoiceSubmit = driver.findElement(By.id("checkfp"));
        //等待按钮可点
        new WebDriverWait(driver, 1000).until(new ExpectedCondition<Boolean>(){
            public Boolean apply(WebDriver d){
                return d.findElement(By.id("checkfp"))
                        .getAttribute("style")
                        .contains("inline-block");
            }
        });
        //移动到上面点击,防止页面CSS遮挡
        new Actions(driver).click(InvoiceSubmit).perform();
        new WebDriverWait(driver, 1500).until(ExpectedConditions.presenceOfElementLocated(By.tagName("dialog")));
        driver.switchTo().frame("dialog-body");
        String example=driver.findElement(By.id("sbbh_dzfp")).getText();
        System.out.println(example);
    }

至此,页面抓取完成。
关于此次抓取,三种浏览器都试了,就速度而言,IE最慢,非常不推荐使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342