Python3爬取搜狗微信公众号

本文主要参考《python3网络爬虫开发实战》,来实现对相应关键词的微信公众号的爬取。

爬取目标

本节主要是利用代理爬取微信公众号文章,主要包括的字段有文章标题、内容、日期、作者等信息,将最终的结果保存到Mysql中。
爬取之前需要的工作:构建好代理池;
需要用到的库有:aiohtt、requests、redis-py、pyquery、Flask、PyMySQL。

爬取分析

爬取主要通过搜狗微信,API借口为https://weixin.sogou.com/。搜狗对微信公众号的文章做了整合,输入相应关键字即可检索到相关文章,以nba为例:
在这里插入图片描述
url中包含很多无关的get请求,我们只保留get和query参数,https://weixin.sogou.com/weixin?type=2&query=nba。
下拉网页,点击下一页即可翻页。登录之后可以看到10页之后的文章,因此本次爬取需要登录并设置cookie。
微信搜狗站点的反爬能力较强,多此刷新页面后就会要求输入验证码。对于高频词访问的解决办法,本文采用构建代理池的方法,采用代理访问,切换Ip地址来跳过验证码。代理池的构建方法参见文章:https://blog.csdn.net/unclezou/article/details/86495425
对于反爬能力较强的网站,很容易出现对某一链接爬取失败的情况。因此我们换一种爬取方式,利用数据库构建一个爬取队列,所有的爬取任务全部放入队列中,如果爬取失败了就重新放回队列,等待下一次继续爬取。
这里我们使用Redis的队列数据结构,新的请求或者失败的请求都加入队列中。调度时如果队列不为空,就把请求取出来一个个执行,知道执行完毕。
因此本文实现功能有如下几点:

  • 构建代理池,将检测站点换成搜狗微信站点
  • 构建Redis请求队列,用队列数据结构实现请求的存取
  • 对请求异常进行处理,失败的请求重新返回请求队列
  • 页面的解析和翻页操作(下一页按钮即使下一页的链接,直接提取并访问),并把相应请求加入队列
  • 提取微信文章,并保存到Mysql数据库中

构造请求

我们使用队列来构造请求,那么自然要使用一个请求Request的数据结构,这个请求要包含一些信息,例如请求的链接、headers、超时时间、是否需要代理等。同时对于不同请求,其解析方法不同,例如针对某一页的请求,我们需要将其中的微信文章url提取出来,并对每个url构造新的请求加入队列。对于微信文章的页面的请求,我们需要提取文章信息,并保存到Mysql中。因此每一个请求,需要包含相应的处理方法,增加一个回调函数Callback。每次翻页请求需要用代理实现,需要增加一个参数needProxy,如果一个请求失败次数太多就不再重新请求,需要加入参数记录失败次数。
因此我们可以构造一个类,来实现请求对象。(这里用类并不是为了面向对象):

from config import *
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

实现请求队列

实现请求的存取,就是两个操作,一个是放,一个是取。因此使用Redis的rpush和lpop方法即可。
另外Redis里边存放的是字符串,不能直接存放Request,因此存Request之前先将其序列化,取出时再反序列化。该过程可用pickle的dumps和loads实现。

from redis import StrictRedis
from config import *
from pickle import dumps, loads


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 clear(self):
        self.db.delete(REDIS_KEY)

    def empty(self):
        return self.db.llen(REDIS_KEY) == 0

构建爬取模块

  • 定义一个Spider类,
from requests import Session
from config import *



from urllib.parse import urlencode
import requests
from pyquery import PyQuery as pq
from requests import ReadTimeout, ConnectionError
import re
class Spider():
    base_url = 'https://weixin.sogou.com/weixin'
    keyword = 'NBA'
    headers = {
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        'Accept-Encoding': 'gzip, deflate',
        'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4,zh-TW;q=0.2,mt;q=0.2',
        'Cache-Control': 'max-age=0',
        'Connection': 'keep-alive',
        'Cookie': 'IPLOC=CN1100; SUID=6FEDCF3C541C940A000000005968CF55; SUV=1500041046435211; ABTEST=0|1500041048|v1; SNUID=CEA85AE02A2F7E6EAFF9C1FE2ABEBE6F; weixinIndexVisited=1; JSESSIONID=aaar_m7LEIW-jg_gikPZv; ld=Wkllllllll2BzGMVlllllVOo8cUlllll5G@HbZllll9lllllRklll5@@@@@@@@@@; LSTMV=212%2C350; LCLKINT=4650; ppinf=5|1500042908|1501252508|dHJ1c3Q6MToxfGNsaWVudGlkOjQ6MjAxN3x1bmlxbmFtZTo1NDolRTUlQjQlOTQlRTUlQkElODYlRTYlODklOEQlRTQlQjglQTglRTklOUQlOTklRTglQTclODV8Y3J0OjEwOjE1MDAwNDI5MDh8cmVmbmljazo1NDolRTUlQjQlOTQlRTUlQkElODYlRTYlODklOEQlRTQlQjglQTglRTklOUQlOTklRTglQTclODV8dXNlcmlkOjQ0Om85dDJsdUJfZWVYOGRqSjRKN0xhNlBta0RJODRAd2VpeGluLnNvaHUuY29tfA; pprdig=ppyIobo4mP_ZElYXXmRTeo2q9iFgeoQ87PshihQfB2nvgsCz4FdOf-kirUuntLHKTQbgRuXdwQWT6qW-CY_ax5VDgDEdeZR7I2eIDprve43ou5ZvR0tDBlqrPNJvC0yGhQ2dZI3RqOQ3y1VialHsFnmTiHTv7TWxjliTSZJI_Bc; sgid=27-27790591-AVlo1pzPiad6EVQdGDbmwnvM; PHPSESSID=mkp3erf0uqe9ugjg8os7v1e957; SUIR=CEA85AE02A2F7E6EAFF9C1FE2ABEBE6F; sct=11; ppmdig=1500046378000000b7527c423df68abb627d67a0666fdcee; successCount=1|Fri, 14 Jul 2017 15:38:07 GMT',
        'Host': 'weixin.sogou.com',
        'Upgrade-Insecure-Requests': '1',
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36'
    }
    session = Session()
    queue = RedisQueue()
    mysql = MySQL()

