爬虫教程( 6 ) --- 爬虫 进阶、扩展

 

 

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"
]

# 设定终止条件为:爬取到 100000个页面时就停止爬取
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)我们的任务是爬取1万个网页,按上面这个程序,一个人在默默的爬取,假设爬起一个网页3秒钟,那么,爬一万个网页需要3万秒钟。MGD,我们应当考虑开启多个线程(池)去一起爬取,或者用分布式架构去并发的爬取网页。
  • 2)种子URL后续解析到的URL 都放在一个列表里,应该设计一个更合理的数据结构来存放这些待爬取的 URL ,比如:队列或者优先队列。( scrapy-redis 是 种子URL redis的 list 里面后续解析到的URL 队列里面 )
  • 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去重的问题。当然,这种方法会有误差,只要误差在我们的容忍范围之类,比如1万个网页,我只爬取到了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 include_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 )。

Table 5: False positive rate under various  m/ n and  k combinations.
m/nkk=17k=18k=19k=20k=21k=22k=23k=24
2215.22.67e-05       
2315.91.61e-05       
2416.69.84e-061e-05      
2517.36.08e-066.11e-066.27e-06     
26183.81e-063.76e-063.8e-063.92e-06    
2718.72.41e-062.34e-062.33e-062.37e-06    
2819.41.54e-061.47e-061.44e-061.44e-061.48e-06   
2920.19.96e-079.35e-079.01e-078.89e-078.96e-079.21e-07  
3020.86.5e-076e-075.69e-075.54e-075.5e-075.58e-07  
3121.54.29e-073.89e-073.63e-073.48e-073.41e-073.41e-073.48e-07 
3222.22.85e-072.55e-072.34e-072.21e-072.13e-072.1e-072.12e-072.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

 

pybloomfiltermmap3  和 pybloom 不同 的包。。。。。

Python3 安装( pybloomfiltermmap3 ):pip install pybloomfiltermmap3

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

所以安装命令是:pip install pybloom3

and you should use the package like this:

from pybloom import BloomFilter

#################################################################

 

pybloomfiltermmap3 快速示例: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.len(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.

 

pybloom  快速示例:

 

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 优化了一下,内存占用立马降回到了二十多G,保证漏失概率小于万分之一的情况下可以容纳50亿条URL的去重,效果还是很不错的!在此记录一下,最后附上 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)。

 

是否可用 Bloomfilter 进行优化?

以上步骤可以看出,我们只要在 request_seen() 方法上面动些手脚即可。由于现有的七亿多去重数据存的都是这个 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()类,看下面的 基于 Redis 的 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')

 

说明:

  1. Bloomfilter 算法如何使用位去重,这个百度上有很多解释。简单点说就是有几个 seeds,现在申请一段内存空间,一个seed 可以和字符串哈希映射到这段内存上的一个位,几个位都为1即表示该字符串已经存在。插入的时候也是,将映射出的几个位都置为1。
  2. 需要提醒一下的是 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 种子优化

 

前言:

继 scrapy_redis去重优化(已有7亿条数据)【 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 写好一个爬虫后,使用 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 这个包管理工具,就会把上述那一段代码生成放在可执行路径下。

 

入口(execute.py)

既然现在已经知道了 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}的字典。

 

执行 crawl 和 start 方法

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.红色框垂直以下的黑色框即为本模块或者对象执行流程的伪代码描述。

几个关键的模块和类介绍如下:

  • cmdline:命令行执行模块,主要用于配置的获取,并执行相应的ScrapyCommand。
  • ScrapyCommand:命令对象,用于执行不同的命令。对于crawl任务,主要是调用CrawlerProcess的crawl和start方法。
  • CrawlerProcess:顾名思义,爬取进程,主要用于管理Crawler对象,可以控制多个Crawler对象来同时进行多个不同的爬取任务,并调用Crawler的crawl方法。
  • Crawler:爬取对象,用来控制爬虫的执行,里面会通过一个执行引擎engine对象来控制spider从打开到启动等生命周期。
  • ExecutionEngine:执行引擎,主要控制整个调度过程,通过twisted的task.LoopingCall来不断的产生爬取任务。

 

scrapy 源码解析

<scrapy>scrapy源码剖析https://www.cnblogs.com/shuimohei/p/13363462.html

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"
            return _dnscache[args]
        else:
            # print str(args) + " not in cache"
            _dnscache[args] = socket._getaddrinfo(*args, **kwargs)
            return _dnscache[args]

    if not hasattr(socket, '_getaddrinfo'):
        socket._getaddrinfo = socket.getaddrinfo
        socket.getaddrinfo = _getaddrinfo

 

说明:

其实也没什么难度,就是将 socket 里面的缓存保存下来,避免重复获取。
可以将上面的代码放在一个 dns_cache.py 文件里,爬虫框架里调用一下这个 _setDNSCache()方法就行了。

需要说明一下的是,如果你使用了 gevent 协程,并且用上了 monkey.patch_all(),要注意此时爬虫已经改用 gevent 里面的socket 了,DNS 解析缓存模块也应该要用 gevent 的 socket 才行。

 

 

10. Scrapy cookies 浅析

 

首先打消大家的疑虑, Scrapy 会自动管理 cookies, 就像浏览器一样:

Does Scrapy manage cookies automatically?

Yes, Scrapy receives and keeps track of cookies sent by servers, and sends them back on subsequent requests, like any regular web browser does.

Cookies 的管理是通过 CookiesMiddleware, 它属于 DownloadMiddleware 的一部分,所有的 requests 和 response 都要经过它的处理。

首先看下处理 request 的部分,代码如下:

class CookiesMiddleware(object):
    """This middleware enables working with sites that need cookies"""

    def __init__(self, debug=False):
        # 用字典生成多个cookiesjar
        self.jars = defaultdict(CookieJar)
        self.debug = debug



    def process_request(self, request, spider):
        if request.meta.get('dont_merge_cookies', False):
            return
        # 每个cookiesjar的key都存储在 meta字典中
        cookiejarkey = request.meta.get("cookiejar")
        jar = self.jars[cookiejarkey]
        cookies = self._get_request_cookies(jar, request)
        # 把requests的cookies存储到cookiesjar中
        for cookie in cookies:
            jar.set_cookie_if_ok(cookie, request)

        # set Cookie header
        # 删除原有的cookies
        request.headers.pop('Cookie', None)
        # 添加cookiesjar中的cookies到requests header
        jar.add_cookie_header(request)
        self._debug_cookie(request, spider)

流程如下:

  • 使用字典初始化多个 cookies jar
  • 把每个 requests 指定的 cookies jar 提取出来
  • 然后根据 policy 把 requests 中的 cookies 添加 cookies jar
  • 最后把 cookies jar 中合适的 cookies 添加到 requests 首部

接下来看看如何处理 response 中的 cookies:

    def process_response(self, request, response, spider):
        if request.meta.get('dont_merge_cookies', False):
            return response

        # extract cookies from Set-Cookie and drop invalid/expired cookies
        cookiejarkey = request.meta.get("cookiejar")
        jar = self.jars[cookiejarkey]
        jar.extract_cookies(response, request)
        self._debug_set_cookie(response, spider)

        return response

流程如下:

  • 首先从 cookies jar 字典中把 requests 对应的 cookiesjar 提取出来.
  • 使用 extract_cookies 把 response 首部中的 cookies 添加到 cookies jar

 

 

11. 扩展部分

 

python 之 goose3 库 --- 文章提取工具

 

github 地址:https://github.com/goose3/goose3

goose3 官网文档:https://goose3.readthedocs.io/en/latest/

 

goose3 是什么?

GOOSE3 最初是用 Java 编写的一篇文章提取器,最近将它(Auff2011)转换成Scala项目,这是 python 中的完全重写。该软件的目标是获取任何新闻文章或文章类型的网页,不仅提取文章的主体,而且还提取所有元数据和图片。

官方网站:https://github.com/goose3/goose3

 

安装

  • pip install goose3
  • mkvirtualenv --no-site-packages goose3
    git clone https://github.com/goose3/goose3.git
    cd goose3
    pip install -r ./requirements/python
    python setup.py install

 

使用:

示例用法:https://pypi.org/project/goose3/3.0.4/

python 之 goose3 库:https://blog.csdn.net/weixin_42547344/article/details/100735035

 

 

动手实践 CURL

 

CURL 是利用 URL 语法在命令行方式下工作的开源文件传输工具。它支持http、https、ftp、ftps、telnet 等多种协议,常被用来抓取网页和监控Web服务器状态。

 

CURL 使用

curl 命令可以用来构造http请求。
通用语法:curl [option] [URL...]

 

使用实例

curl 是 Linux下一个很强大的 http 命令行工具,其功能十分强大。

基本用法

curl http://www.baidu.com

抓取 www.ip138.com 查询网: 如发现乱码,可以使用iconv转码:

curl http://ip138.com|iconv -f gb2312

