python爬虫进阶(十):日志系统、守护线程以及验证码处理

一、日志系统

首先,关日志系统的设计参考这篇博客


1、日志系统基本用途


(1)多线程情况下,debug调试非常困难

(2)错误出现可能有一些随机性

(3)性能分析

(4)错误记录与分析

(5)运行状态的实时监测


2、日志系统设计


(1)错误级别:Debug,Info,Warning,Error,Fatal

错误级别逐渐增强,  logging.DEBUG   可查询其错误级别等级(大写)

当设置一个级别后,小于该级别的日志不能输出,只能输出大于该级别的


(2)日志的来源(通道):MySQL,Connection,Threading,etc

logging.getLogger('redis').debug('Simple Log Test!')
‘redis’:通道

debug:错误等级

‘Simple Log Test !’:错误信息


(3)日志输出位置:file,console,database

    'handlers': {
        'file': {
            'level': 'WARN',
            'formatter': 'simple',
            'class': 'logging.FileHandler',
            'filename': 'spider.log',
            'mode': 'a',
            'filters': ['warn']
        },
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'database': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'formatter': 'simple',
            'filename': 'spider.log',
            'mode': 'a',
            'filters': ['basic']
        }
‘class’键对应的value是写入日志的形式

logging.FileHandler  继承  logging.StreamHandler

 'propagate': True,
该语句的意思是是否让它在更高层也运行。如这里针对子类logging.FileHandler显示日志,那么针对父类logging.StreamHandler也显示日志;

'propagate': False,
如这里针对子类logging.FileHandler显示日志,那么针对父类logging.StreamHandler就不会显示日志;


3、Python日志系统

(1)loggers:创建日志并指明文件

(2)handlers:处理器,配置过滤器,输出等

(3)filters:配置过滤规则

(4)formatters:配置输出的文件格式


import logging

# 创建一个logger
logger = logging.getLogger('mylogger')
logger.setLevel(logging.DEBUG)

# 创建一个handler,用于写入日志文件
fh = logging.FileHandler('test.log')
fh.setLevel(logging.DEBUG)

# 再创建一个handler,用于输出到控制台
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)

# 定义handler的输出格式
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
fh.setFormatter(formatter)
ch.setFormatter(formatter)

# 给logger添加handler
logger.addHandler(fh)
logger.addHandler(ch)

# 记录一条日志
logger.info('foorbar')



4、一个比较完美的日志代码

# -*- coding: utf-8 -*-

import logging
import logging.config

class SpiderFilter(logging.Filter):

    def __init__(self, allow=None, disable=None):
        self.allow_channels = allow
        self.disable_channels = disable

    def filter(self, record):
        if self.allow_channels is not None:
            if record.name in self.allow_channels:
                allow = True
            else:
                allow = False
        elif self.disable_channels is not None:
            if record.name in self.disable_channels:
                allow = False
            else:
                allow = True
        else:
            allow = False
        return allow


LOGGING = {
    'version': 1,
    'disable_existing_loggers': True,
    'formatters': {
        'verbose': {
            'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
        },
        'simple': {
            'format': '%(asctime)s -- %(name)s !!!%(levelname)s!!!: %(message)s'
        },
    },
    'filters': {
        'basic': {
            '()': SpiderFilter,
            'allow': ('mongo', 'redis', 'mysql'),
        },
        'warn': {
            '()': SpiderFilter,
            'disable': ()
        }
    },
    'handlers': {
        'file': {
            'level': 'WARN',
            'formatter': 'simple',
            'class': 'logging.FileHandler',
            'filename': 'spider.log',
            'mode': 'a',
            'filters': ['warn']
        },
        'console': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'simple'
        },
        'database': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'formatter': 'simple',
            'filename': 'spider.log',
            'mode': 'a',
            'filters': ['basic']
        }
    },
    'loggers': {
        'mongo': {
            'handlers':['file'],
            'propagate': True,
            'level':'DEBUG',
        },
        'mysql': {
            # 使用database的handler
            'handlers': ['database'],
            # log 的级别为 DEBUG
            'level': 'DEBUG',
            # 是否要把log继续传递给更高级别(ancestor)的logger
            'propagate': False,
        },
        'redis': {
            'handlers': ['file', 'database'],
            'level': 'INFO',
            'filters': ['basic'],
            'propagate': False,
        }
    },
    'root': {
        'level': 'DEBUG',
        'handlers': ['console']
    }
}