headers是请求头,登录之后打开开发者工具,将cookie字段复制出来替换。

  • 第一个请求
	def start(self):
        """
        初始化工作
        """
        # 全局更新Headers,使得所有请求都能应用cookie
        self.session.headers.update(self.headers)
        start_url = self.base_url + '?' + urlencode({'type': 2, 'query': self.keyword})
        weixin_request = WeixinRequest(url=start_url, callback=self.parse_index, need_proxy=False)
        # 第一个请求加入队列
        self.queue.add(weixin_request)
  • 代理获取方法
    翻页时用代理来访问。如果暂时没有配置代理池,在WeixinRequest数据结构中将need_proxy设置为False也可以爬取,不过容易被封禁。
	def get_proxy(self):
        """
        从代理池获取代理
        :return:
        """
        try:
            response = requests.get(PROXY_POOL_URL)
            if response.status_code == 200:
                print('Get Proxy', response.text)
                return response.text
            return None
        except requests.ConnectionError:
            return None
  • 执行请求
    对于请求队列中pop取出的请求进行执行。
    def request(self, weixin_request):
        """
        执行请求
        :param weixin_request: 请求
        :return: 响应
        """
        try:
            if weixin_request.need_proxy:
                proxy = self.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

这里采用的是Session的send方法来执行这个请求,requests库的session对象能够帮我们跨请求保持某些参数,也会在同一个session实例发出的所有请求之间保持cookies。可参考https://www.jb51.net/article/127159.htm

  • 页面解析
    执行request方法后,无非返回两种结果:一种False即请求失败。另一种是Response对象,还需判断状态码,如果状态码合法200,那么久进行解析。
    解析方法粉两种,一种解析某一页的url索引内容,获取该页所有温馨文章链接,之后yield返回,返回对象是WeixinRequest;一种解析微信文章,yield返回对象是dict。
    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')
            if not re.match('https://', url):
                url = url.replace('http:','https:',1)
            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
        
    
    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(),
            
            'nickname': doc('#js_profile_qrcode > div > strong').text(),
            'wechat': doc('#js_profile_qrcode > div > p:nth-child(3) > span').text()
        }
        yield data
  • 错误处理函数
    主要返回错误相关参数,便于分析错误原因
    def error(self, weixin_request):
        """
        错误处理
        :param weixin_request: 请求
        :return:
        """
        weixin_request.fail_time = weixin_request.fail_time + 1
        print('Request Failed', weixin_request.fail_time, 'Times', weixin_request.url)
        if weixin_request.fail_time < MAX_FAILED_TIME:
            self.queue.add(weixin_request)
  • 结果保存至Mysql
    事先需要建立一个数据表命名为articls.
    在这里插入图片描述
import pymysql
from config import *
class MySQL():
    def __init__(self, host=MYSQL_HOST, username=MYSQL_USER, password=MYSQL_PASSWORD, port=MYSQL_PORT,
                 database=MYSQL_DATABASE):
        """
        MySQL初始化
        :param host:
        :param username:
        :param password:
        :param port:
        :param database:
        """
        try:
            self.db = pymysql.connect(host, username, password, database, charset='utf8', port=port)
            self.cursor = self.db.cursor()#创建游标
        except pymysql.MySQLError as e:
            print(e.args)
    
    def insert(self, table, data):
        """
        插入数据
        :param table:
        :param data:
        :return:
        """
        keys = ', '.join(data.keys())
        values = ', '.join(['%s'] * len(data))
        sql_query = 'insert into %s (%s) values (%s)' % (table, keys, values)
        try:
            print('正在插入')
            self.cursor.execute(sql_query, tuple(data.values()))
            print('插入成功')
            self.db.commit()
        except pymysql.MySQLError as e:
            print(e.args,'插入失败')
            self.db.rollback()
  • 调度函数
    把以上所有函数进行调度,实现从请求队列中获取请求,将结果解析出来,如果获得新的请求,将新请求加入队列,如果获得字典类型,将结果保存到Mysql。
    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 = callback(response)
                if results:
                    for result in results:
                        print('New Result', type(result))
                        #判断返回类型,做相应操作
                        if isinstance(result, WeixinRequest):
                            self.queue.add(result)
                        if isinstance(result, dict):
                            self.mysql.insert('articles1', result)
                            print(result)
                else:
                    self.error(weixin_request)
            else:
                self.error(weixin_request)
  • 开始代码
    def run(self):
        """
        入口
        :return:
        """
        self.start()
        self.schedule()
if __name__ == '__main__':
    spider = Spider()
    spider.run()

最后把以上所需要的所有配置参数提前配置好,保存为config.py文件,放入以上代码统一目录下,作为支持文件。

REDIS_HOST = 'localhost'

REDIS_PORT = 6379

REDIS_PASSWORD = None

REDIS_KEY = 'weixinConfig'

PROXY_POOL_URL = 'http://127.0.0.1:5555/random'

MYSQL_HOST = 'localhost'

MYSQL_PORT = 3306

MYSQL_USER = 'root'

MYSQL_PASSWORD = '123456'

MYSQL_DATABASE = 'weixin'

TIMEOUT = 10

MAX_FAILED_TIME = 20

VALID_STATUSES = [200]

最后的爬取结果如图:
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值