本文主要介绍并使用在爬虫工作中经常使用的一些工具
爬虫是什么
关于爬虫是什么,我们可以直接看百度百科的介绍,不多做解释,通俗一点说,涉及到网络请求的任何操作都能认为是一个爬虫,其实际内容就是获取网页数据的一个过程。打个比方,我们打开浏览器,输入:www.baidu.com 进入到百度的首页,浏览器给我们看到的页面,就是我们本地的程序从百度的服务器上“爬取”下来的内容,经过浏览器这个媒介展现在我们眼前
简单而言,爬虫就是一个个网络请求,由我们的代码发出,将请求拿到的数据存储到本地,爬虫的目标只有一个,那就是数据
通用爬虫和聚焦爬虫
通用爬虫:通用爬虫是搜索引擎抓取系统(如百度、谷歌等)的重要组成部分。其不关心数据内容,只需要将整个网站全部下载到本地,如我们常看到的百度快照就是原始网页的备份,其保存了原始网页的大部分内容。
聚焦爬虫:也叫垂直爬虫,是面向特定需求的一种网络爬虫程序,他与通用爬虫的区别在于:聚焦爬虫在实施网页抓取的时候会对内容进行筛选和处理,尽量保证只抓取与需求相关的网页信息。
这里我们所编写的爬虫都是聚焦类爬虫,即针对于我们想要的网页进行数据抓取
常用爬虫的工具库介绍
requests库
python的requests模块
使用python的requests能够快速地完成一个简单爬虫的代码实现
Import requests
#get请求
r = requests.get('https://api.github.com/events')
#post请求,data是你要提交的值,比如账号密码
r = requests.post('http://httpbin.org/post', data = {‘key':'value'})
#一个网页需要的url参数,可以写成urldata以字典格式提交
urldata = {'key1': 'value1', 'key2': 'value2'}
r = requests.get("http://httpbin.org/get", params=urldata)
以上是简单的使用,更多附加的功能还需要你在实际的使用中去应用到,requests的官方参考文档:https://requests-docs-cn.readthedocs.io/zh_CN/latest/user/quickstart.html
requests库仅仅只是提供了爬虫最基础的功能,其更像一个螺丝钉,随使用者去拼装使用,后面两个框架则涉及到爬虫的整个数据流,格式化数据存储,控制并发,爬虫的实现逻辑,等等,都变成了组件供你自己去使用,更为强大。
scrapy框架
Scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。其自定义化了四个功能模块,这也是绝大部分爬虫框架工作的基础形式,使用者只需要在对应的模块实现自己的功能代码,即可通过scrapy提供的命令行指令去启动这些爬虫工程
![](https://img.haomeiwen.com/i6283324/b0c977f4f574f125.png)
Scheduler(调度器): 它负责接受引擎发送过来的requests请求,并按照一定的方式进行整理排列,入队、并等待Scrapy Engine(引擎)来请求时,交给引擎。
Downloader(下载器):负责下载Scrapy Engine(引擎)发送的所有Requests请求,并将其获取到的Responses交还给Scrapy Engine(引擎),由引擎交给Spiders来处理,
Spiders:它负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据,并将需要跟进的URL提交给引擎,再次进入Scheduler(调度器),
Item Pipeline:它负责处理Spiders中获取到的Item,并进行处理,比如去重,持久化存储(存数据库,写入文件,总之就是保存数据用的)
四个组件通过Scrapy Engine 这个引擎互相传递信息,其中我们需要关心的是Spiders 和 Item Pipeline 两个模块,前者是在抽取页面目标信息,以及页面层级之间的跳转,即模拟我们获取页面信息的逻辑,需要实现一个Spider爬虫类;后者是定义你需要抓取的字段(Item.py),以及抓取到的数据,后端该如何处理(Pipeline),去重或者是写入到mysql呢还是mongodb,又或者是写到文本,或者都需要,由你决定,添加Item Pipeline 类。
两个模块均有一些简单且强大的通用函数供你使用,完成自己的功能。
本文贴一个比较简单的实现代码,并附上各个代码的意义
西安政府网站一些新闻信息
items.py
import scrapy
from scrapy import Field
class XianItem(scrapy.Item):
# define the fields for your item here like:
# name = scrapy.Field()
name = Field()
link = Field()
time = Field()
配置好我们需要抓取的数据字段,这里只有三种,name,link,time,具体是什么值看下面的spider类中的parse_item函数
m51job_spider.py
class XiAn_Spider(RedisSpider):
#这个名字供你使用scrapy命令行操作单个爬虫,暂停,重启等
name = 'xian'
#指定只抓取以其中的域名开头的url
allowed_domains = []
custom_settings={
#单个线程下载网页的间隔时间
'DOWNLOAD_DELAY':0.1,
'TELNETCONSOLE_PORT':[8888],
'DEFAULT_REQUEST_HEADERS':{
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding':'gzip, deflate, sdch',
'Accept-Language':'zh-CN,zh;q=0.8',
'Cache-Control':'max-age=0',
'Connection':'keep-alive',
'Host':'www.xa.gov.cn',
}
}
#种子url
start_urls = [
'http://www.xa.gov.cn/ptl/index.html'
]
#指定redis key,需要在setting中添加相关配置
redis_key = 'all:start_urls'
def __init__(self,*args,**kwargs):
super(XiAn_Spider,self).__init__(*args,**kwargs)
self.dbindex_table = self.name + '_index'
#parse是默认入口函数,后续的其他函数由你自己决定,在call_back中回调即可
def parse(self,response):
headers={'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.75 Safari/537.36'}
req1=urllib2.Request('http://www.xa.gov.cn:80/cisite_www.xa.gov.cn/def/def/index_1121_6910.jsp',headers=headers)
response=urllib2.urlopen(req1)
html=BeautifulSoup(response.read(),'lxml')
number = (html.select('a[href="#"]')[1].text)[2:-1]
number = int(number)
print "程序启动,开始爬取新闻页,总共有%d个页面....."%number
for i in range(number):
url = "http://www.xa.gov.cn/ptl/def/def/index_1121_6910.jsp?hidden_UT_CAT_MDDATA_4345936_pindex=%d"%i
req = Request(url,callback = self.parse_item)
yield req
def parse_item(self,response):
sel = Selector(response)
print "*******************The Current URL is :"+response.url
item = XianItem()
items = sel.xpath('//a[@class="default"]')
time = sel.xpath('//li[@class="col-md-2"]/text()').extract()
for i in range(len(items)):
item['link'] = items[i].xpath('@href').extract()
item['name'] = items[i].xpath('@title').extract()
item['time'] = ''.join(str(time[i]).split())
yield item
以上是一个最简单的二层解析,也就是从一级页面进入,二级页面就是我们想要的数据,于是直接parse_item,解析页面数据,然后使用yiled异步丢给pipeline中处理
settings
#指定redis地址
REDIS_URL=None
REDIS_HOST='127.0.0.1'
REDIS_PORT=6379
ITEM_PIPELINES={
'xian.pipelines.RedisDuplicatesPipeline':200, #redis去重
'xian.pipelines.XianSqlPipeline':300, #使用数据库存储
}
DOWNLOAD_TIMEOUT=80
其中需要注意的是ITEM_PIPELINES的配置,其为dict格式,前者是具体的实现类,后者是优先级,数字越小,表示越先执行,如上代码中是:先经过redis去重(具体去重你可以在pipeline中实现,以什么形式去重,我这里是以url),再插入到数据库中,保证数据不会重复
pipelines.py
class RedisDuplicatesPipeline(object):
def process_item(self,item,spider):
if Redis.exists('url:%s'%item['link']):
raise DropItem("Duplicates item found: %s"%item)
else:
Redis.set('url:%s'%item['link'],1)
return item
class XianSqlPipeline(object):
def __init__(self):
self.dbpool=adbapi.ConnectionPool('MySQLdb',
host='127.0.0.1',
db='xian',
user='root',
passwd='liaohong',
cursorclass=MySQLdb.cursors.DictCursor,
charset='utf8',
use_unicode=True)
def process_item(self,item,spider):
query=self.dbpool.runInteraction(self.conditional_insert,item)
query.addErrback(self.handle_error)
return item
def conditional_insert(self,tx,item):
tx.execute(\
"insert into xian (name,link,time)\
values(%s,%s,%s)",
(item['name'],
item['link'],
item['time'])
)
def handle_error(self,e):
log.err(e)
这里可以自定义抓取到的字段的处理,可以自定义去重(使用布隆或者redis),可以自定义存储(导出成excel,json,或mysql),只需要添加一个class,然后在settings中指定执行顺序即可,高度可定制化
result
抓取到的结果数据如下,这是我新加了一个写到json的pipeline插件导出的结果
{"link": ["http://www.xaedu.gov.cn/ptl/def/def/index_902_3181.jsp?trid=2148619"], "name": ["关于公示2016年西安市特设岗位教师招聘拟聘用人员名单的公告"], "time": "2016-09-01"}
{"link": ["http://www.xa.gov.cn/ptl/def/def/index_1121_6774.jsp?trid=2148157"], "name": ["西安派出所即日起不再开具以下证明"], "time": "2016-09-01"}
{"link": ["http://www.xawgxj.gov.cn/ptl/def/def/index_1385_5923.jsp?trid=2148202"], "name": ["西安市2016年千场戏剧惠民演出活动 9月份安排表"], "time": "2016-08-81"}
{"link": ["http://www.xafda.gov.cn/ptl/def/def/index_1259_2780.jsp?trid=2147999"], "name": ["关于公布《西安市允许食品小作坊生产加工食品目录》的公告"], "time": "2016-08-31"}
{"link": ["http://www.xafda.gov.cn/ptl/def/def/index_1259_2780.jsp?trid=2147992"], "name": ["西安市第一类医疗器械产品备案信息公布2016年(第7期)"], "time": "2016-08-31"}
{"link": ["http://www.xaepb.gov.cn/ptl/def/def/index_982_4434.jsp?trid=2147189"], "name": ["西安市环境保护局关于2016年8月31日排污许可证发放受理情况的公示"], "time": "2016-08-31"}
more
scrapy还有很多其他很方便的命令可以使用,甚至支持自定义命令(网上常见的有scrapy start all 一次性启动全部爬虫),要学习更多请到scrapy中文文档
在使用scrapy中遇到的一些不足,就是其配置文件不能做到动态可配置化,如果数据出了问题,很难单单重跑部分数据(比如被反爬了,网页结构变了),只能修改代码(添加代理,修改网页入口等,字段解析方法),然后重新跑全量的数据,这样会造成很多没必要的访问
pyspider框架
国人编写的一款强大的可视化抓取框架,pyspider通过点击css选择器自动生成抓取规则,并且有一个可视化的控制页面,让你能看见你的抓取任务的状况(速度/工作情况/抓取结果….),能通过配置下载速度和并发数控制抓取的快慢。且有追踪的log日志,记录了抓取过的每一条url链的情况
安装相关本文暂略,需要注意的是centos在安装pycurl的时候可能会出现一些小插曲,解决方法按照如下:https://blog.csdn.net/qq_32502511/article/details/80606636
以下以一份简单的抓取腾讯视频的代码来说明pyspider的使用:
crawl_config 全局设定,遇到反爬的情况需要在其中配置对应的措施(如代理,UA,Cookies,渲染....),当然你也可以在下面的crawl函数中局部设定
on_start 项目的入口,self.crawl("Start Url",callback=self.index_page),这里传递进来了你在创建项目的初始url.只能存在一个
self.crawl 项目的核心函数,支持多个参数,默认是待爬url和callback函数.将抓取url的response返回给callback函数
index_page 回调函数,名字不重要,只需要你在callback的时候写好对应的函数名,可以有多个
detail_page 处理详情页,将处理结果以json格式的result返回给结果处理函数on_result
on_result 默认是存储到data目录下的result.db中,可以显示申明重写这个函数,其参数为(self , result)
Write Code
首先更改我们找到的入口页面,腾讯视频下的电影类别分类http://v.qq.com/x/list/movie?subtype=-1&offset=0
@every(minutes=24 * 60)
def on_start(self):
self.crawl('http://v.qq.com/x/list/movie?subtype=-1&offset=0#0', callback=self.index_page)
点击左边调试栏中的run按钮,底下会出现一个follow,表示这个页面中符合你当前(index_page)crawl函数中的规则的url,默认的规则是全部能提取到的url,但我们只关心我们想要的页面,这个规则我们怎么写呢?
点击follow左边的web按钮,我们进到如下的页面,这是pyspider为你加载好的静态页面
![](https://img.haomeiwen.com/i6283324/4832aab315175b44.jpg)
体现我们的可视化的时刻到了,点击最左边的enable css selector helper,当你的鼠标再移到页面中,会发现鼠标移到的地方变成了黄色,这即使和Portia一样的地方。我们点击下图红色框中的copy按钮,粘贴到index_page中第一个self.crawl()中去
![](https://img.haomeiwen.com/i6283324/dcb5bb062897eb4d.jpg)
@config(age=10 * 24 * 60 * 60)
def index_page(self, response):
for each in response.doc('.figure_title > a').items():
self.crawl(each.attr.href, callback=self.detail_page)
因为这些页面是我们的final_page,所以直接回调给detail_page
继续往下拉,我们能看到很多的分页,我们让项目按照下一页的方式进行抓取,同理用enable css selector helper选出下一页的css规则再写一个crawl函数。下一页的解析规则和这一页是一样的,都是将这一页的final_page回调给detail_page,同时继续分析下一页.
合起来的代码即是
@config(age=10 * 24 * 60 * 60)
def index_page(self, response):
for each in response.doc('.figure_title > a').items():
self.crawl(each.attr.href, callback=self.detail_page)
self.crawl([x.attr.href for x in response.doc('[class="page_next"]').items()],callback=self.index_page)
Detail Page
这里就是类似上一步的过程重复了,pyspider内置了一个pyquery解析response,并将得到的结果填充到对应的字段中
常用的有
response.doc('css rules').text() / resposne.content / response.json['key1']['key2']....
@config(priority=2)
def detail_page(self, response):
return {
"url": response.url,
"site_name":"tencent_movie",
"movie_name":response.doc('._base_title').text(),
"publish_time":re.search("(?<=uploadDate\" content=).*(?=/)",response.content).group(),
"primary_stars":response.doc('.video_info_cell_2 > div:contains("导演")').text().split(' ')[3:],
"director":response.doc('.video_info_cell_2 > div:contains("导演")').text().split(' ')[1],
"area":re.search("(?<=contentLocation\" content=).*(?=/)",response.content).group(),
"style_classify":response.doc('.video_info_cell_1 a').text(),
"language":re.search("(?<=inLanguage\" content=).*(?=/)",response.content).group(),
"comment_counts":response.doc('[id="commentTotleNum"] > a').text(),
"introduce":re.search("(?<=description\" content=).*?(?=>)",response.content,re.S).group(),
"fav_counts":None,
"step_counts":None,
"score_level":response.doc('.video_score').text(),
"score_counts":None,
"fans_counts":None,
"index":response.doc('.douban_score > .num').text(),
"evaluation_counts":None,
"vote_counts":None,
"play_counts":response.doc('.icon_text > em').text(),
"duration":float(re.search(r"(?<=duration\":\")\d+?(?=\")",requests.get("https://union.video.qq.com/fcgi-bin/data?otype=json&tid=682&appid=20001238&appkey=6c03bbe9658448a4&idlist={list_id}".format(list_id=[x.attr.id for x in response.doc('[class="player_figure"]').items()][0])).content).group())/3600,
}
python的可操作性是很高的,只要你引入包就什么都能做,像我在如上的代码中,因为有些数据并不能从静态网页上得到,我通过了抓包的方式找到了获取这个参数的接口,经由requests向其发送请求获取我需要的数据,最后填充到duration字段中
duration---电影的时长,为秒数,将其转换成float并除以3600s,得到时长
Result Test
再写完如上代码,继续点击run,检查我们的字段是不是都提取正确,像之前那种网页无法获取我们就需要使用其他方法(比如渲染,接口)
![](https://img.haomeiwen.com/i6283324/ca89200e1c5aa24c.jpg)
具体的json数据如下
{'area': '"美国" ',
'comment_counts': '',
'director': u'莱塞·霍尔斯道姆',
'duration': 1.6683333333333332,
'evaluation_counts': None,
'fans_counts': None,
'fav_counts': None,
'index': '7.7',
'introduce': '"影片以汪星人的视角展现狗狗和人类的微妙情感,一只狗狗陪伴小主人长大成人,甚至为他追到了女朋友,后来它年迈死去又转世投胎变成其他性别和类型的汪,第二次轮回狗狗变成了警犬威风凛凛,再次转轮回,又成了陪伴一位单身女青年的小柯基犬。在经历了多次轮回之后,最终回到最初的主人身边。"',
'language': '"英语" ',
'movie_name': u'一条狗的使命(英语版)',
'play_counts': u'1.3亿',
'primary_stars': [u'乔什·加德',
u'布丽特·罗伯森',
u'丹尼斯·奎德'],
'publish_time': '"2017-03-03" ',
'score_counts': None,
'score_level': '8.8',
'site_name': 'tencent_movie',
'step_counts': None,
'style_classify': u'美国 剧情 喜剧',
'url': 'https://v.qq.com/x/cover/950h5k5p7h7m2qn.html',
'vote_counts': None}
敲完代码,或者将如上三段代码粘贴到你的项目中,点击右上角的saved,再用run测试下我们的代码,没有出现错误即可回到我们的控制台
运行Demo
当我们修改完代码,默认项目的status会变成亮黄色的CHECKING。点击它,将其设定成RUNNING,确定.
![](https://img.haomeiwen.com/i6283324/82de5d4aad922ca8.jpg)
最后点击这一行最右侧的Run按钮,点击完如果你的任务执行得快的话,能看见run左边的任务状态栏的颜色由灰色变成了蓝色,表示任务开始
查看当前的任务状态,点击Run旁边的Active Tasks,能看到最近抓取记录,在其中你能追踪任务的状态以及查看任务失败的原因
![](https://img.haomeiwen.com/i6283324/d1003d8928318ba1.jpg)
如图所示表面这条链接已经被放在tasks队列中,其原因有两种:1.任务被暂停 2.失败重试,该链被放到tasks队列的尾部等待重试
上图中的这条链接是失败重试的链,具体错误信息也能在下拉的页面中找到,如下图
![](https://img.haomeiwen.com/i6283324/8bb1ee13c9364afa.jpg)
显示任务已经运行到了detail_page处,错误原因是list index out of range
这样以来定位错的原因也变得很方便了,我此处的有两种:
1.是由于传递给接口的参数并没有找到,导致返回数据失败,错误
2.有些电影没有导演(如这部),导致解析失败,无法return
所以后续考虑做在抓取时候也作字段容错,单个字段出错给空置,起码保证正常的数据返回
结果
![](https://img.haomeiwen.com/i6283324/2468c72472f41855.jpg)
关于图中的一些字段意思如下
1.队列统计:方便查看爬虫状态,优化爬虫爬取速度新增的状态统计。
每个组件之间的数字就是对应不同队列的排队数量,通常就是0或个位数,如果达到了几十甚至一百说明下游组件出现了瓶颈或错误,需要分析处理
2.组名:新建project后一般是不能修改project名字,如果需要特殊标记,可以通过更改group名字。
注:组名改为delete后如果状态是stop状态,24小时后会被系统自动删除
3.运行状态:五个
TODO : 新建项目后的默认状态
STOP : 停止
CHECKING :只要修改了代码,自动变成检查状态
DEBUG :在这个模式下运行,遇到错误信息会停止继续运行
RUNNING : 这里运行时遇到错误会尝试,如果还是错误会跳过这个任务继续运行
4.速度控制:rate是每秒爬取页面数 , burst是并发数 ,如1/3是三个并发,每秒爬取一个页面。
5.简单统计:5m是五分钟内任务执行的情况 , 1h是一小时内任务统计 , 1d是一天内运行任务统计 , all是所有任务统计
6.任务列表:显示最新任务列表,方便查看状态,查看错误等
7.结果查看:查看项目爬取的结果(默认是保存到sqlite3 db中,支持以json和csv格式下载)
更详细的解释可以查看官方解释
点击Result可以查看到抓取到的数据,点击下载或者使用
wget http://127.0.0.1:5000/results/dump/tencent_movie.json
获取结果到当前路径
pyspider这个框架还存在一些不足之处,且由于是脚本驱动,所以抓取脚本的解析错误将造成整个任务的停滞,所以我们需要不断地跟进网页的变化情况,确保在网页更新的短时间内同步更新代码,这不仅仅是某个框架的痛处,而是所有垂直网络爬虫都需要面对的一个问题。
网友评论