聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制

爬虫基本概念

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

  • 关于误伤:
    假如网站管理人员发现某个 IP 访问过于频繁,判定为爬虫,可以将其 IP 禁封,这是最有效的方法。但是这样做就会带来误伤,①比如学校或者网吧,他们对外的 IP 只有一个或者几个,内部全部属于局域网,如果学校或者网吧的某一个人写了一个爬虫,那么如果禁用掉这个对外的公网 IP ,内部所有人就都不能访问这个网站了,损失广大用户。②现在 IP 通常都是动态分配的 IP,比如某个小区、某个区域,当我们重启路由器后网络 IP 实际上是会变的(大多数情况下),假如某个人写了一个爬虫,禁用掉这个人所用 IP,过段时间这个 IP 分配给了另一个人,那么另一个人就无法访问这个网站,即使他并没有写过爬虫。
    所以网站通常采用的是禁用某个 IP 一段时间。

反爬虫的目的

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

爬虫和反爬虫对抗过程

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

  • 其他反爬虫策略:
    ①当判断是一个爬虫在访问的时候,可以返回假的数据,而不是直接禁用掉
    ②分析用户行的的时候发现某些 IP 请求的时候只请求 HTML 页面,而不请求 CSS、JS、图片等文件(爬虫为了并发),这就可以判断明显的是爬虫行为,这种判断方式非常有效
    ③但是如果通过 selenium + 浏览器的策略,是无法判断是否为爬虫的,一切请求和真实用户并无差别,所以理论上网站是不可能从技术上根本的解决爬虫问题,成本过高只好放弃

Scrapy 架构图

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 Scrapy 架构图

  • engine 是最核心的部分,爬虫所有流向都经过引擎
  • 爬虫的第一步是从 spiders 开始的
  • 注意 spiders 过来的 requests 不是直接交给 downloader 去下载的,而是交给调度器 scheduler,然后 engine 再从 scheduler 里面区区,取出来才交给下载器 downloader 去下载

Scrapy 的 Request 和 Response

官方文档:https://doc.scrapy.org/en/latest/topics/request-response.html

Request 部分源码

class Request(object_ref):

    def __init__(self, url, callback=None, method='GET', headers=None, body=None,
                 cookies=None, meta=None, encoding='utf-8', priority=0,
                 dont_filter=False, errback=None, flags=None):

        self._encoding = encoding  # this one has to be set first
        self.method = str(method).upper()
        self._set_url(url)
        self._set_body(body)
        assert isinstance(priority, int), "Request priority not an integer: %r" % priority
        self.priority = priority

        if callback is not None and not callable(callback):
            raise TypeError('callback must be a callable, got %s' % type(callback).__name__)
        if errback is not None and not callable(errback):
            raise TypeError('errback must be a callable, got %s' % type(errback).__name__)
        assert callback or not errback, "Cannot use errback without a callback"
        self.callback = callback
        self.errback = errback

        self.cookies = cookies or {}
        self.headers = Headers(headers or {}, encoding=encoding)
        self.dont_filter = dont_filter

        self._meta = dict(meta) if meta else None
        self.flags = [] if flags is None else list(flags)

    @property
    def meta(self):
        if self._meta is None:
            self._meta = {}
        return self._meta
    ...

参数说明:

  • url:需要请求的 URL
  • callback:请求返回的 Response 由这个指定的方法来处理
  • method:请求方法,默认 GET,注意大写
  • headers:请求时包含的头信息
  • body:请求体
  • cookies:指定请求的 cookies,如果请求的网页带有 cookies,scrapy 在发送第二次请求的时候会默认携带 cookies
  • meta:在不同的请求之间传递数据,会经常用到,类型为 dict
  • encoding:指定编码格式,默认 ‘utf-8’
  • priority:指定请求的优先级,会影响 scheduler 的优先调度顺序,可以通过设定此参数的值来改变请求顺序,加入设置某个 Request 的 priority
    比较高,那么即使这个 Request 是后传递过来的,也会先调度
  • dont_filter:yield 过去重复的 Request 要不要过滤,默认 False 是会过滤重复请求的,设为 True 就可以不去重
  • errback:是一个回调方法,错误处理,比如请求返回的响应是 500或者404等的时候,可以通过这个回调设置后续的处理
  • flags

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

