1. 前言
1. 先看最简单的爬虫。
import requests url = "http://www.cricode.com" r = requests.get(url) print(r.text)
2. 正常的爬虫程序
上面最简单的爬虫是不完整的残疾爬虫。爬虫程序通常需要做以下事情:
- 1)给定种子 URLs,爬虫程序将所有种子 URL 爬下页面
- 2)爬虫程序分析爬行 URL 将这些链接放入页面中的链接进行爬行 URL 集合中
- 3)重复 1、2 爬行直到达到指定条件才结束
所以,一个完整的爬虫大概是这样的:
import requests # 爬网页 from bs4 import BeautifulSoup # 用于分析网页 # 我们的种子 seeds = [ "http://www.hao123.com", "http://www.csdn.net", "http://www.cricode.com" ] # 设定终止条件为:爬到 一万页停止爬行 end_sum = 0 def do_save_action(text=None): pass while end_sum < 10000: if end_sum < len(seeds): r = requests.get(seeds[end_sum]) end_sum = end_sum 1 do_save_action(r.text) soup = BeautifulSoup(r.content) urls = soup.find_all('a') # 解析网页 for url in urls: seeds.append(url) else: break
3. 现在来找茬。上面完整的爬虫有太多的缺点。下面一一列举N宗罪:
- 1)我们的任务是爬10000页,按照上面的程序,一个人在默默爬,假设爬3秒,那么爬1000页需要3000秒。MGD,我们应当考虑开启多个线程(池)去一起爬取,或者用分布式架构去并发的爬取网页。
- 2) 和 列表中应设计一个更合理的数据结构来存储这些待爬行的数据 URL ,例如:队列或优先队列。( scrapy-redis 是 , )
- 3)每个网站 url,我们一视同仁。事实上,我们应该区别对待它。应考虑大站好站的优先原则。
- 4)每次发起请求都是基于 url 发起请求,这个过程起请求 DNS 解析,将 url 转换成 ip 地址。一个网站通常是成千上万的 URL,因此,可以考虑将这些网站域名的 IP 地址进行缓存,避免每次都发起 DNS 请求,费时费力。
- 5)分析到网页 urls 之后,我们没有做任何重处理,所有这些都被列入要爬的列表。事实上,可能有很多链接是重复的,我们做了很多重复的工作。
- 6)…..
4.找了这么多茬,现在就来讨论一下问题的解决方案。
- 1)并行爬行问题。实现并行的方法有很多。多线程或线程池模式,在爬虫程序中打开多线程。同一台机器打开多个爬虫程序,这样我们就有N多个爬行线程同时工作。时间可以大大减少。另外,当我们有很多任务要爬的时候,机器和网点肯定是不够的。我们必须考虑分布式爬虫。常见的分布式架构有:主从(Master——Slave)结构,点对点(Peer to Peer)结构、混合结构等。说到分布式架构,我们需要考虑很多问题。我们需要分配任务,每只爬虫需要通信合作,共同完成任务,不要重复爬同一页。为了实现公平公正的分配任务,我们需要考虑如何平衡负载。负载均衡,我们第一个想到的就是Hash,例如,根据网站域名hash。负载均衡分配后,千万不要以为万事大吉,万一哪台机器挂了?谁最初指派给悬挂的机器?或者哪天要增加几台机器,如何重新分配任务? ?一个更好的解决方案是使用一致性 Hash 算法。
- 2)爬上网页队列。如何处理抓取队列类似于操作系统如何调度过程。不同的网站有不同的重要性。因此,可以设计一个优先级队列来存储要爬升的网页链接。这样,每次我们抓取它,我们都会优先抓取重要的网页。当然,您也可以遵循操作系统的过程调度策略。
- 3)DNS缓存DNS我们可以查询DNS进行缓存。DNS当然,缓存是设计的hash存储现有域名及其表面存储IP。
- 4)网页去重。说到网页重,首先想到的是垃圾邮件过滤。过滤垃圾邮件的经典解决方案是 Bloom Filter(布隆过滤器)。布隆过滤器的原理是建立一个大的位数组,然后使用多个位数组 Hash 函数对同一个函数 url 进行 hash 获得多个数字,然后将这些数字对应数字对应的位置为1。下次再来一个url同样使用多个Hash函数进行hash,要获得多个数字,我们只需要判断位数组中的这些数字对应于1。如果都是1,说明这个url已经出现了。这样,就完成了url去重问题。当然,这种方法会有误差。只要误差在我们的容忍范围内,比如1000个网页,我只爬到9999个网页,这是可以容忍的。。。
- 5)数据存储问题。数据存储也是一个技术性很强的问题。用关系数据库存取还是用 NoSQL,或者设计一个特定的文件格式来存储,有很多文章要做。
- 6)进程间通信。分布式爬虫离不开进程间的通信。我们可以以规定的数据格式交互数据,完成进程间通信。
- 7)……
如何实现以上这些东西? ???
在实现的过程中,你会发现我们要考虑的问题远不止以上。纸上得来终觉浅,觉知此事要练!
2. 如何 "跟踪"和 "过滤"
在许多情况下,我们不仅需要抓取某个页面,还需要 "顺藤摸瓜",从几个种子页面,通过超链接索,最终定位到我们想要的页面。Scrapy 抽象这个功能很好:
from abc import ABC from scrapy.spiders import CrawlSpider, Rule from scrapy.linkextractors import LinkExtractor from scrapy.selector import Selector from scrapy.item import Item class Coder4Spider(CrawlSpider, ABC): name = 'coder4' allowed_domains = ['xxx.com'] start_urls = ['http://www.xxx.com'] rules = ( Rule(LinkExtractor(allow=('page/[0-9] ',))), Rule(LinkExtractor(allow=('archives/[0-9] ',)), callback='parse_item'), ) def parse_item(self, response): self.log(f'request url : {response.url}')
以上,我们用过 CrawlSpider 而不是 Spider。其中 name、 allowed_domains、start_urls 不解释。
重点说下 Rule:
-
第 1 条不带 callback 是的,表示只是 跳板是指只下载网页并根据 allow 匹配链接,继续遍历下一个页面,事实上 Rule 还可以指定 deny=xxx 表示过滤掉哪些页面。
-
第 2 条带 callback 是的,最后会回调 parse_item 函数网页。
3. 如何 "过滤重复" 的页面
Scrapy 支持通过 RFPDupeFilter 完成页面去重(防止重复抓取)。
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
RFPDupeFilter 实际是根据 request_fingerprint 实现过滤。
在源码中实现如下:
def request_fingerprint(request, include_headers=None, keep_fragments=False): if inlude_headers:
include_headers = tuple(to_bytes(h.lower()) for h in sorted(include_headers))
cache = _fingerprint_cache.setdefault(request, {})
cache_key = (include_headers, keep_fragments)
if cache_key not in cache:
fp = hashlib.sha1()
fp.update(to_bytes(request.method))
fp.update(to_bytes(canonicalize_url(request.url, keep_fragments=keep_fragments)))
fp.update(request.body or b'')
if include_headers:
for hdr in include_headers:
if hdr in request.headers:
fp.update(hdr)
for v in request.headers.getlist(hdr):
fp.update(v)
cache[cache_key] = fp.hexdigest()
return cache[cache_key]
我们可以看到,去重指纹是 sha1(method + url + body + header),所以,实际能够去掉重复的比例并不大。如果我们需要自己提取去重的 finger,需要自己实现 Filter,并配置上它。下面这个 Filter 只根据 url 去重:
from scrapy.dupefilters import RFPDupeFilter
class SeenURLFilter(RFPDupeFilter):
"""A dupe filter that considers the URL"""
def __init__(self, path=None):
self.urls_seen = set()
RFPDupeFilter.__init__(self, path)
def request_seen(self, request):
if request.url in self.urls_seen:
return True
else:
self.urls_seen.add(request.url)
不要忘记配置上:
DUPEFILTER_CLASS ='scraper.custom_filters.SeenURLFilter'
4. 海量数据处理算法 Bloom Filter
海量数据处理算法—Bloom Filter:https://www.cnblogs.com/zhxshseu/p/5289871.html
结合 Guava 源码解读布隆过滤器:http://cyhone.com/2017/02/07/Introduce-to-BloomFilter/
更多:https://www.baidu.com/s?wd=Bloomfilter%20%E7%AE%97%E6%B3%95
Bloom-Filter,即布隆过滤器,1970年由 Bloom 中提出。是一种多哈希函数映射的快速查找算法。通常应用在一些需要快速判断某个元素是否属于集合,但是并不严格要求100%正确的场合。Bloom Filter 有可能会出现错误判断,但不会漏掉判断。也就是Bloom Filter 如果判断元素不在集合中,那肯定就是不在。如果判断元素存在集合中,有一定的概率判断错误。。。
因此,Bloom Filter 不适合那些 "零错误" 的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter 比其他常见的算法(如hash,折半查找)极大节省了空间。
- 优点是空间效率和查询时间都远远超过一般的算法,
- 缺点是有一定的误识别率和删除困难。
一. 实例
为了说明 Bloom Filter 存在的重要意义,举一个实例:假设要你写一个网络蜘蛛(web crawler)。由于网络间的链接错综复杂,蜘蛛在网络间爬行很可能会形成 “环”。为了避免形成“环”,就需要知道蜘蛛已经访问过那些 URL。给一个 URL,怎样知道蜘蛛是否已经访问过呢?稍微想想,就会有如下几种方案:
- 1. 将访问过的 URL 保存到数据库。
- 2. 用 HashSet 将访问过的 URL 保存起来。那只需接近 O(1) 的代价就可以查到一个 URL 是否被访问过了。
- 3. URL 经过 MD5 或 SHA-1 等单向哈希后再保存到 HashSet 或数据库。
- 4. Bit-Map 方法。建立一个 BitSet,将每个 URL 经过一个哈希函数映射到某一位。
方法 1~3 都是将访问过的 URL 完整保存,方法4 则只标记 URL 的一个映射位。以上方法在数据量较小的情况下都能完美解决问题,但是当数据量变得非常庞大时问题就来了。
- 方法 1 的 缺点:数据量变得非常庞大后关系型数据库查询的效率会变得很低。而且每来一个URL就启动一次数据库查询是不是太小题大做了?
- 方法 2 的 缺点:太消耗内存。随着 URL 的增多,占用的内存会越来越多。就算只有1亿个 URL,每个 URL 只算 50 个字符,就需要 5GB 内存。
- 方法 3 :由于字符串经过 MD5 处理后的信息摘要长度只有128Bit,SHA-1 处理后也只有 160Bit,因此 方法3 比 方法2 节省了好几倍的内存。
- 方法 4 :消耗内存是相对较少的,但缺点是单一哈希函数发生冲突的概率太高。还记得数据结构课上学过的 Hash 表冲突的各种解决方法么?若要降低冲突发生的概率到1%,就要将 BitSet 的长度设置为 URL 个数的 100 倍。
实质上,上面的算法都忽略了一个重要的隐含条件:允许小概率的出错,不一定要100%准确!也就是说少量 url 实际上没有没网络蜘蛛访问,而将它们错判为已访问的代价是很小的——大不了少抓几个网页呗。
二. Bloom Filter 的算法
废话说到这里,下面引入本篇的主角——Bloom Filter。其实上面方法4的思想已经很接近 Bloom Filter 了。方法四的致命缺点是冲突概率高,为了降低冲突的概念,Bloom Filter 使用了多个哈希函数,而不是一个。
Bloom Filter 算法如下:创建一个 m位 BitSet,先将所有位初始化为0,然后选择 k个 不同的哈希函数。第 i个 哈希函数对 字符串str 哈希的结果记为 h(i,str),且 h(i,str)的范围是 0 到 m-1 。
- (1)下面是每个字符串处理的过程,首先是将字符串 str “记录” 到 BitSet 中的过程:对于字符串 str,分别计算 h(1,str),h(2,str)…… h(k,str)。然后将 BitSet 的第 h(1,str)、h(2,str)…… h(k,str)位设为1。下图是 Bloom Filter 加入字符串过程,很简单吧?这样就将字符串 str 映射到 BitSet 中的 k 个二进制位了。
- (2) 下面是检查字符串str是否被BitSet记录过的过程:对于字符串 str,分别计算 h(1,str),h(2,str)…… h(k,str)。然后检查 BitSet 的第 h(1,str)、h(2,str)…… h(k,str)位是否为1,若其中任何一位不为1则可以判定str一定没有被记录过。若全部位都是1,则 “认为” 字符串 str 存在。若一个字符串对应的 Bit 不全为1,则可以肯定该字符串一定没有被 Bloom Filter 记录过。(这是显然的,因为字符串被记录过,其对应的二进制位肯定全部被设为1了)。但是若一个字符串对应的Bit全为1,实际上是不能100%的肯定该字符串被 Bloom Filter 记录过的。(因为有可能该字符串的所有位都刚好是被其他字符串所对应)这种将该字符串划分错的情况,称为 false positive 。
三. Bloom Filter 参数选择
- (1) 哈希函数选择。哈希函数的选择对性能的影响应该是很大的,一个好的哈希函数要能近似等概率的将字符串映射到各个Bit。选择k个不同的哈希函数比较麻烦,一种简单的方法是选择一个哈希函数,然后送入k个不同的参数。
- (2) m,n,k 值,我们如何取值。我们定义:
可能把不属于这个集合的元素误认为属于这个集合(False Positive)
不会把属于这个集合的元素误认为不属于这个集合(False Negative)。
哈希函数的个数 k、位数组大小 m、加入的字符串数量 n 的关系。哈希函数个数k取10,位数组大小m设为字符串个数 n 的20倍时,false positive 发生的概率是0.0000889 ,即10万次的判断中,会存在 9 次误判,对于一天1亿次的查询,误判的次数为9000次。
哈希函数个数 k、位数组大小 m、加入的字符串数量 n 的关系可以参考参考文献 ( http://pages.cs.wisc.edu/~cao/papers/summary-cache/node8.html )。
m/n | k | k=17 | k=18 | k=19 | k=20 | k=21 | k=22 | k=23 | k=24 |
22 | 15.2 | 2.67e-05 | |||||||
23 | 15.9 | 1.61e-05 | |||||||
24 | 16.6 | 9.84e-06 | 1e-05 | ||||||
25 | 17.3 | 6.08e-06 | 6.11e-06 | 6.27e-06 | |||||
26 | 18 | 3.81e-06 | 3.76e-06 | 3.8e-06 | 3.92e-06 | ||||
27 | 18.7 | 2.41e-06 | 2.34e-06 | 2.33e-06 | 2.37e-06 | ||||
28 | 19.4 | 1.54e-06 | 1.47e-06 | 1.44e-06 | 1.44e-06 | 1.48e-06 | |||
29 | 20.1 | 9.96e-07 | 9.35e-07 | 9.01e-07 | 8.89e-07 | 8.96e-07 | 9.21e-07 | ||
30 | 20.8 | 6.5e-07 | 6e-07 | 5.69e-07 | 5.54e-07 | 5.5e-07 | 5.58e-07 | ||
31 | 21.5 | 4.29e-07 | 3.89e-07 | 3.63e-07 | 3.48e-07 | 3.41e-07 | 3.41e-07 | 3.48e-07 | |
32 | 22.2 | 2.85e-07 | 2.55e-07 | 2.34e-07 | 2.21e-07 | 2.13e-07 | 2.1e-07 | 2.12e-07 | 2.17e-07 |
该文献证明了对于给定的 m、n,当 k = ln(2)* m/n 时出错的概率是最小的。(log2 e ≈ 1.44倍),同时该文献还给出特定的k,m,n的出错概率。例如:根据参考文献1,哈希函数个数k取10,位数组大小m设为字符串个数n的20倍时,false positive 发生的概率是 0.0000889 ,这个概率基本能满足网络爬虫的需求了。
四. Python 实现 Bloom filter
和 是 的包。。。。。
Python3 安装( pybloomfiltermmap3 ):
- pybloomfiltermmap3 github 地址:https://pypi.org/project/pybloomfiltermmap3/
- pybloomfiltermmap3 官方文档:https://pybloomfiltermmap3.readthedocs.io/en/latest/
- pybloomfiltermmap 的 github:https://github.com/axiak/pybloomfiltermmap
pybloomfiltermmap3 is a Python 3 compatible fork of pybloomfiltermmap by @axiak。pybloomfiltermmap3 的目标:在 python3 中为 bloom过滤器 提供一个快速、简单、可伸缩、正确的库。
Python 中文网:https://www.cnpython.com/pypi/pybloomfiltermmap3
#################################################################
Windows 安装报错解决方法:Python - 安装pybloomfilter遇到的问题及解决办法:https://blog.csdn.net/tianbianEileen/article/details/75059132
Stack Overflow 上的回答如下: this problem looks like one “sys/mman.h:No such file or directory” And is a Unix header and is not available on Windows. I suggest you should ues pybloom instead on windows:
pip install pybloom
通过 pypi 搜索发现,最新的 pybloom 是 pybloom3 0.0.3
pybloom 的 github 地址:https://github.com/Hexmagic/pybloom3
所以安装命令是:
and you should use the package like this:
from pybloom import BloomFilter
#################################################################
快速示例:https://pybloomfiltermmap3.readthedocs.io/en/latest/
BloomFilter.copy_template(filename[, perm=0755]) → BloomFilter Creates a new BloomFilter object with the same parameters–same hash seeds, same size.. everything. Once this is performed, the two filters are comparable, so you can perform logical operators. Example:
>>> apple = BloomFilter(100, 0.1, '/tmp/apple')
>>> apple.add('apple')
False
>>> pear = apple.copy_template('/tmp/pear')
>>> pear.add('pear')
False
>>> pear |= apple
BloomFilter.(item) → Integer Returns the number of distinct elements that have been added to the BloomFilter object, subject to the error given in error_rate.
>>> bf = BloomFilter(100, 0.1, '/tmp/fruit.bloom')
>>> bf.add("Apple")
>>> bf.add('Apple')
>>> bf.add('orange')
>>> len(bf)
2
>>> bf2 = bf.copy_template('/tmp/new.bloom')
>>> bf2 |= bf
>>> len(bf2)
Traceback (most recent call last):
...
pybloomfilter.IndeterminateCountError: Length of BloomFilter object is
unavailable after intersection or union called.
快速示例:
from pybloom import BloomFilter
from pybloom import ScalableBloomFilter
f = BloomFilter(capacity=1000, error_rate=0.001)
print([f.add(x) for x in range(10)])
# [False, False, False, False, False, False, False, False, False, False]
print(all([(x in f) for x in range(10)]))
# True
print(10 in f)
# False
print(5 in f)
# True
f = BloomFilter(capacity=1000, error_rate=0.001)
for i in range(0, f.capacity):
_ = f.add(i)
print((1.0 - (len(f) / float(f.capacity))) <= f.error_rate + 2e-18)
# True
sbf = ScalableBloomFilter(mode=ScalableBloomFilter.SMALL_SET_GROWTH)
count = 10000
for i in range(0, count):
_ = sbf.add(i)
print((1.0 - (len(sbf) / float(count))) <= sbf.error_rate + 2e-18)
# True
# len(sbf) may not equal the entire input length. 0.01% error is well
# below the default 0.1% error threshold. As the capacity goes up, the
# error will approach 0.1%.
五:Bloom Filter 的优缺点。
- 优点:节约缓存空间(空值的映射),不再需要空值映射。减少数据库或缓存的请求次数。提升业务的处理效率以及业务隔离性。
- 缺点:存在误判的概率。传统的 Bloom Filter 不能作删除操作。
六:Bloom-Filter 的应用场景
Bloom-Filter 一般用于在大数据量的集合中判定某元素是否存在。
- (1) 适用于一些黑名单,垃圾邮件等的过滤,例如邮件服务器中的垃圾邮件过滤器。像网易,QQ这样的公众电子邮件(email)提供商,总是需要过滤来自发送垃圾邮件的人(spamer)的垃圾邮件。一个办法就是记录下那些发垃圾邮件的 email 地址。由于那些发送者不停地在注册新的地址,全世界少说也有几十亿个发垃圾邮件的地址,将他们都存起来则需要大量的网络服务器。如果用哈希表,每存储一亿个 email地址,就需要 1.6GB的内存(用哈希表实现的具体办法是将每一个 email地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有 50%,因此一个 email地址需要占用十六个字节。一亿个地址大约要 1.6GB,即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百 GB的内存。而 Bloom Filter 只需要哈希表 1/8 到 1/4 的大小就能解决同样的问题。BloomFilter 决不会漏掉任何一个在黑名单中的可疑地址。而至于误判问题,常见的补救办法是在建立一个小的白名单,存储那些可能别误判的邮件地址。
- (2) 在搜索引擎领域,Bloom-Filte r最常用于网络蜘蛛(Spider)的 URL 过滤,网络蜘蛛通常有一个 URL 列表,保存着将要下载和已经下载的网页的 URL,网络蜘蛛下载了一个网页,从网页中提取到新的 URL 后,需要判断该 URL 是否已经存在于列表中。此时,Bloom-Filter 算法是最好的选择。
Google 的 BigTable。 Google 的 BigTable 也使用了 Bloom Filter,以减少不存在的行或列在磁盘上的查询,大大提高了数据库的查询操作的性能。
key-value 加快查询。
一般 Bloom-Filter 可以与一些 key-value 的数据库一起使用,来加快查询。一般 key-value 存储系统的 values 存在硬盘,查询就是件费时的事。将 Storage 的数据都插入Filter,在 Filter 中查询都不存在时,那就不需要去Storage 查询了。当 False Position 出现时,只是会导致一次多余的Storage查询。
由于 Bloom-Filter 所用空间非常小,所有 BF 可以常驻内存。这样子的话对于大部分不存在的元素,只需要访问内存中的 Bloom-Filter 就可以判断出来了,只有一小部分,需要访问在硬盘上的 key-value 数据库。从而大大地提高了效率。如图:
5. scrapy_redis 去重优化 ( 7亿数据 )
原文链接:https://blog.csdn.net/Bone_ACE/article/details/53099042
使用布隆去重代替scrapy_redis(分布式爬虫)自带的dupefilter:https://blog.csdn.net/qq_36574108/article/details/82889744
前些天接手了上一位同事的爬虫,一个全网爬虫,用的是 scrapy + redis 分布式,任务调度用的 scrapy_redis 模块。
大家应该知道 scrapy 是默认开启了去重的,用了 scrapy_redis 后去重队列放在 redis 里面,爬虫已经有7亿多条URL的去重数据了,再加上一千多万条 requests 的种子,redis 占用了160多G的内存(服务器,Centos7),总共才175G好么。去重占用了大部分的内存,不优化还能跑?
一言不合就用 Bloomfilter+Redis 优化了一下,,效果还是很不错的!在此记录一下,最后附上 Scrapy+Redis+Bloomfilter 去重的 Demo(可将去重队列和种子队列分开!),希望对使用 scrapy 框架的朋友有所帮助。
- 因为 scrapy_redis 会用自己 scheduler 替代 scrapy 框架的 scheduler 进行任务调度,所以直接去 scrapy_redis 模块下查看scheduler.py 源码即可。
-
在 open() 方法中有句:self.df = load_object(self.dupefilter_cls).from_spider(spider),其中 load_object(self.dupefilter_cls) 是根据对象的绝对路径而载入一个对象并返回,self.dupefilter_cls 就是 SCHEDULER_DUPEFILTER_CLASS = 'scrapy_redis.dupefilter.RFPDupeFilter',from_spider(spider) 是返回一个 RFPDupeFilter类 的实例。
再看下面的 enqueue_request() 方法,
里面有句 if not request.dont_filter and self.df.request_seen(request) ,self.df.request_seen()这就是用来去重的了。按住Ctrl再左键点击request_seen查看它的代码,可看到下面的代码:
首先得到一个 request 的指纹,然后使用 Redis 的 set 保存指纹。可见 scrapy_redis 是利用 set 数据结构来去重的,去重的对象是 request 的 fingerprint。至于这个 fingerprint 到底是什么,可以再深入去看 request_fingerprint() 方法的源码(其实就是用 hashlib.sha1() 对 request 对象的某些字段信息进行压缩)。我们用调试也可以看到,其实 fp 就是 request 对象加密压缩后的一个字符串(40个字符,0~f)。
以上步骤可以看出,我们只要在 方法上面动些手脚即可。由于现有的七亿多去重数据存的都是这个 fingerprint,所有 Bloomfilter 去重的对象仍然是 request 对象的 fingerprint。更改后的代码如下:
def request_seen(self, request):
fp = request_fingerprint(request)
if self.bf.isContains(fp): # 如果已经存在
return True
else:
self.bf.insert(fp)
return False
self.bf 是类 Bloomfilter() 的实例化,关于这个Bloomfilter()类,看下面的
- 内存将爆,动作稍微大点机器就能死掉,更别说Bloomfilter在上面申请内存了。当务之急肯定是将那七亿多个fingerprint导出到硬盘上,而且不能用本机导,并且先要将redis的自动持久化给关掉。
- 因为常用Mongo,所以习惯性首先想到Mongodb,从redis取出2000条再一次性插入Mongo,但速度还是不乐观,瓶颈在于MongoDB。(猜测是MongoDB对_id的去重导致的,也可能是物理硬件的限制)
- 后来想用SSDB,因为SSDB和Redis很相似,用list存肯定速度快很多。然而SSDB唯独不支持Centos7,其他版本的系统都可。。
- 最后才想起来用txt,这个最傻的方法,却是非常有效的方法。速度很快,只是为了防止读取时内存不足,每100万个fingerprint存在了一个txt,四台机器txt总共有七百个左右。
- fingerprint取出来后redis只剩下一千多万的Request种子,占用内存9G+。然后用Bloomfilter将txt中的fingerprint写回Redis,写完以后Redis占用内存25G,开启redis自动持久化后内存占用49G左右。
6. 基于 Redis 的 Bloomfilter 去重
原文链接:http://blog.csdn.net/bone_ace/article/details/53107018
“去重” 是日常工作中会经常用到的一项技能,在爬虫领域更是常用,并且规模一般都比较大。去重需要考虑两个点:去重的数据量、去重速度。为了保持较快的去重速度,一般选择在内存中进行去重。
- 数据量不大时,可以直接放在内存里面进行去重,例如 python 可以使用 set() 进行去重。
- 当去重数据需要持久化时可以使用 redis 的 set 数据结构。
- 当数据量再大一点时,可以用不同的加密算法先将长字符串压缩成 16/32/40 个字符,再使用上面两种方法去重;
- 当数据量达到亿(甚至十亿、百亿)数量级时,内存有限,必须用 “位” 来去重,才能够满足需求。Bloomfilter 就是将去重对象映射到几个内存“位”,通过几个位的 0/1值来判断一个对象是否已经存在。
- 然而 Bloomfilter 运行在一台机器的内存上,不方便持久化(机器 down 掉就什么都没啦),也不方便分布式爬虫的统一去重。如果可以在 Redis 上申请内存进行 Bloomfilter,以上两个问题就都能解决了。
本文即是用 Python 基于 Redis 实现 Bloomfilter 去重。下面先放代码,最后附上说明。
# encoding=utf-8
import redis
from hashlib import md5
class SimpleHash(object):
def __init__(self, cap, seed):
self.cap = cap
self.seed = seed
def hash(self, value):
ret = 0
for i in range(len(value)):
ret += self.seed * ret + ord(value[i])
return (self.cap - 1) & ret
class BloomFilter(object):
def __init__(self, host='localhost', port=6379, db=0, blockNum=1, key='bloomfilter'):
"""
:param host: the host of Redis
:param port: the port of Redis
:param db: witch db in Redis
:param blockNum: one blockNum for about 90,000,000; if you have more strings for filtering, increase it.
:param key: the key's name in Redis
"""
self.server = redis.Redis(host=host, port=port, db=db)
# Redis 的 String 类型最大容量为512M,现使用 256M= 2^8 * 2^20 字节 = 2^28 * 2^3 bit
self.bit_size = 1 << 31
self.seeds = [5, 7, 11, 13, 31, 37, 61]
self.key = key
self.blockNum = blockNum
self.hashfunc = []
for seed in self.seeds:
self.hashfunc.append(SimpleHash(self.bit_size, seed))
def isContains(self, str_input):
if not str_input:
return False
m5 = md5()
m5.update(str_input.encode("utf8"))
str_input = m5.hexdigest()
ret = True
name = self.key + str(int(str_input[0:2], 16) % self.blockNum)
for f in self.hashfunc:
loc = f.hash(str_input)
ret = ret & self.server.getbit(name, loc)
return ret
def insert(self, str_input):
m5 = md5()
m5.update(str_input.encode("utf8"))
str_input = m5.hexdigest()
name = self.key + str(int(str_input[0:2], 16) % self.blockNum)
for f in self.hashfunc:
loc = f.hash(str_input)
self.server.setbit(name, loc, 1)
if __name__ == '__main__':
""" 第一次运行时会显示 not exists!,之后再运行会显示 exists! """
bf = BloomFilter()
if bf.isContains('http://www.baidu.com'): # 判断字符串是否存在
print('exists!')
else:
print('not exists!')
bf.insert('http://www.baidu.com')
- Bloomfilter 算法如何使用位去重,这个百度上有很多解释。简单点说就是有几个 seeds,现在申请一段内存空间,一个seed 可以和字符串哈希映射到这段内存上的一个位,几个位都为1即表示该字符串已经存在。插入的时候也是,将映射出的几个位都置为1。
-
需要提醒一下的是 Bloomfilter 算法会有漏失概率,即不存在的字符串有一定概率被误判为已经存在。这个概率的大小与seeds 的数量、申请的内存大小、去重对象的数量有关。下面有一张表,m 表示内存大小(多少个位),n 表示去重对象的数量,k 表示seed的个数。例如我代码中申请了256M,即1<<31(m=2^31,约21.5亿。即 256 * 1024 *1024 * 8),seed设置了7个。看k=7那一列,当漏失率为8.56e-05时,m/n值为23。所以n = 21.5/23 = 0.93(亿),表示漏失概率为 8.56e-05 时,256M 内存可满足0.93亿条字符串的去重。同理当漏失率为 0.000112 时,256M内存可满足 0.98 亿条字符串的去重。
基于 Redis 的 Bloomfilter 去重,其实就是利用了 Redis的String 数据结构,但 Redis 一个 String 最大只能 512M,所以如果去重的数据量大,需要申请多个去重块(代码中 blockNum 即表示去重块的数量)。
代码中使用了 MD5 加密压缩,将字符串压缩到了 32 个字符(也可用 hashlib.sha1()压缩成40个字符)。
它有两个作用,
-
一是 Bloomfilter 对一个很长的字符串哈希映射的时候会出错,经常误判为已存在,压缩后就不再有这个问题;
-
二是压缩后的字符为 0~f 共16中可能,我截取了前两个字符,再根据blockNum将字符串指定到不同的去重块进行去重。
基于 Redis 的 Bloomfilter 去重,既用上了 Bloomfilter 的海量去重能力,又用上了 Redis 的可持久化能力,基于 Redis 也方便分布式机器的去重。在使用的过程中,要预算好待去重的数据量,则根据上面的表,适当地调整 seed 的数量和 blockNum 数量(seed 越少肯定去重速度越快,但漏失率越大)。
7. scrapy_redis 种子优化
继 https://blog.csdn.net/bone_ace/article/details/53099042,优化去重之后,Redis 的内存消耗降了许多,然而还不满足。这次对 scrapy_redis 的种子队列作了一些优化(严格来说并不能用上“优化”这词,其实就是结合自己的项目作了一些改进,对本项目能称作优化,对 scrapy_redis 未必是个优化)。
scrapy_redis 默认是将 Request 对象序列化后(变成一条字符串)存入 Redis 作为种子,需要的时候再取出来进行反序列化,还原成一个 Request 对象。
现在的问题是:序列化后的字符串太长,短则几百个字符,长则上千。我的爬虫平时至少也要维护包含几千万种子的种子队列,占用内存在20G~50G之间(Centos)。想要缩减种子的长度,这样不仅 Redis 的内存消耗会降低,各个 slaver 从 Redis 拿种子的速度也会有所提高,从而整个分布式爬虫系统的抓取速度也会有所提高(效果视具体情况而定,要看爬虫主要阻塞在哪里)。
1、首先看调度器,即 scrapy_redis 模块下的 scheduler.py 文件,可以看到 enqueue_request()
方法和 next_request()
方法就是种子 和 的地方,self.queue
指的是我们在 setting.py 里面设定的 SCHEDULER_QUEUE_CLASS
值,常用的是 'scrapy_redis.queue.SpiderPriorityQueue'
。
2、进入 scrapy_redis 模块下的 queue.py 文件,SpiderPriorityQueue 类的代码如下:
class SpiderPriorityQueue(Base):
"""Per-spider priority queue abstraction using redis' sorted set"""
def __len__(self):
"""Return the length of the queue"""
return self.server.zcard(self.key)
def push(self, request):
"""Push a request"""
data = self._encode_request(request)
pairs = {data: -request.priority}
self.server.zadd(self.key, **pairs)
def pop(self, timeout=0):
"""
Pop a request
timeout not support in this queue class
"""
pipe = self.server.pipeline()
pipe.multi()
pipe.zrange(self.key, 0, 0).zremrangebyrank(self.key, 0, 0)
results, count = pipe.execute()
if results:
return self._decode_request(results[0])
可以看到,上面用到了 Redis 的 zset 数据结构(它可以给种子加优先级),在进 Redis 之前用 _encode_request() 方法将Request 对象转成字符串,_encode_request() 和 _decode_request 是 Base类下面的两个方法:
def _encode_request(self, request):
"""Encode a request object"""
return pickle.dumps(request_to_dict(request, self.spider), protocol=-1)
def _decode_request(self, encoded_request):
"""Decode an request previously encoded"""
return request_from_dict(pickle.loads(encoded_request), self.spider)
可以看到,这里先将 Request 对象转成一个字典,再将字典序列化成一个字符串。Request 对象怎么转成一个字典呢?看下面的代码,一目了然。
def request_to_dict(request, spider=None):
"""Convert Request object to a dict.
If a spider is given, it will try to find out the name of the spider method
used in the callback and store that as the callback.
"""
cb = request.callback
if callable(cb):
cb = _find_method(spider, cb)
eb = request.errback
if callable(eb):
eb = _find_method(spider, eb)
d = {
'url': to_unicode(request.url), # urls should be safe (safe_string_url)
'callback': cb,
'errback': eb,
'method': request.method,
'headers': dict(request.headers),
'body': request.body,
'cookies': request.cookies,
'meta': request.meta,
'_encoding': request._encoding,
'priority': request.priority,
'dont_filter': request.dont_filter,
}
return d
调试截图:( 注:d 为 Request 对象转过来的字典,data 为字典序列化后的字符串。 )
3、了解完 scrapy_redis 默认的种子处理方式,现在针对自己的项目作一些调整。我的是一个全网爬虫,每个种子需要记录的信息主要有两个:url 和 callback 函数名。此时我们选择不用序列化,直接用简单粗暴的方式,将 callback 函数名和 url 拼接成一条字符串作为一条种子,这样种子的长度至少会减少一半。另外我们的种子并不需要设优先级,所以也不用 zset 了,改用 Redis 的list。以下是我新建的 SpiderSimpleQueue 类,加在 queue.py 中。如果在 settings.py 里将
SCHEDULER_QUEUE_CLASS
值设置成 'scrapy_redis.queue.SpiderSimpleQueue'
即可使用我这种野蛮粗暴的种子。
from scrapy.utils.reqser import request_to_dict, request_from_dict, _find_method
class SpiderSimpleQueue(Base):
""" url + callback """
def __len__(self):
"""Return the length of the queue"""
return self.server.llen(self.key)
def push(self, request):
"""Push a request"""
url = request.url
cb = request.callback
if callable(cb):
cb = _find_method(self.spider, cb)
data = '%s--%s' % (cb, url)
self.server.lpush(self.key, data)
def pop(self, timeout=0):
"""Pop a request"""
if timeout > 0:
data = self.server.brpop(self.key, timeout=timeout)
if isinstance(data, tuple):
data = data[1]
else:
data = self.server.rpop(self.key)
if data:
cb, url = data.split('--', 1)
try:
cb = getattr(self.spider, str(cb))
return Request(url=url, callback=cb)
except AttributeError:
raise ValueError("Method %r not found in: %s" % (cb, self.spider))
__all__ = ['SpiderQueue', 'SpiderPriorityQueue', 'SpiderSimpleQueue', 'SpiderStack']
4、另外需要提醒的是,如果 scrapy 中加了中间件 process_request()
,当 yield 一个 Request 对象的时候,scrapy_redis 会直接将它丢进 Redis 种子队列,未执行 process_requset()
;需要一个 Request 对象的时候,scrapy_redis 会从 Redis 队列中取出种子,此时才会处理 process_request()
方法,接着去抓取网页。 所以并不需要担心 process_request()
里面添加的 Cookie 在 Redis 中放太久会失效,因为进 Redis 的时候它压根都还没执行process_request()
。事实上 Request 对象序列化的时候带上的字段很多都是没用的默认字段,很多爬虫都可以用 “callback+url” 的方式来优化种子。
5、最后,在 Scrapy_Redis_Bloomfilter ( https://github.com/LiuXingMing/Scrapy_Redis_Bloomfilter)这个 demo 中我已作了修改,大家可以试试效果。
经过以上优化,Redis 的内存消耗从 42G 降到了 27G!里面包含7亿多条种子的去重数据 和 4000W+ 条种子。并且六台子爬虫的抓取速度都提升了一些。
两次优化,内存消耗从160G+降到现在的27G,效果也是让人满意!
原文链接:http://blog.csdn.net/bone_ace/article/details/53306629
8. scrapy 引擎源码解析
本节内容将介绍下 scrapy 引擎具体实现的功能。 engine.py 提供了2个类:Slot 和 ExecutionEngine
- Slot: 提供了几个方法,添加请求,删除请求,关闭自己,触发关闭方法。它使用 Twisted 的主循环 reactor 来不断的调度执行 Engine 的 "_next_request" 方法,这个方法也是核心循环方法。
- ExecutionEngine: 引擎的执行任务 。
是 ,和 的。 This is the Scrapy engine which controls the Scheduler, Downloader and Spiders. For more information see docs/topics/architecture.rst
引擎 是 整个 scrapy 的核心控制和调度scrapy运行的。Engine 的 open_spider 方法完成了一些初始化,以及启动调度器获取种子队列,以及去重队列,最后调用 self._nest_request 开始一次爬取过程。
open_spider 中 slot 调用 _next_request,接下来我们看看 _next_request ,先是通过 _needs_backout(spider) 判断是否需要结束爬虫,然后返回,然后通过 self._next_request_from_scheduler(spider) 方法判断是否还有 URL 需要去爬。
def _next_request(self, spider):
slot = self.slot
if not slot:
return
if self.paused:
return
while not self._needs_backout(spider): # 是否需要返回
if not self._next_request_from_scheduler(spider): # 是否还有 URL 需要爬取
break
if slot.start_requests and not self._needs_backout(spider):
try:
request = next(slot.start_requests)
except StopIteration:
slot.start_requests = None
except Exception:
slot.start_requests = None
logger.error('Error while obtaining start requests',
exc_info=True, extra={'spider': spider})
else:
self.crawl(request, spider)
if self.spider_is_idle(spider) and slot.close_if_idle:
self._spider_idle(spider)
_next_request 循环通过 _next_request_from_scheduler(self, spider) 方法从 scheduler 获取下一个需要爬取的 request,然后送到下载器下载页面。
def _next_request_from_scheduler(self, spider):
slot = self.slot
request = slot.scheduler.next_request() # 从队列获取下一个待爬取的 request
if not request:
return
d = self._download(request, spider) # 使用 download 下载 request
d.addBoth(self._handle_downloader_output, request, spider) # 输出下载的 response
d.addErrback(lambda f: logger.info('Error while handling downloader output',
exc_info=failure_to_exc_info(f),
extra={'spider': spider}))
d.addBoth(lambda _: slot.remove_request(request))
d.addErrback(lambda f: logger.info('Error while removing request from slot',
exc_info=failure_to_exc_info(f),
extra={'spider': spider}))
d.addBoth(lambda _: slot.nextcall.schedule())
d.addErrback(lambda f: logger.info('Error while scheduling new request',
exc_info=failure_to_exc_info(f),
extra={'spider': spider}))
return d
继续看 _download(request, spider) 函数
ccdef _download(self, request, spider):
slot = self.slot
slot.add_request(request)
def _on_success(response):
assert isinstance(response, (Response, Request))
if isinstance(response, Response): # 如果返回的是 Response 对象打印日志
response.request = request # tie request to response received
logkws = self.logformatter.crawled(request, response, spider)
logger.log(*logformatter_adapter(logkws), extra={'spider': spider})
self.signals.send_catch_log(signal=signals.response_received, \
response=response, request=request, spider=spider)
return response
def _on_complete(_):
slot.nextcall.schedule()
return _
dwld = self.downloader.fetch(request, spider) # 使用downloader的fetch下载request
dwld.addCallbacks(_on_success) # 添加成功回掉方法
dwld.addBoth(_on_complete)
return dwld
scrapy源码分析 < 一 >:入口函数以及是如何运行
运行 scrapy crawl example 命令的时候,就会执行我们写的爬虫程序。
下面我们从源码分析一下scrapy执行的流程:入口函数以及是如何运行:http://www.30daydo.com/article/530
Scrapy 阅读源码分析
原文链接:https://blog.csdn.net/weixin_37947156/category_6959928.html
当用 scrapy 写好一个爬虫后,使用 scrapy crawl <spider_name>
命令就可以运行这个爬虫,那么这个过程中到底发生了什么?
scrapy
命令从何而来? 实际上,当你成功安装 scrapy 后,使用如下命令,就能找到这个命令:
$ which scrapy
/usr/local/bin/scrapy
使用 vim
或其他编辑器打开它:$ vim /usr/local/bin/scrapy 。其实它就是一个 python 脚本,而且代码非常少。
#!/usr/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from scrapy.cmdline import execute
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(execute())
安装 scrapy 后,为什么入口点是这里呢? 原因是在 scrapy 的安装文件 setup.py
中,声明了程序的入口处:
from os.path import dirname, join
from setuptools import setup, find_packages
with open(join(dirname(__file__), 'scrapy/VERSION'), 'rb') as f:
version = f.read().decode('ascii').strip()
setup(
name='Scrapy',
version=version,
url='http://scrapy.org',
description='A high-level Web Crawling and Screen Scraping framework',
long_description=open('README.rst').read(),
author='Scrapy developers',
maintainer='Pablo Hoffman',
maintainer_email='pablo@pablohoffman.com',
license='BSD',
packages=find_packages(exclude=('tests', 'tests.*')),
include_package_data=True,
zip_safe=False,
entry_points={
'console_scripts': ['scrapy = scrapy.cmdline:execute']
},
classifiers=[
'Framework :: Scrapy',
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Software Development :: Libraries :: Application Frameworks',
'Topic :: Software Development :: Libraries :: Python Modules',
],
install_requires=[
'Twisted>=10.0.0',
'w3lib>=1.8.0',
'queuelib',
'lxml',
'pyOpenSSL',
'cssselect>=0.9',
'six>=1.5.2',
],
)
entry_points
指明了入口是 cmdline.py
的 execute
方法,在安装过程中,setuptools
这个包管理工具,就会把上述那一段代码生成放在可执行路径下。
既然现在已经知道了 scrapy 的入口是 scrapy/cmdline.py
的 execute
方法,我们来看一下这个方法。
def execute(argv=None, settings=None):
if argv is None:
argv = sys.argv
if settings is None:
settings = get_project_settings()
# set EDITOR from environment if available
try:
editor = os.environ['EDITOR']
except KeyError:
pass
else:
settings['EDITOR'] = editor
inproject = inside_project()
cmds = _get_commands_dict(settings, inproject)
cmdname = _pop_command_name(argv)
parser = optparse.OptionParser(formatter=optparse.TitledHelpFormatter(),
conflict_handler='resolve')
if not cmdname:
_print_commands(settings, inproject)
sys.exit(0)
elif cmdname not in cmds:
_print_unknown_command(settings, cmdname, inproject)
sys.exit(2)
cmd = cmds[cmdname]
parser.usage = "scrapy %s %s" % (cmdname, cmd.syntax())
parser.description = cmd.long_desc()
settings.setdict(cmd.default_settings, priority='command')
cmd.settings = settings
cmd.add_options(parser)
opts, args = parser.parse_args(args=argv[1:])
_run_print_help(parser, cmd.process_options, args, opts)
cmd.crawler_process = CrawlerProcess(settings)
_run_print_help(parser, _run_command, cmd, args, opts)
sys.exit(cmd.exitcode)
函数主要是初始化项目配置,在函数最后是初始化 CrawlerProcess
实例,然后运行对应命令实例的run
方法。如果运行命令是scrapy crawl <spider_name>
,则运行的就是 commands/crawl.py
的 run
:
run
方法中调用了 CrawlerProcess
实例的 crawl
和 start
,就这样整个爬虫程序就会运行起来了。
再来看 CrawlerProcess
初始化。scrapy.core.engine.py( 其他代码省略。。。 ):
class CrawlerProcess(CrawlerRunner):
def __init__(self, settings=None, install_root_handler=True):
super(CrawlerProcess, self).__init__(settings)
install_shutdown_handlers(self._signal_shutdown)
configure_logging(self.settings, install_root_handler)
log_scrapy_info(self.settings)
构造方法中调用了父类 CrawlerRunner
的构造:
class CrawlerRunner:
def __init__(self, settings=None):
if isinstance(settings, dict) or settings is None:
settings = Settings(settings)
self.settings = settings
# 获取爬虫加载器
self.spider_loader = self._get_spider_loader(settings)
self._crawlers = set()
self._active = set()
self.bootstrap_failed = False
self._handle_twisted_reactor()
初始化时,调用了 _get_spider_loader
方法:
默认配置文件中的 spider_loader
配置的是 scrapy.spiderloader.SpiderLoader:
爬虫加载器会加载所有的爬虫脚本,最后生成一个{spider_name: spider_cls}
的字典。
CrawlerProcess
初始化完之后,调用 crawl
方法:
这个过程会创建 Cralwer
实例,然后调用它的 crawl
方法,最后调用 start
方法:
reactor
是个什么东西呢?它是 Twisted
模块的事件管理器,只要把需要执行的事件方法注册到reactor
中,然后调用它的run
方法,它就会帮你执行注册好的事件方法,如果遇到网络IO等待,它会自动帮你切换可执行的事件方法,非常高效。
大家不用在意reactor
是如何工作的,你可以把它想象成一个线程池,只是采用注册回调的方式来执行事件。
到这里,爬虫的之后调度逻辑就交由引擎ExecuteEngine
处理了。
scrapy 源码分析( 系列 ):https://blog.csdn.net/happyAnger6/category_6085726_2.html
scrapy 是一个基于 twisted 实现的开源爬虫,要读懂其源码,需要对twisted的异步编程模型有一定了解。可以通过之前3篇deferred的相关教程了解。
下面是总结的执行一个爬虫任务的整体执行流程,即运行"scrapy crawl xxxSpider"的执行流程:
流程中主要的颜色框的含义如下 :
- 1.红色框是模块或者类。
- 2.紫色框是向模块或者类发送的消息,一般为函数调用。
- 3.红色框垂直以下的黑色框即为本模块或者对象执行流程的伪代码描述。
几个关键的模块和类介绍如下:
- 命令行执行模块,主要用于配置的获取,并执行相应的ScrapyCommand。
- 命令对象,用于执行不同的命令。对于crawl任务,主要是调用CrawlerProcess的crawl和start方法。
- 顾名思义,爬取进程,主要用于管理Crawler对象,可以控制多个Crawler对象来同时进行多个不同的爬取任务,并调用Crawler的crawl方法。
- 爬取对象,用来控制爬虫的执行,里面会通过一个执行引擎engine对象来控制spider从打开到启动等生命周期。
- 执行引擎,主要控制整个调度过程,通过twisted的task.LoopingCall来不断的产生爬取任务。
scrapy 源码解析
:
scrapy 源码解析 (一):启动流程源码分析(一)命令行启动:https://www.cnblogs.com/qiu-hua/p/12930422.html scrapy 源码解析 (二):启动流程源码分析(二) CrawlerProcess 主进程:https://www.cnblogs.com/qiu-hua/p/12930707.html scrapy 源码解析 (三):启动流程源码分析(三) ExecutionEngine 执行引擎:https://www.cnblogs.com/qiu-hua/p/12930803.html scrapy 源码解析 (四):启动流程源码分析(四) Scheduler调度器:https://www.cnblogs.com/qiu-hua/p/12932254.html scrapy 源码解析 (五):启动流程源码分析(五) Scraper刮取器:https://www.cnblogs.com/qiu-hua/p/12932818.html
Python 之 Scrapy 框架源码解析:https://blog.csdn.net/cui_yonghua/article/details/107040329
scrapy 信号
官网说明:https://docs.scrapy.org/en/latest/topics/signals.html
scrapy 基础组件专题(四):信号运用:https://www.cnblogs.com/qiu-hua/p/12638683.html
scrapy 的信号(signal)以及对下载中间件的一些总结:https://blog.csdn.net/fiery_heart/article/details/82229871
9. DNS 解析缓存
原文链接:https://blog.csdn.net/bone_ace/article/details/55000101
这是 Python 爬虫中 DNS 解析缓存模块中的核心代码,是去年的代码了,现在放出来 有兴趣的可以看一下。 一般一个域名的 DNS 解析时间在 10~60 毫秒之间,这看起来是微不足道,但是对于大型一点的爬虫而言这就不容忽视了。例如我们要爬新浪微博,同个域名下的请求有1千万(这已经不算多的了),那么耗时在 10~60 万秒之间,一天才 86400 秒。也就是说单 DNS 解析这一项就用了好几天时间,此时加上 DNS 解析缓存,效果就明显了。
下面直接放代码,说明在后面。
# encoding=utf-8
# ---------------------------------------
# 版本:0.1
# 日期:2016-04-26
# 作者:九茶<bone_ace@163.com>
# 开发环境:Win64 + Python 2.7
# ---------------------------------------
import socket
# from gevent import socket
_dnscache = {}
def _setDNSCache():
""" DNS缓存 """
def _getaddrinfo(*args, **kwargs):
if args in _dnscache:
# print str(args) + " in cache"