笔者这几天在爬取数据的时候遇到了一个很闹心的问题,就是在我爬取数据的时候遇到了验证码,而这个验证码又是动态生成的,尝试了很多方法都没能绕开这个验证码问题。
我的解决方案是:使用selenium模拟浏览器行为,获取到动态生成的验证码后用python脚本解析验证码图片,返回验证码的值,再用selenium输入该值,进行下一步的爬取工作。
目录
使用selenium模拟浏览器行为
使用selenium截取到验证码图片
将验证码图片保存到本地
使用python脚本解析验证码图片
如果使用ddddocr库报错
使用java调用python脚本
获取解析得到的验证码并填入
项目源代码
总结
1、首先需要导入selenium依赖:
org.seleniumhq.selenium
selenium-java
4.11.0
我这里是使用的Chrome浏览器,需要下载Chrome驱动,使用其他浏览器也是同理,下载好浏览器驱动后找到该驱动的路径(我的路径是“C:\\ChromeDriver\\chromedriver.exe”):
System.setProperty("webdriver.chrome.driver", "C:\\ChromeDriver\\chromedriver.exe");
ChromeOptions options = new ChromeOptions();
//通过配置参数禁止data;的出现,不会弹出浏览器,默认是后台静默运行
//options.addArguments("--headless","--disable-gpu");
//最大化浏览器窗口,否则location定位可能不准
options.addArguments("--disable-blink-features=AutomationControlled");
options.addArguments("--start-maximized");
// 创建 ChromeDriver 对象
WebDriver driver = new ChromeDriver();
driver.get(url);
这里的location定位指的是定位后续会出现的验证码图片
2、这里执行get(url)之后,如果你没有额外设置一些参数,系统会自动唤起你的Chrome浏览器并跳转到该url,此时重点来了,需要输入搜索参数:
public static void doSearch(WebDriver driver, String filterName, String eudName) {
//两个搜索参数
driver.findElement(By.id("filterName")).sendKeys(filterName);
driver.findElement(By.id("eudName")).sendKeys(eudName);
//触发"搜索"按钮的点击事件
driver.findElement(By.xpath("/html/body/div[2]/div/div[1]/div[1]/form/div/div[5]/input")).click();
}
这里需要打开网页的开发者工具,找到对应的搜索框的id或xpath等,只要能定位到这个元素即可,然后使用sendKeys()方法,就可以往搜索框内输入参数,然后找到“搜索”这个按钮,定位一下,使用click()方法触动点击事件,完成selenium模拟浏览器实现带参数的搜索。
这里多说一句,可以在网页的开发者工具里找到需要定位的元素之后右键copy,可以选择直接copy下来该元素的xpath
由于我爬取的网站的验证码资源是带时间戳的,暂时没想到什么比较好的方法能够直接从网站上定位到该资源的位置从而下载下来,所以我这里使用了selenium的截图方法获取到该验证码。
public static BufferedImage getImage(WebDriver driver){
//根据验证码图片的id属性定位到该元素
WebElement img = driver.findElement(By.id("dynamic_img_code"));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Point location = img.getLocation();
Dimension size = img.getSize();
//这里统一乘1.25是因为电脑的缩放比是125%,乘1.25能让location的定位精确
int left = (int) (location.getX() * 1.25);
int top = (int) (location.getY() * 1.25);
int right = (int) (left + size.getWidth() * 1.25);
int bottom = (int) (top + size.getHeight() * 1.25);
File screenshot = ((ChromeDriver) driver).getScreenshotAs(org.openqa.selenium.OutputType.FILE);
BufferedImage fullImage = null;
try {
fullImage = ImageIO.read(screenshot);
} catch (IOException e) {
throw new RuntimeException(e);
}
return fullImage.getSubimage(left, top, right - left, bottom - top); // 得到的就是验证码
}
这段代码的思路是先获取全屏截图,然后根据验证码图片的location定位到,获取全屏截图的子截图,即验证码图片。
上一步截取到验证码图片之后将其保存到本地,方便后续使用python脚本对其进行解析:
BufferedImage image = getImage(driver);
try {
ImageIO.write(image, "png", new File(outputDir,"captcha.png"));
} catch (IOException e) {
throw new RuntimeException(e);
}
这里的outputDir也是File类型的,代指captcha图片存放的文件夹
这里我使用了ddddocr库
pip install ddddocr
这是一个github上的免费开源项目,我爬取的网站的验证码是纯数字的,这个库的识别正确率很高,同时这个库还支持其他类型的验证码的识别,我没有研究,有需求的可以进作者的github了解
解析验证码的python脚本:
import ddddocr
import sys
# def get_captcha(path):
# ocr = ddddocr.DdddOcr()
# with open(path, "rb") as f:
# img_bytes = f.read()
# res = ocr.classification(img_bytes)
# return res
if __name__ == "__main__":
path = sys.argv[1]
ocr = ddddocr.DdddOcr()
with open(path, "rb") as f:
img_bytes = f.read()
res = ocr.classification(img_bytes)
print(res)
报错提示为:
AttributeError: module ‘PIL.Image‘ has no attribute ‘ANTIALIAS‘
可以参照这个解决: https://blog.csdn.net/light2081/article/details/131517132
如果你的python是3.0以上,就不要使用jython了,jython已经停止更新不支持3.0以上的python了,我这里是采用的进程调用的方法,需要传入的两个参数分别为python脚本路径和需要解析的验证码图片路径:
public static String pythonGetCaptcha(String pythonScriptPath,String imagePath){
String captcha = null;
try {
// 1. 构建命令行执行的命令
String[] command = {"python", pythonScriptPath, imagePath};
// 2. 执行命令
Process process = Runtime.getRuntime().exec(command);
// 3. 获取命令输出
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder captchaBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
captchaBuilder.append(line);
}
captcha = captchaBuilder.toString();
inputStream.close();
reader.close();
// 4. 等待命令执行完毕并获取返回值
} catch (IOException e) {
e.printStackTrace();
}
return captcha;
}
到这一步基本就结束了,只需要把解析得到的验证码填入框内点击提交即可,原理跟之前的doSearch()一样:
String captcha = CaptchaPictureUtil.pythonGetCaptcha(pythonScriptPath, outputDir.getPath()+"/captcha.png");
driver.findElement(By.id("dynamic_imgcode")).sendKeys(captcha);
driver.findElement(By.id("dynamic_submit")).click();
try {
Thread.sleep(5000);
html = driver.getPageSource();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
driver.quit(); // 一定要退出!不退出会有残留进程!
package com.spidertest.Utils;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
public abstract class CaptchaPictureUtil {
/**
* @since 2023/8/6
* @author HWJ
* @DESC 针对所爬取的网页特地做的爬虫工具类,因为爬虫的过程中会出现验证码,而验证码是输入了搜索参数之后才会出现
* @param url
* @param filterName
* @param eudName
* @param outputDir
*/
public static String getCaptchaPicture(String url,String filterName,String eudName,File outputDir,String pythonScriptPath){
String html = null;
System.setProperty("webdriver.chrome.driver", "C:\\ChromeDriver\\chromedriver.exe");
ChromeOptions options = new ChromeOptions();
//通过配置参数禁止data;的出现,不会弹出浏览器,默认是后台静默运行
//options.addArguments("--headless","--disable-gpu");
//最大化浏览器窗口,否则location定位可能不准
options.addArguments("--disable-blink-features=AutomationControlled");
options.addArguments("--start-maximized");
// 创建 ChromeDriver 对象,同时传入 ChromeOptions 对象
WebDriver driver = new ChromeDriver(options);
driver.get(url);
doSearch(driver, filterName, eudName);
BufferedImage image = getImage(driver);
try {
ImageIO.write(image, "png", new File(outputDir,"captcha.png"));
} catch (IOException e) {
throw new RuntimeException(e);
}
String captcha = CaptchaPictureUtil.pythonGetCaptcha(pythonScriptPath, outputDir.getPath()+"/captcha.png");
driver.findElement(By.id("dynamic_imgcode")).sendKeys(captcha);
driver.findElement(By.id("dynamic_submit")).click();
try {
Thread.sleep(5000);
html = driver.getPageSource();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
driver.quit(); // 一定要退出!不退出会有残留进程!
return html;
}
/**
* @since 2023/8/6
* @author HWJ
* @DESC 针对所爬取的网站让selenium做的模拟浏览器发起的请求,目的是为了触发网页弹出验证码
* @param driver
* @param filterName
* @param eudName
*/
public static void doSearch(WebDriver driver, String filterName, String eudName) {
//两个搜索参数
driver.findElement(By.id("filterName")).sendKeys(filterName);
driver.findElement(By.id("eudName")).sendKeys(eudName);
//触发"搜索"按钮的点击事件
driver.findElement(By.xpath("/html/body/div[2]/div/div[1]/div[1]/form/div/div[5]/input")).click();
}
public static BufferedImage getImage(WebDriver driver){
WebElement img = driver.findElement(By.id("dynamic_img_code"));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Point location = img.getLocation();
System.out.println(location);
Dimension size = img.getSize();
System.out.println(size);
//这里统一乘1.25是因为电脑的缩放比是125%,乘1.25能让location的定位精确
int left = (int) (location.getX() * 1.25);
int top = (int) (location.getY() * 1.25);
int right = (int) (left + size.getWidth() * 1.25);
int bottom = (int) (top + size.getHeight() * 1.25);
File screenshot = ((ChromeDriver) driver).getScreenshotAs(org.openqa.selenium.OutputType.FILE);
BufferedImage fullImage = null;
try {
fullImage = ImageIO.read(screenshot);
} catch (IOException e) {
throw new RuntimeException(e);
}
return fullImage.getSubimage(left, top, right - left, bottom - top); // 得到的就是验证码
}
/**
* @since 2023/8/6
* @DESC 使用py脚本解析验证码图片获取验证码
* @param pythonScriptPath
* @param imagePath
* @return captcha
*/
public static String pythonGetCaptcha(String pythonScriptPath,String imagePath){
String captcha = null;
try {
// 1. 构建命令行执行的命令
String[] command = {"python", pythonScriptPath, imagePath};
// 2. 执行命令
Process process = Runtime.getRuntime().exec(command);
// 3. 获取命令输出
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder captchaBuilder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
captchaBuilder.append(line);
}
captcha = captchaBuilder.toString();
inputStream.close();
reader.close();
// 4. 等待命令执行完毕并获取返回值
} catch (IOException e) {
e.printStackTrace();
}
return captcha;
}
}
本人是爬虫小白,在做这个爬取工具之前也只有两天学习爬虫的经历,本来是打算用HttpClient和JSoup做爬虫的,但是遇到了烦人的验证码,只能用这种方法解决。这个代码依旧有很多不足的地方需要改进,同时由于是针对我需要爬取的网站所编写的爬虫,耦合度还是很高,所以我尽量解释了我的想法,阅读者可以根据自己的需要找到有启发的地方就再好不过了。
这也是我的第一篇博客,发出来是为了记录一下这两天的工作,代码和博客内容还有很多不足,欢迎大家批评指正。
ps:上述代码的部分启发如下()
https://www.cnblogs.com/xuehaiwuya0000/p/11509435.html
https://blog.csdn.net/light2081/article/details/131517132
https://github.com/sml2h3/ddddocr
https://www.selenium.dev/documentation/