zoukankan      html  css  js  c++  java
  • 使用代理的爬虫

    信息源是搜狗微信,就爬到的数据保存到MySQL中

    搜狗对微信公众号和文章做了整合,我们可以直接通过链接搜索到相关的公众号和文章

    例如搜索NBA,搜索的结果的URL中有很多无关的GET请求的参数,手动将无关的请求参数去掉,其中只保留type和query,其中type表示的是搜索微信文章,query表示搜索关键词为NBA  https://weixin.sogou.com/weixin?query=NBA&type=2&page=2

    要注意的点就是如果没有输入账号的话,那么只能看到十页内容,登录之后可以看到一百页的内容,如果想要抓取更多的内容,就需要登录并使用cookies来进行爬取,搜狗微信的反爬能力很强,如果要是连续的刷新话站点就会弹出验证码

      网络请求出现了302跳转,返回状态码是302,这时候就进入了验证界面,所以可以得出结论,如果服务器返回的状态码是302而不是200的话就说明IP访问次数过高了,IP早到了封禁,此次请求失败

      要是遇到这种情况,我们可以选择识别这个验证码并进行解封操作,或者也可以选择IP代理来进行直接切换

      

      对于反爬能力很强的网站来说,如果我们遇到这种返回状态就需要重试,所以可以采取另外一种爬取方式,借助数据库来自己构造一个爬虫队列,将待爬取的请求都放到队列中,如果请求失败了就重新放回到队列中,等待被重新进行调用 --> 这里可以借助redis的队列,要是碰到新的请求就加入队列中,或者有需要重试的请求也加入到队列中。在调度的时候要是队列不为空的话就将请求挨个取出来执行,得到响应的内容,提取出来我们想要的东西

      采取MySQL进行存储,需要借助与pymysql库,将爬取的结果构造成一个字典,实现动态存储

    功能:

      1、借助Redis数据库构造爬虫队列,来实现请求的存取

      2、实现异常处理,失败的请求重新加入队列

      3、实现翻页和提取文章列表并对应加入到队列中

      4、实现微信文章的提取

      5、保存到数据库中

    构造Request

      如果是要用队列来存储请求,那么就需要实现一个请求Request的数据结构,在这个请求头中必须要包含的一些信息(请求URL、请求头、请求方式、超时时间等),还有就是对于某个请求我们要实现对应的方法来处理它的响应,所以也就需要一个回调函数,每次翻页的操作都需要代理来实现,所以也就需要一个代理的参数,最后就是要是一个请求的失败次数过多,那么就不再需要重新进行请求了,所以还要对失败次数进行记录

      上面说说到的参数都是Request的一部分,组成了一个完整的Request放到队列中去等待调度,这样从队列中拿出来的时候直接执行Request就好了

      实现:

          我们可以采用继承requests库中的Request对象的方式来实现我们所需要的数据结构,在requests库中已经有了Request对象,它将请求作为一个整体的对象去执行,当得到响应之后在进行返回,其实在requests库中所构造的Request对象中,已经包含了请求方式、请求链接、请求头这些参数了,但是跟我们想要的还是差了几个。我们需要的是一个特定的数据结构,所以可以在原先的基础上加入剩下的几个属性,在这里我们继承Request对象,重新实现一个请求

    TIMEOUT = 10
    from requests import Request
     
    class WeixinRequest(Request):
        def __init__(self, url, callback, method='GET', headers=None, need_proxy=False, fail_time=0, timeout=TIMEOUT):
            Request.__init__(self, method, url, headers)
          # 回调函数
            self.callback = callback
          # 代理
            self.need_proxy = need_proxy
          # 失败次数
            self.fail_time = fail_time
         # 超时时间
            self.timeout = timeout

        首先init方法先调用了Request的init方法,然后加入了额外的几个参数,callback、need_proxy、timeout,分别表示回调函数、是否需要代理进行爬取、失败次数、超时时间

        我们可以将新定义的Request看成是一个整体来进行执行,每个Request都是独立的,每个请求中都有自己的属性,例如,我们可以调用callback就可以知道这个请求的响应应该调用哪个方法来执行,调用fail_time就可以知道已经失败了多少次了,是否需要进行丢弃等等

    实现请求队列

      在构造请求队列的时候其实就是实现请求的存取操作,所以就可以利用redis中的rpush和lpop方法

      注意:存取的时候不能直接存Request对象,redis里面存的是字符串。所以在存Request对象之前我们要先把它序列化,取出来的时候再将它反序列化,可以利用pickle模块实现

    from pickle import dumps, loads
    from request import WeixinRequest
     
    class RedisQueue():
        def __init__(self):
            """初始化 Redis"""
            self.db = StrictRedis(host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD)
     
        def add(self, request):
            """
            向队列添加序列化后的 Request
            :param request: 请求对象
            :param fail_time: 失败次数
            :return: 添加结果
            """
            if isinstance(request, WeixinRequest):
                return self.db.rpush(REDIS_KEY, dumps(request))
            return False
     
        def pop(self):
            """
            取出下一个 Request 并反序列化
            :return: Request or None
            """
            if self.db.llen(REDIS_KEY):
                return loads(self.db.lpop(REDIS_KEY))
            else:
                return False
     
        def empty(self):
            return self.db.llen(REDIS_KEY) == 0

      写了一个RedisQueue类,在init方法中初始化了一个StrictRedis对象,之后实现了add方法,首先判断Request的类型,如果是我们自己定义的Request对象的话,那么就利用pickle序列化之后调用rpush方法加入到队列中去。pop方法则相反,调用lpop方法将请求从队列中拿出去,然后调用pickle的loads方法转成我们自定义的Request类型

      在调度的时候只需要新建一个RedisQueue对象,然后再调用add方法在队列中传入Request对象,就可以实现入队操作了,调用pop方法就可以取出下一个Request对象

      

    创建IP代理池

    准备第一个请求

    class Spider():
        base_url = 'http://weixin.sogou.com/weixin'
        keyword = 'NBA'
        headers = {
            
        }
        session = Session()
        queue = RedisQueue()
     
        def start(self):
            """初始化工作"""
            # 全局更新 Headers
            self.session.headers.update(self.headers)
            start_url = self.base_url + '?' + urlencode({'query': self.keyword, 'type': 2})
            weixin_request = WeixinRequest(url=start_url, callback=self.parse_index, need_proxy=True)
            # 调度第一个请求
            self.queue.add(weixin_request)

      在这里定义了Spider类,设置了很多全局变量,headers就是请求头,在你的浏览器中登录账号,然后再开发者工具中将请求头复制出来,一定要带上cookie字段,因为这里面保存了你的登录状态,然后就是初始化Session和RedisQueue对象,分别来执行请求和存储请求

      这里面的start方法全局更新了headers,使得所有的请求都能应用到cookies,然后构造了一个起始的URL,之后用这个URL构造了一个Request对象。回调函数是当前类中的parse_index方法,也就是当这个请求成功之后就用parse_index来处理和解析。need_proxy参数设置为True,表示的是执行这个请求需要用到代理。最后我们用到了RedisQueue的add方法,将这个请求加入到队列中,等待调度

    调度请求

      当地一个请求加入之后,调度就开始了。我们首先从队列中取出这个请求,将它的结果解析出来,生成新的请求加入到队列中,然后拿出新的请求,将结果来进行解析,在生成新的请求加入到队列中,就这样不断的循环,知道队列中没有请求为止,就代表爬取结束了

    VALID_STATUSES = [200]
     
    def schedule(self):
        """
        调度请求
        :return:
        """
        while not self.queue.empty():
            weixin_request = self.queue.pop()
            callback = weixin_request.callback
            print('Schedule', weixin_request.url)
            response = self.request(weixin_request)
            if response and response.status_code in VALID_STATUSES:
                results = list(callback(response))
                if results:
                    for result in results:
                        print('New Result', result)
                        if isinstance(result, WeixinRequest):
                            self.queue.add(result)
                        if isinstance(result, dict):
                            self.mysql.insert('articles', result)
                else:
                    self.error(weixin_request)
            else:
                self.error(weixin_request)

      在schedule方法中,其实就是一个内部循环,来判断这个队列是否为空,当队列不为空的时候,调用pop方法从队列中取出一个请求,调用requests方法来执行这个请求,

    from requests import ReadTimeout, ConnectionError
     
    def request(self, weixin_request):
        """
        执行请求
        :param weixin_request: 请求
        :return: 响应
        """
        try:
            if weixin_request.need_proxy:
                proxy = get_proxy()
                if proxy:
                    proxies = {
                        'http': 'http://' + proxy,
                        'https': 'https://' + proxy
                    }
                    return self.session.send(weixin_request.prepare(),
                                             timeout=weixin_request.timeout, allow_redirects=False, proxies=proxies)
            return self.session.send(weixin_request.prepare(), timeout=weixin_request.timeout, allow_redirects=False)
        except (ConnectionError, ReadTimeout) as e:
            print(e.args)
            return False

      首先要判断这个请求是否需要代理,如果需要代理,就调用get_proxy方法获取代理,然后调用Session的send方法执行这个请求。这里的请求调用了prepare方法转化成了Prepared Request,同时设置allow_redirects为False,timeout是该请求的超时时间,最后响应返回

      执行request方法之后会得到两种结果,一种就是False,也就是请求失败了,另一种就是Response对象,这之前可以对状态码进行判断,要是状态码合法的话就进行解析,否则就重新将请求放回队列中

      如果状态码合法,解析的时候会调用Request对象的回调函数进行解析,

    from pyquery import PyQuery as pq
     
    def parse_index(self, response):
        """
        解析索引页
        :param response: 响应
        :return: 新的响应
        """
        doc = pq(response.text)
        items = doc('.news-box .news-list li .txt-box h3 a').items()
        for item in items:
            url = item.attr('href')
            weixin_request = WeixinRequest(url=url, callback=self.parse_detail)
            yield weixin_request
        next = doc('#sogou_next').attr('href')
        if next:
            url = self.base_url + str(next)
            weixin_request = WeixinRequest(url=url, callback=self.parse_index, need_proxy=True)
            yield weixin_request

      在这个回调函数中主要就是做了两件事,1、获取本页所有微信文章的链接2、获取下一页的链接,在构造成Request对象之后通过yield进行返回,然后,schedule方法将返回的结果进行遍历,利用isinstance方法判断返回的结果,如果返回的结果是Request对象的话,就重新加入到队列中去,到这里第一遍循环就结束了

      其实这个时候while循环还会继续执行。队列已经包含第一页内容的文章详情页请求和下一页请求,所以第二次循环得到的下一个请求就是下一页文章详情页的链接,程序重新调用request方法获取其响应,然后调用它对应的回调函数解析,这个时候详情页请求的回调方法就不同了

    def parse_detail(self, response):
        """
        解析详情页
        :param response: 响应
        :return: 微信公众号文章
        """
        doc = pq(response.text)
        data = {'title': doc('.rich_media_title').text(),
            'content': doc('.rich_media_content').text(),
            'date': doc('#post-date').text(),
            'nickname': doc('#js_profile_qrcode> div > strong').text(),
            'wechat': doc('#js_profile_qrcode> div > p:nth-child(3) > span').text()}
        yield data

      这个回调函数解析了微信文章详情页的内容,提取出来了它的标题、正文文本、发布日期、发布人昵称、微信公众号名称。将这些信息组合成一个字典进行返回,结果返回之后还需要判断类型,如果是字典类型,就通过mysql将数据存到数据库中

    保存到数据库

      

  • 相关阅读:
    URL中#号(井号)的作用
    Sublime Text2 快捷键汇总
    Sublime Text2 使用及插件配置
    Sumblime Text 2 常用插件以及安装方法
    CSS禁止选择文本功能(兼容IE,火狐等浏览器)
    JavaScript 判断 URL
    纯真IP数据库格式读取方法(JAVA/PHP/Python)
    Sublime Text 2 性感无比的代码编辑器!程序员必备神器!跨平台支持Win/Mac/Linux
    base64:URL背景图片与web页面性能优化
    数独DFS实现
  • 原文地址:https://www.cnblogs.com/tulintao/p/11734572.html
Copyright © 2011-2022 走看看