├── aioScrapy ├── aioScrapy │ ├── https │ │ ├── __init__.py │ │ ├── __pycache__ │ │ │ ├── form.cpython-36.pyc │ │ │ ├── __init__.cpython-36.pyc │ │ │ ├── request.cpython-36.pyc │ │ │ └── response.cpython-36.pyc │ │ ├── response.py │ │ ├── form.py │ │ └── request.py │ ├── core │ │ ├── __init__.py │ │ ├── downloader │ │ │ ├── __init__.py │ │ │ └── downloader.py │ │ ├── scheduler.py │ │ ├── spider.py │ │ └── engine.py │ ├── __init__.py │ └── utils │ │ ├── __init__.py │ │ ├── __pycache__ │ │ ├── tools.cpython-36.pyc │ │ └── __init__.cpython-36.pyc │ │ └── tools.py ├── baiduhanyuSpider.py └── cfcSpider.py ├── README.md └── minScrapy.py /aioScrapy/aioScrapy/https/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/core/__init__.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | # author : "挖掘机小王子" 3 | # date : 2019年6月7日17:23:09 4 | # desc : "核心" -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/__init__.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | 3 | # author : "挖掘机小王子" 4 | # date : 2019年6月7日15:37:16 5 | # desc : "描述" 6 | -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | # author : "挖掘机小王子" 3 | # date : 2019年6月9日17:33:26 4 | # desc : "工具函数聚集" 5 | -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/core/downloader/__init__.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | # author : "挖掘机小王子" 3 | # date : 2019年6月7日15:27:03 4 | # desc : "下载器" -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/https/__pycache__/form.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebuff/aioScrapy/HEAD/aioScrapy/aioScrapy/https/__pycache__/form.cpython-36.pyc -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/utils/__pycache__/tools.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebuff/aioScrapy/HEAD/aioScrapy/aioScrapy/utils/__pycache__/tools.cpython-36.pyc -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/https/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebuff/aioScrapy/HEAD/aioScrapy/aioScrapy/https/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/https/__pycache__/request.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebuff/aioScrapy/HEAD/aioScrapy/aioScrapy/https/__pycache__/request.cpython-36.pyc -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/https/__pycache__/response.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebuff/aioScrapy/HEAD/aioScrapy/aioScrapy/https/__pycache__/response.cpython-36.pyc -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/utils/__pycache__/__init__.cpython-36.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bytebuff/aioScrapy/HEAD/aioScrapy/aioScrapy/utils/__pycache__/__init__.cpython-36.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aioScrapy 2 | ## Overview 3 | aioScrapy是一款基于asyncio与aiohttp的异步协程爬虫框架。 4 | 5 | ![Scrapy-Dataflow.png](https://i.loli.net/2019/08/16/blNEjKVvZzagfd8.png) 6 | 7 | ## Requirements 8 | 9 | - Python3.6 10 | - aiohttp3.4.4 11 | 12 | ## Install 13 | 14 | 直接下载源码即可 15 | -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/https/response.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | 3 | """ 响应类 """ 4 | 5 | class Response(object): 6 | """响应对象""" 7 | 8 | def __init__(self, url, status=200, headers=None, text='', request=None): 9 | self.url = url 10 | self.status = status 11 | self.headers = headers or {} 12 | self.text = text 13 | self.request = request 14 | self._cached_selector = None 15 | 16 | @property 17 | def selector(self): 18 | from parsel import Selector 19 | if self._cached_selector is None: 20 | self._cached_selector = Selector(self.text) 21 | return self._cached_selector 22 | 23 | def xpath(self, query, **kwargs): 24 | return self.selector.xpath(query, **kwargs) 25 | 26 | def css(self, query): 27 | return self.selector.css(query) 28 | -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/https/form.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | 3 | ''' 发送POST请求 ''' 4 | from .request import Request 5 | from aioScrapy.utils.tools import url_encode, is_listlike # 工具函数 6 | 7 | 8 | class FormRequest(Request): 9 | """ 实现POST请求 """ 10 | 11 | def __init__(self, *args, **kwargs): 12 | formdata = kwargs.pop('data', None) 13 | if formdata and kwargs.get('method') is None: 14 | kwargs['method'] = 'POST' 15 | 16 | super(FormRequest, self).__init__(*args, **kwargs) 17 | 18 | if formdata: 19 | # 字典表单形式的-->查询字符串形式的 x-www-form-urlencoded 20 | query_str = url_encode(formdata.items(), self.encoding) if isinstance(formdata, dict) else formdata 21 | if self.method == 'POST': 22 | # 表单形式发送 23 | kwargs.setdefault(b'Content-Type', b'application/x-www-form-urlencoded') 24 | self._set_data(query_str) # 传递字符形式的 x-www-form-urlencoded 25 | else: # 如果不是POST请求 就默认是GET请求 那么久拼接查询字符串 26 | # 拼接网址 27 | self._set_url(self.url + ('&' if '?' in self.url else '?') + query_str) 28 | 29 | def __str__(self): 30 | return "<%s %s>" % (self.method, self.url) -------------------------------------------------------------------------------- /minScrapy.py: -------------------------------------------------------------------------------- 1 | import aiohttp 2 | import asyncio 3 | 4 | 5 | # 初始化网址 6 | start_urls = [] 7 | 8 | 9 | settings = { 10 | 'headers': { 11 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3', 12 | 'Accept-Encoding': 'gzip, deflate', 13 | 'Accept-Language': 'zh-CN,zh;q=0.9', 14 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36', 15 | }, # dict 字典格式 16 | 'timeout': 20 # 超时时间 17 | } 18 | 19 | 20 | # 下载器 用来下载响应 21 | async def download(session, url): 22 | # 在此处激活配置文件 23 | async with session.get(url, headers=settings['headers']) as response: 24 | return await response.text() 25 | 26 | 27 | # engine引擎 处理相关事务 (下载器、 爬虫文件、管道文件) 28 | async def engine(): 29 | async with aiohttp.ClientSession() as session: 30 | for url in start_urls: 31 | response = await download(session, url) 32 | await parse(response) 33 | 34 | 35 | # 解析数据(spider文件) 36 | async def parse(response): 37 | pass 38 | 39 | # 管道文件(item) 40 | async def item(): 41 | pass 42 | 43 | 44 | 45 | if __name__ == '__main__': 46 | 47 | loop = asyncio.get_event_loop() 48 | loop.run_until_complete(engine()) 49 | -------------------------------------------------------------------------------- /aioScrapy/baiduhanyuSpider.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 采集百度汉语 aioScrapy 3 | ''' 4 | from aioScrapy.https.request import Request 5 | from aioScrapy.core.spider import Spider 6 | import re 7 | from urllib.parse import unquote 8 | 9 | 10 | class BaiduSpider(Spider): 11 | 12 | settings = { 13 | 'TASK_LIMIT': 5, # 并发个数限制 14 | } 15 | 16 | # 从‘王’开始 17 | start_urls = ['https://hanyu.baidu.com/zici/s?wd=王&query=王'] 18 | 19 | def parse(self, response): 20 | # 提取图片网址 21 | img_url = response.xpath('//img[@class="bishun"]/@data-gif').get() 22 | chinese_character = re.search('wd=(.*?)&', response.url).group(1) 23 | item = { 24 | 'img_url': img_url, 25 | 'response_url': response.url, 26 | 'chinese_character': unquote(chinese_character) 27 | } 28 | yield item 29 | # 提取相关字 提取热搜字 进行迭代 30 | new_character = response.xpath('//a[@class="img-link"]/@href').getall() 31 | for character in new_character: 32 | # 拼接 33 | new_url = 'https://hanyu.baidu.com/zici' + character 34 | # 发送请求 35 | yield Request(new_url, callback=self.parse) 36 | 37 | def process_item(self, item): 38 | ''' 数据存储 ''' 39 | print('管道文件-->', item) 40 | 41 | 42 | if __name__ == '__main__': 43 | baiduspider = BaiduSpider() 44 | baiduspider.start() -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/core/scheduler.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | 3 | """ 调度器 """ 4 | import queue 5 | 6 | 7 | class Scheduler(object): 8 | """ 9 | 调度器的实现 主要是请求的入队和出队 (去重暂时没有加上) 10 | """ 11 | 12 | def __init__(self, spider): 13 | ''' 14 | 初始化队列 15 | :param spider: 爬虫对象 16 | ''' 17 | # 存放请求的队列 18 | self.queue = queue.Queue() 19 | 20 | def __len__(self): 21 | ''' 22 | 队列的长度 (目前没有使用-->用的是下面的has_pending_requests方法) 23 | :return: 返回队列的长度 24 | ''' 25 | # 返回队列的长度 26 | return self.queue.qsize() 27 | 28 | def enqueue_request(self, request): 29 | ''' 30 | 将请求添加到队列 31 | :param request: start_requests中的请求 / 回调函数中的请求 32 | :return: True(可以不要返回值) 33 | ''' 34 | print(f'请求添加进队列 --> {request}') 35 | self.queue.put(request) 36 | return True 37 | 38 | def next_request(self): 39 | ''' 40 | 取出下一个请求 取出请求 41 | :return: 返回从队列中取出的请求 42 | ''' 43 | try: 44 | request = self.queue.get(block=False) # pop剔除一个元祖 并将这个元素赋值给requests 45 | except Exception as e: # 如果没有取出队列中的元素(队列是空的) 就默认request是None 46 | request = None 47 | return request # 返回请求 48 | 49 | def has_pending_requests(self): 50 | ''' 51 | 判断队列中是否有请求 52 | :return: 53 | ''' 54 | # 如果有请求那么就返回True 55 | # 没有请求就返回False 56 | return self.queue.qsize() > 0 -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/core/spider.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | 3 | """ Base Spider""" 4 | import logging 5 | 6 | from aioScrapy.https.request import Request 7 | from aioScrapy.core.engine import Engine 8 | 9 | 10 | class Spider(object): 11 | ''' 爬虫文件 ''' 12 | 13 | # 爬虫名字 默认aioscrapy 14 | name = 'aioscrapy' 15 | 16 | # 个性化配置文件 微框架就用这个作为配置项 不再另外创建一个配置文件了 17 | settings = { 18 | 'headers': {}, # 请求头 dict 19 | 'timeout': 10, # 延迟时间 int 20 | 'proxy': '', # IP代理 21 | 'proxy_file': "proxy_list.txt", # 代理保存的文件 一般代理的时效性都不高 【暂未实现】 22 | 'proxy_interval': 1, # 每个代理的时间间隔 【暂未实现】 23 | 'task_limit': 5, # 并发个数限制 24 | } 25 | 26 | def __init__(self): 27 | ''' 28 | 初始化start_urls属性 如果用户没有在爬虫文件中定义这个方法就默认等于空列表 29 | ''' 30 | # 如果没有这个属性就置为空 self.start_urls属性 31 | if not hasattr(self, "start_urls"): 32 | self.start_urls = [] 33 | 34 | def start_requests(self): 35 | ''' 36 | * 发送self.start_urls里面的网址 37 | * 没有self.start_urls的时候可以自定义该方法 38 | ''' 39 | for url in self.start_urls: 40 | yield Request(url) 41 | 42 | def start(self): 43 | ''' 44 | * 把爬虫传递到引擎中 用于初始化爬虫对象 45 | * engine.start()来启动爬虫 46 | ''' 47 | engine = Engine(self) # 传入爬虫对象 48 | # 该方法中封装了execute方法 49 | # execute方法封装了事件循环 用来实现爬虫的初始化的启动 50 | engine.start() # 启动爬虫 51 | 52 | def parse(self, response): 53 | ''' 54 | 解析下载器回来的响应数据 默认的解析回调方法 55 | 返回有三种类型: 56 | * 请求Request 57 | * 数据dict/str 58 | * None值 59 | ''' 60 | raise NotImplementedError('{}.parse callback is not defined'.format(self.__class__.__name__)) 61 | 62 | def process_item(self, item): 63 | ''' 64 | 管道函数:处理从爬虫文件过来的数据 65 | 在引擎中被使用 66 | ''' 67 | pass -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/utils/tools.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | 3 | """ 公共工具 """ 4 | 5 | from urllib.parse import urlencode 6 | 7 | 8 | # 在引擎中使用 9 | def result2list(result): 10 | ''' 11 | 处理回调函数返回的结果 12 | :param result: 传递的是回调函数返回的结果 也就是回调函数的返回值 13 | :return: 空列表, 返回响应的列表, 直接返回结果 14 | ''' 15 | # 如果回调函数的返回值是None 或者没有返回值 就返回空列表 16 | if result is None: 17 | return [] 18 | # 如果回调函数的返回值是字典 或者是字符串对象 就将其放置在列表中去 (这个返回的代表是数据) 19 | if isinstance(result, (dict, str)): 20 | return [result] # 返回的数据 21 | # 如果回调函数返回的是可迭代对象 那么就代表是返回的是新的请求 那么直接返回这个请求 22 | if hasattr(result, "__iter__"): 23 | return result # 返回请求 24 | 25 | # 工具函数 用在 FormRequest 里面 26 | def url_encode(seq, enc): 27 | values = [(to_bytes(k, enc), to_bytes(v, enc)) for k, vs in seq for v in (vs if is_listlike(vs) else [vs])] 28 | return urlencode(values, doseq=1) 29 | 30 | # 工具函数 用在 FormRequest 里面 31 | def is_listlike(x): 32 | """ 33 | >>> is_listlike("foo") 34 | False 35 | >>> is_listlike(5) 36 | False 37 | >>> is_listlike(b"foo") 38 | False 39 | >>> is_listlike([b"foo"]) 40 | True 41 | >>> is_listlike((b"foo",)) 42 | True 43 | >>> is_listlike({}) 44 | True 45 | >>> is_listlike(set()) 46 | True 47 | >>> is_listlike((x for x in range(3))) 48 | True 49 | >>> is_listlike(six.moves.xrange(5)) 50 | True 51 | """ 52 | return hasattr(x, "__iter__") and not isinstance(x, (str, bytes)) 53 | 54 | # 工具函数 用在 request 里面 对参数data表单数据进行编码 55 | def to_bytes(data, encoding=None, errors='strict'): 56 | """ 57 | 返回“text”的二进制表示形式。如果“文本”已经是bytes对象,按原样返回。 58 | :param data: 传递的post数据 59 | :param encoding: 编码格式 60 | :param errors: encode函数的一个参数 设置不同错误的处理方案。默认为 'strict',意为编码错误引起一个UnicodeError。 61 | :return: 返回字节形式的数据(经过了编码) 62 | """ 63 | if isinstance(data, bytes): 64 | return data 65 | # 如果数据不是字符串形式的就报错 66 | if not isinstance(data, str): 67 | raise TypeError(f'to_bytes 必须接受参数类型是: unicode, str 或者 bytes, 传递的是{type(data).__name__}') 68 | if encoding is None: 69 | encoding = 'utf-8' # 默认是utf-8 70 | # 转化编码格式默认utf-8 71 | # errors --> 设置不同错误的处理方案。默认为 'strict',意为编码错误引起一个UnicodeError。 72 | return data.encode(encoding, errors) -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/https/request.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | 3 | """ 请求类 """ 4 | from w3lib.url import safe_url_string 5 | from aioScrapy.utils.tools import to_bytes # 工具函数 6 | 7 | 8 | class Request(object): 9 | """ 实现请求方法 """ 10 | 11 | def __init__(self, url, method='GET', callback=None, 12 | headers=None, encoding='utf-8', data=None, meta=None): 13 | ''' 14 | 初始化请求方法 15 | :param url: 请求的网址 16 | :param method: 请求的方法 17 | :param callback: 回调函数 18 | :param headers: 请求头 19 | :param encoding: 请求的编码方式 20 | :param data: 请求的数据POST请求 21 | :param meta: 请求的额外参数 22 | ''' 23 | self.encoding = encoding # 先设置请求编码信息 24 | self.url = self._set_url(url) # 判断网址/修正网址 25 | self.method = method.upper() # 请求方法大写 26 | self.callback = callback # 回调函数 27 | self.headers = headers or {} # 请求头 不指定就是空字典 28 | self.data = self._set_data(data) # 请求的数据POST请求 29 | self.meta = meta if meta else {} # 请求的额外参数 默认是空字典 30 | 31 | # 网址的设置 32 | def _set_url(self, url): 33 | ''' 34 | 网址的修正以及判断 35 | :param url: 请求网址 36 | :return: 修正后的网址 37 | ''' 38 | # 如果不是字符类型肯定报错 39 | if not isinstance(url, str): # 如果不是字符串 就报错 提醒用户 40 | # raise TypeError('Request url must be str or unicode, got %s:' % type(url).__name__) 41 | raise TypeError(f'请求的网址必须是字符串类型, 您指定的是: {type(url).__name__}') 42 | # 如果没有冒号 肯定不是完整的网址 43 | if ':' not in url: 44 | raise ValueError('网址协议不正确') 45 | # safe_url_string 返回一个安全的网址 46 | self.url = safe_url_string(url, self.encoding) 47 | # 返回安全的网址 48 | return self.url 49 | 50 | # 表单的设置 主要用于POST表单 51 | def _set_data(self, data): 52 | ''' 53 | 表单数据的设置 主要用于POST表单 将数据转换成字节的形式 54 | :param data: POST的数据 55 | :return: 返回POST的表单数据(字节形式) 56 | ''' 57 | # 如果POST的表单数据没有指定就是空值 58 | if data is None: # 设置为空 59 | self.data = b'' # 设置为空字节 60 | else: 61 | # 如果有数据 就将其转化成字节的形式 并赋予编码 默认utf-8 62 | self.data = to_bytes(data, self.encoding) 63 | # 返回字节数据 64 | return self.data 65 | 66 | def __str__(self): 67 | ''' 68 | 用于打印请求对象 69 | :return: 返回请求方法 请求的网址 70 | ''' 71 | return "<%s %s>" % (self.method, self.url) 72 | # return f'<<{self.method} {self.url}>>' -------------------------------------------------------------------------------- /aioScrapy/cfcSpider.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | 3 | from base.https.form import FormRequest 4 | from base.core.spider import Spider 5 | 6 | 7 | class CFCSpider(Spider): 8 | """ 中国基金中心数据抓取 """ 9 | 10 | settings = { 11 | "headers":{'Accept-Language': 'zh-CN,zh;q=0.9', 12 | 'Accept': '*/*', 13 | 'Content-Length': '123', 14 | 'X-Requested-With': 'XMLHttpRequest', 15 | 'Accept-Encoding': 'gzip, deflate', 16 | 'Referer': 'http://data.foundationcenter.org.cn/foundation.html', 17 | 'Origin': 'http://data.foundationcenter.org.cn', 18 | 'Host': 'data.foundationcenter.org.cn', 19 | 'Connection': 'keep-alive', 20 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0', 21 | 'Content-Type': 'application/x-www-form-urlencoded' 22 | } 23 | } 24 | 25 | # 初始网址 26 | start_urls = [ 27 | "http://blog.jobbole.com/all-posts/", 28 | ] 29 | 30 | # 最好是在配置文件中可以这么配置 31 | heardes = { 32 | 'Accept-Language': 'zh-CN,zh;q=0.9', 33 | 'Accept': '*/*', 34 | 'Content-Length': '123', 35 | 'X-Requested-With': 'XMLHttpRequest', 36 | 'Accept-Encoding': 'gzip, deflate', 37 | 'Referer': 'http://data.foundationcenter.org.cn/foundation.html', 38 | 'Origin': 'http://data.foundationcenter.org.cn', 39 | 'Host': 'data.foundationcenter.org.cn', 40 | 'Connection': 'keep-alive', 41 | 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko/20100101 Firefox/50.0', 42 | 'Content-Type': 'application/x-www-form-urlencoded' 43 | } 44 | 45 | 46 | def parse(self, response): 47 | post_url = "http://data.foundationcenter.org.cn/NewFTI/GetFDOPagedFoundation.ashx" 48 | data = { 49 | "keyWord": "", 50 | "pageIndex": "1", 51 | "pageSize": "25", 52 | "type": "2", 53 | "sqlWhere": "", 54 | "sqlTop": "", 55 | "flag": "0", 56 | "financeField": "%u51C0%u8D44%u4EA7", 57 | "searchMode": "0", 58 | "biaoji": "" 59 | } 60 | yield FormRequest(url=post_url, 61 | data=data, 62 | # headers=self.heardes, 63 | callback=self.parse_detail, 64 | method='POST') 65 | 66 | def parse_detail(self, response): 67 | 68 | print(response.text[0:100]) 69 | 70 | def process_item(self, item): 71 | pass 72 | 73 | if __name__ == '__main__': 74 | cfc_spider = CFCSpider() 75 | cfc_spider.start() -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/core/downloader/downloader.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | 3 | # 下载器和中间件执行模块 4 | import logging 5 | import aiohttp 6 | from urllib.parse import urlparse 7 | import chardet 8 | 9 | from aioScrapy.https.response import Response 10 | 11 | 12 | class DownloadHandler(object): 13 | 14 | """ DownloadHandler """ 15 | 16 | def __init__(self, spider): 17 | self.settings = spider.settings # 读取配置文件 18 | 19 | async def fetch(self, request): 20 | kwargs = {} # 用来存储aiohttp.get & aiohttp.post的参数 21 | try: 22 | # 请求头 23 | 24 | # 先提取用户指定的headers参数里面的请求头 优先级较高 25 | if request.headers: 26 | headers = request.headers 27 | # 再提取用户在配置文件指定的headers 优先级较低 28 | elif self.settings.get('headers', False): 29 | headers = self.settings.get('headers') 30 | else: 31 | headers = {} 32 | kwargs['headers'] = headers # 请求头 33 | 34 | # 时间延迟 35 | timeout = self.settings.get("timeout", 10) 36 | kwargs['timeout'] = timeout # 时间延迟 37 | 38 | # IP 39 | proxy = request.meta.get("proxy", False) 40 | if proxy: 41 | kwargs["proxy"] = proxy # IP代理 meta指定方式 42 | print(f"user proxy {proxy}") 43 | 44 | # 并发的控制 在这里会更好 45 | url = request.url 46 | # 替换Cookie应该在ClientSession(cookies=cookies)里面替换 在get post里面也行的 47 | async with aiohttp.ClientSession() as session: 48 | if request.method == "POST": 49 | print('post的数据', request.data) 50 | response = await session.post(url, data=request.data, **kwargs) 51 | else: 52 | response = await session.get(url, **kwargs) 53 | content = await response.read() 54 | return Response(str(response.url), 55 | response.status, 56 | response.headers, 57 | content) 58 | except Exception as _e: 59 | logging.exception(_e) 60 | return Response(str(request.url), 404) 61 | 62 | 63 | class Downloader(object): 64 | 65 | """ Downloader """ 66 | 67 | ENCODING_MAP = {} # 用于编码的转换 可以放在下载器中间件 68 | 69 | def __init__(self, spider): 70 | self.hanlder = DownloadHandler(spider) 71 | 72 | async def fetch(self, request): 73 | """ 74 | request, Request, 请求 75 | """ 76 | response = await self.hanlder.fetch(request) 77 | # 返回预处理 下载器中间件 78 | response = await self.process_response(request, response) 79 | return response 80 | 81 | # 编码处理 必须要有这一步 这一步也可以在下载器中实现 82 | async def process_response(self, request, response): 83 | # urlparse()把url拆分为6个部分,scheme(协议),netloc(域名),path(路径),params(可选参数),query(连接键值对),fragment(特殊锚),并且以元组形式返回。 84 | netloc = urlparse(request.url).netloc # netloc(域名) 85 | content = response.text # 响应内容 86 | if self.ENCODING_MAP.get(netloc) is None: 87 | # 自动识别编码 88 | encoding = chardet.detect(content)["encoding"] 89 | # GB 18030 与 GB 2312-1980 和 GBK 兼容 比后两者收录的字更多 90 | encoding = "GB18030" if encoding.upper() in ("GBK", "GB2312") else encoding 91 | self.ENCODING_MAP[netloc] = encoding 92 | text = content.decode(self.ENCODING_MAP[netloc], "replace") 93 | return Response(url=str(response.url), 94 | status=response.status, 95 | headers=response.headers, 96 | text=text) -------------------------------------------------------------------------------- /aioScrapy/aioScrapy/core/engine.py: -------------------------------------------------------------------------------- 1 | # _*_ coding: utf-8 _*_ 2 | 3 | """ Engine """ 4 | 5 | import asyncio # Python 3.6 6 | from datetime import datetime 7 | import logging 8 | 9 | from aioScrapy.core.scheduler import Scheduler # 调度器 10 | from aioScrapy.core.downloader.downloader import Downloader # 下载器 11 | from aioScrapy.utils.tools import result2list # 工具函数 12 | from aioScrapy.https.request import Request # 请求对象 13 | 14 | 15 | class Engine(object): 16 | ''' 17 | 引擎:用于处理爬虫文件,调度器,下载器,启动等事件 18 | ''' 19 | def __init__(self, spider): 20 | ''' 21 | 初始化 爬虫 调度器 下载器 配置文件 事件循环 22 | :爬虫 --> 从爬虫文件传递过来 23 | :调度器 --> 导入 24 | :下载器 --> 导入 25 | :配置文件(在爬虫文件中定义) --> 爬虫文件中定义的类属性 26 | :事件循环 --> asyncio模块 27 | :param spider: 爬虫对象 28 | ''' 29 | ''' 30 | 初始化 31 | :爬虫 --> 从爬虫文件传递过来 32 | :调度器 --> 导入 33 | :下载器 --> 导入 34 | :配置文件(在爬虫文件中定义) --> 爬虫文件中定义的类属性 35 | :事件循环 --> asyncio模块 36 | ''' 37 | self.spider = spider # 爬虫 实例 38 | self.scheduler = Scheduler(spider) # 调度器 39 | self.downloader = Downloader(spider) # 下载器 40 | self.settings = spider.settings # 配置文件 41 | self.loop = asyncio.get_event_loop() # 开始事件循环 一般一个程序只需要一个事件循环对象 42 | 43 | # 启动引擎的方法 44 | def start(self): 45 | # 转化为可迭代的 self.spider.start_requests() 方法返回可迭代的Request() 46 | # iter将其转化为可迭代对象 --> 将其一个个放到队列中 47 | start_requests = iter(self.spider.start_requests()) 48 | # 把start_requests的所有请求全部传到execute里面用来启动初始化的网址(start_urls) 49 | self.execute(self.spider, start_requests) # 执行 50 | 51 | def execute(self, spider, start_requests): 52 | ''' 53 | 执行初始化start_urls里面的请求 54 | :param spider: 爬虫对象 55 | :param start_requests: start_requests里面返回的多个请求 56 | :return: None 57 | ''' 58 | # 打印开始采集 self.spider.name --> 爬虫名字 59 | print(f'{">"*25} {self.spider.name}: 开始采集 {"<"*25}' ) 60 | # 初始化 start_requests 中的多个请求 将请求放入队列中 61 | # _init_start_requests 中的crawl方法就是将请求加入到调度器定义的队列中 62 | self._init_start_requests(start_requests) 63 | # 爬虫开始的时间 64 | start_time = datetime.now() 65 | try: 66 | # 将协程注册到事件循环中 67 | self.loop.run_until_complete(self._next_request(spider)) 68 | finally: 69 | self.loop.run_until_complete(self.loop.shutdown_asyncgens()) 70 | self.loop.close() 71 | print(f'{">" * 25} {self.spider.name}: 爬虫结束 {"<" * 25}') 72 | print(f'{">" * 25} 总共用时: {datetime.now() - start_time} {"<" * 25}') 73 | 74 | def _init_start_requests(self, start_requests): 75 | for req in start_requests: 76 | self.crawl(req) # 传入每个网址 77 | 78 | async def _next_request(self, spider): 79 | ''' 80 | 协程方法 该方法用于不断的获取下一个请求(从调度器中的队列中) 81 | :param spider: 爬虫对象 82 | :return: None 83 | ''' 84 | # self.settings 是在初始化的时候从爬虫文件中的settings传递过来的 85 | task_limit = self.settings.get("TASK_LIMIT", 5) # 同时允许任务数量 配置文件中是5 86 | # 请求并发量的设置 value默认是1 87 | semaphore = asyncio.Semaphore(value=task_limit) 88 | # 死循环 不断的循环 从调度器中的队列不断获取请求 89 | while True: 90 | # 从调度器队列中获取一个请求 next_request() 在调度器中定义的 91 | request = self.scheduler.next_request() 92 | # if not isinstance(request, Request): # 如果取出来的不是请求 就进入等待 93 | if not request: # 如果取出来的不是请求 就进入等待 94 | logging.warning("time.sleep(3)") 95 | # 暂停三秒 协程的方式 暂停过后判断调度器的队列中是否还有请求 96 | await self.heart_beat() # 心跳函数 暂停3秒 97 | # has_pending_requests() 用来判断队列的大小是否大于0 如果大于0代表有请求在队列中 98 | # 如果队列中还是没有数据 那么久跳出整个循环 --> 爬虫终止 (这种判断爬虫终止的方法可以改进) --> 判断调度器,判断下载器等 99 | if not self.scheduler.has_pending_requests(): # 判断 再次判断目前队列的长度 100 | break # 如果3秒过后队列中还是没有数据 那么就退出 这种做法有点生硬 应该同时判断下载器 爬虫文件等是否完成更好 101 | continue 102 | # 上锁 保证前面设置的并发量生效 每次上锁value值会减一 (-1) 103 | # 但是value值是不能低于0的 value等于0就会造成阻塞 --> 从而可以实现并发的控制 104 | await semaphore.acquire() 105 | # 创建任务 用于注册到事件循环中 106 | # 将每个从调度器中取出的请求都创建任务 --> 每次while循环创建一个任务(不断的创建任务) 107 | # _process_request 处理每个请求 request(从队列中取出的) --> 将请求交到下载器中去处理 108 | self.loop.create_task(self._process_request(request, spider, semaphore)) 109 | 110 | @staticmethod 111 | async def heart_beat(): 112 | ''' 113 | 实现的心跳函数 不断的检查调度器中是否有数据 114 | :return: None 115 | ''' 116 | await asyncio.sleep(3) 117 | 118 | # 封装下载器 119 | async def _process_request(self, request, spider, semaphore): 120 | ''' 121 | 协程方法 该方法每个从调度器中取出的请求交给下载器中处理 122 | :param request: 从调度器中的队列取出的请求 在_next_request方法中取出来的 123 | :param spider: 爬虫对象 124 | :param semaphore: 限制并发量 release() 释放 125 | :return: None 126 | ''' 127 | try: 128 | # 调用下载器中的下载请求 129 | # self.download() 方法在引擎中再度封装 主要封装下载器中的下载请求的方法 fetch() 方法 130 | response = await self.download(request, spider) 131 | except Exception as exc: # 有异常执行except 但是不执行下面的else了 132 | # 如果报错 出现下载错误提示 捕获exc错误 133 | print(f'下载错误: {exc}') 134 | else: # 没有异常的时候正常执行else 135 | # 处理响应 传递response(响应) request(请求) spider(爬虫对象) 136 | self._handle_downloader_output(response, request, spider) 137 | # 释放锁资源 释放的时候 value的值会增加一 (+1) 138 | semaphore.release() 139 | 140 | # 封装下载器中的请求响应的方法 141 | async def download(self, request, spider): 142 | ''' 143 | 协程方法 该方法用于封装下载器中的请求响应的方法 fetch() 144 | :param request: 从调度器中的队列中取出的请求 145 | :param spider: 爬虫对象 146 | :return: 响应 --> response 147 | ''' 148 | # self.downloader 在 __init__中封装了 fetch()方法在下载器中定义的 用来下载器请求对应的响应 149 | response = await self.downloader.fetch(request) 150 | # 把请求对象赋值到请求对象中 方便在response中读取对应的请求的数据 151 | response.request = request 152 | return response 153 | 154 | def _handle_downloader_output(self, response, request, spider): 155 | ''' 156 | 该方法用来处理下载器得到的响应的 157 | :param response: 下载器的响应 158 | :param request: 调度器的请求 159 | :param spider: 爬虫对象 160 | :return: None 161 | ''' 162 | # 如果响应是请求类型就加入队列中去 163 | # if isinstance(response, Request): 164 | # self.crawl(response) 165 | # return 166 | # 如果不是请求 就是数据 处理下载后的数据 对于多个返回值 167 | self.process_response(response, request, spider) 168 | 169 | # 处理从下载器返回来的响应 170 | def process_response(self, response, request, spider): 171 | ''' 172 | 处理响应 173 | :param response: 下载器返回的响应 174 | :param request: 调度器中响应 175 | :param spider: 爬虫对象 176 | :return: None 177 | ''' 178 | # 请求中是否有回调函数 如果没有就默认是parse函数作为回调 179 | callback = request.callback or spider.parse # 调用回调函数 没有指定就调用默认回调函数 180 | # 回调函数来处理响应 第一个参数就是response响应 回调函数是有返回值的 返回值就是result的值 181 | # 回调函数的返回值可能有多个结果 所以需要判断一下 到底是什么结果 182 | result = callback(response) 183 | # result2list() --> 用来判断回调函数返回的是数据,请求,还是None值 184 | ret = result2list(result) 185 | # 将回调函数返回的结果进行处理 --> handle_spider_output() 186 | self.handle_spider_output(ret, spider) 187 | 188 | def handle_spider_output(self, result, spider): 189 | ''' 190 | 集中处理回调函数返回的数据 191 | :param result: 回调函数返回是数据是什么 来自-->result2list() 192 | :param spider: 爬虫对象 193 | :return: None 194 | ''' 195 | # 因为result2list中返回的要么是 列表(两种情况) 要么是可迭代的请求对象 196 | for item in result: # 回调函数返回的结果 197 | if item is None: # 代表回调函数返回None 或者没有返回值 198 | continue # 继续循环 199 | # 如果返回的是请求对象 那么就把请求加入到队列中去 crawl方法来实现 200 | elif isinstance(item, Request): 201 | self.crawl(item) 202 | # 如果结果是字典类型的 那么就交给管道函数来处理 203 | elif isinstance(item, dict): # 如果是字典就代表是数据 交给管道函数(文件)处理 204 | # 管道函数来处理 item数据 205 | self.process_item(item, spider) 206 | # 如果不是上面的数据类型 就报错 207 | else: 208 | print("爬虫文件中的回调函数必须返回请求, 数据, None值") 209 | 210 | def process_item(self, item, spider): 211 | ''' 212 | 封装爬虫中的管道函数 管道函数 用来保存数据 处理数据 213 | :param item: 爬虫采集的数据 214 | :param spider: 爬虫对象 215 | :return: None 216 | ''' 217 | # 爬虫中的管道函数 用来处理数据 218 | spider.process_item(item) # 这个要求spider实例中有这个方法 219 | 220 | def crawl(self, request): # 将网址 请求加入队列 221 | ''' 222 | 将请求加到调度器中的队列中去 223 | :param request: 请求 224 | :return: True (enqueue_request会返回True) 225 | ''' 226 | self.scheduler.enqueue_request(request) --------------------------------------------------------------------------------