本文主要参考《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]
最后的爬取结果如图: