美文网首页
python爬虫工具库

python爬虫工具库

作者: 听云_ | 来源:发表于2018-09-07 17:04 被阅读354次

本文主要介绍并使用在爬虫工作中经常使用的一些工具

爬虫是什么

关于爬虫是什么,我们可以直接看百度百科的介绍,不多做解释,通俗一点说,涉及到网络请求的任何操作都能认为是一个爬虫,其实际内容就是获取网页数据的一个过程。打个比方,我们打开浏览器,输入: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提供的命令行指令去启动这些爬虫工程


eb3d796384e6bdc44d45dafa2.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为你加载好的静态页面


3.jpg

体现我们的可视化的时刻到了,点击最左边的enable css selector helper,当你的鼠标再移到页面中,会发现鼠标移到的地方变成了黄色,这即使和Portia一样的地方。我们点击下图红色框中的copy按钮,粘贴到index_page中第一个self.crawl()中去

4.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,检查我们的字段是不是都提取正确,像之前那种网页无法获取我们就需要使用其他方法(比如渲染,接口)


5.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,确定.


6.jpg

最后点击这一行最右侧的Run按钮,点击完如果你的任务执行得快的话,能看见run左边的任务状态栏的颜色由灰色变成了蓝色,表示任务开始

查看当前的任务状态,点击Run旁边的Active Tasks,能看到最近抓取记录,在其中你能追踪任务的状态以及查看任务失败的原因

7.jpg
如图所示表面这条链接已经被放在tasks队列中,其原因有两种:1.任务被暂停 2.失败重试,该链被放到tasks队列的尾部等待重试

上图中的这条链接是失败重试的链,具体错误信息也能在下拉的页面中找到,如下图


8.jpg

显示任务已经运行到了detail_page处,错误原因是list index out of range

这样以来定位错的原因也变得很方便了,我此处的有两种:
1.是由于传递给接口的参数并没有找到,导致返回数据失败,错误
2.有些电影没有导演(如这部),导致解析失败,无法return

所以后续考虑做在抓取时候也作字段容错,单个字段出错给空置,起码保证正常的数据返回

结果

9.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这个框架还存在一些不足之处,且由于是脚本驱动,所以抓取脚本的解析错误将造成整个任务的停滞,所以我们需要不断地跟进网页的变化情况,确保在网页更新的短时间内同步更新代码,这不仅仅是某个框架的痛处,而是所有垂直网络爬虫都需要面对的一个问题。

相关文章

网友评论

      本文标题:python爬虫工具库

      本文链接:https://www.haomeiwen.com/subject/omicgftx.html