一、简介

  为了提高爬虫程序效率,由于python解释器GIL,导致同一进程中即使有多个线程,实际上也只会有一个线程在运行,但通过request.get发送请求获取响应时有阻塞,所以采用了多线程依然可以提高爬虫效率。

多线程爬虫注意点
1.解耦

  整个程序分为4部分,url list模块、发送请求,获取响应模块、数据提取模块、保存模块,如果某一模块出现问题,互相之间不会影响。

2. 资源竞争

  由于使用了多线程,不同线程在共享数据时,容易产生资源竞争,假设共享数据放入列表中,那么同一时刻有可能2个线程去列表中取同一个数据,重复使用。解决办法是使用队列,使得某一线程get数据时,其他线程无法get同一数据,真正起到保护作用,类似互斥锁。

队列常用方法介绍

  1. from queue import Queue
  2.  
  3. q = Queue()
  4. q.put(url)
  5. q.get() # 当队列为空时,阻塞
  6. q.empty() # 判断队列是否为空,True/False

注意:

  • get和get_nowait两者的区别是当队列取完了即队列为空时,get()会阻塞,等待着新数据继续取,而get_nowait()会报错;
  • put和put_nowait 两者的区别是当队列为满时,put_nowait()会报错;

队列其他方法join task_done setDaemon

  • 在python3中,join()会等待子线程、子进程结束之后,主线程、主进程才会结束.
  • 队列中put队列计数会+1,get时计数不会减1,但当get+task_done时,队列计数才会减1,如果没有task_done则程序跑到最后不会终止。task_done()的位置,应该放在方法的最后以保证所有任务全部完成.
  • setDaemon方法把子线程设置为守护线程,即认为该方法不是很重要,记住主线程结束,则该子线程结束
  • join方法和setDaemon方法搭配使用。主线程进行到join()处,join的效果是让主线程阻塞,等待子线程中队列任务完成之后再解阻塞,等子线程结束,join效果失效,之后主线程结束,由于使用了setDaemon(True),所以子线程跟着结束,此时整个程序结束。

 

线程模块

  1. from threading import Thread

  2. # 使用流程
  3. t = Thread(target=函数名) # 创建线程对象
  4. t.start() # 创建并启动线程
  5. t.join() # 阻塞等待回收线程

应用场景

  • 多进程 :CPU密集程序
  • 多线程 :爬虫(网络I/O)、本地磁盘I/O

二、案例

1. 小米应用商店抓取

目标

  1. 网址 :百度搜 - 小米应用商店,进入官网,应用分类 - 聊天社交
  2. 目标 :爬取应用名称和应用链接

实现步骤

1、确认是否为动态加载:页面局部刷新,查看网页源代码,搜索关键字未搜到,因此此网站为动态加载网站,需要抓取网络数据包分析

2、抓取网络数据包

  • 抓取返回json数据的URL地址(Headers中的Request URL)http://app.mi.com/categotyAllListApi?page={}&categoryId=2&pageSize=30
  • 查看并分析查询参数(headers中的Query String Parameters)只有page在变,0 1 2 3 ... ... ,这样我们就可以通过控制page的值拼接多个返回json数据的URL地址
  1. page: 1
  2. categoryId: 2
  3. pageSize: 30

3、将抓取数据保存到csv文件。注意多线程写入的线程锁问题

  1. from threading import Lock
  2.  
  3. lock = Lock()
  4. lock.acquire()
  5. lock.release()

