社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

全新Python爬虫方案,解决加密字体问题!

Python开发 • 2 年前 • 242 次点击  
👇👇关注后回复 “进群” ,拉你进程序员交流群👇👇
作者丨那个百分十先生
来源丨快学Python

人生苦短,快学Python!

最近看到一些博主在讲解加密字体的破解方式,大体的解决方式是分析网页源代码,通过请求查看自定义字体,然后经过数据抓取完成需求。

这个方法确实很不错,但是对于我这种不太会爬虫的小白来说就与一些超纲了。另外这种爬虫在一定程度上可能会造成律师函警告,为此本次将向大家展示另一种全新的获取数据方式,对爬取的网站不起到任何“破解”行为。

1、需求分析

获取某某网站火锅主页的商户评价数据信息,如下图绿色箭头所示:

我们通过F12查看一下评价数据:

可以看出这个数据是被字体加密的,无法直接获取。

接下来就开始我的表演。

2、自动化打开网址

自动化打开网址使用的是subprocess和uiautomation来完成的,注意parameter和startmax两个参数的作用:

如果对uiautomation不熟悉的同学,可以参考我之前的文章:uiautomation批量获取百度指数!

import subprocess
import uiautomation as auto

def show_index_window():
    print('root Control:', auto.GetRootControl())
    chromePath = "C:\\Users\\TH\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe"
    url = r'http://www.dianping.com/xiamen/ch10/g110'
    parameter = '--force-renderer-accessibility'
    startmax = '-start-maximized'

    run_cmd = chromePath + ' ' + url + ' ' + parameter + ' ' + startmax
    subprocess.Popen(run_cmd)

    mainWindow = auto.DocumentControl(ClassName='Chrome_RenderWidgetHostHWND')
    print('mainWindow Name:',mainWindow.Name)

show_index_window()

3、获取评价数据截图文件

在此之后,我们打开控件识别工具 Inspect.exe工具(可从网上下载)来查看元素情况,我使用的是另一个工具,各个元素的分层情况就显而易见了:

可以看出,多少条评价也是隐藏的。从元素层级上来看,可以从文档元素->列表元素->列表项目元素的大体思路找到目标元素。也就是"XXXX条评论"这个元素,定位这个元素之后,就获取这个元素的大小,然后截图保存。具体的代码如下:

import time
import uiautomation as auto

# 获取每一个商户根元素
def get_root_control():
    documentControl = auto.DocumentControl(ClassName='Chrome_RenderWidgetHostHWND')

    # 层层搜索找到目标元素
    documentControl_f_son_control = documentControl.GetChildren()[0]
    # 子元素列表
    f_son_control = documentControl_f_son_control.GetChildren()

    target_list_control = None
    for each_control in f_son_control:
        son_control = each_control.GetChildren()
        if len(son_control) == 1:
            # 元素属性ControlTypeName为ListControl
            if son_control[0].ControlTypeName == 'ListControl':
                target_list_control = son_control[0]
                break

    return target_list_control

# 获取数据截图
def get_comment_pic():
    mainWindow = auto.PaneControl(ClassName='Chrome_WidgetWin_1')
    print('mainWindow Name:', mainWindow.Name)

    # 窗口存在,切换窗口
    if mainWindow.Exists(31):
        handle = mainWindow.NativeWindowHandle
        auto.SwitchToThisWindow(handle)

    target_list_control = get_root_control()

    # 当前页面展示的商户的数量
    business_num = len(target_list_control.GetChildren()) - 1
    print('business_num:', business_num)


    # 当前数据所在页数
    PageNum = 0
    # 获取xxxx评价元素的大小并截图
    for i in range(business_num):
        # 第一层,第二层,第三层数据
        layer_1_ele = target_list_control.GetChildren()[i]
        layer_2_ele = layer_1_ele.GetChildren()[1]
        layer_2_son_ele = layer_2_ele.GetChildren()


        layer_3_ele = None
        for each in layer_2_son_ele:
            if "条评价" in each.Name:
                layer_3_ele = each
        if layer_3_ele is None:
            raise Exception('获取元素失败')
        print(layer_3_ele)

        # 获取目标元素的底部坐标,如果底部坐标等于0,说明元素在当前窗口未显示出来,需要pagedown操作
        layer_3_ele_bottom = layer_3_ele.BoundingRectangle.bottom
        print('layer_3_ele_bottom:', layer_3_ele_bottom)

        if layer_3_ele_bottom > 0:
            time.sleep(1)
            # 获取截图
            layer_3_ele.CaptureToImage('./pic/%d%d.png' % (PageNum, i))


        if layer_3_ele_bottom <= 0:
            print('目标元素未显示')
            # auto.WheelDown()
            # auto.mouse_event(auto.MouseEventFlag.Wheel, 0, 0, -435, 0)
            auto.SendKeys("{PAGEDOWN}")
            time.sleep(1)
            
            # pagedown后重新获取元素
            target_list_control = get_root_control()
            layer_1_ele = target_list_control.GetChildren()[i]
            layer_2_ele = layer_1_ele.GetChildren()[1]
            layer_2_son_ele = layer_2_ele.GetChildren()

            layer_3_ele = None
            for each in layer_2_son_ele:
                if "条评价" in each.Name:
                    layer_3_ele = each
            if layer_3_ele is None:
                raise Exception('获取元素失败')
            print(layer_3_ele)

            # 获取截图
            layer_3_ele.CaptureToImage('./pic/%d%d.png' % (PageNum, i))