if __name__ == '__main__':
    logging.config.dictConfig(LOGGING)
    logging.getLogger('redis').debug('Simple Log Test!')


二、守护线程

参考这篇博客


大型网络爬虫经常会挂机或者死掉,因此我们需要一个守护线程去监督整个过程的状态,如果死掉后要立即启动起来。


Daemontool 是应用在Linux系统上的一款工具,它能很好的起到守护线程的作用。


工作原理:

Daemontools是一个包含了很多管理Unix服务的工具的软件包。其中最核心的工具是supervise,它的功能是监控一个指定的服务,当该服务进程消亡,则重新启动该进程。而要添加让supervise监控的服务非常容易,只需要添加一个被监控的服务的目录,在该目录中添加启动服务器的名字为run的脚本文件即可。
    其中svscan工具是为指定的工作目录(缺省是/service/目录)下的所有子目录中的每一个子目录都启动一个supervise进程,最多可以启动多达1000个supervise进程(也就是工作目录下可以有多达1000个子目录)。其中每个子目录下都会有一个名为run的用来启动对应服务的脚本程序。Supervise会监控该服务,在服务消亡时使用run脚本来自动启动该服务。若svscan的工作目录下的子目录的sticky位被置位,则svscan将为该子目录启动两个supervise进程,一个监控子目录中的run对应的服务,另外一个监控子目录下的log子目录的记录服务,两者之间通过管道来相互联系。
    Svscan每5秒钟检测一次子目录,若出现新的目录则为该目录启动supervise,若某个老的子目录对应的supervise退出,则重新启动它。


安装





三、验证码识别


(1)pillow

有时候验证码的图片的网页存储形式并不是以一张图片的格式保存在网页上,而是以base64编码;因此我们想要得到这张图片必须对其进行base64解码。



(2)Tesseract-Ocr

Tesseract-Ocr是一个Google主导的开源OCR引擎,Tesseract-Ocr有很多python开源版本。

pip  install pytesseract


import pytesseract

pytesseract.image_to_string(img)

上面语句即可将验证码字符图片转换成对应字符。但是这仅仅适用于比较简单的图片,如:能比较清晰的识别出来;对于比较模糊的,如:识别就比较困难了。




错误信息

错误及解决办法

>>> img = Image.open(file)
>>> word = pytesseract.image_to_string(img)
Traceback (most recent call last):
  File "<pyshell#6>", line 1, in <module>
    word = pytesseract.image_to_string(img)
  File "D:\360Downloads\Python\Python3\lib\site-packages\pytesseract\pytesseract.py", line 126, in image_to_string
    raise TesseractError(status, errors)
pytesseract.pytesseract.TesseractError: (1, 'Error opening data file \\Program Files (x86)\\Tesseract-OCR\\tessdata/eng.traineddata')
>>> tessdata_dir_config = '--tessdata-dir "C:\\Program Files (x86)\\Tesseract-OCR\\tessdata"'
>>> word = pytesseract.image_to_string(img, lang='eng', config=tessdata_dir_config)
>>> word
'Sean'
>>> 


参数意义:
>>> word = pytesseract.image_to_string(img, lang='eng', config=tessdata_dir_config)
img:图片

lang:图片字符语言

config:指定数据集


(3)字母相连


有些图片(类似上图)中字母有一点重合,用之前的pytesseract的方法就不能准确识别出来,现在需要将其切割(从连接点),然后进行单个字母识别。

对验证码图片进行色彩统计:

