Python 100%解析svg-captcha验证码

前言

前段时间接到一个需求,登陆某一个网站,然后录入数据;本来以为是一个很简单的需求,结果遇到几个难点:

  1. 登陆的时候需要有验证码
  2. 验证码是一个请求路径,每请求一次验证码都不一样

本来一开始以为是常用的图片验证码,但是当查看页面源码是,并没有发现对应的验证码图片,而是发现了一个由svg标签组装的代码,如下:

<svg xmlns="http://www.w3.org/2000/svg" width="80" height="32" viewBox="0,0,80,32">
    <path fill="#111" d="M34.71 25.15L34.77 ......"/>
    <path fill="#222" d="M61.76 17.99L61.68 ......"/>
    <path fill="#222" d="M8.54 8.38L8.63 8.47L8.47 ......"/>
    <path d="M7 23 C57 24,22 9,75 17" stroke="#333" fill="none"/>
    <path fill="#333" d="M39.66 8.30L39.83 8.47L39.70......"/>
    <path d="M19 15 C53 9,43 29,62 27" stroke="#666" fill="none"/>
</svg>

上面代码对应验证码如下:

image

我对前端不怎么熟悉,所以在网上查,在github上看到了一个100%验证svg验证码的项目,但他这个是nodejs写的,我想用python写一个,于是学习了一下大佬的代码,并自己改了一个python版本的

大佬项目地址:svg-captcha-recognize

简单分析

首先查看大佬这串代码:

// 从svg中把几个字母的d内容取出来,同时把字母按照它们在svg中的顺序排列
const getLetters = (svg) => {
  let i = 0
  const letters = []
  while (i < svg.length - 1 && i !== -1) {
    const pathStart = svg.indexOf('<path', i)
    if (pathStart === -1) {
      break
    }
    let pathEnd = svg.indexOf('>', pathStart)
    if (pathEnd === -1) {
      pathEnd = svg.length
    } else {
      pathEnd++
    }

    // 太短的是噪点
    if (pathEnd - pathStart > 500) {
      const path = svg.substring(pathStart, pathEnd)
      const [, d] = path.match(/d="([^"]+)"/) || []
      if (d) {
        letters.push(d)
      }
    }

    i = pathEnd
  }

  // 给字母按照位置排序
  if (letters.length) {
    letters.sort((a, b) => {
      const [ax] = a.match(/\d+(\.\d*)?/)
      const [bx] = b.match(/\d+(\.\d*)?/)
      return parseFloat(ax) - parseFloat(bx)
    })
  }

  return letters
}

const utils = {
  getMoveY (path) {
    const [,,, moveY] = path.match(/M(\d+(\.\d*)?)\s+(\d+(\.\d*)?)/) || []
    return parseFloat(moveY)
  },

  getAllXY (path) {
    return (path.match(/(\d+(\.\d*)?)/g) || []).map(v => parseFloat(v))
  },

  getMinXY (path) {
    const xs = []
    const ys = []
    this.getAllXY(path).forEach((v, i) => {
      (i % 2 ? ys : xs).push(v)
    })
    return [
      Math.min(...xs),
      Math.min(...ys)
    ]
  },

  // 获取宽高
  getWH (path) {
    const xs = []
    const ys = []
    this.getAllXY(path).forEach((v, i) => {
      (i % 2 ? ys : xs).push(v)
    })
    const maxXY = [
      Math.max(...xs),
      Math.max(...ys)
    ]
    const minXY = [
      Math.min(...xs),
      Math.min(...ys)
    ]
    return [
      maxXY[0] - minXY[0],
      maxXY[1] - minXY[1]
    ]
  }
}

module.exports = {
  recognize (svg) {
    const letters = getLetters(svg)
    return letters.map(l => {
      if (lengthSameMap[l.length]) {
        return lengthSameMap[l.length](l)
      }
      const letters = lengthMap[l.length] || ['']
      if (!letters[0]) { // 这个值没有记录到
        console.log(`had not train : ${l}`)
      }
      return letters[0]
    }).join('')
  }
}

通过查看这串代码,我们可以分析出以下几步:

  1. 通过getLetters()方法获取到svg标签中所有path标签中的d属性值
  2. 较短的则是干扰线,直接剔除
  3. 获取d属性的长度
  4. 然后记录这个长度以及对应的数字
  5. 在这种情况下,从【a-Z】【1-9】中一定会有相同数字对应不用结果的,所以,大佬通过不同的宽高来区分

如果是在svg默认字体的情况下,我们可以得到以下结果:

const lengthMap = {
  986: ['I', 'l'],
  998: ['1'],
  1068: ['I', 'l'],
  1081: ['1'],
  1082: ['v'],
  1130: ['Y'],
  1134: ['Y'],
  1172: ['v'],
  1224: ['Y'],
  1274: ['L', 'y'],
  1298: ['V'],
  1311: ['V'],
  1360: ['i'],
  1380: ['L', 'y'],
  1406: ['V'],
  1473: ['i'],
  1478: ['T']
  ......
}