get_comment_pic()

上述代码中PageNum的作用是给采集的图片进行命名。当我们采集第一页商户评价数据的时候,可以更新PageNum的值为1,当我们将页面点击到第二页的时候就更新PageNum为2。

当然了为了偷懒,我们这没有编写点击下一页按钮和更新PageNum值的脚本,这个作业就交给大家实现了。

采集几页数据之后,我们就可以得到下列数据图:

获取截图文件之后,我们要做的就是识别其中的字符就可以了。

对于获取到的截图文件的说明:

(1)不同分辨率电脑获取的截图文件大小存在差异;

(2)评价数值是几位数也会直接影响截图的大小;

(3)同一电脑获取的截图文件高度是一致的。

4、识别评价数据截图文件

评价数据截图文件的识别可以借助OCR工具来进行提取,如果感兴趣的小伙伴,可以点击蓝字查看:

5行Python实现验证码识别,太稳了!

所以本文其实到这里就可以结束了,那么为什么进度条才到一半呢?

那是为了与众不同,我决定使用机器学习算法来识别截图文件。

我们先将截图文件转换为下列样式,即去掉干扰部分并清晰化数据截图文件:

具体实现的代码可以这样:

import os
from PIL import Image

# 裁剪后观察样本数据可以发现,含有不同数目的字符图片的长度是不一样的,即:
# 1个-->9
# 2个-->16
# 3个-->23
# 4个-->30
# 5个-->37
# 大家可以自己像一个很好的办法来区分,这里我就简单的人工罗列。
# 注意不同分辨率的电脑这个数据也会改变的,大家可以做一个映射表即可。

def get_each_pic_num(imgPath, savePath):
    global rct
    files = os.listdir(imgPath)
    files.sort()
    for file in files:
        fileType = os.path.splitext(file)
        if fileType[1] == '.png':
            img = Image.open(imgPath + '/' + file)
            img = img.convert("L")
            # pixdata = img.load()
            w, h = img.size

            cut_times = 0
            if w == 9:
                rct = ((007, h),)
                cut_times = 1
            if w == 16:
                rct = ((007, h),
                       (7014, h))
                cut_times = 2
            if w == 23:
                rct = ((007, h),
                       (7014, h),
                       (14 021, h),)
                cut_times = 3
            if w == 30:
                rct = ((007, h),
                       (7014, h),
                       (14021, h),
                       (21028, h),)
                cut_times = 4
            if w == 37:
                rct = ((007, h),
                       (7014, h),
                       (14021, h),
                       (21028, h),
                       (28035, h),)
                cut_times = 5

            cut_img = []
            for part in range(cut_times):
                cut_img.append(img.crop(rct[part]))
            # return img
                d = 0
                for im in cut_img:
                    d += 1
                    im.save(savePath + str(d) + str(file))

get_each_pic_num('./cutpic/''./each_character/')

之后我们对图片进行切割,这里使用固定坐标切割,得到如下所示单个字符数据:

实现代码如下:

import os
from PIL import Image