回车之后,html显示在屏幕上了 ~

1.1 get方式提交数据:

curl -G -d "name=value&name2=value2" http://www.baidu.com

1.2 post方式提交数据:

curl -d "name=value&name2=value2" http://www.baidu.com #post数据
curl -d a=b&c=d&txt@/tmp/txt http://www.baidu.com  #post文件

以表单的方式上传文件:

curl -F file=@/tmp/me.txt http://www.aiezu.com

相当于设置form表单的method="POST"和enctype='multipart/form-data'两个属性。

保存访问的网页

curl http://www.baidu.com > page.html

或者用 curl 的内置 option 就好,存下 http 的结果,用这个 option: -o

curl -o page.html http://www.baidu.com
curl -O http://sh.meituan.com/shop/42030772

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                              Dload  Upload   Total   Spent    Left  Speed
100  159k    0  159k    0     0   328k      0 --:--:-- --:--:-- --:--:--  328k

下载过程中标准输出还会显示下载的统计信息,比如进度、下载字节数、下载速度等

这样,自动保存文件42030772,看到屏幕上出现一个下载页面进度指示。显示100%则表示保存成功

设置 Header

curl -H 'Host: 157.166.226.25'-H 'Accept-Language: es'-H 'Cookie: ID=1234' http://cnn.com

显示文档信息

-I:显示文档信息

curl -I http://www.sina.com.cn/ -H Accept-Encoding:gzip,defalte

指定proxy服务器以及其端口

-x :可以指定http访问所使用的proxy服务器及其端口

curl -x 123.45.67.89:1080 -o page.html http://www.linuxidc.com
curl -x http://username:pwd@ip:port http://www.baidu.com

使用cookie

有些网站是使用cookie来记录session信息。对于chrome这样的浏览器,可以轻易处理cookie信息,但在curl中只要增加相关参数也是可以很容易的处理cookie

-c: 保存http的response里面的cookie信息。

curl -c cookiec.txt  http://www.baidu.com

执行后cookie信息就被存到了cookiec.txt里面了

-D: 保存http的response里面的header信息

curl -D cookied.txt http://www.baidu.com

执行后cookie信息就被存到了cookied.txt里面了
注意:-c(小写)产生的cookie和-D里面的cookie是不一样的。

-b:使用cookie

很多网站都是通过监视你的cookie信息来判断你是否按规矩访问他们的网站的,因此我们需要使用保存的cookie信息。

curl -b cookiec.txt http://www.baidu.com

模仿浏览器信息

有些网站需要使用特定的浏览器去访问他们,有些还需要使用某些特定的版本。

-A :指定浏览器去访问网站

curl -A "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.0)" http://www.baidu.com

这样服务器端就会认为是使用IE8.0去访问的

伪造referer(盗链)

很多服务器会检查http访问的referer从而来控制访问。比如:你是先访问首页,然后再访问首页中的邮箱页面,这里访问邮箱的referer地址就是访问首页成功后的页面地址,如果服务器发现对邮箱页面访问的referer地址不是首页的地址,就断定那是个盗连了

-e:设定referer

curl -e "www.baidu.com" http://news.baidu.com/

这样就会让服务器其以为你是从www.baidu.com点击某个链接过来的

下载文件

9.1 -o/-O文件下载

-o: 把输出写到该文件中

curl -o dodo1.jpg http://www.shaimn.com/uploads/allimg/160613/1-160613121111.jpg

-O:把输出写到该文件中,保留远程文件的文件名

curl -O http://www.shaimn.com/uploads/allimg/160613/1-160613121111.jpg

这样就会以服务器上的名称保存文件到本地

9.2 循环下载

有时候下载图片可以能是前面的部分名称是一样的,就最后的尾椎名不一样

curl -O http://www.shaimn.com/uploads/allimg/160613/1-16061312111[1-5].jpg

这样就会把
1-16061312111.jpg、
1-16061312112.jpg、
1-16061312113.jpg、
1-16061312114.jpg、
1-16061312115.jpg 全部保存下来

9.3 下载重命名

curl http://www.shaimn.com/uploads/allimg/160613/1-16061312111[1-5].jpg -o dodo#1.jpg

dodo1.jpg,dodo2.jpg,dodo3.jpg,dodo4.jpg,dodo5.jpg

9.4 分块下载

有时候下载的东西会比较大,这个时候我们可以分段下载。使用内置option:-r

curl -r 0-100 -o dodo1_part1.JPG http://www.shaimn.com/uploads/allimg/160613/1-160613121111.jpg
curl -r 100-200 -o dodo1_part2.JPG http://www.shaimn.com/uploads/allimg/160613/1-160613121111.jpg
curl -r 200- -o dodo1_part3.JPG http://www.shaimn.com/uploads/allimg/160613/1-160613121111.jpg
cat dodo1_part* > dodo1.JPG

这样就可以查看dodo1.JPG的内容了

9.5 通过ftp下载文件

curl可以通过ftp下载文件,curl提供两种从ftp中下载的语法

curl -O -u 用户名:密码 ftp://www.linux.com/dodo1.JPG
curl -O ftp://用户名:密码@www.linux.com/dodo1.JPG

9.6 显示下载进度条

curl -# -O http://www.linux.com/dodo1.JPG

9.7:不会显示下载进度信息

curl -s -O http://www.linux.com/dodo1.JPG

断点续传

在windows中,我们可以使用迅雷这样的软件进行断点续传。

curl可以通过内置option:-C同样可以达到相同的效果
如果在下载dodo1.JPG的过程中突然掉线了,可以使用以下的方式续传

curl -C -O http://www.linux.com/dodo1.JPG

通过使用-C选项可对大文件使用断点续传功能,如:

# 当文件在下载完成之前结束该进程
curl -O http://www.gnu.org/software/gettext/manual/gettext.html
##############             20.1%

# 通过添加-C选项继续对该文件进行下载,已经下载过的文件不会被重新下载
curl -C - -O http://www.gnu.org/software/gettext/manual/gettext.html
###############            21.1%

上传文件

curl不仅仅可以下载文件,还可以上传文件。通过内置option:-T来实现

# curl -T dodo1.JPG -u 用户名:密码 ftp://www.linux.com/img/

这样就向ftp服务器上传了文件dodo1.JPG

显示抓取错误

# curl -f http://www.linux.com/error

对 CURL 使用网络限速

通过--limit-rate选项对CURL的最大网络使用进行限制

下载速度最大不会超过1000B/second

curl --limit-rate 1000B -O http://www.gnu.org/software/gettext/manual/gettext.html

 

linux curl 命令

