前言
前段时间接到一个需求,登陆某一个网站,然后录入数据;本来以为是一个很简单的需求,结果遇到几个难点:
- 登陆的时候需要有验证码
- 验证码是一个请求路径,每请求一次验证码都不一样
本来一开始以为是常用的图片验证码,但是当查看页面源码是,并没有发现对应的验证码图片,而是发现了一个由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>
上面代码对应验证码如下:
我对前端不怎么熟悉,所以在网上查,在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('')
}
}
通过查看这串代码,我们可以分析出以下几步:
- 通过getLetters()方法获取到svg标签中所有path标签中的d属性值
- 较短的则是干扰线,直接剔除
- 获取d属性的长度
- 然后记录这个长度以及对应的数字
- 在这种情况下,从【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]