Response部分源码
class Response(object_ref):

    def __init__(self, url, status=200, headers=None, body=b'', flags=None, request=None):
        self.headers = Headers(headers or {})
        self.status = int(status)
        self._set_body(body)
        self._set_url(url)
        self.request = request
        self.flags = [] if flags is None else list(flags)

    @property
    def meta(self):
        try:
            return self.request.meta
        except AttributeError:
            raise AttributeError(
                "Response.meta not available, this response "
                "is not tied to any request"
            )
    ...

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

参数说明:

  • url:网页 URL
  • status:返回状态码
  • headers:服务器返回的 headers
  • body:服务器返回的 HTML 内容
  • flags:
  • request:就是之前 yield 过来的 Request,可以通过这个参数知道当前
    Response 是对哪个 Request 进行的下载

Response 是有子类的:https://doc.scrapy.org/en/latest/topics/request-response.html#response-subclasses
用的最多的就是 HtmlResponse

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

总结:
spiders 产出 Request
downloader 返回 Resonse
整体流程:
spider yield Request 给 engine,engine 将这个 Request 发给 scheduler,scheduler 将 Request 再发送给 engine,engine 再将 Request 发送给 downloader,downloader 下载完成后返回 Response 给 engine,engine 将 Response 交个 spider,spider 解析 Response 数据,提取出 item 交给 item pipelines,提取出 Request 再次交个 scheduler,循环 1~7 的步骤

随机更换 User-Agent

  • 以知乎爬虫为例:
方法 1

在 settings.py 中自定义一个存储所有 User-Agent 的列表
然后在 zhihu spider 中随机取出一个 User-Agent

  • settings.py 中添加如下代码
# ArticleSpider/settings.py

# User-Agent 列表
user_agent_list = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
    'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0',
]

# ArticleSpider/spiders/zhihu_question.py

from ArticleSpider.settings import user_agent_list


class ZhihuQuestionSpider(scrapy.Spider):
    name = 'zhihu_question'
    allowed_domains = ['www.zhihu.com']
    # start_urls = ['https://www.zhihu.com/explore']  # 重写了爬虫入口 start_requests,所以 start_urls 也就没必要了
    # question 的第一页 answer 请求 URL
    start_answer_url = 'https://www.zhihu.com/api/v4/questions/{question}/answers?sort_by=default&include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit={limit}&offset={offset}'

    # 随机取出一个 User-Agent
    import random
    random_agent = random.choice(user_agent_list)
    headers = {
        'authorization': 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20',
        'Host': 'www.zhihu.com',
        'Referer': 'https://www.zhihu.com/',
        'User-Agent': random_agent,
    }

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

但是,事实上这样做是无效的,是不能真正实现每次请求时随机取一个 User-Agent 的,因为这是写在类变量里面的,只在类初始化的时候随机取一次,以后的每次请求都是用的这个,而不会再次随机取一个 User-Agent

方法 2

settings.py 内容不变,zhihu_question.py 代码要修改下,在每个 scrapy.Request 请求发送出去之前,都随机获取一个 User-Agent,这样就能够真正达到每次发送请求爬取页面都会随机切换 User-Agent

# ArticleSpider/spiders/zhihu_question.py

from ArticleSpider.settings import user_agent_list


class ZhihuQuestionSpider(scrapy.Spider):
    name = 'zhihu_question'
    allowed_domains = ['www.zhihu.com']
    # start_urls = ['https://www.zhihu.com/explore']  # 重写了爬虫入口 start_requests,所以 start_urls 也就没必要了
    # question 的第一页 answer 请求 URL
    start_answer_url = 'https://www.zhihu.com/api/v4/questions/{question}/answers?sort_by=default&include=data%5B%2A%5D.is_normal%2Cadmin_closed_comment%2Creward_info%2Cis_collapsed%2Cannotation_action%2Cannotation_detail%2Ccollapse_reason%2Cis_sticky%2Ccollapsed_by%2Csuggest_edit%2Ccomment_count%2Ccan_comment%2Ccontent%2Ceditable_content%2Cvoteup_count%2Creshipment_settings%2Ccomment_permission%2Ccreated_time%2Cupdated_time%2Creview_info%2Crelevant_info%2Cquestion%2Cexcerpt%2Crelationship.is_authorized%2Cis_author%2Cvoting%2Cis_thanked%2Cis_nothelp%3Bdata%5B%2A%5D.mark_infos%5B%2A%5D.url%3Bdata%5B%2A%5D.author.follower_count%2Cbadge%5B%3F%28type%3Dbest_answerer%29%5D.topics&limit={limit}&offset={offset}'
    headers = {
        'authorization': 'oauth c3cef7c66a1843f8b3a9e6a1e3160e20',
        'Host': 'www.zhihu.com',
        'Referer': 'https://www.zhihu.com/',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
    }

    def start_requests(self):
        """
            重写爬虫入口 start_requests
        """
        url = 'https://www.zhihu.com/node/ExploreAnswerListV2?params='
        # offset = 0
        for offset in range(4):
            time.sleep(random.random())
            paramse = {"offset": offset * 5, "type": "day"}
            full_url = f'{url}{json.dumps(paramse)}'

            # 以后每次发送请求都会随机获取一个 User-Agent
            import random
            random_agent = random.choice(user_agent_list)
            self.headers['User-Agent'] = random_agent
            yield scrapy.Request(url=full_url, headers=self.headers, callback=self.parse_question)

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

但是,这个方法也有一个弊端,就是每个要发送请求的方法中都要写一遍同样的代码,代码重复

方法 3

通过自定义 downloader middleware 来实现随机切换 User-Agent

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

settings.py 中事实上已经配置了 DOWNLOADER_MIDDLEWARES,只不过默认是被注释掉的
DOWNLOADER_MIDDLEWARES 和 ITEM_PIPELINES 类似,同样需要配置类名以及后面的数字,数字代表顺序

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

scrapy 本身提供了一个 UserAgentMiddleware,默认 user_agent=’Scrapy’

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

Downloader Middleware 官方文档:https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
Downloader Middleware 是一个介于 scrapy 的 Request 和 Response 之间的一个钩子框架,可以用来全局修改 scrapy 的 Request 和 Response,这里处理的是 User-Agent 所以是对 Request 的修改(重载 process_request),Response 修改实际上是和 Request 修改(重载 process_response)是一样的

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

  • 具体实现

首先 settings.py 中的 user_agent_list 配置不变

# ArticleSpider/settings.py

# User-Agent 列表
user_agent_list = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
    'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0',
]

middlewares.py 中编写自定义 middleware

# ArticleSpider/middlewares.py

class RandomUserAgentMiddleware(object):
    """
        随机更换 User-Agent
    """
    def __init__(self, crawler):
        super().__init__()
        self.user_agent_list = crawler.settings.get('user_agent_list', [])

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def process_request(self, request, spider):
        import random
        random_agent = random.choice(self.user_agent_list)
        request.headers.setdefault('User-Agent', random_agent)

将 RandomUserAgentMiddleware 配置到 settings.py 中

# ArticleSpider/settings.py

DOWNLOADER_MIDDLEWARES = {
    # 'ArticleSpider.middlewares.ArticlespiderDownloaderMiddleware': 543,
    'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None,
    'ArticleSpider.middlewares.RandomUserAgentMiddleware': 1,
}

现在这样写,已经基本满足需求了,单位一不足的是要自己维护这个 user_agent_list 列表,如果每次新增 User-Agent 的话,就要重启一次爬虫,会显得稍有麻烦

方法 4

使用 GitHub 上开源的库 fake-useragent,来实现随机切换 useragent

文档给出了使用方法

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

事实上 fake-useragent 维护了大量的 UserAgent,查看源代码可以发现其维护 UserAgent 的网址

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