-a/--append 上传文件时,附加到目标文件
-A/--user-agent <string> 设置用户代理发送给服务器
- anyauth 可以使用“任何”身份验证方法
-b/--cookie <name=string/file> cookie字符串或文件读取位置
- basic 使用HTTP基本验证
-B/--use-ascii 使用ASCII /文本传输
-c/--cookie-jar <file> 操作结束后把cookie写入到这个文件中
-C/--continue-at <offset> 断点续转
-d/--data <data> HTTP POST方式传送数据
--data-ascii <data> 以ascii的方式post数据
--data-binary <data> 以二进制的方式post数据
--negotiate 使用HTTP身份验证
--digest 使用数字身份验证
--disable-eprt 禁止使用EPRT或LPRT
--disable-epsv 禁止使用EPSV
-D/--dump-header <file> 把header信息写入到该文件中
--egd-file <file> 为随机数据(SSL)设置EGD socket路径
--tcp-nodelay 使用TCP_NODELAY选项
-e/--referer 来源网址
-E/--cert <cert[:passwd]> 客户端证书文件和密码 (SSL)
--cert-type <type> 证书文件类型 (DER/PEM/ENG) (SSL)
--key <key> 私钥文件名 (SSL)
--key-type <type> 私钥文件类型 (DER/PEM/ENG) (SSL)
--pass <pass> 私钥密码 (SSL)
--engine <eng> 加密引擎使用 (SSL). "--engine list" for list
--cacert <file> CA证书 (SSL)
--capath <directory> CA目录 (made using c_rehash) to verify peer against (SSL)
--ciphers <list> SSL密码
--compressed 要求返回是压缩的形势 (using deflate or gzip)
--connect-timeout <seconds> 设置最大请求时间
--create-dirs 建立本地目录的目录层次结构
--crlf 上传是把LF转变成CRLF
-f/--fail 连接失败时不显示http错误
--ftp-create-dirs 如果远程目录不存在,创建远程目录
--ftp-method [multicwd/nocwd/singlecwd] 控制CWD的使用
--ftp-pasv 使用 PASV/EPSV 代替端口
--ftp-skip-pasv-ip 使用PASV的时候,忽略该IP地址
--ftp-ssl 尝试用 SSL/TLS 来进行ftp数据传输
--ftp-ssl-reqd 要求用 SSL/TLS 来进行ftp数据传输
-F/--form <name=content> 模拟http表单提交数据
-form-string <name=string> 模拟http表单提交数据
-g/--globoff 禁用网址序列和范围使用{}和[]
-G/--get 以get的方式来发送数据
-h/--help 帮助
-H/--header <line>自定义头信息传递给服务器
--ignore-content-length 忽略的HTTP头信息的长度
-i/--include 输出时包括protocol头信息
-I/--head 只显示文档信息
从文件中读取-j/--junk-session-cookies忽略会话Cookie
- 界面<interface>指定网络接口/地址使用
- krb4 <级别>启用与指定的安全级别krb4
-j/--junk-session-cookies 读取文件进忽略session cookie
--interface <interface> 使用指定网络接口/地址
--krb4 <level> 使用指定安全级别的krb4
-k/--insecure 允许不使用证书到SSL站点
-K/--config 指定的配置文件读取
-l/--list-only 列出ftp目录下的文件名称
--limit-rate <rate> 设置传输速度
--local-port<NUM> 强制使用本地端口号
-m/--max-time <seconds> 设置最大传输时间
--max-redirs <num> 设置最大读取的目录数
--max-filesize <bytes> 设置最大下载的文件总量
-M/--manual 显示全手动
-n/--netrc 从netrc文件中读取用户名和密码
--netrc-optional 使用 .netrc 或者 URL来覆盖-n
--ntlm 使用 HTTP NTLM 身份验证
-N/--no-buffer 禁用缓冲输出
-o/--output 把输出写到该文件中
-O/--remote-name 把输出写到该文件中,保留远程文件的文件名
-p/--proxytunnel 使用HTTP代理
--proxy-anyauth 选择任一代理身份验证方法
--proxy-basic 在代理上使用基本身份验证
--proxy-digest 在代理上使用数字身份验证
--proxy-ntlm 在代理上使用ntlm身份验证
-P/--ftp-port <address> 使用端口地址,而不是使用PASV
-Q/--quote <cmd>文件传输前,发送命令到服务器
-r/--range <range>检索来自HTTP/1.1或FTP服务器字节范围
--range-file 读取(SSL)的随机文件
-R/--remote-time 在本地生成文件时,保留远程文件时间
--retry <num> 传输出现问题时,重试的次数
--retry-delay <seconds> 传输出现问题时,设置重试间隔时间
--retry-max-time <seconds> 传输出现问题时,设置最大重试时间
-s/--silent静音模式。不输出任何东西
-S/--show-error 显示错误
--socks4 <host[:port]> 用socks4代理给定主机和端口
--socks5 <host[:port]> 用socks5代理给定主机和端口
--stderr <file>
-t/--telnet-option <OPT=val> Telnet选项设置
--trace <file> 对指定文件进行debug
--trace-ascii <file> Like --跟踪但没有hex输出
--trace-time 跟踪/详细输出时,添加时间戳
-T/--upload-file <file> 上传文件
--url <URL> Spet URL to work with
-u/--user <user[:password]>设置服务器的用户和密码
-U/--proxy-user <user[:password]>设置代理用户名和密码
-v/--verbose
-V/--version 显示版本信息
-w/--write-out [format]什么输出完成后
-x/--proxy <host[:port]>在给定的端口上使用HTTP代理
-X/--request <command>指定什么命令
-y/--speed-time 放弃限速所要的时间。默认为30
-Y/--speed-limit 停止传输速度的限制,速度时间'秒
-z/--time-cond 传送时间设置
-0/--http1.0 使用HTTP 1.0
-1/--tlsv1 使用TLSv1(SSL)
-2/--sslv2 使用SSLv2的(SSL)
-3/--sslv3 使用的SSLv3(SSL)
--3p-quote like -Q for the source URL for 3rd party transfer
--3p-url 使用url,进行第三方传送
--3p-user 使用用户名和密码,进行第三方传送
-4/--ipv4 使用IP4
-6/--ipv6 使用IP6
-#/--progress-bar 用进度条显示当前的传送状态

 

 

一些常见的限制方式

 

上述都是讲的都是一些的基础的知识,现在我就列一些比较常见的限制方式,如何突破这些限制抓取数据。

Basic Auth 一般会有用户授权的限制,会在headers的Autheration字段里要求加入;

Referer 通常是在访问链接时,必须要带上Referer字段,服务器会进行验证,例如抓取京东的评论;

User-Agent 会要求真是的设备,如果不加会用编程语言包里自有User-Agent,可以被辨别出来;

Cookie 一般在用户登录或者某些操作后,服务端会在返回包中包含Cookie信息要求浏览器设置Cookie,没有Cookie会很容易被辨别出来是伪造请求;

也有本地通过JS,根据服务端返回的某个信息进行处理生成的加密信息,设置在Cookie里面;

Gzip 请求headers里面带了gzip,返回有时候会是gzip压缩,需要解压;

JavaScript 加密操作 一般都是在请求的数据包内容里面会包含一些被javascript进行加密限制的信息,例如新浪微博会进行SHA1和RSA加密,之前是两次SHA1加密,然后发送的密码和用户名都会被加密;

其他字段 因为http的headers可以自定义地段,所以第三方可能会加入了一些自定义的字段名称或者字段值,这也是需要注意的。

真实的请求过程中,其实不止上面某一种限制,可能是几种限制组合在一次,比如如果是类似RSA加密的话,可能先请求服务器得到Cookie,然后再带着Cookie去请求服务器拿到公钥,然后再用js进行加密,再发送数据到服务器。所以弄清楚这其中的原理,并且耐心分析很重要。

 

 

防封禁策略

 

Scrapy:

http://doc.scrapy.org/en/master/topics/practices.html#avoiding-getting-banned

 

如何让你的 scrapy 爬虫不再被 ban

根据 scrapy 官方文档:https://docs.scrapy.org/en/master/topics/practices.html#avoiding-getting-banned 里面的描述,要防止 scrapy 被 ban,主要有以下几个策略。

由于 Google cache 受国内网络的影响,你懂得; 所以主要从动态随机设置user agent、禁用cookies、设置延迟下载和使用代理IP这几个方式。

 

本文以 cnblogs 为例

创建 middlewares.py

scrapy代理IP、user agent 的切换都是通过 DOWNLOADER_MIDDLEWARES 进行控制,下面我们创建 middlewares.py文件。

[root@bogon cnblogs]# vi cnblogs/middlewares.py

如下内容:

import random
import base64
from settings import PROXIES

class RandomUserAgent(object):
    """Randomly rotate user agents based on a list of predefined ones"""

    def __init__(self, agents):
        self.agents = agents

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.getlist('USER_AGENTS'))

    def process_request(self, request, spider):
        #print "**************************" + random.choice(self.agents)
        request.headers.setdefault('User-Agent', random.choice(self.agents))

class ProxyMiddleware(object):
    def process_request(self, request, spider):
        proxy = random.choice(PROXIES)
        if proxy['user_pass'] is not None:
            #request.meta['proxy'] = "http://YOUR_PROXY_IP:PORT"
            request.meta['proxy'] = "http://%s" % proxy['ip_port']
            #proxy_user_pass = "USERNAME:PASSWORD"
            encoded_user_pass = base64.encodestring(proxy['user_pass'])
            request.headers['Proxy-Authorization'] = 'Basic ' + encoded_user_pass
            print "**************ProxyMiddleware have pass************" + proxy['ip_port']
        else:
            print "**************ProxyMiddleware no pass************" + proxy['ip_port']
            request.meta['proxy'] = "http://%s" % proxy['ip_port']

类 RandomUserAgent 主要用来动态获取 user agent,user agent 列表 USER_AGENTS 在 settings.py 中进行配置。

类 ProxyMiddleware 用来切换代理,proxy 列表 PROXIES 也是在 settings.py 中进行配置。

如果你用的是 socks5 代理,那么对不起,目前 scrapy 还不能直接支持,可以通过 Privoxy 等软件将其本地转化为 http 代理

修改 settings.py 配置 USER_AGENTS 和 PROXIES

USER_AGENTS = [
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
    "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
    "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
    "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
    "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
    "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
    "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
    "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
]


PROXIES = [
    {'ip_port': '111.11.228.75:80', 'user_pass': ''},
    {'ip_port': '120.198.243.22:80', 'user_pass': ''},
    {'ip_port': '111.8.60.9:8123', 'user_pass': ''},
    {'ip_port': '101.71.27.120:80', 'user_pass': ''},
    {'ip_port': '122.96.59.104:80', 'user_pass': ''},
    {'ip_port': '122.224.249.122:8088', 'user_pass': ''},
]

代理 IP可以网上搜索一下,上面的代理IP获取自:http://www.xici.net.co/

禁用 cookies:

COOKIES_ENABLED = False

设置下载延迟:

DOWNLOAD_DELAY=3

设置 DOWNLOADER_MIDDLEWARES 

DOWNLOADER_MIDDLEWARES = {
    'cnblogs.middlewares.RandomUserAgent': 1,
    'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 110,
    'cnblogs.middlewares.ProxyMiddleware': 100,
}