# 灰度和二值化处理
def binarizing(imgPath, savePath):
    files = os.listdir(imgPath)
    files.sort()
    # img=Image.open(img).convert("L")
    for file in files:
        fileType = os.path.splitext(file)
        if fileType[1] == '.png':
            img = Image.open(imgPath + '/' + file)
            img = img.convert("L")
            pixdata = img.load()
            w, h = img.size
            for y in range(h):
                for x in range(w):
                    if pixdata[x, y] 220:
                        pixdata[x, y] = 0
                    else:
                        pixdata[x, y] = 255
            # removeFrame(img,1)
            img.save(savePath + '/' + file)  # 保存图片
            # return img

def get_cut(file_name):
    img = Image.open(file_name)
    # 不同分辨率减去的值可能不同
    # 可以做一个字典映射
    right = img.size[0] - 39
    # right = img.size[0] - 47
    cut_img = []
    rct = (
        (00, right, 28),  # 左边距   上边距     右边距     下边距
    )
    for part in range(1):
        cut_img.append(img.crop(rct[part]))
    return cut_img

# 二值化
binarizing('./pic/''./binpic/')


# 切割保存
imgPath = './binpic/'
files = os.listdir(imgPath)
files.sort()
for file in files:
    fileType = os.path.splitext(file)
    if fileType[1] == '.png':
        # img = Image.open(imgPath + '/' + file)
        img = get_cut(imgPath + '/' + file)
        d = 0
        for im in img:
            d += 1
            im.save('./cutpic/' + str(file))

注意,切割后的单个图片要保证h和w是一致的。当然了大家可以尝试使用连通区域分割算法进行切割。

5、人工标注数据集

建立0-9一共10个文件夹,人工判断字符属于哪个文件夹,并将拆分的字符文件移动到对应的文件夹中。由于字符不是很复杂每一个文件夹只需大概20个文件即可:

6、机器学习

得到数据集之后,我们就可以构建机器学习模型。机器学习算法采用的是SVM算法,具体就不详解了,先图片数据转文本数据,

代码如下:

# 获取图像二值化数值
import numpy as np
from PIL import Image
import os, sys

def getBinaryPix(im):
    im = Image.open(im)
    im = im.convert("L")
    img = np.array(im)
    rows, cols = img.shape
    # print(img.shape)
    for i in range(rows):
        for j in range(cols):
            if (img[i, j] <= 220):
                img[i, j] = 0
            else:
                img[i, j] = 1
    binpix = np.ravel(img)
    # binpix=img.reshape(1,rows*cols)
    return binpix


def getfiles(path):
    files = []
    for eachf in os.listdir(path):
        f = path + eachf
        if f.rfind(u'.DS_Store') == -1:
           files.append(f)
    return files