整体实现思路

  1. 在 __init__(self) 中创建文件对象,多线程操作此对象进行文件写入;
  2. 每个线程抓取数据后将数据进行文件写入,写入文件时需要加锁;
  3. 所有数据抓取完成关闭文件;
  1. import requests
  2. from threading import Thread
  3. from queue import Queue
  4. import time
  5. from lxml import etree
  6. import csv
  7. from threading import Lock
  8.  
  9. class XiaomiSpider(object):
  10. def __init__(self):
  11. self.url = 'http://app.mi.com/categotyAllListApi?page={}&categoryId={}&pageSize=30'
  12. self.ua = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1'}
  13. self.q = Queue() # 存放所有URL地址的队列
  14. self.i = 0
  15. self.id_list = [] # 存放所有类型id的空列表
  16. # 打开文件
  17. self.f = open('xiaomi.csv', 'a', newline="")
  18. self.writer = csv.writer(self.f)
  19. self.lock = Lock() # 创建锁
  20.  
  21. def get_cateid(self):
  22. url = 'http://app.mi.com/'
  23. html = requests.get(url=url, headers=self.ua).text
  24.  
  25. parse_html = etree.HTML(html)
  26. li_list = parse_html.xpath('//ul[@class="category-list"]/li')
  27. for li in li_list:
  28. typ_name = li.xpath('./a/text()')[0]
  29. typ_id = li.xpath('./a/@href')[0].split('/')[-1]
  30. pages = self.get_pages(typ_id) # 计算每个类型的页数
  31. self.id_list.append((typ_id, pages))
  32.  
  33. self.url_in() # 入队列
  34.  
  35. def get_pages(self, typ_id):
  36. # 每页返回的json数据中,都有count这个key
  37. url = self.url.format(0, typ_id)
  38. html = requests.get(url=url, headers=self.ua).json()
  39. count = html['count'] # 类别中的数据总数
  40. pages = int(count) // 30 + 1 # 每页30个,看有多少页
  41.  
  42. return pages
  43.  
  44. # url入队列
  45. def url_in(self):
  46. for id in self.id_list:
  47. # id为元组,(typ_id, pages)-->('2',pages)
  48. for page in range(2):
  49. url = self.url.format(page, id[0])
  50. print(url)
  51. # 把URL地址入队列
  52. self.q.put(url)
  53.  
  54. # 线程事件函数: get() - 请求 - 解析 - 处理数据
  55. def get_data(self):
  56. while True:
  57. # 当队列不为空时,获取url地址
  58. if not self.q.empty():
  59. url = self.q.get()
  60. html = requests.get(url=url, headers=self.ua).json()
  61. self.parse_html(html)
  62. else:
  63. break
  64.  
  65. # 解析函数
  66. def parse_html(self, html):
  67. # 存放1页的数据 - 写入到csv文件
  68. app_list = []
  69. for app in html['data']:
  70. # 应用名称 + 链接 + 分类
  71. name = app['displayName']
  72. link = 'http://app.mi.com/details?id=' + app['packageName']
  73. typ_name = app['level1CategoryName']
  74. # 把每一条数据放到app_list中,目的为了 writerows()
  75. app_list.append([name, typ_name, link])
  76. print(name, typ_name)
  77. self.i += 1
  78.  
  79. # 开始写入1页数据 - app_list
  80. self.lock.acquire()
  81. self.writer.writerows(app_list)
  82. self.lock.release()
  83. # 主函数
  84. def main(self):
  85. self.get_cateid() # URL入队列
  86. t_list = []
  87.  
  88. # 创建多个线程
  89. for i in range(1):
  90. t = Thread(target=self.get_data)
  91. t_list.append(t)
  92. t.start()
  93.  
  94. # 统一回收线程
  95. for t in t_list:
  96. t.join()
  97.  
  98. # 关闭文件
  99. self.f.close()
  100. print('数量:', self.i)
  101.  
  102. if __name__ == '__main__':
  103. start = time.time()
  104. spider = XiaomiSpider()
  105. spider.main()
  106. end = time.time()
  107. print('执行时间:%.2f' % (end - start))

2.腾讯招聘数据抓取(Ajax)

确定URL地址及目标

要求与分析

  1. 通过查看网页源码,得知所需数据均为 Ajax 动态加载
  2. 通过F12抓取网络数据包,进行分析
  3. 一级页面抓取数据: 职位名称
  4. 二级页面抓取数据: 工作职责、岗位要求

一级页面json地址(pageIndex在变,timestamp未检查)

  1. https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1563912271089&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex={}&pageSize=10&language=zh-cn&area=cn

二级页面地址(postId在变,在一级页面中可拿到)

  1. https://careers.tencent.com/tencentcareer/api/post/ByPostId?timestamp=1563912374645&postId={}&language=zh-cn

