信息源是搜狗微信,就爬到的数据保存到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将数据存到数据库中

 

保存到数据库