一、出发点
在dodo团队知乎号开刊文章中已介绍过本团队平常的实际工作,我们是一个从事游戏与金融结合的项目开发与运营团队。技术上主要是从事游戏分期、玩后付支付插件、游戏充值app等前后端开发,主要使用java。另一部分主要的技术内容是风控系统的构建,这部分主要使用python。
作者本人主要从事数据分析、风控建模等工作,团队大部分成员和作者之前都有从事手机游戏数据平台搭建、数据分析、BI等方面经历。对数据收集、数据处理、数据分析、数据报表、数据应用建模有一定实践经验。
团队最近在官网和自媒体方面的开发和运营工作,主要目的是提高充多多、玩后付等产品和自有流量。官网和自媒体面临的第一个问题就是内容缺乏,纯靠自媒体运营人员写不太现实,数量跟不上,去一般网站爬取一些文章又会降低网站在百度的权重(全是网上重复内容)。当时想到两个方面的内容百度应该不会收录和比对,一个是微信公众号部分文章(一些作者也会发布在其它自媒体平台),一个是国外纯外文类文章内容。并且我们只找游戏方面的文章内容,基于这个需求由我去利用爬虫到一些游戏类公众号、欧美、韩国网站收集一些百度并没有收录的文章内容。
二、scrapy框架简介
之前对爬虫有一定接触,目前爬虫主要有两类。一类是针对静态网页的,对于python来说scrapy是最流行和易用的了;另一类是针对动态加载的网页,主要是利用selenium的webdriver自动化测试技术进行爬取。
在本次工作中两种技术都有用到,主要是把selenium的webdriver组合到scrapy的middlerware。在本部分先简单介绍下scrapy这个爬虫框架和网页自动化测试框架selenium。
1.scrapy简介
scrapy的数据流和基本框架如下图1,由引擎、调度、下载器、爬虫、管道等五个最主要的部分组成,其中在下载器、爬虫部分有一个中间件部分,可这里面进行一些自定义内容实现自己想要的东西。这部分内容也可以从官网其具体内容。使用scrapy书写爬虫的最主要优势是两个,一个是爬虫类项目架构已帮你实现(各组件部分,让你思路更清晰,我不觉得现在很多大哥说的帮你完成很多部分的代码是其中一个优势),另一个是帮你完成最烦的异步部分,不用在爬取效率方面花时间。
引擎:引擎负责控制所有组件之间的数据流,并在发生某些操作时触发事件
调度程序:调度程序接收来自引擎的请求,并把它们放入请求队列中
下载器:负责获取网页内容并将其提供给引擎,引擎把网页内容提供给蜘蛛进行处理
爬虫:爬虫是由用户编自己写的解析处理程序,用于解析网页内容并从中提取想要的内容
管道:管道负责在内容被蜘蛛提取后进行一些处理、验证或持久存储
图1:scrapy_框架_数据流
1.引擎从爬虫部分获得初始请求,进行网页请求
2.引擎把请求交给调度程序并进行下一个请求
3.调度程序把下一请求交给引擎
4.引擎把请求交给下载器,通过下载器中间件(如果有的话)
5.请求完成下载后,下载器会生成一个Response(请求页面html)并发送给引擎,通过下载中间件(如果有)
6.引擎接收到下载器的response将其发送到爬虫部分进行解析和处理,需通过爬虫中间件(如果有)
7.爬虫部分处理完后把需提取的内容和新的请求发给引擎,通过爬虫中间件(如果有)
8.引擎把提取到需要的内容交给管道部分,并把接下来的请求发送给调度程序,再次进行入请求
9.重复上述1~8的步骤,直到调度程序不再有请求为止
2.自动化测试框架selenium简介
Selenium也是一个用于Web应用程序测试的工具集合。Selenium测试直接运行在浏览器中,就像测评人员在手动操作测试一样。Selenium支持IE、Edge、Firefox、Chrome等主流浏览器。它主要功能包括:测试程序与浏览器的兼容性,看你程序能否在不同的浏览器内进行运行。程序功能测试——根据需求和工具要求进行测试,看功能是否准确。
Selenium包括Selenium-IDE、Selenium-RC、Selenium-Grid三个主要工具:
Selenium-IDE是构建Selenium测试用例的集成开发环境。
Selenium-RC允许测试自动化开发人员使用编程语言以最大的灵活性和可扩展性开发测试逻辑。Selenium-Grid允许把Selenium-RC的解决方案应用到大型测试套件或者需要多环境运行的测试套件。
爬虫主要是利用Selenium-IDE获取动态加载的网页html源码,去弥补scrapy只能获取静态网页请求的缺陷,当然还有其它工具可以实现。
三、微信公众号爬虫思路
以下思路由下述实际情况决定:
1.我们所需文章内容只需要跟游戏相关的
2.文章不能在百度搜索引擎中找到
3.搜狗在微信文章这块有很强的反爬虫,经常会出现验证码
4.公众号文章、公众号等url都有时效性、加密等参数
5.搜狗在公众号文章这块的网页内容上大多数内容都在js相关变量中
6.具体文章url是微信的,通过搜狗获取到文章的微信url是没有反爬虫的
微信公众号爬虫思路过程如下:
1.利用scrapy的下载中间件结合selenium进行模拟操作浏览器搜索所需爬的公众号,然后获取公众号url
2.通过selenium浏览器加载公众号url获取文章列表网页源码,通过解析获取每篇文章的url
3.为了效率,在获取具体文章内容时利用scrapy的request进行请求获取具体文章内容
4.解析文章具体内容,因为要放到自己网页,所以内容需分段,同时考虑文章中有文字、图片、视频三种形式
5.为了seo,所有文章页面需要有key、describe,利用NPL技术提取每篇文章3个关键词和三句话摘要
6.利用异步入库技术,对最终内容进行入库存储
四、分析和实践
1.准备工作
1.1.安装anaconda
从官网下载对应版本进行安装,与其它软件安装无区别,下载地址:anaconda。其它依赖的相关第三方包可以先不要安装,等需要时再进行安装。
1.2.安装scrapy
Win10系统按住 Ctrl + R运行cmd或进入anaconda的cmd命令行,在命令行执行 pip install scrapy
pip install Scrapy
1.3.创建scrapy项目
进入cmd,先进入项目需存放的盘,使用命令 E:并按回车,然后再进入具体的路径,使用命令 cd: E:\my\Pythonlearn\spider.然后运行 scrapy startproject SpiderSogouWx。然后就会在对应目录下生成项目所需的基本模块文件。具体如下2所示。
2.项目结构与模块
2.1.主体有5个模块,分别是爬虫、提取内容、下载中间件、管道、全局配置。
2.2.其它几个文件可以不用看,主要是我实际要用的其它内容,比如上传CDN,还有一些log文件。
2.3.具体内容在第3部分一一讲来
3.爬虫模块
就是2部分的第1个文件,它主要负责发起请求和解析网页提取需要爬取的内容。
第三部分已经讲过,因为sogou的反爬虫,我们在获取公众号文章列表时须要采用selenium做为下载中间件。先直接上代码然后再一一讲解。
具体代码如下:
# -*- coding: utf-8 -*-
"""Created on Thu Nov 22 17:11:58 2018@author: changlin"""
import scrapy
import requests
from SpiderSogouWx.items import ArtListItem, ArtContItem
from SpiderSogouWx.pipelines import SpidersogouwxPipeline
from qiniu import Auth, put_file, etag
class SpiderSogouWx(scrapy.Spider):
"""通过sogou获取公众号在百度没收录的文章parse:请求soguou获取网页原素parsepubartinfo:获取公众号信息、公众号文章列表、公众号文章具体内容procontentlist:把获取到的内容进行整理组合sogou反爬可以用selenium取链接,用scrapy取内容,微信里无反爬虫"""
name = 'spidersogouwx'
allowed_domains = ["sogou.com", "baidu.com"]
start_urls = ["https://www.baidu.com/"]
def __init__(self):
""""""
self.conmysql = SpidersogouwxPipeline()
self.pubdict = self.conmysql.do_select() # ConMysql()先要实例化,否则会出现少self参数
self.access_key = '********************' # 初始化七牛化参数,填写实际的就成
self.secret_key = '********************'
self.q = Auth(self.access_key, self.secret_key)
self.bucket_name = '*****'
self.path = '*****/SpiderSogouWx/'
def start_reuqests(self, url):
"""一个无意义的初始请求"""
print('--- Start Request!!! ---')
def parse(self, response):
"""获取对应公众号的临时链接"""
pubs = self.pubdict # 转的成类似这种格式:[{'conid': '1020', 'conbtype': '17173新游戏'}]
print('----',pubs)
url = 'https://weixin.sogou.com/'
for i in pubs:
j = tuple(i.values())
pub = {'pub': j}
yield scrapy.Request(url=url, callback=self.parsepubartinfo, meta=pub, dont_filter=True)
self.conmysql.do_update(j[0])
def parsepubartinfo(self, response):
"""获取公众号信息、公众号文章列表、公众号文章具体内容"""
name = self.procontentlist(response.xpath('/html/body/div/div[1]/div[1]/div[1]/div/strong/text()').extract())
wxname = self.procontentlist(response.xpath('/html/body/div/div[1]/div[1]/div[1]/div/p/text()').extract())
intro = self.procontentlist(response.xpath('/html/body/div/div[1]/div[1]/ul/li[1]/div/text()').extract())
owner = self.procontentlist(response.xpath('/html/body/div/div[1]/div[1]/ul/li[2]/div/text()').extract())
artautor = wxname.replace('微信号:', '')
print('Pub Iinfo Is ---%s,%s,%s,%s\n' % (name, wxname, intro, owner))
for i in response.flags:
url = 'https://mp.weixin.qq.com' + i['arturl']
i['artautor'] = artautor
yield scrapy.Request(url=url, callback=self.parseartdetail, meta=i, dont_filter=True)
def parseartdetail(self, response):
""""""
n = 0
artlist = ArtListItem()
artcot = ArtContItem()
artlistinfo = response.meta
ovimgurl = artlistinfo['otherinfo']['thumphoto']
if ovimgurl == '':
pass
else:
ovimgname = self.get_img_name(ovimgurl, artlistinfo['artid'], 'overview')
self.get_img_content(ovimgurl, ovimgname)
artlistinfo['otherinfo']['thumphoto'] = ovimgname
artlist['artid'] = artlistinfo['artid']
artlist['conid'] = artlistinfo['conid']
artlist['game'] = artlistinfo['game']
artlist['btype'] = artlistinfo['btype']
artlist['stype'] = artlistinfo['sbype']
artlist['title'] = artlistinfo['title']
artlist['pubtime'] = artlistinfo['pubtime']
artlist['otherinfo'] = artlistinfo['otherinfo']
artinfo = response.xpath('//*[@id="js_content"]//p')
n += 1
q = 0
infodict = {}
for l in artinfo:
q += 1
istext = l.xpath('.//text()').extract()
isphoto = l.xpath('.//img//@data-src').extract()
isvideo = l.xpath('.//iframe//@src').extract()
if len(isvideo) > 0:
infodict[q] = self.procontentlist(istext)
q += 1
infodict[q] = isvideo[0]
elif len(isphoto) > 0:
infodict[q] = self.procontentlist(istext)
q += 1
imgurl = isphoto[0]
imgname = self.get_img_name(imgurl, artlist['artid'], q)
self.get_img_content(imgurl, imgname)
infodict[q] = imgname
else:
infodict[q] = self.procontentlist(istext)
artcot['artid'] = artlist['artid']
artcot['title'] = artlist['title']
artcot['autor'] = artlistinfo['artautor']
artcot['conid'] = artlist['conid']
artcot['content'] = infodict
artcot['pubtime'] = artlist['pubtime']
artcot['otherinfo'] = None
if (artcot['content'] == {1:''}) or (artcot['content'] == {}): # 取到空内容时不调用yield item入库
pass
else:
yield artlist
yield artcot # 只有是item类型才能调用pipelines,dict类型不会调用
def get_img_name(self, imgurl, artid, imgid):
""""""
if imgurl.find('wx_fmt=') == -1:
phototype = 'png'
else:
phototype = imgurl[imgurl.find('wx_fmt=') + 7:]
imgname = 'wx' + '_' + artid + '_' + str(imgid) + '.' + phototype
return imgname
def get_img_content(self, imgurl, imgname):
""""""
content = requests.get(imgurl)
with open(imgname, 'wb') as f:
f.write(content.content)
key = '8888888888' + '/' + imgname # 上传到七牛后保存的文件名
token = self.q.upload_token(self.bucket_name, key, 36000) # 生成上传 Token,可以指定过期时间
localfile = self.path + imgname # 要上传文件的本地路径
ret, info = put_file(token, key, localfile)
assert ret['key'] == key
assert ret['hash'] == etag(localfile)
def procontentlist(self, lists):
"""对提取的具体内容list进入字符串连接"""
content = ''
for i in lists:
i = str(i).replace(' ','').replace('\r', '').replace('\t', '').replace('\n', '').replace('"', '')
content = content + str(i)
return content
代码讲解和解释如下,也可以点击查看(这部分可以放在官网)
3.1.__init__函数:
这块大家可以不看,是初始化一些mysql查询、上传cdn的参数,是我们实际工作中用到的,一般在学习过程中用不到这些。我在pipeline模块单独写了一个异步连接mysql的类,包含了查询、更新、插入等操作,方便其它模块和mysql进行交互。
3.2.start_requests函数:
一般默认,很多小伙伴有重写这个函数的习惯,建议不要重写,这里面的坑有点多,容易出现各种易想不到的问题。可以把自己的初始化请求逻辑放到parse函数中。
3.3.parse函数:
从mysql中查询须爬取的微信公众号列表,交给SeleniumMiddlerware的selenium中间件进行请求和下载。这里有传递meta参数给到下一个函数或流程,主要目的是为了处分请求和下载是哪个公众号的内容。最后一句是对爬取过的公众号在数据库更新时间进行标志。
3.4. parsepubartinfo函数:
负责处理从下载中间件SeleniumMiddlerware拿到的公众号主页网页源码,通过此函数获取这个公众号的基本信息和所有文章的url,然后再把文章的url交给scrapy的request进行请求(此处没有用下载中间件,通过meta参数判断、return None实现)。
函数的xpath路径大家利用chrome、firefox自带的右击复制功能就能拿到,再看下xpath的基本语法和原理就成,这块建议大家不用花时间研究。
3.5. parseartdetail函数:
此函数负责解析公众号具体文章内容,中间调用get_img_name,get_img_content两个函数的地方大家可以不看,这两个函数是用来生成文章中图片名字和上传cdn的。
在解析具体内容时大家注意下,因为大部分的公众号运营者,包括我们充多多做游戏分期的也是,都是使用第三方编辑工具进行编辑的,所以内容处的html代码是千奇百怪的。大家如果对内容分段没有要求的话,比较容易提取,如果要求高,建议针对不同的公众号写成不同的提取内容函数。
3.6.此模块中的其它函数可以不看,都是它用的。
具体代码细节有问题的可以评论我们会回复,大家也可以跟运营人员沟通,可以电话沟通。
4.middlewares下载中间件
具体代码如下:
# -*- coding: utf-8 -*-
"""Created on Thu Nov 22 17:11:58 2018@author: changlin"""
import scrapy
from selenium import webdriver
from time import sleep
from fake_useragent import UserAgent
import requests
import random
import datetime
class SeleniumMiddlerware(object):
"""获取网页原素,主要为了防止处理js请求过来的内容"""
def __init__(self):
"""初始化ua头和ip实例化"""
self.getua = UserAgent()
self.getip = GetProxyIp()
def process_request(self, request, spider):
"""下载中间件,用于获取网页原素"""
if request.url == 'https://www.baidu.com/':
pass
elif 'mp.weixin.qq.com/' in request.url:
print('Do --- strat request ArtDetail ---')
return None
else:
pub = request.meta['pub']
print('Do --- strat request artlist---', pub, '\n')
options = webdriver.FirefoxOptions() # options = webdriver.ChromeOptions()
options.add_argument('-headless') # 无头模式
options.add_argument('--disable-gpu')
options.add_argument('user-agent=%s' % self.getua.firefox) # self.getua.chrome
proxyip = self.getip.getproxyip()
ip = '--proxy-server=http://' + proxyip[0] + ':' + proxyip[1]
options.add_argument(ip) # 设置代理IP
driver = webdriver.Firefox(executable_path='geckodriver.exe', firefox_options=options)
driver.delete_all_cookies()
driver.get(request.url)
sleep(random.randint(2, 4))
driver.find_element_by_xpath('//*[@id="query"]').send_keys(pub[1])
sleep(random.randint(1, 3))
driver.find_element_by_css_selector("#searchForm > div > input.swz2").click()
sleep(random.randint(3, 6))
driver.find_element_by_xpath('//*[@id="sogou_vr_11002301_box_0"]/div/div[2]/p[1]/a').click()
sleep(random.randint(9, 12))
handles = driver.window_handles # 获取当有所有窗口列表
driver.switch_to.window(handles[1]) # 切换窗口
html = driver.page_source
response = scrapy.http.HtmlResponse(url=request.url, body=html, request=request, encoding='utf-8')
driver.quit()
leninfo = response.xpath('//*[@id="history"]/div') # 获取第一层列表
sourceid = pub[0]
game = '其它游戏'
typedict = {'游戏资讯':['最热', '最新', '网游评测', '网游视频', 'MM'],
'游戏攻略':['新手篇', '致富篇', '任务篇', '装备篇', '综合篇']}
n = 0
m = 0
reslist = [] # 存储具体文章response
for j in leninfo:
m += 1 # 第一层列表中的第几个
p = 0
arttime = j.xpath('./div[1]/text()').extract()[0]
arttime = arttime.replace('年', '-').replace('月', '-').replace('日', '')
dtarttime = datetime.datetime.strptime(arttime,'%Y-%m-%d') # 转换成日期
arttime = dtarttime.strftime("%Y%m%d") # 格式化成字符
jinfo = j.xpath('./div[2]/div') # 获取第二层中的列表
for i in jinfo:
n += 1 # 总文章数
p += 1 # 第二层中的文章数
artdict = {}
btype = random.choice(['游戏资讯', '游戏攻略'])
title = self.procontentlist(i.xpath('.//div[1]/h4/text()').extract())
overview = self.procontentlist(i.xpath('.//div[1]/p[1]/text()').extract())
thumphotourl = i.xpath('.//span/@style').extract()[0]
thumphoto = thumphotourl[thumphotourl.find('(') + 1:thumphotourl.find(')')]
arturl = i.xpath('.//div[1]/h4/@hrefs').extract()[0]
artid = str(pub[0]) + '_' + arttime + '_' + str(p)
artdict['artid'] = artid
artdict['conid'] = sourceid
artdict['game'] = game
artdict['btype'] = btype
artdict['sbype'] = random.choice(typedict[btype])
artdict['title'] = title
artdict['pubtime'] = dtarttime
artdict['otherinfo'] = {'overview':overview, 'thumphoto':thumphoto}
artdict['arturl'] = arturl
reslist.append(artdict)
response.flags = reslist
return response
def procontentlist(self, lists):
"""对提取的具体内容list进入字符串连接"""
content = ''
for i in lists:
i = str(i).replace(' ','').replace('\r', '').replace('\t', '').replace('\n', '').replace('"', '')
content = content + str(i)
return content
class GetProxyIp(object):
"""获取第三方代理IP"""
def __init__(self):
pass
def getproxyip(self):
"""调用第三方接口把ip和prot"""
url = '*******************************' # 请写自己实际的就成
appkey = 'appKey=*********************&'
counts = 'count=1'
otherparams = '&expiryDate=0&format=1&newLine=2'
params = url + appkey + counts + otherparams
r = requests.get(params)
ips = eval(r.text)['msg']
if len(ips) > 0:
ip = ips[0]['ip']
port = ips[0]['port']
else:
ip = ''
port = ''
return ip, port # 函数返回多个值时是一个tuple
代码讲解和解释如下:
4.1.初始化请求头和请求UserAgent的ip,主要目的防止sogou的验证码反爬虫,这里我们有买第三方ip服务,建议大家直接买,自己学着玩的话6块1000个,网上很多免费的或自己构建IP库的费时费力还效果差,没必要。
4.2. process_request函数:
重写了中间件的process_request函数,增加自己的下载组件。一开始通过请求的url判断是使用scrapy的系统请求组件还是使用自己写的selenium下载组件。
Selenium下载组件先设置chrome的参数,然后完成微信公从号在sogou的搜索,最后点击进入微信公众号的主页,把主页的源码组装成scrapy的response对象并返回给下一个处理函数parsepubartinfo。
在此函数最后部分有处理获取此微信公众号的基础信息,比如说公众号名字、公众号微信、文章列表的文章标题、发布时间、文章概要、缩略图等。
4.3. GetProxyIp类可以不看,是获取第三方随机IP的一个方法。
5.爬虫提取的具体内容
items代码具体如下:
# -*- coding: utf-8 -*-
"""Created on Thu Nov 22 17:11:58 2018@author: changlin"""
import scrapy
class ArtListItem(scrapy.Item):
"""文章列表、类型信息"""
artid = scrapy.Field()
conid = scrapy.Field()
game = scrapy.Field()
btype = scrapy.Field()
stype = scrapy.Field()
title = scrapy.Field()
pubtime = scrapy.Field()
otherinfo = scrapy.Field()
class ArtContItem(scrapy.Item):
"""文章具体内容信息"""
artid = scrapy.Field()
title = scrapy.Field()
autor = scrapy.Field()
conid = scrapy.Field()
content = scrapy.Field()
pubtime = scrapy.Field()
otherinfo = scrapy.Field()
代码讲解和解释如下
我们这里定义了两个爬取内容类,一个存文章的类别、标题等信息ArtListItem,另一个存放文章的具体内容ArtContItem。这块不需要理解和研究,根据自己需要的信息进行书写,需要啥就写啥。
6.进行持久存储的pipelines模块
pipelines代码如下:
# -*- coding: utf-8 -*-
"""Created on Thu Nov 22 17:11:58 2018@author: changlin"""
import json
import copy
from SpiderSogouWx.items import ArtListItem, ArtContItem
import mysql.connector
from sshtunnel import SSHTunnelForwarder
from pyhanlp import *
class SpidersogouwxPipeline(object):
"""利用twisted的adbapi实现异步入库,也可以批量入库"""
def __init__(self):
"""初始化连接参数和连接"""
self.server = SSHTunnelForwarder(
ssh_address_or_host = ('888.88.8.888', 88), # 指定ssh登录的跳转机的address
ssh_username = '8888', # 跳转机的用户
ssh_password = '88888888888888888888', # 跳转机的密码
remote_bind_address = ('888.88.88.88', 8888)) # 需要连接的数据库ip
self.server.start()
self.dbparms = {'host': '127.0.0.1', # 执行程序的机器ip
'user': '8888', # 数据库的配置
'password': '888888',
'port': self.server.local_bind_port,
'database': '888888',
'charset': 'utf8mb4'} # 现在很多有表情符,编码要用这个
self.cnx = mysql.connector.connect(**self.dbparms)
self.cursor = self.cnx.cursor(cursor_class=mysql.connector.cursor.MySQLCursorDict) # 使用dict类型结果
def process_item(self, item, spider):
"""执行入库的发起点"""
copyitem = copy.deepcopy(item) # 先复制防止重复数据出现
if isinstance(copyitem, ArtListItem):
pass
elif isinstance(copyitem, ArtContItem):
otherinfo = {}
strs = self.split_dict(copyitem['content'])
otherinfo['keywords'] = self.procontentlist(HanLP.extractKeyword(strs, 4)) + ',充多多,玩后付'
otherinfo['description'] = self.procontentlist(HanLP.extractSummary(strs, 3))
copyitem['otherinfo'] = otherinfo
self.do_insert(copyitem)
return item
def procontentlist(self, lists):
"""对提取的具体内容list进入字符串连接"""
content = ''
n = 0
for i in lists:
n += 1
i = str(i).strip().replace('\r', '').replace('\t', '').replace('\n', '') # strip替换始尾空格\t\n\r等
if (n == 1) or (i == ''):
content = content + str(i)
else:
content = content + ',' + str(i)
return content
def split_dict(self, dicts):
"""对未翻译的dicts拆分成texts"""
strs = ''
for i in dicts.items():
if ('.com/' in i[1]) or ('.cn/' in i[1]):
pass
else:
strs = strs + str(i[1])
return strs
def do_select(self):
"""执行查询语句的方法"""
sql_select = 'SELECT conid,conbtype FROM 表名 WHERE source = "微信公众号";'
self.cursor.execute(sql_select)
res = self.cursor.fetchall()
return res
def do_update(self, conid):
"""执行更新语句的方法"""
sql_update = "update 表名 set upatetime=now() where conid=%s;" % (conid)
self.cursor.execute(sql_update)
self.cnx.commit()
def do_insert(self, item):
"""执行插入的方法,cursor是adbapi自动生成有的"""
insert_sql, params = self.get_insert_sql(item)
self.cursor.execute(insert_sql, params)
self.cnx.commit() # 修改类操作记得提交才能生效
def get_insert_sql(self, item):
"""可通过传递过来的item名字判断选择执行通过isinstance函数判断item是不是我们想要的类"""
if isinstance(item, ArtListItem):
insert_sql = """insert into 表名(artid, conid, artgame, artcbtype, artcstype, arttitle, artpubtime,\artotherinfo, is_show)VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s);"""
params = (item['artid'], item['conid'], item['game'], item['btype'],
item['stype'], item['title'], item['pubtime'],
json.dumps(item['otherinfo'], ensure_ascii=False), '0')
return insert_sql, params
elif isinstance(item, ArtContItem):
insert_sql = """insert into 表名(artid, arttitle, artautor, conid, artcontent, artpubtime, artotherinfo)VALUES (%s,%s,%s,%s,%s,%s,%s);"""
params = (item['artid'], item['title'], item['autor'], item['conid'],
json.dumps(item['content'],ensure_ascii=False), item['pubtime'],
json.dumps(item['otherinfo'],ensure_ascii=False))
return insert_sql, params # json.dumps变成json对象中的"或替换"'"
def close_spider(self, spider):
"""爬虫结束时关闭服务、连接"""
self.cursor.close()
self.cnx.close()
self.server.close()
print ("\n--- Closed Spider And Closed MysqlCon ---\n")
代码讲解和解释如下,也可以点击查看(这部分可以放在官网)
此部分对提取的内容进行入库,因为我们的文章要进行seo,所以中间增加了一个对所有文章关键词、摘要进行主动提取的功能,跟爬虫无关,文章太多不太可能手动增加这两个信息,所以利用了第三方的npl库进行处理。
6.1.__init__函数:
初始化与mysql的连接,这里不同的是用了跳板机,因为公司的原因是不能直连数据库的,所以要ssl连接第三方服务器再连接mysql。
我这里的游标类型用的是MySQLCursorDict,目的是把查询的数据转换成python的dict类型,方便后续操作,减少后续对数据的处理。
6.2. process_item函数:
重写process_item函数,由这里发起入库。Copy的操作是防止scrapy异步和mysql的性能冲突导致数据掉失入库失败进行的操作。
对文章具体内容进行了自动化提取关键词和摘要。
6.3.数据库操作函数:do_select、do_update、do_insert
这里写了与mysql交互常用的三种形式的方法。注意的是插入操作记得commit一下,要不会插入失败。
6.4. close_spider函数:
在爬虫结束时进行数据库、跳板机服务的关闭操作。
7.项目全局基本设置
Settings设置如下:
# -*- coding: utf-8 -*-
# Scrapy settings for SpiderSogouWx project
#
# For simplicity, this file contains only settings considered important or
# commonly used. You can find more settings consulting the documentation:
#
# https://doc.scrapy.org/en/latest/topics/settings.html
# https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
# https://doc.scrapy.org/en/latest/topics/spider-middleware.html
BOT_NAME = 'SpiderSogouWx'
SPIDER_MODULES = ['SpiderSogouWx.spiders']
NEWSPIDER_MODULE = 'SpiderSogouWx.spiders'
# Crawl responsibly by identifying yourself (and your website) on the user-agent
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'
# Obey robots.txt rules
ROBOTSTXT_OBEY = False
# Configure maximum concurrent requests performed by Scrapy (default: 16)
CONCURRENT_REQUESTS = 32
# Configure a delay for requests for the same website (default: 0)
# See https://doc.scrapy.org/en/latest/topics/settings.html#download-delay
# See also autothrottle settings and docs
DOWNLOAD_DELAY = 2.2 #同一网站下下载前等待时间,以秒为单位
# The download delay setting will honor only one of:
#CONCURRENT_REQUESTS_PER_DOMAIN = 16
#CONCURRENT_REQUESTS_PER_IP = 16
# Disable cookies (enabled by default)
COOKIES_ENABLED = False #禁用cookie
# Disable Telnet Console (enabled by default)
#TELNETCONSOLE_ENABLED = False
# Override the default request headers:
#DEFAULT_REQUEST_HEADERS = {
# 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
# 'Accept-Language': 'en',
#}
# Enable or disable spider middlewares
# See https://doc.scrapy.org/en/latest/topics/spider-middleware.html
#SPIDER_MIDDLEWARES = {
# 'SpiderSogouWx.middlewares.SpidersogouwxSpiderMiddleware': 543,
#}
# Enable or disable downloader middlewares
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
'SpiderSogouWx.middlewares.SeleniumMiddlerware': 543,
}
# Enable or disable extensions
# See https://doc.scrapy.org/en/latest/topics/extensions.html
#EXTENSIONS = {
# 'scrapy.extensions.telnet.TelnetConsole': None,
#}
# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'SpiderSogouWx.pipelines.SpidersogouwxPipeline': 100,
}
# Enable and configure the AutoThrottle extension (disabled by default)
# See https://doc.scrapy.org/en/latest/topics/autothrottle.html
#AUTOTHROTTLE_ENABLED = True
# The initial download delay
#AUTOTHROTTLE_START_DELAY = 5
# The maximum download delay to be set in case of high latencies
#AUTOTHROTTLE_MAX_DELAY = 60
# The average number of requests Scrapy should be sending in parallel to
# each remote server
#AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0
# Enable showing throttling stats for every response received:
#AUTOTHROTTLE_DEBUG = False
# Enable and configure HTTP caching (disabled by default)
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html#httpcache-middleware-settings
#HTTPCACHE_ENABLED = True
#HTTPCACHE_EXPIRATION_SECS = 0
#HTTPCACHE_DIR = 'httpcache'
#HTTPCACHE_IGNORE_HTTP_CODES = []
#HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'
Settings的常用设置如下,建议项目一创建时就进行最基础的设置,防止最基础的反爬虫。
7.1.设置下请求头USER_AGENT,请求头百度可以找到很多,也可以使用fake_useragent这个第三方库会随机生成,还可以选择系统和浏览版本。
7.2.ROBOTSTXT_OBEY设置成False,不遵守爬虫协议。很多网站会在根目录加入这个协议就会导致爬取失败。
7.3. CONCURRENT_REQUESTS默认设置是16,建议设置成32,尤其是在使用selenium时,防止同时请求限制导致失败。
7.4. DOWNLOAD_DELAY设置为1-3,单位是秒,下次请求前间隔时间,建议慢一点,太快sogou会有验证码机制,很烦人,如果出现不太好绕过。
7.5. COOKIES_ENABLED设置为False,把cookie禁止,防止网站跟踪。
7.6.其它pipelines和middle配置根据自己写的内容进行配置,如果不配相关模块就不会生效。
五、部结
1.多分析目标网站的数据特点,建议先用requests模块对目标网站进行请求,把请求的text存储到text中,然后把这个文本内容与网站在浏览器中的源码进行比对,以确实后续数据请求是用scrapy的Request还是用selenium等第三方进入请求下载。
2.先确实带个项目爬取的流程的,然后一个一个执行,解析代码一般不会有难度,现在的chrome或firefox浏览器的调试工具很强大,右击对应位置源码选择copy xpath都会自动生成提取路径。关键是项目执行浏览要事先想清楚,才能提高效率和找到自己在此类项目的难点在那。
3.在利用sogou进行微信公众号文章提取时通过分析sogou和微信网站特点后利用selenium提取文章url,然后再利用scrapy的request进行微信文章提取。既利用了selenim绕过了sogou的反爬虫,又利用了scrapy的速度获取文章。充分利用两者的优势解决实际问题。