useragents.py文件

  1. ua_list = [
  2. 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1',
  3. 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:6.0) Gecko/20100101 Firefox/6.0',
  4. 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; InfoPath.3)',
  5. ]

非多线程爬取

  1. import time
  2. import json
  3. import random
  4. import requests
  5. from useragents import ua_list
  6.  
  7. class TencentSpider(object):
  8. def __init__(self):
  9. self.one_url = 'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1563912271089&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex={}&pageSize=10&language=zh-cn&area=cn'
  10. self.two_url = 'https://careers.tencent.com/tencentcareer/api/post/ByPostId?timestamp=1563912374645&postId={}&language=zh-cn'
  11. self.f = open('tencent.json', 'a') # 打开文件
  12. self.item_list = [] # 存放抓取的item字典数据
  13.  
  14. # 获取响应内容函数
  15. def get_page(self, url):
  16. headers = {'User-Agent': random.choice(ua_list)}
  17. html = requests.get(url=url, headers=headers).text
  18. html = json.loads(html) # json格式字符串转为Python数据类型
  19.  
  20. return html
  21.  
  22. # 主线函数: 获取所有数据
  23. def parse_page(self, one_url):
  24. html = self.get_page(one_url)
  25. item = {}
  26. for job in html['Data']['Posts']:
  27. item['name'] = job['RecruitPostName'] # 名称
  28. post_id = job['PostId'] # postId,拿postid为了拼接二级页面地址
  29. # 拼接二级地址,获取职责和要求
  30. two_url = self.two_url.format(post_id)
  31. item['duty'], item['require'] = self.parse_two_page(two_url)
  32. print(item)
  33. self.item_list.append(item) # 添加到大列表中
  34.  
  35. # 解析二级页面函数
  36. def parse_two_page(self, two_url):
  37. html = self.get_page(two_url)
  38. duty = html['Data']['Responsibility'] # 工作责任
  39. duty = duty.replace('\r\n', '').replace('\n', '') # 去掉换行
  40. require = html['Data']['Requirement'] # 工作要求
  41. require = require.replace('\r\n', '').replace('\n', '') # 去掉换行
  42.  
  43. return duty, require
  44.  
  45. # 获取总页数
  46. def get_numbers(self):
  47. url = self.one_url.format(1)
  48. html = self.get_page(url)
  49. numbers = int(html['Data']['Count']) // 10 + 1 # 每页有10个推荐
  50.  
  51. return numbers
  52.  
  53. def main(self):
  54. number = self.get_numbers()
  55. for page in range(1, 3):
  56. one_url = self.one_url.format(page)
  57. self.parse_page(one_url)
  58.  
  59. # 保存到本地json文件:json.dump
  60. json.dump(self.item_list, self.f, ensure_ascii=False)
  61. self.f.close()
  62.  
  63. if __name__ == '__main__':
  64. start = time.time()
  65. spider = TencentSpider()
  66. spider.main()
  67. end = time.time()
  68. print('执行时间:%.2f' % (end - start))

多线程爬取