fake-useragent 帮我们在线维护了很多 UserAgent,所以我们就没必要自己去维护了,可以直接拿过来用,将 fake-useragent 集成到 自己定义的 RandomUserAgentMiddleware 中间件中

# ArticleSpider/middlewares.py

from fake_useragent import UserAgent


class RandomUserAgentMiddleware(object):
    """
        随机更换 User-Agent
    """
    def __init__(self, crawler):
        super().__init__()
        self.ua = UserAgent()

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def process_request(self, request, spider):
        request.headers.setdefault('User-Agent', self.ua.random)

还可以对上面的配置做下优化,在 settings.py 中增加一个变量 RANDOM_UA_TYPE 用来改变随机获取的 UserAgent 类型,可以通过这个变量值来设定到底需要获取哪种 UserAgent

# ArticleSpider/settings.py

# 随机 User-Agent 类型配置
RANDOM_USER_AGENT = 'random'
# ArticleSpider/middlewares.py

from fake_useragent import UserAgent


class RandomUserAgentMiddleware(object):
    """
        随机更换 User-Agent
    """
    def __init__(self, crawler):
        super().__init__()
        self.ua = UserAgent()
        self.ua_type = crawler.settings.get('RANDOM_USER_AGENT', 'random')

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def process_request(self, request, spider):
        def get_ua():
            return getattr(self.ua, self.ua_type)  # 获取 self.ua 对象的 self.ua_type 属性的值
        request.headers.setdefault('User-Agent', get_ua())

Scrapy 实现 IP 代理池

如果只是切换一条代理,只需要在自定义 middleware 中设置 request.meta[‘proxy’] 即可,如下,格式为 ‘http/https://IP地址:port’

request.meta['proxy'] = 'http://120.39.167.238:38806'

实际代码中例子,仍然使用随机更换 UserAgent 的中间件 RandomUserAgentMiddleware 测试

# ArticleSpider/middlewares.py

from fake_useragent import UserAgent


class RandomUserAgentMiddleware(object):
    """
        随机更换 User-Agent
    """
    def __init__(self, crawler):
        super().__init__()
        self.ua = UserAgent()
        self.ua_type = crawler.settings.get('RANDOM_USER_AGENT', 'random')

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler)

    def process_request(self, request, spider):
        def get_ua():
            return getattr(self.ua, self.ua_type)  # 获取 self.ua 对象的 self.ua_type 属性的值
        request.headers.setdefault('User-Agent', get_ua())
        # 设置代理 IP,只适用于设置一条代理 IP 的情况
        request.meta['proxy'] = 'http://120.39.167.238:38806'

测试 IP 是否切换成功,编写一个简单的 HTTP Server 文件 simple_http_server.py,这个文件功能很简单,就是在 8088 端口启动一个服务,然后可以打印访问者的 IP 地址

# simple_http_server.py

#!/usr/bin/env python
"""
Very simple HTTP server in python.
Usage::
    ./dummy-web-server.py [<port>]
Send a GET request::
    curl http://localhost
Send a HEAD request::
    curl -I http://localhost
Send a POST request::
    curl -d "foo=bar&bin=baz" http://localhost
"""
from http.server import BaseHTTPRequestHandler, HTTPServer


