python协程学习——写个并发获取网站标题的工具
平时做渗透的时候,有时候给的是一些域名、一些 url 、一些 ip 或者三者都有,手动去一个个地打开比较浪费时间。我们需要用最短时间发现一些有趣的目标,如 xx 管理后台。于是让我们用 python 的协程来写个并发获取网站标题的工具吧,还可以顺便学习下协程的使用。
——人生苦短,我用python
1. 需求分析
先对工具做个需求分析:
可以并发获取标题,并且可以根据网络速度设置协程数目。
可以读取指定文件中的 url 、域名和 ip 来获取标题。
对于 ip 列表,需要支持 CIDR 格式的 ip 地址段,也就是可以解析如 192.168.1.0/24 这样的 C 段地址来获取标题。
可以把存在标题的网站输出到文件中,也就是80和443端口存在 web 应用的 url 和标题输出到指定的文件中。
程序具有复用性,也就是可以很方便地集成到以后开发的工具中。
2. 原理介绍
在开始开发前,先来解释下什么是协程,它和线程有什么区别。
协程,又称微线程,纤程,英文名Coroutine。协程在执行函数A时,可以随时中断,去执行函数B,接着继续执行函数A。但这一过程并不是函数调用,有点类似CPU的中断。这一整个过程看起来有点像多线程。
比如子程序A、B:
def A():print '1'print '2'print '3'
def B():print 'x'print 'y'print 'z'
假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,结果可能是:
1
2
x
y
3
z
看起来A、B的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有什么优势呢?
协程具有极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。
因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
协程主要用来处理异步 IO,异步 IO 是指非阻塞的资源读取。如在发起一个网络请求时,由于需要下载完数据才能完成读取,这需要一段时间,通常这个时候会一直处于阻塞状态,如果在UI线程中执行这种阻塞的操作,还会使程序卡死。
而异步 IO 中存在一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程: 遇到IO操作时,代码只负责发出IO请求,不等待IO结果,然后直接结束本轮消息处理,进入下一轮消息处理过程。当IO操作完成后,将收到一条“IO完成”的消息,处理该消息时就可以直接获取IO操作结果。
简单来说,可以把异步IO理解成一个圆形循环的工厂流水线,流水线上有一个工人,当工人拿到一个零件后,开始处理,接着遇到了个阻塞的操作,无法继续处理了,于是工人发出一个IO请求,然后把零件放回流水线上,去处理下一个零件。在之前阻塞的零件转回到工人面前时,该零件已经完成了IO请求,于是工人继续处理零件剩下的步骤。
asyncio 是用来编写并发代码的库,使用 async/await 语法, 被用作多个提供高性能 Python 异步框架的基础,包括网络和网站服务,数据库连接库,分布式任务队列等等。asyncio 往往是构建 IO 密集型和高层级结构化网络代码的最佳选择。
在 python 3.5 以后,可以通过 async 关键字来定义一个函数为协程。然后通过 await 关键词来等待一个可等待对象,这个对象一般为协程。
如下代码所示, asyncio.sleep() 是一个可等待对象。
async def hello():print("Hello world!")r = await asyncio.sleep(1)print("Hello again!")
定义协程后,可以通过 asyncio.run() 运行传入的协程,此函数还负责管理 asyncio 事件循环并 完结异步生成器。
async def main():await asyncio.sleep(1)print('hello')
asyncio.run(main())
为了完成并发操作,我们可以创建多个任务来并发执行协程,如果需要同时运行20个协程,则可以通过创建20个任务来运行协程。
asyncio.create_task() 函数将协程 打包为一个 Task 排入日程准备执行。返回 Task 对象。当一个协程通过 asyncio.create_task() 等函数被打包为一个任务,该协程将自动排入日程准备立即运行:
import asyncio
async def nested():return 42
async def main():# Schedule nested() to run soon concurrently# with "main()".task = asyncio.create_task(nested())
# "task" can now be used to cancel "nested()", or# can simply be awaited to wait until it is complete:await task
asyncio.run(main())
注意,create_task() 函数在 Python 3.7 中被加入。在 Python 3.7 之前,可以改用低层级的 asyncio.ensure_future() 函数,但可读性不高。
要并发运行多个任务,可以使用
asyncio.gather(*aws, loop=None, return_exceptions=False) 方法,
该方法可并发运行 aws 序列中的可等待对象,直到所有任务都结束。下面是并发执行任务的一个例子:
import asyncio
import random
import time
async def worker(name, queue):while True:# Get a "work item" out of the queue.sleep_for = await queue.get()
# Sleep for the "sleep_for" seconds.await asyncio.sleep(sleep_for)
# Notify the queue that the "work item" has been processed.queue.task_done()
print(f'{name} has slept for {sleep_for:.2f} seconds')
async def main():# Create a queue that we will use to store our "workload".queue = asyncio.Queue()
# Generate random timings and put them into the queue.total_sleep_time = 0for _ in range(20):sleep_for = random.uniform(0.05, 1.0)total_sleep_time += sleep_forqueue.put_nowait(sleep_for)
# Create three worker tasks to process the queue concurrently.tasks = []for i in range(3):task = asyncio.create_task(worker(f'worker-{i}', queue))tasks.append(task)
# Wait until the queue is fully processed.started_at = time.monotonic()await queue.join()total_slept_for = time.monotonic() - started_at
# Cancel our worker tasks.for task in tasks:task.cancel()# Wait until all worker tasks are cancelled.await asyncio.gather(*tasks, return_exceptions=True)
print('====')print(f'3 workers slept in parallel for {total_slept_for:.2f} seconds')print(f'total expected sleep time: {total_sleep_time:.2f} seconds')
asyncio.run(main())
maiI() 函数也是一个协程,通过最后一行中的 run() 方法来运行,函数中先创建了一个队列,接着往队列中放进了20个0.05至1之间的随机数。
接着通过 create_task() 方法创建了3个任务,这三个任务都用于执行 worker 协程。这三个任务被添加到 tasks 数组中。
其中的 worker() 函数是一个协程,该函数从队列中获取一个要处理的数据,在处理完后,调用 queue 的 task_done() 函数来通知该数据已经处理完。这样 queue 中已完成的任务数会减一。
此时3个任务已经开始并发运行,接着调用 queue.join() 来等待队列中的所有数据被处理完,也就是所有元素数量的 task_done() 被调用。
当队列中的数据被处理完后,把3个任务都取消,但由于任务的 cancel() 函数被调用后不会马上被取消,而是要等到下一个消息循环,所以需要调用 gather() 函数等待所有任务结束。
3. 工具实现
为了实现可重用性,我们可以创建个 WebTitle 类来运行任务。
构造函数中的 urls 为需要并发获取标题的 url, coroutine_count 为协程数目。result 是个字典,通过键值的方式存储 url 和相应的标题。
class WebTitle:def __init__(self, urls, coroutine_count=20):self.urls = urlsself.coroutine_count = coroutine_countself.result = {}
接着定义个 start() 方法来启动并发获取 url 标题的协程任务, 其中的
asyncio.run() 启动一个消息循环并开始运行 self.start_task() 方法。
def start(self):asyncio.run(self.start_task())
start_task() 方法是一个协程, 先调用 self.init_queue() 方法来生成所有 url 的队列,然后根据指定的协程数来生成相应数目的task,每个task 都会运行 get_title() 函数 。接着调用 queue.join() 来等待队列中的所有 url 被处理完。url 被处理完后,把所有任务都取消,然后等待所有任务都取消完。
def init_queue(self):queue = asyncio.Queue()for url in self.urls:queue.put_nowait(url)return queueasync def start_task(self):queue = self.init_queue()tasks = []for i in range(self.coroutine_count):task = asyncio.create_task(self.get_title(queue))tasks.append(task)
await queue.join()
for task in tasks:task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
get_title() 函数是一个协程,在一个 while 循环中一直取 url 出来处理,接着使用 aiohttp 库来获取网页源码。aiohttp 为异步 http 库,通过 await 来等待网络请求完成并获取网页源码。
获取完源码后调用 get_title_from_html() 函数来获取网页的标题,最后把 url 和标题保存在 result 字典中。
最后调用 queue.task_done() 来通知该 url 已经处理完成,以便前面的 queue.join() 函数最后可以解除阻塞。
def get_title_from_html(self, html):title = 'not content!'title_patten = r'<title>(\s*?.*?\s*?)</title>'result = re.findall(title_patten, html)if len(result) >= 1:title = result[0]title = title.strip()return title
async def get_title(self, queue):while True:url = await queue.get()print('get title for {}'.format(url))try:async with aiohttp.ClientSession() as session:async with session.get(url, timeout=3, ssl=ssl.SSLContext()) as resp:html = await resp.text()title = self.get_title_from_html(html)print('{}:{}'.format(url,title))self.result[url] = titleexcept Exception as e:print('{} has error: {} '.format(url,str(e))) queue.task_done()
获取完网页标题后,把所有结果写进 csv 文件中。
def write_result(self, outfile):with open(outfile, 'w') as f:writer = csv.writer(f)writer.writerow(['url','title'])urls = self.result.keys()for url in urls:title = self.result[url]writer.writerow([url, title])print('result write to {}'.format(outfile))
到这里WebTitle 类就实现完成了,接下来写个 main() 函数来解析文件中的内容并生成 url 来给 webtitle 实例来获取标题。
def parse_args():parser = argparse.ArgumentParser(description='A tool that can get title for domains or urls')parser.add_argument('-d','--domain', metavar='domain.txt', dest='domain_file', type=str, help=u'domain to get title')parser.add_argument('-u','--url', metavar='url.txt', dest='url_file', type=str, help=u'urls to get title')parser.add_argument('-i','--ip', metavar='ip.txt', dest='ip_file', type=str, help=u'ips to get title')parser.add_argument('-t','--coroutine', metavar='20', dest='coroutine_count', type=int, default=20,help=u'coroutines to get title')parser.add_argument('-o','--outfile', metavar='result.txt', dest='outfile', type=str, default='result.csv',help=u'file to result')args = parser.parse_args()if args.url_file == None and args.domain_file == None and args.ip_file == None:parser.print_help()sys.exit()return args
def main():try:args = parse_args()urls = []
if args.domain_file:with open(args.domain_file) as f:domains = f.readlines()for domain in domains:domain = domain.strip()if domain != '':urls.append('http://' + domain)urls.append('https://' + domain)
if args.url_file:with open(args.url_file) as f:urls2 = f.readlines()for url in urls2:url = url.strip()if url != '':urls.append(url)
if args.ip_file:with open(args.ip_file) as f:ips = f.readlines()for ip in ips:ip = ip.strip()if ip != '':cidr_ip = IPy.IP(ip)for i in cidr_ip:urls.append('http://' + str(i))urls.append('https://' + str(i))
web_title = WebTitle(urls, args.coroutine_count)web_title.start()web_title.write_result(args.outfile)except Exception as e:print(e)
4. 工具用法
该工具仅在 python 3.7 测试,可以稳定使用, python 3.8 还不是稳定版本,3.8 的协程有有 bug ,建议在 3.5 - 3.7 中使用。
optional arguments:
-h, --help show this help message and exit
-d domain.txt, --domain domain.txt
domain to get title
-u url.txt, --url url.txt
urls to get title
-i ip.txt, --ip ip.txt
ips to get title
-t 20, --coroutine 20
coroutines to get title
-o result.txt, --outfile result.txt
file to result
# 指定要获取标题的域名列表文件
python3 web_title.py -d domain.txt
# 指定 url 文件,格式为 http://www.baidu.com
python3 web_title.py -u url.txt`在这里插入代码片`
# 指定 ip 文件,格式为 192.168.1.1 或 192.168.1.1/24
python3 web_title.py -i ip.txt
# 同时指定三种格式的文件
python3 web_title.py -i ip.txt -d domain.txt -u url.txt
# 指定协程数
python3 web_title.py -u url.txt -t 50
5. 工具使用截图
需要工具完整源码的,请在公众号后台回复 web_tilte 获取。
本文章也在我的公众号发布、
python协程学习——写个并发获取网站标题的工具相关推荐
- python协程学习
学习知识点: 1.知识点叫什么 2.知识点用在哪 3.知识如何实现 一. 线程.进程.协程傻傻分不清楚 1.进程 :启动多个进程 进程之间是由操作系统负责调用 线程 :启动多个线程 真正被cpu执 ...
- python协程实现一万并发_python中的协程并发
python asyncio 网络模型有很多中,为了实现高并发也有很多方案,多线程,多进程.无论多线程和多进程,IO的调度更多取决于系统,而协程的方式,调度来自用户,用户可以在函数中yield一个状态 ...
- python协程实现一万并发_python进阶:服务端实现并发的八种方式
[本文导读]文中有许多不妥之处,敬请批评指正!python编写的服务端,有八种实现并发的方式,如阻塞(对等)套接字实现并发.非阻塞套接字实现并发.epoll实现并发.多进程实现并发.多线程实现并发.进 ...
- Python协程(真才实学,想学的进来)
真正有知识的人的成长过程,就像麦穗的成长过程:麦穗空的时候,麦子长得很快,麦穗骄傲地高高昂起,但是,麦穗成熟饱满时,它们开始谦虚,垂下麦芒. --蒙田<蒙田随笔全集> 在这里还是要推荐下我 ...
- python gevent async_谈谈Python协程技术的演进
原标题:谈谈Python协程技术的演进 Coding Crush Python开发工程师 主要负责岂安科技业务风险情报系统redq. 引言 1.1. 存储器山 存储器山是 Randal Bryant ...
- python协程和异步编程
文章目录 协程 & 异步编程(asyncio) 1. 协程的实现 1.1 greenlet 1.2 yield 1.3 asyncio 1.4 async & awit 1.5 小结 ...
- Python协程原理介绍及基本使用
目录 1.什么是协程? 2.协程运行主要原理 3.小结 1.什么是协程? 协程是实现并发编程的一种方式.一说到并发,你肯定想到了多线程 / 多进程模型,没错,多线程 / 多进程,正是解决并发问题的经典 ...
- python协程详解_彻底搞懂python协程-第一篇(关键词1-4)
任何复杂的概念或系统都不是凭空出现的,我们完全可以找到它的演化历程,寻根究底终会发现,其都是在一系列并不那么复杂的简单组件上发展演化而来! by 落花僧 本文通过一系列关键概念,逐步递进理解协程. 0 ...
- 夜来风雨声,Python协程知多少
最近有很多的同学问,大家都知道多线程,多进程,那么这个协程有什么什么东西?难不成还是携程旅游(此处没有广告费)?能不能说一下Python协程,而且最好要讲清楚! 那行,今天将来讲解一下Python协程 ...
最新文章
- php windows应用开发,开发老手谈Windows平台的PHP应用开发
- 用python画烟花-python 实现漂亮的烟花,樱花,玫瑰花
- 入门顶点动画纹理的实例化绘制
- java基础格式_Java基础之代码的基本格式
- mysql pt_MySQL慢查询之pt-query-digest分析慢查询日志
- linux清理整个磁盘空间,一次Linux磁盘空间清理的经历
- 数据结构期末复习之二叉排序树
- 几何修复_*ST海润:实施终止退市 光伏产业修复成几何?
- Effective C# 原则33:限制类型的访问(译)
- java kinect 人体识别_基于三个kinect的人体建模
- 『数据可视化』基于Python的数据可视化工具
- 图像处理之理想低通滤波器、巴特沃斯低通滤波器和高斯低通滤波器的matlab实现去噪
- Linux 优秀软件资源大全中文版
- php实现两张图片合成一张,如何把两张图片拼成一张
- Make the Team(匈牙利算法)
- error: You must be logged in to the server (Unauthorized)
- spark 读取本地文件
- 首款双频GNSS智能手机进入市场
- [9i] 猪年说猪,属相,本命年,十二生肖用英语该怎么说
- 神技能:Python控制键盘鼠标
热门文章
- linux putty 标题栏显示ip putty本地设置 服务器设置
- linux 内核 目录项高速缓存 dentry cache 简介
- linux shell 单行多行注释
- golang 切片的三种简单使用方式及区别
- (导航页)Amazon Lightsail 部署LAMP应用程序(HA)
- linux kernel makefile analysis
- matlab识别不出linux链接,在Ubuntu上,从matlab调用外部脚本失败_linux_开发99编程知识库...
- java 调用word插件_java一键生成word操作,比poi简单
- pg多行合并为逗号分隔一行_postgresql 将逗号分隔的字符串转为多行的实例_PostgreSQL_数据库...
- java linux urlencode_java字符编码转换研究(转)