多线程即把所有一级页面链接提交到队列,进行多线程数据抓取

  1. import requests
  2. import json
  3. import time
  4. import random
  5. from useragents import ua_list
  6. from threading import Thread
  7. from queue import Queue
  8.  
  9. class TencentSpider(object):
  10. def __init__(self):
  11. self.one_url = 'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1563912271089&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex={}&pageSize=10&language=zh-cn&area=cn'
  12. self.two_url = 'https://careers.tencent.com/tencentcareer/api/post/ByPostId?timestamp=1563912374645&postId={}&language=zh-cn'
  13. self.q = Queue()
  14. self.i = 0 # 计数
  15.  
  16. # 获取响应内容函数
  17. def get_page(self, url):
  18. headers = {'User-Agent': random.choice(ua_list)}
  19. html = requests.get(url=url, headers=headers).text
  20. # json.loads()把json格式的字符串转为python数据类型
  21. html = json.loads(html)
  22.  
  23. return html
  24.  
  25. # 主线函数: 获取所有数据
  26. def parse_page(self):
  27. while True:
  28. if not self.q.empty():
  29. one_url = self.q.get()
  30. html = self.get_page(one_url)
  31. item = {}
  32. for job in html['Data']['Posts']:
  33. item['name'] = job['RecruitPostName'] # 名称
  34. post_id = job['PostId'] # 拿postid为了拼接二级页面地址
  35. # 拼接二级地址,获取职责和要求
  36. two_url = self.two_url.format(post_id)
  37. item['duty'], item['require'] = self.parse_two_page(two_url)
  38. print(item)
  39. # 每爬取按完成1页随机休眠
  40. time.sleep(random.uniform(0, 1))
  41. else:
  42. break
  43.  
  44. # 解析二级页面函数
  45. def parse_two_page(self, two_url):
  46. html = self.get_page(two_url)
  47. # 用replace处理一下特殊字符
  48. duty = html['Data']['Responsibility']
  49. duty = duty.replace('\r\n', '').replace('\n', '')
  50. # 处理要求
  51. require = html['Data']['Requirement']
  52. require = require.replace('\r\n', '').replace('\n', '')
  53.  
  54. return duty, require
  55.  
  56. # 获取总页数
  57. def get_numbers(self):
  58. url = self.one_url.format(1)
  59. html = self.get_page(url)
  60. numbers = int(html['Data']['Count']) // 10 + 1
  61.  
  62. return numbers
  63.  
  64. def main(self):
  65. # one_url入队列
  66. number = self.get_numbers()
  67. for page in range(1, number + 1):
  68. one_url = self.one_url.format(page)
  69. self.q.put(one_url)
  70.  
  71. t_list = []
  72. for i in range(5):
  73. t = Thread(target=self.parse_page)
  74. t_list.append(t)
  75. t.start()
  76.  
  77. for t in t_list:
  78. t.join()
  79.  
  80. print('数量:', self.i)
  81.  
  82. if __name__ == '__main__':
  83. start = time.time()
  84. spider = TencentSpider()
  85. spider.main()
  86. end = time.time()
  87. print('执行时间:%.2f' % (end - start))

多进程实现

  1. import requests
  2. import json
  3. import time
  4. import random
  5. from useragents import ua_list
  6. from multiprocessing import Process
  7. from queue import Queue
  8.  
  9. class TencentSpider(object):
  10. def __init__(self):
  11. self.one_url = 'https://careers.tencent.com/tencentcareer/api/post/Query?timestamp=1563912271089&countryId=&cityId=&bgIds=&productId=&categoryId=&parentCategoryId=&attrId=&keyword=&pageIndex={}&pageSize=10&language=zh-cn&area=cn'
  12. self.two_url = 'https://careers.tencent.com/tencentcareer/api/post/ByPostId?timestamp=1563912374645&postId={}&language=zh-cn'
  13. self.q = Queue()
  14.  
  15. # 获取响应内容函数
  16. def get_page(self, url):
  17. headers = {'User-Agent': random.choice(ua_list)}
  18. html = requests.get(url=url, headers=headers).text
  19. # json格式字符串 -> Python
  20. html = json.loads(html)
  21.  
  22. return html
  23.  
  24. # 主线函数: 获取所有数据
  25. def parse_page(self):
  26. while True:
  27. if not self.q.empty():
  28. one_url = self.q.get()
  29. html = self.get_page(one_url)
  30. item = {}
  31. for job in html['Data']['Posts']:
  32. # 名称
  33. item['name'] = job['RecruitPostName']
  34. # postId
  35. post_id = job['PostId']
  36. # 拼接二级地址,获取职责和要求
  37. two_url = self.two_url.format(post_id)
  38. item['duty'], item['require'] = self.parse_two_page(two_url)
  39.  
  40. print(item)
  41. else:
  42. break
  43.  
  44. # 解析二级页面函数
  45. def parse_two_page(self, two_url):
  46. html = self.get_page(two_url)
  47. # 用replace处理一下特殊字符
  48. duty = html['Data']['Responsibility']
  49. duty = duty.replace('\r\n', '').replace('\n', '')
  50. # 处理要求
  51. require = html['Data']['Requirement']
  52. require = require.replace('\r\n', '').replace('\n', '')
  53.  
  54. return duty, require
  55.  
  56. # 获取总页数
  57. def get_numbers(self):
  58. url = self.one_url.format(1)
  59. html = self.get_page(url)
  60. numbers = int(html['Data']['Count']) // 10 + 1
  61.  
  62. return numbers
  63.  
  64. def main(self):
  65. # url入队列
  66. number = self.get_numbers()
  67. for page in range(1, number + 1):
  68. one_url = self.one_url.format(page)
  69. self.q.put(one_url)
  70.  
  71. t_list = []
  72. for i in range(4):
  73. t = Process(target=self.parse_page)
  74. t_list.append(t)
  75. t.start()
  76.  
  77. for t in t_list:
  78. t.join()
  79.  
  80. if __name__ == '__main__':
  81. start = time.time()
  82. spider = TencentSpider()
  83. spider.main()
  84. end = time.time()
  85. print('执行时间:%.2f' % (end - start))