class S(BaseHTTPRequestHandler):
    def _set_headers(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()

    def do_GET(self):
        self._set_headers()
        print(self.address_string())  # 打印访问用户的 IP 地址
        self.wfile.write("<html><body><h1>Hello!</h1></body></html>".encode("utf-8"))

    def do_HEAD(self):
        self._set_headers()

    def do_POST(self):
        # Doesn't do anything with posted data
        self._set_headers()
        self.wfile.write("<html><body><h1>POST!</h1></body></html>")


def run(server_class=HTTPServer, handler_class=S, port=8088):
    server_address = ('', port)
    httpd = server_class(server_address, handler_class)
    print('Starting httpd...')
    httpd.serve_forever()


if __name__ == "__main__":
    from sys import argv

    if len(argv) == 2:
        run(port=int(argv[1]))
    else:
        run()

在服务器上启动这个 Server

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

在浏览器中访问效果

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

启动爬虫测试

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

可以发现,只需要简单的一个设置,既可以成功切换 Scrapy 爬虫的 IP

但是只是切换一条代理不能满足爬虫需求,只是演示还可以,实际项目中还是要使用 IP 池来随机切换 IP

大致思路:自己写一个爬虫,来爬取分享免费代理 IP 的网址上面可用的 IP,然后放到数据库或文件中,这样就有了 IP 代理的数据源,然后运行爬虫的时候从这些代理中随机取出一个来请求网页,类似之前的随机设置 UserAgent 一个道理,这样就实现了 IP 代理池随机切换 IP

以西刺网站为例进行爬取

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

首先在项目根目录下创建 tools python package,专门用来存放一些脚本文件,在这里新建一个 crawl_xici_ip.py 爬虫文件,专门用来爬取 西刺网站上免费的 IP 代理

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

新建表 proxy_ip,用来存放 IP

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

爬取西刺免费 IP 代理代码实现

# tools/crawl_xici_ip.py

"""
爬取西刺免费 IP 代理
"""
import requests
from scrapy.selector import Selector
import pymysql

conn = pymysql.Connect(host='127.0.0.1', user='pythonic', passwd='pythonic', db='articles', port=3306)
cursor = conn.cursor()


def crawl_ips():
    """
        爬取西刺免费 IP 代理
    Returns:
        None
    """
    url = 'http://www.xicidaili.com/nn/{0}'
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0',
    }
    for i in range(1, 101):  # 获取 100 页的数据
        response = requests.get(url=url.format(i), headers=headers)
        # 使用 Scrapy 的 Selector 来提取数据
        selector = Selector(text=response.text)

        trs = selector.xpath('//table[@id="ip_list"]//tr')
        # trs = selector.css('#ip_list tr')

        ip_list = []
        for tr in trs[1:]:  # 去掉第一行表头
            ip = tr.xpath('./td[2]/text()').extract_first()
            port = tr.xpath('./td[3]/text()').extract_first()
            speed = tr.xpath('./td[7]/div/@title').extract_first().split('秒')[0]
            proxy_type = tr.xpath('./td[6]/text()').extract_first()

            ip_list.append((ip, port, proxy_type, speed))
        print(ip_list)

        for ip_info in ip_list:
            cursor.execute(
                """
                insert into proxy_ip(ip, port, proxy_type, speed)
                values('{0}', '{1}', '{2}', '{3}')
                """.format(ip_info[0], ip_info[1], ip_info[2], ip_info[3])
            )  # 注意因为数据库字段中这几个值都为 varchar 类型,所以 format 的时候 values 里面的值又要加引号
            conn.commit()


class GetIP(object):
    """
        获取 IP
    """

    def get_random_ip(self):
        """
            从数据库中随机获取一个 IP
        Returns:

        """
        random_sql = """select ip,port from articles.proxy_ip where proxy_type='HTTP' order by rand() limit 1"""
        cursor.execute(random_sql)
        ip, port = cursor.fetchone()
        judge_result = self.judge_ip(ip, port)
        if judge_result:
            return 'http://{0}:{1}'.format(ip, port)
        else:
            return self.get_random_ip()

    def judge_ip(self, ip, port):
        """
            判断 IP 是否可用
        """
        http_url = 'http://www.baidu.com'  # 访问百度首页来测试 IP 是否可用
        proxy_url = 'http://{0}:{1}'.format(ip, port)
        proxy_dict = {
            'http': http_url,
            # 'https': ''
        }
        try:
            response = requests.get(http_url, proxies=proxy_dict)
        except Exception as e:
            print('无效 IP')
            self.delete_ip(ip)
            return False
        else:
            code = response.status_code
            if 200 <= code < 300:
                print('可用 IP')
                return True
            else:
                print('无效 IP')
                self.delete_ip(ip)
                return False

    def delete_ip(self, ip):
        """
            删除数据库中无效 IP
        """
        delete_sql = """delete from proxy_ip where ip='{0}'""".format(ip)
        cursor.execute(delete_sql)
        conn.commit()
        return True