def wirteFile(content):
    with open('./traindata/train.txt''a+'as f:
        f.write(content)
        f.write('\n')
        f.close()


if __name__ == '__main__':
    file_path = './correct_categroy/%s/'
    for i in ['0''1''2''3''4''5''6''7''8''9']:
        for f in getfiles(file_path % (i)):
            pixs = getBinaryPix(f).tolist()
            pixs.append(i)
            pixs = [str(i) for i in pixs]
            content = '.'.join(pixs)
            wirteFile(content)

得到的train数据集train.txt如下:

接下来就是使用上述数据构建SVC模型:

from sklearn.svm import SVC
from sklearn.model_selection import GridSearchCV
from sklearn.model_selection import cross_val_score
import numpy as np
import pandas as pd
import joblib
from PIL import  Image, ImageFilter, ImageEnhance
# from picPreprocessing import loadPredict
import warnings
import os
warnings.filterwarnings('ignore')

PKL = './model.pkl'

# 加载数据
def load_data():
    dataset = pd.read_table('./traindata/train.txt', header=None, delimiter='.', index_col=-1)
    return dataset

# 参数寻优
def searchBestParameter():
    parameters = {'kernel': ('linear''poly''rbf''sigmoid'), 'C': [1100]}
    dataset = load_data()
    row, col = dataset.shape
    X = dataset.values
    Y = dataset.index
    svr = SVC()
    clf = GridSearchCV(svr, parameters)
    clf.fit(X, Y)
    print(clf.best_params_)

    # 该工程的最佳参数为
    # {'C': 1, 'kernel': 'linear'}

def train():
    dataset = load_data()
    # row, col = dataset.shape
    # X = dataset[:,:col-1]
    # Y=dataset[:,-1]
    X = dataset.values
    Y = dataset.index
    clf = SVC(kernel='linear', C=1)
    clf.fit(X, Y)
    joblib.dump(clf, PKL)

# 交叉验证
def cross_validation():
    dataset = load_data()
    row, col = dataset.shape
    # X=dataset[:,:col-1]
    # Y=dataset[:,-1]
    X = dataset.values
    Y = dataset.index
    clf = SVC(kernel='linear', C=1)
    scores = cross_val_score(clf, X, Y, cv=5)
    print("Accuracy: %0.2f (+/- %0.2f)" % (scores.mean(), scores.std() * 2))
        
      # 该工程下打印的 cross_validation Accuracy: 0.99 (+/- 0.02)


def predict(pic_name): 
    clf = joblib.load(PKL)
    img = Image.open(pic_name)

    img = img.convert("L")
    w, h = img.size
    cut_times = 0

    # 这里的处理与一开始生成测试数据的时候保持一致
    if w == 9:
        rct = ((007, h),)
        cut_times = 1
    if w == 16:
        rct = ((007, h),
               (7014, h))
        cut_times = 2
    if w == 23:
        rct = ((007, h),
               (7014, h),
               (14021, h),)
        cut_times = 3
    if w == 30:
        rct = ((007, h),
               (7014, h),
               (14021, h),
               (21028, h),)
        cut_times = 4
    if w == 37:
        rct = ((007, h),
               (7014, h),
               (14021, h),
               (21028, h),
               (28035, h),)
        cut_times = 5

    print('cut_times:',cut_times)
    predictValue = []
    for part in range(cut_times):
        img = im.crop(rct[part])
        img = img.convert('L')
        # img.show()
        img2 = np.array(img)  
        rows, cols = img2.shape
        for a in range(rows):
            for b in range(cols):
                if (img2[a, b] <= 220):
                    img2[a, b] = 0
                else:
                    img2[a, b] = 1
        binpix = np.ravel(img2)
        # pixs=binpix.tolist()
        pixs = binpix.reshape(1-1)
        predictValue.append(clf.predict(pixs)[0])

    predictValue = [str(x) for  x in predictValue]
    print('Number of reviews for this business: %s' % (''.join(predictValue)))

if __name__ == '__main__':
    # searchBestParameter()
    # train()
    # cross_validation()

    file_name = './testPic/712.png'
    savePath = './savaPic/712.png'

    img = Image.open(file_name)
    right = img.size[0] - 39
    # right = img.size[0] - 47
    cut_img = []
    rct = (
        (00, right, 28),
    )
    for part in range(1):
        cut_img.append(img.crop(rct[part]))
        d = 0
        for im in cut_img:
            d += 1
            im.save(savePath)

    predict(savePath)

searchBestParameter()中的寻优参数结果供train()函数中进行模型的训练。这三个函数运行结束,再次运行cross_validation()之后的代码即可,当然了在执行者三个函数的时候,cross_validation()后的代码先不执行。

交叉验证得知,我们的识别准确率高达:0.99 (+/- 0.02)

最后我们可以正确的识别目录下文件 './testPic/712.png',通过predict(savePath)打印出结果。上述工程没有拆分数据集为训练集和测试集,大家可以自己实现,当然了在拆分数据集的时候做一个打乱数据集的操作即可。

至此我们的项目就结束了。

7、总结

在各种自动化的工具中,Uiautomation是非常好用的一个工具,可以快速的帮助我们构建一个数据采集系统或其他自动化系统。这种方式采集加密的数据是模拟人工来进行的,这对于采集一些数据提供了一个新的思路

机器学习的使用使得我们不用借助其他识别工具就可以完成目标需求,这个项目中使用的机器学习的思路可以应用于简单的验证码识别。

最后,大家可以好好体会这种技术,上述方案仅供学习使用。

-End-

最近有一些小伙伴,让我帮忙找一些 面试题 资料,于是我翻遍了收藏的 5T 资料后,汇总整理出来,可以说是程序员面试必备!所有资料都整理到网盘了,欢迎下载!

点击👆卡片,关注后回复【面试题】即可获取

在看点这里好文分享给更多人↓↓

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/122378
 
242 次点击