基于multiprocessing.dummy线程池的数据爬取

案例:爬取梨视频数据。在爬取和持久化存储方面比较耗时,所以两个都需要多线程

  1. import requests
  2. import re
  3. from lxml import etree
  4. from multiprocessing.dummy import Pool
  5. import random
  6.  
  7. pool = Pool(5) # 实例化一个线程池对象
  8.  
  9. url = 'https://www.pearvideo.com/category_1'
  10. headers = {
  11. 'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36'
  12. }
  13. page_text = requests.get(url=url,headers=headers).text
  14. tree = etree.HTML(page_text)
  15. li_list = tree.xpath('//div[@id="listvideoList"]/ul/li')
  16.  
  17. video_url_list = []
  18. for li in li_list:
  19. detail_url = 'https://www.pearvideo.com/'+li.xpath('./div/a/@href')[0]
  20. detail_page = requests.get(url=detail_url,headers=headers).text
  21. video_url = re.findall('srcUrl="(.*?)",vdoUrl',detail_page,re.S)[0]
  22. video_url_list.append(video_url)
  23.  
  24. # pool.map(回调函数,可迭代对象)函数依次执行对象
  25. video_data_list = pool.map(getVideoData,video_url_list) # 获取视频
  26.  
  27. pool.map(saveVideo,video_data_list) # 持久化存储
  28.  
  29. def getVideoData(url):
  30. return requests.get(url=url,headers=headers).content
  31.  
  32. def saveVideo(data):
  33. fileName = str(random.randint(0,5000))+'.mp4' # 因回调函数只能传一个参数,所以没办法再传名字了,只能自己取名
  34. with open(fileName,'wb') as fp:
  35. fp.write(data)
  36.  
  37. pool.close()
  38. pool.join()