保存 settings.py

 

3、测试

[root@bogon cnblogs]# scrapy crawl CnblogsSpider

本文的 user agent 和 proxy 列表都是采用 settings.py 的方式进行设置的,实际生产中 user agent 和 proxy 有可能会经常更新,每次更改配置文件显得很笨拙也不便于管理。因而,可以根据需要保存在 mysql 数据库

 

 

SSL中间人监测关键技术 --- SSL会话劫持

 

数据流重定向技术是SSL中间人监测的基础,该技术的使用使得被监测主机与SSL服务器的通信流量都会经过监测主机。对于一般的中间人监测来说,再加上数据转发机制就已经足够。但对于SSL中间人监测来说,仅仅通过数据流重定向得到的都是经过加密后的数据,无法直接用来进行HTTP协议解析。故此必需使用SSL会话劫持技术,才能得到被监测主机与SSL服务器之间通信数据的明文。

自SSL问世以来,在其应用范围越来越广泛同时,多种针对SSL协议本身的缺陷或者其不规范引用的SSL会话劫持方法也随之出现,下面将详细分析两种典型的SSL会话劫持的实现原理和实现条件。

 

一、利用伪造的 X.509 证书

1.1 会话劫持原理

当 SSL 客户端与 SSL 服务端建立连接时,在正常的连接握手阶段,客户端必定会要求服务端出示其X.509公钥证书,并根据以下3个要素验证服务器证书的有效性:

  • a) 该公钥证书的subject name(主题名)和所访问的服务器站点的名称是否一致;
  • b) 该公钥证书的是否过期;
  • c) 该公钥证书及其签发者证书链中的证书的数字签名是否有效(层层验证,一直验证到根CA证书为止)。

当 SSL 客户端访问一个基于 HTTPS 的加密 Web 站点时,只要上述三个要素有一个验证没有通过,SSL 协议就会发出告警,大多数浏览器会弹出一个提示框,提示服务器证书存在的问题,但不会直接断开SSL连接,而是让用户决定是否继续。下图展示了IE 浏览器弹出的安全警报提示框。

大多数浏览器在验证到服务器证书存在问题后的处理方式是存在巨大隐患的,因为用户往往由于缺乏安全意识或者图方便而选择接受不安全的证书,这就使得伪造一个和合法证书极为相似的“伪证书”骗取 SSL 客户端用户信任的手段成为可能。下图展示了这种 SSL 会话劫持的主要流程(图中,C 为 SSL 客户端,M 为监测主机,S 为 SSL 服务端)

上图就是基于伪造证书进行劫持的流程,文字描述如下 :主机M通过数据流重定向技术,使得主机C与主机S之间的通信流量都流向主机M,主机C本欲与主机S建立SSL连接,但发送的连接建立请求被重定向到了主机M; 主机C首先与主机M建立TCP连接,然后向主机M发起SSL连接请求; 主机M收到来自主机C的连接请求后,首先与主机S建立TCP连接,然后向主机S发起SSL连接请求; 主机S响应主机M的请求,由此主机M与主机S之间成功建立SSL连接,主机M同时获得主机S的X.509公钥证书Certificate_S; 主机M根据Certificate_S中的关键信息(主要是subject name、有效期限等)伪造一个极相似的自签名证书Certificate_S’,并以此证书响应第②步中,来自主机C的SSL连接请求; 主机C的浏览器验证Certificate_S’的有效性,发现subject name与请求的站点名称一致,证书还在有效期内,但是并非由信任的机构颁发。于是弹出提示框,让用户选择是否继续。由于Certificate_S’与Certificate_S从外表上几乎看不出来差别,大部分用户会选择继续( 这是SSL会话劫持可以成功的关键 ),由此主机C与主机M成功建立SSL连接。 这样以后,主机C发往SSL服务端的数据,主机M可以捕获并解密查看;主机S返回给SSL客户端的数据,主机M也可以捕获并解密查看。至此,主机M实现了完整的SSL中间人监测。 经过以上步骤,主机M成功实现了主机C(SSL客户端)与主机S(SSL服务端)之间的会话劫持,并可以对明文形式的会话内容进行监测。

1.2 成功的必要条件 这种类型的 SSL 会话劫持成功的必要条件如下:

  • a) 能够通过ARP欺骗、DNS欺骗或者浏览器数据重定向等欺骗技术,使得SSL客户端C和服务端S之间的数据都流向中间人监测主机;
  • b) SSL客户端在接收到伪造的X.509证书后,用户选择信任该证书,并继续SSL连接;
  • c) SSL服务端未要求对SSL客户端进行认证。

 

二、利用 HTTP 与 HTTPS 之间跳转的验证漏洞

2.1 会话劫持原理

用户浏览网页时,使用 SSL 协议的方式一般有两种。一种是在浏览器地址栏输入网址时直接指定协议类型为HTTPS,另一种是通过HTTP响应的302状态将网页重定向到HTTPS 链接。2009年2月在美国拉斯维加斯举行的BlackHat黑客大会上,安全研究人员Moxie Marlinspike 演示了通过自己研发的SSLstrip工具劫持SSL会话来截获注册数据的方法,为SSL会话劫持提供了新思路。

SSLstrip 使用了社会工程学的原理:许多人为了图方便省事,在输入网址时一般不考虑传输协议,习惯上只是简单输入主机名,浏览器默认在这种情况下会使用 HTTP 协议。例如用户为了使用Gmail邮箱,直接输入accounts.google.com,浏览器会给谷歌服务器发送一个HTTP 请求,谷歌服务器认为电子邮件属于应加密的重要事务,使用HTTP不恰当,应改为使用HTTPS,于是它返回一个状态码为302的HTTP 响应,给出一个重定向网址https://accounts.google.com/ServiceLogin?passive=1209600&continue=https%3A%2F%2Faccounts.google.com%2FManageAccount&followup=https%3A%2F%2Faccounts.google.com%2FManageAccount,浏览器再使用这个重定向网址发出HTTPS 请求。 一个原本应该从头到尾使用HTTPS加密会话的过程中混入了使用明文传输的HTTP会话,一旦HTTP会话被劫持,HTTPS会话就可能受到威胁 。SSLstrip 正是利用这一点,通过劫持HTTP 会话劫持了SSL会话,下图所示 SSLstrip 原理示意图。

下面具体阐述基于SSLstrip的SSL会话劫持流程(阐述中依然以主机C为SSL客户端,主机M为监测主机,主机S为SSL服务端):主机M通过ARP重定向技术,使得主机C所有与外网的通信流都会从主机M处经过。 主机C向主机S的一个HTTPS页面发出一个HTTP请求,主机M监听这个请求并转发给主机S。 主机S返回一个状态码为302的HTTP 响应报文,报文消息头中Location头域以及消息实体中都给出了重定向网址,形式分别为,“Location: https://***.com/…”与“”。 主机M解析来自主机S的响应报文,将其中所有的https替换成http,指定主机M另一个未使用的端口为通信端口(假设为8181端口),并且记录修改过的url。需要做的替换包括:消息头中的“Location: https://***.com/…”替换成“Location: http://***.com:8181/…”;消息实体中链接“< a href=”https://***.com/…”>”替换成“”。 主机C解析经过篡改后的HTTP响应报文,经过重定向与主机M的8181端口通过HTTP方式建立了连接,二者之间通信数据明文传输。 主机M冒充客户端与主机S建立HTTPS会话,二者之间的通信数据通过密文传输,但主机M可以自由地解密这些数据。 经过以上步骤,主机M成功实现了主机C(SSL客户端)与主机S(SSL服务端)之间的会话劫持,并可以对明文形式的会话内容进行监测。

2.2 成功的必要条件 这种类型的SSL会话劫持成功的必要条件如下:

  • a) 能够通过ARP欺骗、DNS欺骗或者浏览器数据重定向等欺骗技术,使得SSL客户端和服务端S之间的数据都流向中间人监测主机;
  • b) 客户端访问的Web页面存在http页面至https页面的跳转;
  • c) SSL服务端未要求对SSL客户端进行认证。

 

三、两种典型 SSL 会话劫持技术的对比小结

传统的基于伪造 X.509 证书的 SSL 会话劫持方式,其最大的问题就在于客户端浏览器会弹出警告提示对话框,这个提示是如此醒目,以至于只要用户有一定的安全意识和网络知识,劫持成功的概率就会大大降低。随着网络知识的慢慢普及,这种技术的生存空间会越来越小。

基于 HTTP 与 HTTPS 之间跳转验证漏洞的 SSL 会话劫持方式,是近几年新出的一种技术。在此种方式下,客户端浏览器不会有任何不安全的警告或提示,只是原先的HTTPS连接已经被HTTP连接所替换,迷惑性大大增强。一般为了进一步加强欺骗效果,监测主机还可以一个银色的“安全锁”图案显示在非安全的网址前面。但其缺陷也很明显,一旦用户在浏览器地址栏的输入中指定使用HTTPS协议,就会发现网页根本无法打开。因此只要用户养成良好的上网习惯,这种方式的会话劫持就会失败。

 

 