一般情况下不会是默认字体的,所以需要我们自己灵活获取

如果实在需要使用svg-captcha,请一定准备多套字体,并且经常更换,英文字体还是很容易找到的。

使用Python解析

将使用同上面一样的步骤解析,由于我不是很懂前端,所以也没怎么去研究svg-captcha到底是个啥,以及它的一些规则

这个方式有一个缺点就是:需要自己录入数据,如果没有根据输入的数据自己生成svg图片的时候,就需要自己一个一个获取指定svg验证码,以及自己判断完之后,从而进行记录

1、获取svg中所有path标签中的d属性值

import pymysql
from xml.dom import minidom
import re
import os

# 读取当前文件下的 image.scg 文件 并获取所有path标签
def get_path_elements(self, svg_file_path):
    doc = minidom.parse(svg_file_path)
    path_elements = doc.getElementsByTagName('path')
    return path_elements

# 把获取到的path标签通过坐标从左到右排序好
def sort_paths_by_left_position(self, path_elements):
    paths_without_stroke = []
    for path in path_elements:
        if not self.has_stroke_attribute(path):
            paths_without_stroke.append(path)
            sorted_paths = sorted(paths_without_stroke, key=lambda path: self.get_path_element_left_position(path))
            return sorted_paths

# 获取svg中各个path标签中对应的数量,也就是key值
def get_value(self, code):
    svg_file_path = 'image.svg'
    path_elements = self.get_path_elements(svg_file_path)
    sorted_paths = self.sort_paths_by_left_position(path_elements)
    # 打印排序后的路径顺序
    code_list = []
    for index, path in enumerate(sorted_paths):
        d = path.getAttribute('d')
        path_data = d.split(" ")
        key_value = (len(path_data), code[index])
        code_list.append(key_value)  

2、较短的线直接剔除

我是通过判断他是否有stroke这个属性,从而区分的

# 判断path标签中是否存在stroke属性
def has_stroke_attribute(self, path_element):
    return path_element.hasAttribute('stroke')

3、长度相同的值通过宽高来区分

大佬的第三步和第4步,我已经在第一步给出,大佬是通过所有长度,我是通过空格分割之后统计的长度,在我的场景下,这样的处理,重复率会降低,大家可根据自己的实际情况调整

# 通过d属性获取值得宽高
def get_path_dimensions(self, d):
    coordinates = re.findall(r'[-+]?\d*\.?\d+', d)
    x_values = [float(x) for x in coordinates[::2]]
    y_values = [float(y) for y in coordinates[1::2]]
    min_x = min(x_values)
    max_x = max(x_values)
    min_y = min(y_values)
    max_y = max(y_values)
    width = round(max_x - min_x, 2)
    height = round(max_y - min_y, 2)
    return width, height

4、通过宽高的差值来判断值是什么

因为就算是相同的字母,也会出现宽高细微上的差别,所以只能通过谁最接近,就取谁,所幸的是,我相同数字的字母,宽高区别大,所以能精确的获取验证码的结果,大家可根据自己的实际情况调整

				closest_value = None
                closest_diff = float('inf')
                for item in row_list:
                    width_diff = abs(float(item['width']) - width)
                    height_diff = abs(float(item['height']) - height)
                    total_diff = width_diff + height_diff
                    if total_diff < closest_diff:
                        closest_diff = total_diff
                        closest_value = item["value"]

完整代码

在code文件里

[ svg-captcha-recognize-python]

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在 Koa2 中使用 svg-captcha 生成验证码可以按照以下步骤进行: 1. 安装 svg-captcha 和 koa-svgrouter 模块 ```bash npm install svg-captcha koa-svgrouter --save ``` 2. 在 Koa2 应用中引入相关模块 ```javascript const Koa = require('koa'); const Router = require('koa-router'); const svgrouter = require('koa-svgrouter'); const svgCaptcha = require('svg-captcha'); const app = new Koa(); const router = new Router(); ``` 3. 设置路由,生成验证码 ```javascript router.get('/captcha', async (ctx, next) => { const captcha = svgCaptcha.create(); ctx.session.captcha = captcha.text; ctx.type = 'svg'; ctx.body = captcha.data; }); ``` 4. 添加 SVG 路由 ```javascript app.use(svgrouter('/captcha/:id', { useSession: true, sessionName: 'captcha' })); ``` 5. 在需要使用验证码的地方,可以通过如下代码获取验证码图片 ```html <img src="/captcha/1" /> ``` 注意,上述代码中的数字 1 是路由参数,可以自行设置。 6. 验证验证码 在需要验证验证码的地方,可以通过如下代码获取用户输入的验证码,并与之前生成的验证码进行比较。 ```javascript const userCaptcha = ctx.request.body.captcha; if (userCaptcha.toLowerCase() === ctx.session.captcha.toLowerCase()) { // 验证码输入正确 } else { // 验证码输入错误 } ``` 以上就是使用 svg-captcha 生成验证码的基本步骤。需要注意的是,在生成验证码和验证验证码时都需要使用到 Koa2 的 session 功能,因此需要在应用中添加相关中间件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值