Python爬虫进阶 | 多线程的更多相关文章

  1. Python爬虫进阶五之多线程的用法

    前言 我们之前写的爬虫都是单个线程的?这怎么够?一旦一个地方卡到不动了,那不就永远等待下去了?为此我们可以使用多线程或者多进程来处理. 首先声明一点! 多线程和多进程是不一样的!一个是 thread ...

  2. Python爬虫进阶四之PySpider的用法

    审时度势 PySpider 是一个我个人认为非常方便并且功能强大的爬虫框架,支持多线程爬取.JS动态解析,提供了可操作界面.出错重试.定时爬取等等的功能,使用非常人性化. 本篇内容通过跟我做一个好玩的 ...

  3. Python爬虫进阶一之爬虫框架概述

    综述 爬虫入门之后,我们有两条路可以走. 一个是继续深入学习,以及关于设计模式的一些知识,强化Python相关知识,自己动手造轮子,继续为自己的爬虫增加分布式,多线程等功能扩展.另一条路便是学习一些优 ...

  4. Python爬虫进阶三之Scrapy框架安装配置

    初级的爬虫我们利用urllib和urllib2库以及正则表达式就可以完成了,不过还有更加强大的工具,爬虫框架Scrapy,这安装过程也是煞费苦心哪,在此整理如下. Windows 平台: 我的系统是 ...

  5. Python爬虫进阶之Scrapy框架安装配置

    Python爬虫进阶之Scrapy框架安装配置 初级的爬虫我们利用urllib和urllib2库以及正则表达式就可以完成了,不过还有更加强大的工具,爬虫框架Scrapy,这安装过程也是煞费苦心哪,在此 ...

  6. python爬虫之多线程、多进程+代码示例

    python爬虫之多线程.多进程 使用多进程.多线程编写爬虫的代码能有效的提高爬虫爬取目标网站的效率. 一.什么是进程和线程 引用廖雪峰的官方网站关于进程和线程的讲解: 进程:对于操作系统来说,一个任 ...

  7. 芝麻软件: Python爬虫进阶之爬虫框架概述

    综述 爬虫入门之后,我们有两条路可以走. 一个是继续深入学习,以及关于设计模式的一些知识,强化Python相关知识,自己动手造轮子,继续为自己的爬虫增加分布式,多线程等功能扩展.另一条路便是学习一些优 ...

  8. Python爬虫之多线程下载豆瓣Top250电影图片

    爬虫项目介绍   本次爬虫项目将爬取豆瓣Top250电影的图片,其网址为:https://movie.douban.com/top250, 具体页面如下图所示:   本次爬虫项目将分别不使用多线程和使 ...

  9. Python爬虫之多线程下载程序类电子书

      近段时间,笔者发现一个神奇的网站:http://www.allitebooks.com/ ,该网站提供了大量免费的编程方面的电子书,是技术爱好者们的福音.其页面如下:   那么我们是否可以通过Py ...

随机推荐

  1. 魔法 [线段树优化DP]

    也许更好的阅读体验 \(\mathcal{Description}\) 小 \(D\) 正在研究魔法. 小 \(D\) 得到了远古时期的魔法咒语 \(S\),这个咒语共有 \(n\) 个音节,每个音节 ...

  2. linux安装mysql后报错启动不了Starting MySQL. ERROR! The server quit without updating PID file (/var/lib/mysql/localhost.localdomain.pid).

    今天安装完Mysql后,开启发生了错误: 2.打开错误信息文件,查看错误原因是:Plugin 'FEDERATED' is disabled. /usr/sbin/mysqld: Table 'mys ...

  3. IIS7 URL重写如何针对二级域名重写

    二级域名与站点主域名是绑在同一目录下,想实现访问二级域名重写至站点下的某个目录.  如:  访问so.abc.cn 实际访问的是站点根目录下的search目录下的文件 相当于so.abc.cn绑定至s ...

  4. C# 获取系统字体方法

    //需要引用命名空间 using System.Drawing; using System.Drawing.Text; //获取系统字体方法 public dynamic GetFontNames() ...

  5. .net 后台以post方式调用微信公众平台接口

    public class Fresult { public int errcode { get; set; } public string errmsg { get; set; } public st ...

  6. 学习笔记之DBeaver

    DBeaver Community | Free Universal Database Tool https://dbeaver.io/ Universal Database Tool Free mu ...

  7. 令人兴奋的TOP Server OPC Server v6.5 五大功能(上)

    Software Toolbox的OPC和原生HMI设备的连接软件:TOP Server OPC Server.TOP Server OPC Server是采用业界领先的Kepware技术的工业4.0 ...

  8. Spark实现二次排序

    一.代码实现 package big.data.analyse.scala.secondsort import org.apache.log4j.{Level, Logger} import org. ...

  9. union的使用

    将多条select语句的结果,合并到一起,称为联合查询 使用union关键字 场景: 获取数据的条件,出现逻辑冲突,或者很难在一个逻辑内表示,就可以拆成多个逻辑,分别实现,最后将结果合并到一起 sel ...

  10. 关于何时执行shiro AuthorizingRealm 里的 doGetAuthenticationInfo与doGetAuthorizationInfo

    1.doGetAuthenticationInfo执行时机如下 当调用Subject currentUser = SecurityUtils.getSubject(); currentUser.log ...