安装 pycurl

安装命令:

sudo apt-get install libcurl4-openssl-dev
pip install pycurl

 

 

杂项

 

  1. scrapy

    了解 scrapy 已经做过的功能,优化等。。。防止重复造轮子,如,去重,编码检测,dns缓存,http长连接,gzip等等。

  2. JS相关。

    这个是被问的最多的。看具体情况解决。可模拟相关js执行、绕过,或直接调浏览器去访问。自己用一个JS引擎+模拟一个浏览器环境难度太大了(参见V8的DEMO)。

    调浏览器有很多方法。难以细说,关键字如下,selenium,phantomjs,casperjs,ghost,webkit,scrapyjs,splash。一些细节如关掉CSS渲染,图片加载等。只有scrapyjs是完全异步的,相对是速度最快的,scrapyjs将webkit的事件循环和twisted的事件循环合在一起了。其他的方案要么阻塞,要么用多进程。简单的js需求(对效率要求不高)随意选,最优方案是scrapyjs+定制webkit(去掉不需要的功能)。调浏览器开页面是比较耗资源的(主要是cpu)

  3. 内容解析。

    对于页面解析最强大的当然是XPATH、css选择器、正则表达式,这个对于不同网站不同的使用者都不一样,就不用过多的说明,附两个比较好的网址:

    正则表达式入门:http://www.cnblogs.com/huxi/archive/2010/07/04/1771073.html

    正则表达式在线测试:http://tool.oschina.net/regex/

    其次就是解析库了,常用的有两个lxml和BeautifulSoup,对于这两个的使用介绍两个比较好的网站:

    lxml:http://my.oschina.net/jhao104/blog/639448

    BeautifulSoup:http://cuiqingcai.com/1319.html

    对于这两个库,都是HTML/XML的处理库,Beautifulsoup纯python实现,效率低,但是功能实用,比如能用通过结果搜索获得某个HTML节点的源码;lxmlC语言编码,高效,支持Xpath

    机器学习不一定好用(效果问题,人工问题-需要训练)。还有写些正则去模糊匹配。

    新闻类似的正文提取有readability,boilerplate。

  4. 分布式。

    首先考虑按任务(目标)切分,然后让不同目标的爬虫在不同机器上跑

    完全的对等分布式(多爬虫爬一个目标),把任务队列替换掉爬虫改改即可。github里面有几个现有的实现参考。

    分布式需求可能是伪命题。想清楚为何要分布式。硬件够不够,像什么拿一个不支持持久化的url队列的爬虫说量大需要分布式的,我只能默念,你为何这么吊。

  5. 部署,调度

    部署推荐scrapyd。这也是官方推荐的方法。

    大量爬虫的调度,这个目前(13-10)没有现成的合适方法,期望是实现爬虫的某些配置放数据库,提供web后台 ,然后按配置周期、定时运行爬虫,终止,暂停爬虫等等。可以实现,但要自己写不少东西。

  6. ip 限制问题

    买的起大量ip的可买(买大量同网段爬可能导致整网段被封)。

    找大量免费的开放http代理,筛选可用的,免费开放代理不可靠,写个调度机制,自动根据成功次数,延迟等选择合适代理,这个功能难以在scrapy内实现,参考scrapinghub的crawlera,我完成了一个本地版。

    在开发爬虫过程中经常会遇到IP被封掉的情况,这时就需要用到代理IP;

    在urllib2包中有ProxyHandler类,通过此类可以设置代理访问网页,如下代码片段:

     import urllib2
    
     proxy = urllib2.ProxyHandler({'http': '127.0.0.1:8087'})
     opener = urllib2.build_opener(proxy)
     urllib2.install_opener(opener)
     response = urllib2.urlopen('http://www.baidu.com')
     print response.read()
    
  7. url 去重

    如果有千万级的URL需要去重,需要仔细看下scrapy的去重机制和bloom filter(布隆过滤器)。bloomfilter有个公式可以算需要多少内存。另bloomfilter + scrapy在github有现有实现可以参考。

  8. 存储。

    mongodb,mongodb不满足某些功能时考虑hbase,参考http://blog.scrapinghub.com/2013/05/13/mongo-bad-for-scraped-data/

  9. 硬件

    硬件扛不住别玩爬虫。。。曾在I3 4G 1T上跑爬虫。卡在磁盘io(量大,磁盘io差,内存低),出现内存占用飙升。很难调试(调试爬虫看实际跑耗时较长),初步以为是爬虫有问题内存占用高导致数据库卡。调试结果确认为,配置低量太大,导致数据库慢,数据库慢之后爬虫任务队列占满内存并开始写磁盘,又循环导致数据库慢。

  10. 爬虫监控

    scrapyd自带简单的监控,不够的话用scrapy的webservice自己写

 

如何防止死循环

在 Scrapy 的默认配置中,是根据 url 进行去重的。这个对付一般网站是够的。但是有一些网站的 SEO 做的很变态:为了让爬虫多抓,会根据 request,动态的生成一些链接,导致爬虫 在网站上抓取大量的随机页面,甚至是死循环。。

为了解决这个问题,有2个方案:

(1) 在 setting.py 中,设定爬虫的嵌套次数上限(全局设定,实际是通过 DepthMiddleware 实现的):

DEPTH_LIMIT = 20

(2) 在 parse 中通过读取 response 来自行判断( spider级别设定 ) :

def parse(self, response):
    if response.meta['depth'] > 100:
        print 'Loop?'

 

学习爬虫的正确打开方式

看了大部分回答不禁叹口气,主要是因为看到很多大牛在回答像  "如何入门爬虫"  这种问题的时候,一如当年学霸讲解题目,跳步无数,然后留下一句  " 不就是这样推嘛 ",让一众小白菜鸟一脸懵逼。。作为一个0起步(之前连python都不会),目前总算掌握基础,开始向上进阶的菜鸟,深知其中的不易,所以我会在这个回答里,尽可能全面、细节地分享给大家从0学习爬虫的各种步骤,如果对你有帮助,请点赞~


首先!你要对爬虫有个明确的认识。。。

 

在战略上藐视:

“所有网站皆可爬”:互联网的内容都是人写出来的,而且都是偷懒写出来的(不会第一页是a,下一页是8),所以肯定有规律,这就给人有了爬取的可能,可以说,天下没有不能爬的网站 “框架不变”:网站不同,但是原理都类似,大部分爬虫都是从 发送请求——获得页面——解析页面——下载内容——储存内容 这样的流程来进行,只是用的工具不同

 

在战术上重视:

持之以恒,戒骄戒躁:对于初学入门,不可轻易自满,以为爬了一点内容就什么都会爬了,爬虫虽然是比较简单的技术,但是往深学也是没有止境的(比如搜索引擎等)!只有不断尝试,刻苦钻研才是王道!(为何有种小学作文即视感)

然后,你需要一个宏伟的目标,来让你有持续学习的动力(没有实操项目,真的很难有动力)

我要爬整个豆瓣!... 我要爬整个草什么榴社区!我要爬知乎各种妹子的联系方式*&^#%^$#

接着,你需要扪心自问一下,自己的 python 基本功吼不吼啊? 吼啊!——OK,开始欢快地学习爬虫吧 !

不吼?你还需要学习一个!赶紧回去看Python核心编程教程。至少这些功能和语法你要有基本的掌握 : list,dict:用来序列化你爬的东西 切片:用来对爬取的内容进行分割,生成 条件判断(if等):用来解决爬虫过程中哪些要哪些不要的问题 循环和迭代(for while ):用来循环,重复爬虫动作 文件读写操作:用来读取参数、保存爬下来的内容等

 

然后,你需要补充一下下面几个内容,作为你的知识储备:

(注:这里并非要求“掌握”,下面讲的两点,只需要先了解,然后通过具体项目来不断实践,直到熟练掌握)

 

1、网页的基本知识:

基本的HTML语言知识(知道href等大学计算机一级内容即可)

理解网站的发包和收包的概念(POST GET)

稍微一点点的js知识,用于理解动态网页(当然如果本身就懂当然更好啦)

 

2、一些分析语言,为接下来解析网页内容做准备

NO.1 正则表达式:扛把子技术,总得会最基础的:

NO.2 XPATH:高效的分析语言,表达清晰简单,掌握了以后基本可以不用正则

