Advertisement

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

基于Google主导开发的一个开源OCR引擎 named Tesseract-Ocr possesses numerous Python open-source variants.

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)

还没有任何评论哟~