这篇文章不规范也不完整,重新整理的更详细规范的介绍见这里,
非常不建议阅读下文。
网上aiohttp
做爬虫的资料太少,官网文档是英文的看起来麻烦,所以自己部分半带翻译式的总结下
通过requests
获取html的函数基本上是这样
import requests
def func(url: str) ->str:
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'}
cookies = {'Cookie': ''}
# 这里暂时懒得用session, verify参数忽略https网页的ssl验证
r = requests.get(url, headers=headers, timeout=10, cookies=cookies, verify=False)
r.encoding = r.apparent_encoding # 自动识别网页编码避免中文乱码,但会拖慢程序
return r.text # 或r.content
func('www.sina.com')
用aiohttp
改写
import asyncio
import aiohttp
async def html(url: str) ->str:
code = 'utf-8'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'}
async with aiohttp.ClientSession() as session:
# 老版本aiohttp没有verify参数,如果报错卸载重装最新版本
async with session.get(url, headers=headers, timeout=10, verify_ssl=False) as r:
# text()函数相当于requests中的r.text,r.read()相当于requests中的r.content
return await r.text()
loop = asyncio.get_event_loop()
loop.run_until_complete(html('www.sina.com'))
# 对需要ssl验证的网页,需要250ms左右等待底层连接关闭
loop.run_until_complete(asyncio.sleep(0.25))
loop.close()
基本上的改写如上,协程本身的概念不是重点,优越性单线程开销小啥的也不说了,这里只讲几个坑/注意事项。参考文档
- 如果要返回text和content:
# requests
return r.text, r.content
# aiohttp
return await r.text(), await r.read() # 不要漏后面的await,每个coroutine都要接await
- r.text()报编码错误
return await r.text(errors='ignore') # 直接忽略那些错误,默认是strict严格模式导致出现错误时会直接抛异常终止程序。
这里注意到,r.encoding = r.apparent_encoding
的原理是什么?为什么aiohttp
没有类似代码?
首先,看一下r.apparent_encoding
的源码
可以看出,写法其实就是
import chardet # 有requests模块的话已经安装了这个
code = chardet.detect(content)['encoding']
换句话说,套用到aiohttp
的代码中,本来应该这么写
import asyncio
import aiohttp
import chardet
async def html(url: str) ->str:
code = 'utf-8'
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'}
async with aiohttp.ClientSession() as session:
# 老版本aiohttp没有verify参数,如果报错卸载重装最新版本
async with session.get(url, headers=headers, timeout=10, verify_ssl=False) as r:
content = await r.read()
code = chardet.detect(content)['encoding']
# text()函数相当于requests中的r.text,不带参数则自动识别网页编码,同样会拖慢程序。r.read()相当于requests中的r.content
return await r.text(encoding=code, errors='ignore')
不过实际上,r.text()
在encoding=None
(默认参数)的时候已经包含了这一步,所以其实无需操心什么chardet
,出现编码错误先ignore
再单个网页具体分析,或者就不管算了。
这部分见文档
If encoding is
None
content encoding is autocalculated usingContent-Type
HTTP header and chardet tool if the header is not provided by server.
cchardet is used with fallback to chardet if cchardet is not available.
- 超时异常处理
捕捉就好了...基本上碰到的有这些异常
asyncio.TimeoutError
aiohttp.client_exceptions.ServerDisconnectedError
aiohttp.client_exceptions.InvalidURL
aiohttp.client_exceptions.ClientConnectorError
文档所写
import async_timeout
with async_timeout.timeout(0.001):
async with session.get('https://github.com') as r:
await r.text()
用了with
还是会抛timeout
异常...这时要把时间设的稍微长一点比如10s,以及捕捉timeout
异常。此外,这种写法会避免concurrent.futures._base.CancelledError
异常。这个异常意思是超时的场合还没完成的任务会被事件循环取消掉。
The event loop will ensure to cancel the waiting task when that timeout is reached and the task hasn't completed yet.
下面是两段作用完全一样的代码(有比较多的简化只保证正常运行),对比aiohttp和多线程
作用是读取网页内容的标题和正文
aiohttp
import asyncio
import aiohttp
# pip install readability-lxml以安装
from readability import Document
def title_summary(content: bytes, url: str):
doc = Document(content, url)
print(doc.short_title(), doc.summary())
async def read_one(id_: int, url: str):
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'}
async with aiohttp.ClientSession() as session:
try:
async with session.get(
url, headers=headers, timeout=1, verify_ssl=False) as r:
await asyncio.sleep(1 + random())
content, text = await r.read(), await r.text(
encoding=None, errors='ignore')
if text:
title_summary(content, url)
except:
pass
def read_many(links: list):
loop = asyncio.get_event_loop()
to_do = [read_one(id_, url) for id_, url in links]
loop.run_until_complete(asyncio.wait(to_do))
# 或loop.run_until_complete(asyncio.gather(*to_do))这两行代码作用似乎没啥区别
loop.close()
def main():
links = [...] # 要跑的所有链接列表
read_many(links)
if __name__ == '__main__':
main()
多线程
from concurrent import futures
import requests
from readability import Document
def title_summary(content: bytes, url: str):
doc = Document(content, url)
print(doc.short_title(), doc.summary())
def read_one(url: str):
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'}
try:
r = requests.get(url, headers=headers, timeout=1, verify=False)
r.encoding = r.apparent_encoding
content, text = r.content, await r.text
if text:
title_summary(content, url)
except:
pass
def read_many(links: list) ->int:
workers = min(100, len(links)) # 线程数
with futures.ThreadPoolExecutor(workers) as e:
res = e.map(read_one, links)
return len(list(res))
def main():
links = [...]
read_many(links)
if __name__ == '__main__':
main()
基本上,协程和线程的使用就是这样。但是,如果,任务数以千计时,asyncio
可能会报错:ValueError: too many file descriptors in select()
这是因为asyncio
内部调用select
,这个打开文件数是有限度的,这部分需要复习深入理解计算机系统一书。
这个场合不能这样写,有可能用到回调,其实也可以不用
def read_many(links: list):
loop = asyncio.get_event_loop()
to_do = [read_one(id_, url) for id_, url in links]
loop.run_until_complete(asyncio.wait(to_do))
# 或loop.run_until_complete(asyncio.gather(*to_do))这两行代码作用似乎没啥区别
loop.close()
以上代码这样改
def read_many(links: list):
loop = asyncio.get_event_loop()
for id_, url in links:
task = asyncio.ensure_future(read_one(id_, url))
loop.run_until_complete(task)
loop.close()
即可。
这样改完不再是并发而是顺序执行,正确的写法见文章开头链接的回调部分。
如果要用回调的话,比较麻烦,不少地方要修改,见下,主要是参数传递上要多多注意。
其实没有必要用回调,虽然拆开写似乎更规范,而且可以在需要请求其他页面时重用,但是受限很多。
import asyncio
import aiohttp
# pip install readability-lxml以安装
from readability import Document
def title_summary(fut):
res = fut.result() # 回调中调用result()才是上个函数的真实返回值
if res:
content, url = res
doc = Document(content, url)
print(doc.short_title(), doc.summary())
async def read_one(id_: int, url: str):
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'}
async with aiohttp.ClientSession() as session:
try:
async with session.get(
url, headers=headers, timeout=1, verify_ssl=False) as r:
await asyncio.sleep(1 + random())
return await r.read(), await r.text(encoding=None, errors='ignore')
except:
pass
def read_many(links: list):
loop = asyncio.get_event_loop()
for id_, url in links:
task = asyncio.ensure_future(read_one(id_, url))
# 注意参数问题,这里不能传递多个参数,要么用functool的partial,要么干脆传递元组解包,也可以用lambda,官方比较推荐functool这里就不写了
task.add_done_callback(title_summary)
loop.run_until_complete(task)
loop.close()
def main():
links = [...] # 要跑的所有链接列表
read_many(links)
if __name__ == '__main__':
main()