TIP

以前总想着如何破解验证码,涉及到图像识别需要学习成本,索性百度有图像识别的API接口,每天可以调用5000次,也够用了,顺便实现了青岛啤酒抽奖。

关于如何在百度智能云调用API接口可以参考链接1,可以用百度云的账号登录,然后创建一个应用,再查看官方的API文档点我 (opens new window)

# 创建maven工程导入依赖

创建一个maven工程,导入依赖。

<dependency>
      <groupId>org.seleniumhq.selenium</groupId>
      <artifactId>selenium-java</artifactId>
      <version>3.141.59</version>
    </dependency>
    <dependency>
      <groupId>com.baidu.aip</groupId>
      <artifactId>java-sdk</artifactId>
      <version>4.12.0</version>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.4</version>
    </dependency>

# 获取验证码

验证码是自动生成的,没有唯一的网络地址,所以根据图片链接并不能获取到图片,好在selenium支持截屏,这里需要注意的一点就是,一定要保证截取的图片只包含验证码,其余部分一点都不要包括进来(是一个坑),最开始的时候,我截取的图片,上下部分包括了少量的其余部分,在去噪和二值化的时候,导致了色彩覆盖,验证码直接看不见了。

public static File elementSnapshot(WebElement element) throws Exception {
    //创建全屏截图
    WrapsDriver wrapsDriver = (WrapsDriver)element;
    File screen = ((TakesScreenshot)wrapsDriver.getWrappedDriver()).getScreenshotAs(OutputType.FILE);
    BufferedImage image = ImageIO.read(screen);
    //获取元素的高度、宽度
    int width = element.getSize().getWidth();
    int height = element.getSize().getHeight();
    //创建一个矩形使用上面的高度,和宽度
    Rectangle rect = new Rectangle(width, height);
    //元素坐标
    Point p = element.getLocation();
    BufferedImage img = image.getSubimage(p.getX(), p.getY()+10, rect.width-15, rect.height-14);
    ImageIO.write(img, "png", screen);
    return screen;
    }

这些后来添加的数字就是调位置用的,保证只截取验证码部分。

# 验证码降噪和二值化

但是仅仅只有这个API接口是不够用的,因为验证码都会有干扰线,如果不加处理就提交,得到的结果基本是没有正确的,所以还要进行降噪和二值化。具体实现代码就是这样,图像处理这部分我也了解很少,只是搬运。

