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