参考:XPath 教程[http://link.zhihu.com/?target=http%3A//www.w3school.com.cn/xpath/]

NO.3 Beautifulsoup:

美丽汤模块解析网页神器,一款神器,如果不用一些爬虫框架(如后文讲到的scrapy),配合request,urllib等模块(后面会详细讲),可以编写各种小巧精干的爬虫脚本

官网文档:Beautiful Soup 4.2.0 文档[http://link.zhihu.com/?target=http%3A//beautifulsoup.readthedocs.org/zh_CN/latest/]

参考案例:

接着,你需要一些高效的工具来辅助

(同样,这里先了解,到具体的项目的时候,再熟悉运用)

NO.1 F12 开发者工具:

看源代码:快速定位元素

分析xpath:1、此处建议谷歌系浏览器,可以在源码界面直接右键看

NO.2 抓包工具:

推荐httpfox,火狐浏览器下的插件,比谷歌火狐系自带的F12工具都要好,可以方便查看网站收包发包的信息

NO.3 XPATH CHECKER (火狐插件):

非常不错的 xpath 测试工具,但是有几个坑,都是个人踩过的,,在此告诫大家:

  • 1、xpath checker 生成的是绝对路径,遇到一些动态生成的图标(常见的有列表翻页按钮等),飘忽不定的绝对路径很有可能造成错误,所以这里建议在真正分析的时候,只是作为参考
  • 2、记得把如下图 xpath 框里的“x:”去掉,貌似这个是早期版本xpath的语法,目前已经和一些模块不兼容(比如scrapy),还是删去避免报错

NO.4 正则表达测试工具:

在线正则表达式测试(http://link.zhihu.com/?target=http%3A//tool.oschina.net/regex/) ,拿来多练练手,也辅助分析!里面有很多现成的正则表达式可以用,也可以进行参考!

ok!这些你都基本有一些了解了,现在开始进入抓取时间,上各种模块吧!python 的火,很大原因就是各种好用的模块,这些模块是居家旅行爬网站常备的——

urllib
urllib2
requests

**不想重复造轮子,有没有现成的框架? 华丽丽的scrapy(这块我会重点讲,我的最爱)**

遇到动态页面怎么办?

selenium,会了这个配合 scrapy 无往不利,是居家旅行爬网站又一神器,

爬来的东西怎么用?

pandas(基于 numpy 的数据分析模块,相信我,如果你不是专门搞 TB 级数据的,这个就够了)

然后是数据库,这里我认为开始并不需要非常深入,在需要的时候再学习即可

mysql
mongodb
sqllite

遇到反爬虫策略验证码之类咋整?

PIL
opencv
pybrain

进阶技术

多线程、分布式

 

Python 网页爬虫 & 文本处理 & 科学计算 & 机器学习 & 数据挖掘兵器谱

曾经因为NLTK的缘故开始学习Python,之后渐渐成为我工作中的第一辅助脚本语言,虽然开发语言是C/C++,但平时的很多文本数据处理任务都交给了Python。离开腾讯创业后,第一个作品课程图谱也是选择了Python系的Flask框架,渐渐的将自己的绝大部分工作交给了Python。这些年来,接触和使用了很多Python工具包,特别是在文本处理,科学计算,机器学习和数据挖掘领域,有很多很多优秀的Python工具包可供使用,所以作为Pythoner,也是相当幸福的。其实如果仔细留意微博,你会发现很多这方面的分享,自己也Google了一下,发现也有同学总结了“Python机器学习库”,不过总感觉缺少点什么。最近流行一个词,全栈工程师(full stack engineer),作为一个苦逼的创业者,天然的要把自己打造成一个full stack engineer,而这个过程中,这些Python工具包给自己提供了足够的火力,所以想起了这个系列。当然,这也仅仅是抛砖引玉,希望大家能提供更多的线索,来汇总整理一套Python网页爬虫,文本处理,科学计算,机器学习和数据挖掘的兵器谱。

 

一、Python网页爬虫工具集

一个真实的项目,一定是从获取数据开始的。无论文本处理,机器学习和数据挖掘,都需要数据,除了通过一些渠道购买或者下载的专业数据外,常常需要大家自己动手爬数据,这个时候,爬虫就显得格外重要了,幸好,Python提供了一批很不错的网页爬虫工具框架,既能爬取数据,也能获取和清洗数据,我们也就从这里开始了:

1. Scrapy

Scrapy, a fast high-level screen scraping and web crawling framework for Python.

鼎鼎大名的Scrapy,相信不少同学都有耳闻,课程图谱中的很多课程都是依靠Scrapy抓去的,这方面的介绍文章有很多,推荐大牛pluskid早年的一篇文章:《Scrapy 轻松定制网络爬虫》,历久弥新。

官方主页:http://scrapy.org/
Github代码页: https://github.com/scrapy/scrapy

2. Beautiful Soup

You didn’t write that awful page. You’re just trying to get some data out of it. Beautiful Soup is here to help. Since 2004, it’s been saving programmers hours or days of work on quick-turnaround screen scraping projects.

读书的时候通过《集体智慧编程》这本书知道Beautiful Soup的,后来也偶尔会用用,非常棒的一套工具。客观的说,Beautifu Soup不完全是一套爬虫工具,需要配合urllib使用,而是一套HTML/XML数据分析,清洗和获取工具。

官方主页:http://www.crummy.com/software/BeautifulSoup/

3. Python-Goose

Html Content / Article Extractor, web scrapping lib in Python

Goose最早是用Java写得,后来用Scala重写,是一个Scala项目。Python-Goose用Python重写,依赖了Beautiful Soup。前段时间用过,感觉很不错,给定一个文章的URL, 获取文章的标题和内容很方便。

Github主页:https://github.com/grangier/python-goose

 

二、Python文本处理工具集

从网页上获取文本数据之后,依据任务的不同,就需要进行基本的文本处理了,譬如对于英文来说,需要基本的tokenize,对于中文,则需要常见的中文分词,进一步的话,无论英文中文,还可以词性标注,句法分析,关键词提取,文本分类,情感分析等等。这个方面,特别是面向英文领域,有很多优秀的工具包,我们一一道来。

1. NLTK — Natural Language Toolkit

NLTK is a leading platform for building Python programs to work with human language data. It provides easy-to-use interfaces to over 50 corpora and lexical resources such as WordNet, along with a suite of text processing libraries for classification, tokenization, stemming, tagging, parsing, and semantic reasoning, and an active discussion forum.

搞自然语言处理的同学应该没有人不知道NLTK吧,这里也就不多说了。不过推荐两本书籍给刚刚接触NLTK或者需要详细了解NLTK的同学: 一个是官方的《Natural Language Processing with Python》,以介绍NLTK里的功能用法为主,同时附带一些Python知识,同时国内陈涛同学友情翻译了一个中文版,这里可以看到:推荐《用Python进行自然语言处理》中文翻译-NLTK配套书;另外一本是《Python Text Processing with NLTK 2.0 Cookbook》,这本书要深入一些,会涉及到NLTK的代码结构,同时会介绍如何定制自己的语料和模型等,相当不错。

官方主页:http://www.nltk.org/
Github代码页:https://github.com/nltk/nltk

2. Pattern

Pattern is a web mining module for the Python programming language.

It has tools for data mining (Google, Twitter and Wikipedia API, a web crawler, a HTML DOM parser), natural language processing (part-of-speech taggers, n-gram search, sentiment analysis, WordNet), machine learning (vector space model, clustering, SVM), network analysis and canvas visualization.

Pattern由比利时安特卫普大学CLiPS实验室出品,客观的说,Pattern不仅仅是一套文本处理工具,它更是一套web数据挖掘工具,囊括了数据抓取模块(包括Google, Twitter, 维基百科的API,以及爬虫和HTML分析器),文本处理模块(词性标注,情感分析等),机器学习模块(VSM, 聚类,SVM)以及可视化模块等,可以说,Pattern的这一整套逻辑也是这篇文章的组织逻辑,不过这里我们暂且把Pattern放到文本处理部分。我个人主要使用的是它的英文处理模块Pattern.en, 有很多很不错的文本处理功能,包括基础的tokenize, 词性标注,句子切分,语法检查,拼写纠错,情感分析,句法分析等,相当不错。

官方主页:http://www.clips.ua.ac.be/pattern

3. TextBlob: Simplified Text Processing

TextBlob is a Python (2 and 3) library for processing textual data. It provides a simple API for diving into common natural language processing (NLP) tasks such as part-of-speech tagging, noun phrase extraction, sentiment analysis, classification, translation, and more.

TextBlob是一个很有意思的Python文本处理工具包,它其实是基于上面两个Python工具包NLKT和Pattern做了封装(TextBlob stands on the giant shoulders of NLTK and pattern, and plays nicely with both),同时提供了很多文本处理功能的接口,包括词性标注,名词短语提取,情感分析,文本分类,拼写检查等,甚至包括翻译和语言检测,不过这个是基于Google的API的,有调用次数限制。TextBlob相对比较年轻,有兴趣的同学可以关注。

官方主页:http://textblob.readthedocs.org/en/dev/
Github代码页:https://github.com/sloria/textblob

4. MBSP for Python

MBSP is a text analysis system based on the TiMBL and MBT memory based learning applications developed at CLiPS and ILK. It provides tools for Tokenization and Sentence Splitting, Part of Speech Tagging, Chunking, Lemmatization, Relation Finding and Prepositional Phrase Attachment.

MBSP与Pattern同源,同出自比利时安特卫普大学CLiPS实验室,提供了Word Tokenization, 句子切分,词性标注,Chunking, Lemmatization,句法分析等基本的文本处理功能,感兴趣的同学可以关注。

官方主页:http://www.clips.ua.ac.be/pages/MBSP

5. Gensim: Topic modeling for humans

Gensim是一个相当专业的主题模型Python工具包,无论是代码还是文档,我们曾经用《如何计算两个文档的相似度》介绍过Gensim的安装和使用过程,这里就不多说了。

官方主页:http://radimrehurek.com/gensim/index.html
github代码页:https://github.com/piskvorky/gensim

6. langid.py: Stand-alone language identification system

语言检测是一个很有意思的话题,不过相对比较成熟,这方面的解决方案很多,也有很多不错的开源工具包,不过对于Python来说,我使用过langid这个工具包,也非常愿意推荐它。langid目前支持97种语言的检测,提供了很多易用的功能,包括可以启动一个建议的server,通过json调用其API,可定制训练自己的语言检测模型等,可以说是“麻雀虽小,五脏俱全”。

Github主页:https://github.com/saffsd/langid.py

7. Jieba: 结巴中文分词

“结巴”中文分词:做最好的Python中文分词组件 “Jieba” (Chinese for “to stutter”) Chinese text segmentation: built to be the best Python Chinese word segmentation module.

好了,终于可以说一个国内的Python文本处理工具包了:结巴分词,其功能包括支持三种分词模式(精确模式、全模式、搜索引擎模式),支持繁体分词,支持自定义词典等,是目前一个非常不错的Python中文分词解决方案。

Github主页:https://github.com/fxsjy/jieba

8. xTAS

xtas, the eXtensible Text Analysis Suite, a distributed text analysis package based on Celery and Elasticsearch.

感谢微博朋友 @大山坡的春 提供的线索:我们组同事之前发布了xTAS,也是基于python的text mining工具包,欢迎使用,链接:http://t.cn/RPbEZOW。看起来很不错的样子,回头试用一下。

Github代码页:https://github.com/NLeSC/xtas

 

三、Python 科学计算工具包

说起科学计算,大家首先想起的是 Matlab,集数值计算,可视化工具及交互于一身,不过可惜是一个商业产品。开源方面除了GNU Octave在尝试做一个类似Matlab的工具包外,Python的这几个工具包集合到一起也可以替代Matlab的相应功能:NumPy+SciPy+Matplotlib+iPython。同时,这几个工具包,特别是NumPy和SciPy,也是很多Python文本处理 & 机器学习 & 数据挖掘工具包的基础,非常重要。最后再推荐一个系列《用Python做科学计算》,将会涉及到NumPy, SciPy, Matplotlib,可以做参考。

1. NumPy

NumPy is the fundamental package for scientific computing with Python. It contains among other things:
1)a powerful N-dimensional array object
2)sophisticated (broadcasting) functions
3)tools for integrating C/C++ and Fortran code
4) useful linear algebra, Fourier transform, and random number capabilities