public static void removeBackground(String imgUrl, String resUrl) {
    //定义一个临界阈值
    int threshold = 300;
    try {
        BufferedImage img = ImageIO.read(new File(imgUrl));
        int width = img.getWidth();
        int height = img.getHeight();
        for (int i = 1; i < width; i++) {
            for (int x = 0; x < width; x++) {
                for (int y = 0; y < height; y++) {
                    Color color = new Color(img.getRGB(x, y));
//                  System.out.println("red:" + color.getRed() + " | green:" + color.getGreen() + " | blue:" + color.getBlue());
                    int num = color.getRed() + color.getGreen() + color.getBlue();
                    if (num >= threshold) {
                        img.setRGB(x, y, Color.WHITE.getRGB());
                    }
                }
            }
        }
        for (int i = 1; i < width; i++) {
            Color color1 = new Color(img.getRGB(i, 1));
            int num1 = color1.getRed() + color1.getGreen() + color1.getBlue();
            for (int x = 0; x < width; x++) {
                for (int y = 0; y < height; y++) {
                    Color color = new Color(img.getRGB(x, y));
                    int num = color.getRed() + color.getGreen() + color.getBlue();
                    if (num == num1) {
                        img.setRGB(x, y, Color.BLACK.getRGB());
                    } else {
                        img.setRGB(x, y, Color.WHITE.getRGB());
                    }
                }
            }
        }
        File file = new File(resUrl);
        if (!file.exists()) {
            File dir = file.getParentFile();
            if (!dir.exists()) {
                dir.mkdirs();
            }
            try {
                file.createNewFile();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        ImageIO.write(img, "png", file);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

经过此代码处理完之后验证码就可以达到此效果。由于我是自动截屏验证码这一部分,大小为8k,处理完就变成3k,但是不知道为什么背景颜色会变成深绿,虽然也是二值化;当我手动下载验证码,大小为1k,处理完之后变成2k,背景颜色是黑色,数字部分为白色,同样也是可以二值化的,但是不理解其中原理,是根据什么二值化的。

知道是什么问题了,我把png改成了jpg。现在再改成png。

处理完之后,验证码还是很清晰的,API识别效果要好上不少。

# 验证码识别

百度智能云地址:click (opens new window)

识别需要查看百度智能云的官方文档。

我是用的是java,可以这样操作

public class Sample {
    //设置APPID/AK/SK
    public static final String APP_ID = "你的 App ID";
    public static final String API_KEY = "你的 Api Key";
    public static final String SECRET_KEY = "你的 Secret Key";
    public static void main(String[] args) {
        // 初始化一个AipOcr
        AipOcr client = new AipOcr(APP_ID, API_KEY, SECRET_KEY);
        // 可选:设置网络连接参数
        client.setConnectionTimeoutInMillis(2000);
        client.setSocketTimeoutInMillis(60000);
        // 调用接口
        String path = "test.jpg";
        JSONObject res = client.basicGeneral(path, new HashMap<String, String>());
        System.out.println(res.toString(2));
    }
}

填好所需要的验证参数和图片地址,就可以得到图片中的文字了。

# 具体思路流程图

创建一个窗口
创建一个窗口
开始
开始
访问抽奖网页
访问抽奖网页
成功?
成功?
填写手机号
填写手机号
截取验证码图片
截取验证码图片
调用文字识别API,并得到结果
调用文字识别API,并得到结果
正确?
正确?
填写正确的验证码
填写正确的验证码
抽奖
抽奖
结束
结束
Viewer does not support full SVG 1.1

这个还是画的太low了,下面再画一个数据流图对比一下。

# 数据流图

项目需求

  • 打开窗口,访问网页。

  • 获取网页手机号元素,从手机号表获取手机号,填写手机号。

  • 获取验证码的截图,降噪二值化验证码截图,调用百度智能云API,得到识别验证码。

  • 获取网页验证码元素,填写由上步骤获取的验证码,并且判断验证码的正确性,如果正确则进行抽奖,如果错误则刷新网页(再次访问网页,从头开始)。

  • 填写正确的验证码,抽奖,关闭窗口。

手机号表
手机号表
验证码识别
验证码识别
填写手机号
填写手机号
检查验证码正确性
检查验证码正确性
抽奖
抽奖
填写验证码
填写验证码
访问抽奖页面
访问抽奖页面
截取验证码
截取验证码
Robot
Robot
窗口
窗口
关闭窗口
关闭窗口
创建窗口
创建窗口
访问网址
访问网址
验证码图片
验证码图片
验证码图片
验证码图片
验证码
验证码
验证码
验证码
验证码
验证码
正确
正确
错误
错误
验证码
验证码
抽奖
抽奖
抽奖完毕
抽奖完毕
抽奖完毕
抽奖完毕
手机号
手机号
手机号
手机号
抽奖完毕
抽奖完毕
降噪二值化
降噪二值化
验证码图片
验证码图片
验证码图片
验证码图片
Viewer does not support full SVG 1.1

气死我了,本地测试是可以的,一转移到github actions上就是错误的,可能是由于国外ip不能调用百度的接口吧,二值化部分咋又有问题,不行了,我要暴力破解了,四个数,其实一个个破解要的时间也挺多的,这个页面还不能停留时间太长,只支持搜索三位数的时间,所以我把第一位设置成了随机数,,搜索后三位,第一位也可以弄成定值,赌赌运气吧*_*

算了,不搞随机数了,你在变,别人也在变,还不如自己以不变应万变,总会有一次是对的。随机的时候,有时候一两次就对了,有时后十来次都没对,还是搞个固定的靠谱点。

我发现问题所在了,原来是在国外访问这个抽奖网页太慢了,很多时候验证码图片并没有显示出来,导致截图失败,所以二值化那个地方会抛异常。还是得搞个服务器。

本地测试了一下无界面,发现不是因为无界面而截不了图,推测应该就是在国外访问这个网址太慢了。暴力六个小时可以破解3个验证码,太慢了,有点浪费了,不过可以把自己的加上去^_^

最新改了一下,发现一个数是固定的,每次搜索的时间太长了,有这么长时间不如去刷新出符合自己规则的验证码,然后我就让两个位数是固定的,果然,本地试了一下35分钟就可以破解出三个验证码,在github的actions上,两次都没超过15分钟就破解出了三个验证码,这个速度还是满意的。

那是不是就意味着三个数固定,破解速度会更快一点?我认为不是的,测试了一下效果没有两个的好,甚至我觉得次于一个数。我觉得这就像是扎金花一样,三张都是单牌会多一点,一对其次,豹子少之又少,折中一下还是取2更好一点。不至于验证码不对浪费太多时间(固定一个数对不上,需要浪费填1000次的机会,而两个数不对只需要浪费100次机会),以及数的组合可能性太低,做无谓的刷新。

参考链接:

链接1 (opens new window)

链接2 (opens new window)

链接3 (opens new window)