利用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
 * @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(){
                            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 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(){
            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最慢,非常不推荐使用。

你可能感兴趣的:(利用Selenium实现交互式WebService接口 --处理验证码)