OverView
写于2017年7月25日,仅从 我的其他博客转移至此,不做维护,如有不当之处,请包含!
作者现在需要爬取一个某政务型 查询网站,实现WebService接口。本文为了方便叙述,把接口和调用接口的业务系统合并---统称为系统,如果不懂,忽略此句
简要概括就是:
- 系统填一个表单、并将验证码交给用户识别
- 用户填完验证码提交后系统继续填写页面表单
- 最后将抓取的结果返回给用户。
为了方便大家理解,画流程图如下:
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方案!
以此政务网站为例,第一、第三行就系统交互:先填写发票代码,网页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最慢,非常不推荐使用。