Besides its obvious scientific uses, NumPy can also be used as an efficient multi-dimensional container of generic data. Arbitrary data-types can be defined. This allows NumPy to seamlessly and speedily integrate with a wide variety of databases.

NumPy几乎是一个无法回避的科学计算工具包,最常用的也许是它的N维数组对象,其他还包括一些成熟的函数库,用于整合C/C++和Fortran代码的工具包,线性代数、傅里叶变换和随机数生成函数等。NumPy提供了两种基本的对象:ndarray(N-dimensional array object)和 ufunc(universal function object)。ndarray是存储单一数据类型的多维数组,而ufunc则是能够对数组进行处理的函数。

官方主页:http://www.numpy.org/

2. SciPy:Scientific Computing Tools for Python

SciPy refers to several related but distinct entities:

1)The SciPy Stack, a collection of open source software for scientific computing in Python, and particularly a specified set of core packages.
2)The community of people who use and develop this stack.
3)Several conferences dedicated to scientific computing in Python – SciPy, EuroSciPy and SciPy.in.
4)The SciPy library, one component of the SciPy stack, providing many numerical routines.

“SciPy是一个开源的Python算法库和数学工具包,SciPy包含的模块有最优化、线性代数、积分、插值、特殊函数、快速傅里叶变换、信号处理和图像处理、常微分方程求解和其他科学与工程中常用的计算。其功能与软件MATLAB、Scilab和GNU Octave类似。 Numpy和Scipy常常结合着使用,Python大多数机器学习库都依赖于这两个模块。”—-引用自“Python机器学习库

官方主页:http://www.scipy.org/

3. Matplotlib

matplotlib is a python 2D plotting library which produces publication quality figures in a variety of hardcopy formats and interactive environments across platforms. matplotlib can be used in python scripts, the python and ipython shell (ala MATLAB®* or Mathematica®†), web application servers, and six graphical user interface toolkits.

matplotlib 是python最著名的绘图库,它提供了一整套和matlab相似的命令API,十分适合交互式地进行制图。而且也可以方便地将它作为绘图控件,嵌入GUI应用程序中。Matplotlib可以配合ipython shell使用,提供不亚于Matlab的绘图体验,总之用过了都说好。

官方主页:http://matplotlib.org/

4. iPython

IPython provides a rich architecture for interactive computing with:

1)Powerful interactive shells (terminal and Qt-based).
2)A browser-based notebook with support for code, text, mathematical expressions, inline plots and other rich media.
3)Support for interactive data visualization and use of GUI toolkits.
4)Flexible, embeddable interpreters to load into your own projects.
5)Easy to use, high performance tools for parallel computing.

“iPython 是一个Python 的交互式Shell,比默认的Python Shell 好用得多,功能也更强大。 她支持语法高亮、自动完成、代码调试、对象自省,支持 Bash Shell 命令,内置了许多很有用的功能和函式等,非常容易使用。 ” 启动iPython的时候用这个命令“ipython –pylab”,默认开启了matploblib的绘图交互,用起来很方便。

官方主页:http://ipython.org/

 

四、Python 机器学习 & 数据挖掘 工具包

机器学习和数据挖掘这两个概念不太好区分,这里就放到一起了。这方面的开源Python工具包有很多,这里先从熟悉的讲起,再补充其他来源的资料,也欢迎大家补充。

1. scikit-learn: Machine Learning in Python

scikit-learn (formerly scikits.learn) is an open source machine learning library for the Python programming language. It features various classification, regression and clustering algorithms including support vector machines, logistic regression, naive Bayes, random forests, gradient boosting, k-means and DBSCAN, and is designed to interoperate with the Python numerical and scientific libraries NumPy and SciPy.

首先推荐大名鼎鼎的scikit-learn,scikit-learn是一个基于NumPy, SciPy, Matplotlib的开源机器学习工具包,主要涵盖分类,回归和聚类算法,例如SVM, 逻辑回归,朴素贝叶斯,随机森林,k-means等算法,代码和文档都非常不错,在许多Python项目中都有应用。例如在我们熟悉的NLTK中,分类器方面就有专门针对scikit-learn的接口,可以调用scikit-learn的分类算法以及训练数据来训练分类器模型。这里推荐一个视频,也是我早期遇到scikit-learn的时候推荐过的:推荐一个Python机器学习工具包Scikit-learn以及相关视频–Tutorial: scikit-learn – Machine Learning in Python

官方主页:http://scikit-learn.org/

2. Pandas: Python Data Analysis Library

Pandas is a software library written for the Python programming language for data manipulation and analysis. In particular, it offers data structures and operations for manipulating numerical tables and time series.

第一次接触Pandas是由于Udacity上的一门数据分析课程“Introduction to Data Science” 的Project需要用Pandas库,所以学习了一下Pandas。Pandas也是基于NumPy和Matplotlib开发的,主要用于数据分析和数据可视化,它的数据结构DataFrame和R语言里的data.frame很像,特别是对于时间序列数据有自己的一套分析机制,非常不错。这里推荐一本书《Python for Data Analysis》,作者是Pandas的主力开发,依次介绍了iPython, NumPy, Pandas里的相关功能,数据可视化,数据清洗和加工,时间数据处理等,案例包括金融股票数据挖掘等,相当不错。

官方主页:http://pandas.pydata.org/

=====================================================================
分割线,以上工具包基本上都是自己用过的,以下来源于其他同学的线索,特别是《Python机器学习库》,《23个python的机器学习包》,做了一点增删修改,欢迎大家补充
=====================================================================

3. mlpy – Machine Learning Python

mlpy is a Python module for Machine Learning built on top of NumPy/SciPy and the GNU Scientific Libraries.

mlpy provides a wide range of state-of-the-art machine learning methods for supervised and unsupervised problems and it is aimed at finding a reasonable compromise among modularity, maintainability, reproducibility, usability and efficiency. mlpy is multiplatform, it works with Python 2 and 3 and it is Open Source, distributed under the GNU General Public License version 3.

