Python 日志logging模块初探及多线程踩坑(1)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Python 日志logging模块初探及多线程踩坑(1)


目录

1、日志记录的重要性

2、Python官方logging模块

3、logging 模块的基本架构和对象功能

4、打印到控制台

5、利用logging.basicConfig()保存log到文件

6、利用logging模块组件灵活实现需求

7、实现logging中TimedRotatingFileHandler多线程应用


1、日志记录的重要性

在开发过程中,如果程序运行出现了问题,我们是可以使用我们自己的 Debug 工具来检测到到底是哪一步出现了问题,如果出现了问题的话,是很容易排查的。但程序开发完成之后,我们会将它部署到生产环境中去,这时候代码相当于是在一个黑盒环境下运行的,我们只能看到其运行的效果,是不能直接看到代码运行过程中每一步的状态的。在这个环境下,运行过程中难免会在某个地方出现问题,甚至这个问题可能是我们开发过程中未曾遇到的问题,碰到这种情况应该怎么办?

如果我们现在只能得知当前问题的现象,而没有其他任何信息的话,如果我们想要解决掉这个问题的话,那么只能根据问题的现象来试图复现一下,然后再一步步去调试,这恐怕是很难的,很大的概率上我们是无法精准地复现这个问题的,而且 Debug 的过程也会耗费巨多的时间,这样一旦生产环境上出现了问题,修复就会变得非常棘手。但这如果我们当时有做日志记录的话,不论是正常运行还是出现报错,都有相关的时间记录,状态记录,错误记录等,那么这样我们就可以方便地追踪到在当时的运行过程中出现了怎样的状况,从而可以快速排查问题。

因此,日志记录是非常有必要的,任何一款软件如果没有标准的日志记录,都不能算作一个合格的软件。作为开发者,我们需要重视并做好日志记录过程。

2、Python官方logging模块