# 获取图片的像素数组
    pixdata = img.load()
    colors = {}
    # 统计字符颜色像素情况
    for y in range(img.size[1]):
        for x in range(img.size[0]):
            if pixdata[x,y] in colors.keys():
                colors[pixdata[x, y]] += 1
            else:
                colors[pixdata[x,y]] = 1

    # 排名第一的是背景色,第二的是主要颜色
    colors = sorted(colors.items(), key=lambda d:d[1], reverse=True)


去噪:

将第二多的颜色用白色(255,255,255)表示,其他颜色用黑色(0,0,0)表示

    significant = colors[1][0]
    for y in range(img.size[1]):
        for x in range(img.size[0]):
            if pixdata[x,y] != significant:
                pixdata[x,y] = (255,255,255)
            else:
                pixdata[x, y] = (0,0,0)
    img.save('bw.png')
得到图片类似:



切割:


按像素点每一列只有一个像素点是黑色的地方切割。


(4)标准字体图片匹配


像上面这种图片用之前的方法就不能识别出来,但是上图的验证码又比较标准,他们在图片中的位置非常标准,因此可以考虑将验证码图片中的每一个字母与标准字母匹配,去相似度最高的作为预测结果。


制作标准图片

把所有可能结果找出来,制作大小和验证码字母大小相同的标准图片:


把验证码图片中字母部分剪裁出来:


    img = Image.open(im)
    img = img.convert("RGB")
    box = (8, 8, 58, 18)
    img = img.crop(box)
    pixdata = img.load()

转换图片颜色,和标准图片一样配色:


将验证码图片的每个字母与标准图片中每个字母匹配(计算像素距离):



代码:

from PIL import Image
import sys


def decoder(
        im,
        threshold=200,
        mask="letters.bmp",
        alphabet="0123456789abcdef"):

    img = Image.open(im)
    img = img.convert("RGB")
    box = (8, 8, 58, 18)
    img = img.crop(box)
    pixdata = img.load()

    # open the mask
    letters = Image.open(mask)
    ledata = letters.load()

    def test_letter(img, letter):
        A = img.load()
        B = letter.load()
        mx = 1000000
        max_x = 0
        x = 0
        for x in range(img.size[0] - letter.size[0]):
            _sum = 0
            for i in range(letter.size[0]):
                for j in range(letter.size[1]):
                    _sum = _sum + abs(A[x + i, j][0] - B[i, j][0])
            if _sum < mx:
                mx = _sum
                max_x = x
        return mx, max_x

    # Clean the background noise, if color != white, then set to black.
    for y in range(img.size[1]):
        for x in range(img.size[0]):
            if (pixdata[x, y][0] > threshold) \
                    and (pixdata[x, y][1] > threshold) \
                    and (pixdata[x, y][2] > threshold):

                pixdata[x, y] = (255, 255, 255, 255)
            else:
                pixdata[x, y] = (0, 0, 0, 255)

    counter = 0
    old_x = -1

    letterlist = []

    for x in range(letters.size[0]):
        black = True
        for y in range(letters.size[1]):
            if ledata[x, y][0] != 0:
                black = False
                break
        if black:
            box = (old_x + 1, 0, x, 10)
            letter = letters.crop(box)
            t = test_letter(img, letter)
            letterlist.append((t[0], alphabet[counter], t[1]))
            old_x = x
            counter += 1

    box = (old_x + 1, 0, 140, 10)
    letter = letters.crop(box)
    t = test_letter(img, letter)
    letterlist.append((t[0], alphabet[counter], t[1]))

    t = sorted(letterlist)
    t = t[0:5]  # 5-letter captcha

    final = sorted(t, key=lambda e: e[2])

    answer = ''.join(map(lambda l: l[1], final))
    return answer

if __name__ == '__main__':
    print(decoder(sys.argv[1]))

(5)拖动验证
(6)验证码总结

同一个网站的验证码风格一般来说是一样的,因此可以用同一种方法解决;对于不同的网站,验证码设计样式不同,具体问题具体分析。





  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值