官方主页:http://mlpy.sourceforge.net/

4. MDP:The Modular toolkit for Data Processing

Modular toolkit for Data Processing (MDP) is a Python data processing framework.
From the user’s perspective, MDP is a collection of supervised and unsupervised learning algorithms and other data processing units that can be combined into data processing sequences and more complex feed-forward network architectures.
From the scientific developer’s perspective, MDP is a modular framework, which can easily be expanded. The implementation of new algorithms is easy and intuitive. The new implemented units are then automatically integrated with the rest of the library.
The base of available algorithms is steadily increasing and includes signal processing methods (Principal Component Analysis, Independent Component Analysis, Slow Feature Analysis), manifold learning methods ([Hessian] Locally Linear Embedding), several classifiers, probabilistic methods (Factor Analysis, RBM), data pre-processing methods, and many others.

“MDP用于数据处理的模块化工具包,一个Python数据处理框架。 从用户的观点,MDP是能够被整合到数据处理序列和更复杂的前馈网络结构的一批监督学习和非监督学习算法和其他数据处理单元。计算依照速度和内存需求而高效的执行。从科学开发者的观点,MDP是一个模块框架,它能够被容易地扩展。新算法的实现是容易且直观的。新实现的单元然后被自动地与程序库的其余部件进行整合。MDP在神经科学的理论研究背景下被编写,但是它已经被设计为在使用可训练数据处理算法的任何情况中都是有用的。其站在用户一边的简单性,各种不同的随时可用的算法,及应用单元的可重用性,使得它也是一个有用的教学工具。”

官方主页:http://mdp-toolkit.sourceforge.net/

5. PyBrain

PyBrain is a modular Machine Learning Library for Python. Its goal is to offer flexible, easy-to-use yet still powerful algorithms for Machine Learning Tasks and a variety of predefined environments to test and compare your algorithms.

PyBrain is short for Python-Based Reinforcement Learning, Artificial Intelligence and Neural Network Library. In fact, we came up with the name first and later reverse-engineered this quite descriptive “Backronym”.

“PyBrain(Python-Based Reinforcement Learning, Artificial Intelligence and Neural Network)是Python的一个机器学习模块,它的目标是为机器学习任务提供灵活、易应、强大的机器学习算法。(这名字很霸气)

PyBrain正如其名,包括神经网络、强化学习(及二者结合)、无监督学习、进化算法。因为目前的许多问题需要处理连续态和行为空间,必须使用函数逼近(如神经网络)以应对高维数据。PyBrain以神经网络为核心,所有的训练方法都以神经网络为一个实例。”

官方主页:http://www.pybrain.org/

6. PyML – machine learning in Python

PyML is an interactive object oriented framework for machine learning written in Python. PyML focuses on SVMs and other kernel methods. It is supported on Linux and Mac OS X.

“PyML是一个Python机器学习工具包,为各分类和回归方法提供灵活的架构。它主要提供特征选择、模型选择、组合分类器、分类评估等功能。”

项目主页:http://pyml.sourceforge.net/

7. Milk:Machine learning toolkit in Python.

Its focus is on supervised classification with several classifiers available:
SVMs (based on libsvm), k-NN, random forests, decision trees. It also performs
feature selection. These classifiers can be combined in many ways to form
different classification systems.

“Milk是Python的一个机器学习工具箱,其重点是提供监督分类法与几种有效的分类分析:SVMs(基于libsvm),K-NN,随机森林经济和决策树。它还可以进行特征选择。这些分类可以在许多方面相结合,形成不同的分类系统。对于无监督学习,它提供K-means和affinity propagation聚类算法。”

官方主页:http://luispedro.org/software/milk

http://luispedro.org/software/milk

8. PyMVPA: MultiVariate Pattern Analysis (MVPA) in Python

PyMVPA is a Python package intended to ease statistical learning analyses of large datasets. It offers an extensible framework with a high-level interface to a broad range of algorithms for classification, regression, feature selection, data import and export. It is designed to integrate well with related software packages, such as scikit-learn, and MDP. While it is not limited to the neuroimaging domain, it is eminently suited for such datasets. PyMVPA is free software and requires nothing but free-software to run.

“PyMVPA(Multivariate Pattern Analysis in Python)是为大数据集提供统计学习分析的Python工具包,它提供了一个灵活可扩展的框架。它提供的功能有分类、回归、特征选择、数据导入导出、可视化等”

官方主页:http://www.pymvpa.org/

9. Pyrallel – Parallel Data Analytics in Python

Experimental project to investigate distributed computation patterns for machine learning and other semi-interactive data analytics tasks.

“Pyrallel(Parallel Data Analytics in Python)基于分布式计算模式的机器学习和半交互式的试验项目,可在小型集群上运行”

Github代码页:http://github.com/pydata/pyrallel

10. Monte – gradient based learning in Python

Monte (python) is a Python framework for building gradient based learning machines, like neural networks, conditional random fields, logistic regression, etc. Monte contains modules (that hold parameters, a cost-function and a gradient-function) and trainers (that can adapt a module’s parameters by minimizing its cost-function on training data).

Modules are usually composed of other modules, which can in turn contain other modules, etc. Gradients of decomposable systems like these can be computed with back-propagation.

“Monte (machine learning in pure Python)是一个纯Python机器学习库。它可以迅速构建神经网络、条件随机场、逻辑回归等模型,使用inline-C优化,极易使用和扩展。”

官方主页:http://montepython.sourceforge.net

11. Theano

Theano is a Python library that allows you to define, optimize, and evaluate mathematical expressions involving multi-dimensional arrays efficiently. Theano features:
1)tight integration with NumPy – Use numpy.ndarray in Theano-compiled functions.
2)transparent use of a GPU – Perform data-intensive calculations up to 140x faster than with CPU.(float32 only)
3)efficient symbolic differentiation – Theano does your derivatives for function with one or many inputs.
4)speed and stability optimizations – Get the right answer for log(1+x) even when x is really tiny.
5)dynamic C code generation – Evaluate expressions faster.
6) extensive unit-testing and self-verification – Detect and diagnose many types of mistake.
Theano has been powering large-scale computationally intensive scientific investigations since 2007. But it is also approachable enough to be used in the classroom (IFT6266 at the University of Montreal).

“Theano 是一个 Python 库,用来定义、优化和模拟数学表达式计算,用于高效的解决多维数组的计算问题。Theano的特点:紧密集成Numpy;高效的数据密集型GPU计算;高效的符号微分运算;高速和稳定的优化;动态生成c代码;广泛的单元测试和自我验证。自2007年以来,Theano已被广泛应用于科学运算。theano使得构建深度学习模型更加容易,可以快速实现多种模型。PS:Theano,一位希腊美女,Croton最有权势的Milo的女儿,后来成为了毕达哥拉斯的老婆。”

12. Pylearn2

Pylearn2 is a machine learning library. Most of its functionality is built on top of Theano. This means you can write Pylearn2 plugins (new models, algorithms, etc) using mathematical expressions, and theano will optimize and stabilize those expressions for you, and compile them to a backend of your choice (CPU or GPU).

“Pylearn2建立在theano上,部分依赖scikit-learn上,目前Pylearn2正处于开发中,将可以处理向量、图像、视频等数据,提供MLP、RBM、SDA等深度学习模型。”

 

 

 

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python爬虫实例教程-代码,是一个指导如何使用Python编写爬虫教程。下面我将用300字中文来回答这个问题。 这个教程的代码示例主要分为以下几个部分。 第一部分是爬虫的准备工作,包括安装Python和相关的第三方库,如requests、BeautifulSoup等。首先需要安装Python,并配置好相关环境变量。然后使用pip安装requests和BeautifulSoup库,这两个库是编写爬虫时常用的工具。 第二部分是使用requests库发送网络请求,获取网页的HTML源代码。首先需要导入requests库,然后使用get方法发送网络请求,传入要爬取的网页地址。之后可以通过response对象的text属性获取网页的HTML源代码。 第三部分是使用BeautifulSoup库解析HTML源代码,提取出需要的数据。首先需要导入BeautifulSoup库,然后将HTML源代码作为参数传入BeautifulSoup类的构造函数。之后可以使用BeautifulSoup对象的find、findAll等方法,根据HTML标签和属性来提取出需要的数据。 第四部分是保存爬取的数据。可以使用Python自带的文件操作函数,如open、write等,将爬取的数据保存到本地文件中。 最后,还可以通过循环、条件判断等控制结构,以及其他的Python编程技巧,对爬虫代码进行更加复杂的处理和优化。 总的来说,Python爬虫实例教程-代码提供了一个从安装环境到写爬虫代码的全过程。通过学习这些代码示例,可以掌握如何使用Python编写爬虫,以及一些常用的爬虫技巧和工具的使用。希望以上回答可以帮助到你。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值