logging 模块相比 print 有这么几个优点:

    • 可以在 logging 模块中设置日志等级,在不同的版本(如开发环境、生产环境)上通过设置不同的输出等级来记录对应的日志,非常灵活。
    • print 的输出信息都会输出到标准输出流中,而 logging 模块就更加灵活,可以设置输出到任意位置,如写入文件、写入远程服务器等。
    • logging 模块具有灵活的配置和格式化功能,如配置输出当前模块信息、运行时间等,相比 print 的字符串格式化更加方便易用。

    虽然logging模块功能很强大,但还是有坑在里面的,下面我们一一道来。

    3、logging 模块的基本架构和对象功能

      • Logger:即 Logger Main Class,是我们进行日志记录时创建的对象,我们可以调用它的方法传入日志模板和信息,来生成一条条日志记录,称作 Log Record。
      • Log Record:就代指生成的一条条日志记录。
      • Handler:即用来处理日志记录的类,它可以将 Log Record 输出到我们指定的日志位置和存储形式等,如我们可以指定将日志通过 FTP 协议记录到远程的服务器上,Handler 就会帮我们完成这些事情。
      • Formatter:实际上生成的 Log Record 也是一个个对象,那么我们想要把它们保存成一条条我们想要的日志文本的话,就需要有一个格式化的过程,那么这个过程就由 Formatter 来完成,返回的就是日志字符串,然后传回给 Handler 来处理。
      • Filter:另外保存日志的时候我们可能不需要全部保存,我们可能只需要保存我们想要的部分就可以了,所以保存前还需要进行一下过滤,留下我们想要的日志,如只保存某个级别的日志,或只保存包含某个关键字的日志等,那么这个过滤过程就交给 Filter 来完成。
      • Parent Handler:Handler 之间可以存在分层关系,以使得不同 Handler 之间共享相同功能的代码。

      以上就是整个 logging 模块的基本架构和对象功能,了解了之后我们详细来了解一下 logging 模块的用法。

      4、打印到控制台

      import logging
      logging.debug('debug 信息')
      logging.warning('只有这个会输出。。。')
      logging.info('info 信息')

      image.gif

      由于默认设置的等级是warning,所有只有warning的信息会输出到控制台。不推荐使用

      image.gif

      -------------------------------------------------------------------

      利用logging.basicConfig()打印信息到控制台

      import logging
      logging.basicConfig(format='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s',
                          level=logging.DEBUG)
      logging.debug('debug 信息')
      logging.info('info 信息')
      logging.warning('warning 信息')
      logging.error('error 信息')
      logging.critical('critial 信息')

      image.gif

      由于在logging.basicConfig()中的level 的值设置为logging.DEBUG, 所有日志级别高于等于debug的log才能打印到控制台。

      日志级别: debug < info < warning < error < critical

      5、利用logging.basicConfig()保存log到文件

      logging.basicConfig(level=logging.DEBUG,
                          filename='new.log',
                          filemode='a',##模式,有w和a,w就是写模式,每次都会重新写日志,覆盖之前的日志
                          #a是追加模式,默认如果不写的话,就是追加模式
                          format=
                          '%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s'
                          #日志格式
                          )注意的是:一旦在logging.basicConfig()设置filename 和filemode,则只会保存log到文件,不会输出到控制台。!!!

      2019/5/29 补充:

       但当某些信息较长,会导致日志输出信息不够美观   我们可以对其的固定长度等进行设置:

      %%(asctime)s %%(name)s %%(levelname)-8s %%(filename)-25s %%(lineno)-4s %%(message)s

      image.gif

      -8 的意思是左对齐,固定长度8 ,默认用空格填充

      下面还有一些常用的设置:

      6、利用logging模块组件灵活实现需求

      咳咳,当正题了~~,推荐使用Logging组件: 记录器(Logger )、处理器(Handler)、过滤器(Filter)和格式化器(Formatter )。

      简单介绍常用的配置一下:

      Logger

      Logger.setLevel()指定logger将会处理的最低的安全等级日志信息,。

      Logger.addHandler()和Logger.removeHandler()从记录器对象中添加和删除处理程序对象。处理器详见Handlers。

      Logger.addFilter()和Logger.removeFilter()从记录器对象添加和删除过滤器对象。

      Handler

      logging.StreamHandler -> 控制台输出

      logging.FileHandler  -> 文件输出

      logging.handlers.RotatingFileHandler -> 按照大小自动分割日志文件,一旦达到指定的大小重新生成文件

      logging.handlers.TimedRotatingFileHandler  -> 按照时间自动分割日志文件 

      Formatters

      Formatter对象设置日志信息最后的规则、结构和内容,默认的时间格式为%Y-%m-%d %H:%M:%S,下面是Formatter常用的一些信息

      %(name)s

      Logger的名字

      %(levelno)s

      数字形式的日志级别

      %(levelname)s

      文本形式的日志级别

      %(pathname)s

      调用日志输出函数的模块的完整路径名,可能没有

      %(filename)s

      调用日志输出函数的模块的文件名

      %(module)s

      调用日志输出函数的模块名

      %(funcName)s

      调用日志输出函数的函数名

      %(lineno)d

      调用日志输出函数的语句所在的代码行

      %(created)f

      当前时间,用UNIX标准的表示时间的浮 点数表示

      %(relativeCreated)d

      输出日志信息时的,自Logger创建以 来的毫秒数

      %(asctime)s

      字符串形式的当前时间。默认格式是 “2003-07-08 16:49:45,896”。逗号后面的是毫秒

      %(thread)d

      线程ID。可能没有

      %(threadName)s

      线程名。可能没有

      %(process)d

      进程ID。可能没有

      %(message)s

      用户输出的消息

      来一个例子,大家就明白了

      import logging
      from logging import handlers
      filename = os.path.join(dirpath+"log")
      fmt='%(asctime)s - %(pathname)s[line:%(lineno)d] - %(levelname)s: %(message)s')
      log_format = logging.Formatter(fmt) #设置日志格式
      sh = logging.StreamHandler() #往屏幕上输出
      sh.setFormatter(fmt) #设置屏幕上显示的格式
      fh = logging.handlers.TimedRotatingFileHandler(filename=filename , interval=1, when='M', backupCount=2, delay=True) #保存日志到文件
      fh.suffix = "%Y-%m-%d_%H-%M"
      fh.extMatch = re.compile(r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$")
      fh.setFormatter(fmt)
      # log
      logger = logging.getLogger(__name__)
      logger.setLevel("DEBUG") #设置日志等级
      logger.addHandler(sh)
      logger.addHandler(fh)

      image.gif

      TimedRotatingFileHandler 是以时间分割

      filename 是输出日志文件名的前缀,比如log/myapp.log

      when 是一个字符串的定义如下:

      “S”: Seconds

      “M”: Minutes

      “H”: Hours

      “D”: Days

      “W”: Week day (0=Monday)

      “midnight”: Roll over at midnight

      interval 是指等待多少个单位when的时间后,Logger会自动重建文件,当然,这个文件的创建取决于filename+suffix,若这个文件跟之前的文件有重名,则会自动覆盖掉以前的文件,所以有些情况suffix要定义的不能因为when而重复。backupCount 是保留日志个数。默认的0是不会自动删除掉日志。若设2,则在文件的创建过程中库会判断是否有超过这个2,若超过,则会从最先创建的开始删除。

      extMatch会根据when的值进行初始化:

      ‘S’: suffix=”%Y-%m-%d_%H-%M-%S”, extMatch=r”\^d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}”;

      ‘M’:suffix=”%Y-%m-%d_%H-%M”,extMatch=r”^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}”;

      ‘H’:suffix=”%Y-%m-%d_%H”,extMatch=r”^\d{4}-\d{2}-\d{2}_\d{2}”;

      ‘D’:suffxi=”%Y-%m-%d”,extMatch=r”^\d{4}-\d{2}-\d{2}”;

      ‘MIDNIGHT’:”%Y-%m-%d”,extMatch=r”^\d{4}-\d{2}-\d{2}”;

      ‘W’:”%Y-%m-%d”,extMatch=r”^\d{4}-\d{2}-\d{2}”;

       

      这边如果delay=False,表面直接输入文件,不入缓存。会导致‘win32Error :  文件被占用的错误’.这是由于TimedRotatingFileHandler并不是线程安全的。

      而且运行发现,每个进程都保持一个固定的文件句柄,导致在达到条件回滚时,相互之间的rename会相互干扰,还有好多问题

      logging中RotatingFileHandler和TimedRotatingFileHandler对于多进程不支持。

      如果觉得文章对你有帮助,欢迎关注微信公众号:小牛呼噜噜

      7、实现logging中TimedRotatingFileHandler多线程应用

      1、修改源码,但在实际开发过程,特别麻烦。

      2、引用第三方库ConcurrentLogHandler,但它只支持文件大小分割日志,但按时间分割日志,是比较常见的。

      3、继承父类,重写方法。简单易用。下面具体讲解这部分:

      网上重写的方法都多多少少有点问题。github上有现成开源的,写得非常完善,经得起实战。但也有缺点(性能较低)

      https://github.com/kieslee/mlogging

      不过要注意的是,作者虽然说它只支持linux,但windows也可以,不过要做一点修改:

      下面是源码:继承重写了logging的StreamHandler:

      from logging import StreamHandler, FileHandler
      from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
      import fcntl, time, os, codecs, string, re, types, cPickle, struct, shutil
      from stat import ST_DEV, ST_INO, ST_MTIME
      class StreamHandler_MP(StreamHandler):
          """
          A handler class which writes logging records, appropriately formatted,
          to a stream. Use for multiprocess.
          """
          def emit(self, record):
              """
              Emit a record.
                  First seek the end of file for multiprocess to log to the same file
              """
              try:
                  if hasattr(self.stream, "seek"):
                      self.stream.seek(0, os.SEEK_END)
              except IOError, e:
                  pass
              StreamHandler.emit(self, record)
      class FileHandler_MP(FileHandler, StreamHandler_MP):
          """
          A handler class which writes formatted logging records to disk files 
              for multiprocess
          """
          def emit(self, record):
              """
              Emit a record.
              If the stream was not opened because 'delay' was specified in the
              constructor, open it before calling the superclass's emit.
              """
              if self.stream is None:
                  self.stream = self._open()
              StreamHandler_MP.emit(self, record)
      class RotatingFileHandler_MP(RotatingFileHandler, FileHandler_MP):
          """
          Handler for logging to a set of files, which switches from one file
          to the next when the current file reaches a certain size.
          Based on logging.RotatingFileHandler, modified for Multiprocess
          """
          _lock_dir = '.lock'
          if os.path.exists(_lock_dir):
              pass
          else:
              os.mkdir(_lock_dir)
          def doRollover(self):
              """
              Do a rollover, as described in __init__().
              For multiprocess, we use shutil.copy instead of rename.
              """
              self.stream.close()
              if self.backupCount > 0:
                  for i in range(self.backupCount - 1, 0, -1):
                      sfn = "%s.%d" % (self.baseFilename, i)
                      dfn = "%s.%d" % (self.baseFilename, i + 1)
                      if os.path.exists(sfn):
                          if os.path.exists(dfn):
                              os.remove(dfn)
                          shutil.copy(sfn, dfn)
                  dfn = self.baseFilename + ".1"
                  if os.path.exists(dfn):
                      os.remove(dfn)
                  if os.path.exists(self.baseFilename):
                      shutil.copy(self.baseFilename, dfn)
              self.mode = 'w'
              self.stream = self._open()
          def emit(self, record):
              """
              Emit a record.
              Output the record to the file, catering for rollover as described
              in doRollover().
              For multiprocess, we use file lock. Any better method ?
              """
              try:
                  if self.shouldRollover(record):
                      self.doRollover()
                  FileLock = self._lock_dir + '/' + os.path.basename(self.baseFilename) + '.' + record.levelname
                  f = open(FileLock, "w+")
                  fcntl.flock(f.fileno(), fcntl.LOCK_EX)
                  FileHandler_MP.emit(self, record)
                  fcntl.flock(f.fileno(), fcntl.LOCK_UN)
                  f.close()
              except (KeyboardInterrupt, SystemExit):
                  raise
              except:
                  self.handleError(record)
      class TimedRotatingFileHandler_MP(TimedRotatingFileHandler, FileHandler_MP):
          """
          Handler for logging to a file, rotating the log file at certain timed
          intervals.
          If backupCount is > 0, when rollover is done, no more than backupCount
          files are kept - the oldest ones are deleted.
          """
          _lock_dir = '.lock'
          if os.path.exists(_lock_dir):
              pass
          else:
              os.mkdir(_lock_dir)
          def __init__(self, filename, when='h', interval=1, backupCount=0, encoding=None, delay=0, utc=0):
              FileHandler_MP.__init__(self, filename, 'a', encoding, delay)
              self.encoding = encoding
              self.when = string.upper(when)
              self.backupCount = backupCount
              self.utc = utc
              # Calculate the real rollover interval, which is just the number of
              # seconds between rollovers.  Also set the filename suffix used when
              # a rollover occurs.  Current 'when' events supported:
              # S - Seconds
              # M - Minutes
              # H - Hours
              # D - Days
              # midnight - roll over at midnight
              # W{0-6} - roll over on a certain day; 0 - Monday
              #
              # Case of the 'when' specifier is not important; lower or upper case
              # will work.
              if self.when == 'S':
                  self.suffix = "%Y-%m-%d_%H-%M-%S"
                  self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$"
              elif self.when == 'M':
                  self.suffix = "%Y-%m-%d_%H-%M"
                  self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}$"
              elif self.when == 'H':
                  self.suffix = "%Y-%m-%d_%H"
                  self.extMatch = r"^\d{4}-\d{2}-\d{2}_\d{2}$"
              elif self.when == 'D' or self.when == 'MIDNIGHT':
                  self.suffix = "%Y-%m-%d"
                  self.extMatch = r"^\d{4}-\d{2}-\d{2}$"
              elif self.when.startswith('W'):
                  if len(self.when) != 2:
                      raise ValueError("You must specify a day for weekly rollover from 0 to 6 (0 is Monday): %s" % self.when)
                  if self.when[1] < '0' or self.when[1] > '6':
                      raise ValueError("Invalid day specified for weekly rollover: %s" % self.when)
                  self.dayOfWeek = int(self.when[1])
                  self.suffix = "%Y-%m-%d"
                  self.extMatch = r"^\d{4}-\d{2}-\d{2}$"
              else:
                  raise ValueError("Invalid rollover interval specified: %s" % self.when)
              self.extMatch = re.compile(self.extMatch)
              if interval != 1:
                  raise ValueError("Invalid rollover interval, must be 1")
          def shouldRollover(self, record):
              """
              Determine if rollover should occur.
              record is not used, as we are just comparing times, but it is needed so
              the method signatures are the same
              """
              if not os.path.exists(self.baseFilename):
                  #print "file don't exist"  
                  return 0 
              cTime = time.localtime(time.time()) 
              mTime = time.localtime(os.stat(self.baseFilename)[ST_MTIME])
              if self.when == "S" and cTime[5] != mTime[5]:
                  #print "cTime:", cTime[5], "mTime:", mTime[5]
                  return 1
              elif self.when == 'M' and cTime[4] != mTime[4]:  
                  #print "cTime:", cTime[4], "mTime:", mTime[4]
                  return 1  
              elif self.when == 'H' and cTime[3] != mTime[3]: 
                  #print "cTime:", cTime[3], "mTime:", mTime[3]
                  return 1
              elif (self.when == 'MIDNIGHT' or self.when == 'D') and cTime[2] != mTime[2]:
                  #print "cTime:", cTime[2], "mTime:", mTime[2]
                  return 1 
              elif self.when == 'W' and cTime[1] != mTime[1]:
                  #print "cTime:", cTime[1], "mTime:", mTime[1]
                  return 1
              else:  
                  return 0 
          def doRollover(self):
              """
              do a rollover; in this case, a date/time stamp is appended to the filename
              when the rollover happens.  However, you want the file to be named for the
              start of the interval, not the current time.  If there is a backup count,
              then we have to get a list of matching filenames, sort them and remove
              the one with the oldest suffix.
              For multiprocess, we use shutil.copy instead of rename.
              """
              if self.stream:
                  self.stream.close()
              # get the time that this sequence started at and make it a TimeTuple
              #t = self.rolloverAt - self.interval
              t = int(time.time())
              if self.utc:
                  timeTuple = time.gmtime(t)
              else:
                  timeTuple = time.localtime(t)
              dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
              if os.path.exists(dfn):
                  os.remove(dfn)
              if os.path.exists(self.baseFilename):
                  shutil.copy(self.baseFilename, dfn)
                  #print "%s -> %s" % (self.baseFilename, dfn)
                  #os.rename(self.baseFilename, dfn)
              if self.backupCount > 0:
                  # find the oldest log file and delete it
                  #s = glob.glob(self.baseFilename + ".20*")
                  #if len(s) > self.backupCount:
                  #    s.sort()
                  #    os.remove(s[0])
                  for s in self.getFilesToDelete():
                      os.remove(s)
              self.mode = 'w'
              self.stream = self._open()
          def emit(self, record):
              """
              Emit a record.
              Output the record to the file, catering for rollover as described
              in doRollover().
              For multiprocess, we use file lock. Any better method ?
              """
              try:
                  if self.shouldRollover(record):
                      self.doRollover()
                  FileLock = self._lock_dir + '/' + os.path.basename(self.baseFilename) + '.' + record.levelname
                  f = open(FileLock, "w+")
                  fcntl.flock(f.fileno(), fcntl.LOCK_EX)
                  FileHandler_MP.emit(self, record)
                  fcntl.flock(f.fileno(), fcntl.LOCK_UN)
                  f.close()
              except (KeyboardInterrupt, SystemExit):
                  raise
              except:
                  self.handleError(record)

      image.gif

      修改的地方:

      import fcntl,cPickle

      fcntl windows下载不到,她是用于linux下的用于文件锁的模块。

      except IOError, e:
          pass
      ================ 修改为
      except :
          pass

      image.gif

      关键是这部分代码

      f = open(FileLock, "w+")
      fcntl.flock(f.fileno(), fcntl.LOCK_EX)
      FileHandler_MP.emit(self, record)
      fcntl.flock(f.fileno(), fcntl.LOCK_UN)
      f.close()

      image.gif

      fcntl.flock(f.fileno(), fcntl.LOCK_EX) 意思是给文件加锁

      fcntl.flock(f.fileno(), fcntl.LOCK_UN)意思是给文件解锁

      这边是利用fcntl来实现文件加锁,大家可以从网上随便找一个文件加锁的工具类,替换一下即可。

      我随便从网上找了一个https://my.oschina.net/1123581321/blog/833480

      换一下即可

      多线程情况下日志不在丢失错乱但性能较低(相当于强制单线程,毕竟文件只有一个,加锁的话,只能线程一个一个排队进行修改),我们可以思考一下还有什么方法能解决这个问题?

      参考:

      python logging详解及自动添加上下文信息 - xybaby - 博客园

      Python + logging 输出到屏幕,将log日志写入文件 - nancy05 - 博客园

      读懂掌握 Python logging 模块源码 (附带一些 example) - piperck - 博客园

      https://avaj.iteye.com/blog/246088


      相关实践学习
      日志服务之使用Nginx模式采集日志
      本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
      相关文章
      |
      2月前
      |
      安全 数据处理 开发者
      Python中的多线程编程:从入门到精通
      本文将深入探讨Python中的多线程编程,包括其基本原理、应用场景、实现方法以及常见问题和解决方案。通过本文的学习,读者将对Python多线程编程有一个全面的认识,能够在实际项目中灵活运用。
      |
      24天前
      |
      数据采集 存储 数据处理
      Python中的多线程编程及其在数据处理中的应用
      本文深入探讨了Python中多线程编程的概念、原理和实现方法,并详细介绍了其在数据处理领域的应用。通过对比单线程与多线程的性能差异,展示了多线程编程在提升程序运行效率方面的显著优势。文章还提供了实际案例,帮助读者更好地理解和掌握多线程编程技术。
      |
      27天前
      |
      监控 开发者 Python
      Python 默认 `logging` 打印级别详解
      本文详细介绍了 Python `logging` 模块的默认打印级别及其配置方法。`logging` 模块支持 `DEBUG`、`INFO`、`WARNING`、`ERROR` 和 `CRITICAL` 五个日志级别,默认级别为 `WARNING`。文章通过示例代码展示了如何设置和使用不同日志级别,并介绍了进一步的配置选项,如日志格式和文件输出。
      48 8
      |
      1月前
      |
      并行计算 数据处理 调度
      Python中的并发编程:探索多线程与多进程的奥秘####
      本文深入探讨了Python中并发编程的两种主要方式——多线程与多进程,通过对比分析它们的工作原理、适用场景及性能差异,揭示了在不同应用需求下如何合理选择并发模型。文章首先简述了并发编程的基本概念,随后详细阐述了Python中多线程与多进程的实现机制,包括GIL(全局解释器锁)对多线程的影响以及多进程的独立内存空间特性。最后,通过实例演示了如何在Python项目中有效利用多线程和多进程提升程序性能。 ####
      |
      1月前
      |
      Java Unix 调度
      python多线程!
      本文介绍了线程的基本概念、多线程技术、线程的创建与管理、线程间的通信与同步机制,以及线程池和队列模块的使用。文章详细讲解了如何使用 `_thread` 和 `threading` 模块创建和管理线程,介绍了线程锁 `Lock` 的作用和使用方法,解决了多线程环境下的数据共享问题。此外,还介绍了 `Timer` 定时器和 `ThreadPoolExecutor` 线程池的使用,最后通过一个具体的案例展示了如何使用多线程爬取电影票房数据。文章还对比了进程和线程的优缺点,并讨论了计算密集型和IO密集型任务的适用场景。
      74 4
      |
      29天前
      |
      监控 JavaScript 前端开发
      python中的线程和进程(一文带你了解)
      欢迎来到瑞雨溪的博客,这里是一位热爱JavaScript和Vue的大一学生分享技术心得的地方。如果你从我的文章中有所收获,欢迎关注我,我将持续更新更多优质内容,你的支持是我前进的动力!🎉🎉🎉
      23 0
      |
      29天前
      |
      数据采集 Java Python
      爬取小说资源的Python实践:从单线程到多线程的效率飞跃
      本文介绍了一种使用Python从笔趣阁网站爬取小说内容的方法,并通过引入多线程技术大幅提高了下载效率。文章首先概述了环境准备,包括所需安装的库,然后详细描述了爬虫程序的设计与实现过程,包括发送HTTP请求、解析HTML文档、提取章节链接及多线程下载等步骤。最后,强调了性能优化的重要性,并提醒读者遵守相关法律法规。
      61 0
      |
      2月前
      |
      存储 消息中间件 资源调度
      C++ 多线程之初识多线程
      这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
      58 1
      C++ 多线程之初识多线程
      |
      2月前
      |
      Java 开发者
      在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
      【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
      27 3
      |
      2月前
      |
      Java 开发者
      在Java多线程编程中,选择合适的线程创建方法至关重要
      【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
      23 2
      下一篇
      DataWorks