if __name__ == '__main__':
    # crawl_ips()
    get_ip = GetIP()
    print(get_ip.get_random_ip())

数据可以正常写入

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

定义一个 middleware 用来切换随机代理 IP

# ArticleSpider/middlewares.py

from tools.crawl_xici_ip import GetIP


class RandomProxyMiddleware(object):
    """
        随机切换代理 IP
    """
    def process_request(self, request, spider):
        get_ip = GetIP()
        request.meta['proxy'] = get_ip.get_random_ip()
        print(request.meta['proxy'])

settings.py 中配置


DOWNLOADER_MIDDLEWARES = {
    ...
    'ArticleSpider.middlewares.RandomProxyMiddleware': 10,
}

这样就可以实现代理池随机切换代理 IP 了

  • 几个好用的 随机切换代理 IP 库

scrapy-proxies:https://github.com/aivarsk/scrapy-proxies [免费]
scrapy-crawlera:https://github.com/scrapy-plugins/scrapy-crawlera [收费]
tor:http://www.theonionrouter.com/ (洋葱浏览器)

云打码实现验证码识别

常用验证码识别方法

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

  • 编码实现(ocr 识别):实现过程繁琐,识别准确率低
  • 在线打码:折中方式,价格比人工打码便宜很多
  • 人工打码:有一些人工打码平台,有很多专门做兼职打码的人,识别效率最高

以在线打码平台 ‘云打码’ 为例:http://www.yundama.com/

云打码需要注册两个账号,一个普通用户账号,一个开发者账户。只有注册开发者账号才可以跟客服申请要测试积分,可以识别几十到上百个验证码

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

关于普通用户账号和开发者账号,开发者账号是和云打码平台合作的开发者,通过开发者账号开发了一个软件,这个软件使用的是云打码平台的 API,然后卖给很多客户的时候(这个客户就是指普通用户),这个打码是要收费的,每识别一个验证码会收一部分钱,这个钱要分成给开发者

开发文档

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

注册开发者账号,我的软件-添加新软件,软件名称随便起一个,自动生成软件代码和通讯密钥

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

注册普通用户,可以在线充值和查看打码记录等

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

查看之前下载的示例代码

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

关于验证码类型有很多种类,类型越明确识别成功率越高

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

对普通用户充值后,测试识别验证码

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

在普通用户的打码记录里可以查看到刚才运行程序打码的这条记录

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

可以说这个方案目前来说是很好的解决办法,通常一分钱即可识别一个验证码

cookie禁用、自动限速、自定义spider的settings

  • cookie禁用
COOKIES_ENABLED = False  # settings.py 中配置禁用 cookies

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

  • 自动限速

Scrapy 默认在每个网页之间下载空隙是为 0 的,也就是不停的去下载网页,事实上 Scrapy 提供了一个扩展,可以动态帮我们设置下载速度
中文文档:https://scrapy-chs.readthedocs.io/zh_CN/latest/topics/autothrottle.html
英文文档:https://doc.scrapy.org/en/latest/topics/autothrottle.html

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

  • 自定义spider的settings

典型应用例子,ArticleSpider 这个项目中,zhihu spider 是不能禁用 cookies 的,禁用的话就不能爬取了,而 jobbole spider 是允许禁用 cookies 的,但是在 settings.py 中只有一个 COOKIES_ENABLED = False 可配置,所以就需要能够自定义每一个 spider 的 setting 功能了,scrapy 已经提供了此功能

《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png
《聚焦Python分布式爬虫必学框架 Scrapy 打造搜索引擎 -- 第7章 Scrapy突破反爬虫的限制》 image.png

经过以上配置,settings.py 中 COOKIES_ENABLED 默认值设为 False 自定义每个 spider 的 custom_settings,就可以实现不同 spider 都有各自的配置,不会相互干扰。
事实上 custom_settings 中可以设置很多自定义配置,如 headers

    原文作者:CursiveS
    原文地址: https://www.jianshu.com/p/0d33420d08bc
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