python网络爬虫之Scrapy

本文分享的大体框架包含以下三部分

(1)首先介绍html网页,用来解析html网页的工具xpath
(2)介绍python中能够进行网络爬虫的库(requests,lxml,scrapy等)
(3)从四个案例出发有易到难依次介绍scrapy集成爬虫框架

下面开始对三部分内容逐一开始介绍。

一、html和xpath说明

1. html

超文本标记语言,是用来描述网页的一种语言。主要用于控制数据的显示和外观。HTML文档一定意义上可以被称为网页。但反过来说网页不仅仅是HTML,网页本质有三部分构成:负责内容结构的HTML,负责表现的CSS,以及负责行为的javascript。本文主要分享的是最核心的内容结构部分。

(1)html结构

完整的HTML文件至少包括<HTML>标签、<HEAD>标签、<TITLE>标签和<BODY>标签,并且这些标签都是成对出现的,开头标签为<>,结束标签为</>,在这两个标签之间添加内容。通过这些标签中的相关属性可以设置页面的背景色、背景图像等。
例如,我们打开豆瓣首页,摁下键盘上的F12键,打开浏览器自带“开发者工具”,可以看到一个完整的html文档结构,如下图

HTML文档结构.jpg

从上图可以看出,一个完整的html文档主要包含三部分:DTD文档头,head头部信息和body正文信息。其中DTD文档头用来告诉浏览器执行标准是什么(比如html4或是html5),head头部信息用来说明浏览器的编码方式和文档头名称,body顾名思义就是浏览器的正文部分。

(2)html标签

作为开始和结束的标记,由尖括号包围的关键词,比如 <html>,标签对中的第一个标签是开始标签,第二个标签是结束标签。html中常见标签如下:

html常用标签.png
html常用标签2.png

其中, “< ul >< li ></li ></ul >”是一种嵌套顺序,无序列表,成对出现;li的父元素必须是ul或者ol,不同之处在于ol是一种有序列列表,而ul是无序列表;

(3)html属性

属性是用来修饰标签的,放在开始标签里里面,html中常见四大属性:

属性 说明
class 规定元素的类名,大多数时候用于指定样式表中的类
id 唯一标识一个元素的属性,在html里面必须是唯一的
href 指定超链接目标的url
src 指定图像的url

2. xpath

(1)xpath定义

是一种路径查询语言,简单的说就是利用一个路径表达式从html文档中找到我们需要的数据位置,进而将其写入到本地或者数据库中。(可以将xpath类比为sql结构化查询语言)

(2)xpath常见使用方法

符号 功能
// 表示在整个文本中查找,是一种相对路径
/ 表示则表示从根节点开始查找,是一种绝对路径
text() 找出文本值
@ 找出标签对应的属性值,比如@href就是找出对应的href链接
. 表示当前节点
.. 表示当前节点的父节点

当然xpath除了上述常见用法外,还存两种比较特殊的用法:以相同的字符开头;标签套标签。

用法1:以相同的字符开头:starts-with(@属性部分,属性字符相同部分

用法2:标签套标签:string(.)

#以相同的字符开头
#比如我们想同时提取到下列html中三条文本内容的话,就需要使用starts-with方法
html1 = """
<!DOCTYPE html>
<html>
 <head lang='en'>
    <meta charest='utf-8'>
    <title></title>
 </head>
 <body>
    <div id="test-1">需要的内容1</div>
    <div id="test-2">需要的内容2</div>
    <div id="testfault">需要的内容3</div>
 </body>
</html>

#爬取代码
from lxml import etree
selector = etree.HTML(html1)
content  = selector.xpath('//div[starts-with(@id,"test")]/text()')
for each in content:
    print each

还有一种是标签套标签形式,参考如下例子

html2 = """
<!DOCTYPE html>
<html>
 <head lang='en'>
    <meta charest='utf-8'>
    <title></title>
 </head>
 <body>
    <div id="test3">
    我左青龙,
        <span id='tiger'>
            右白虎
            <ul>上朱雀,
                <li>下玄武,</li>
            </ul>
        </span>
        龙头在胸口
    </div>
 </body>
</html>
"""
#如果我们想爬取的内容是html文档中的所有文本的话,需要使用string方法进行提取
selector2 = etree.HTML(html2)
content2  = selector2.xpath('//div[@id="test3"]')[0] #列表,只有一个元素
info = content2.xpath('string(.)')
content3 = info.replace('\n','').replace(' ','')
print content3

(3)xpath的谓语结构

该小节参考资料:阮一峰的网络日志

所谓"谓语条件",就是对路径表达式的附加条件。所有的条件,都写在方括号"[]"中,表示对节点进行进一步的筛选。例如:

<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore>
  <book>
    <title lang="eng">Harry Potter</title>
    <price>29.99</price>
  </book>
  <book>
    <title lang="eng">Learning XML</title>
    <price>39.95</price>
  </book>
</bookstore>

下面从几个简单的例子让大家体会一下

  • /bookstore/book[1] :表示选择bookstore的第一个book子元素。
  • /bookstore/book[last()] :表示选择bookstore的最后一个book子元素。
  • /bookstore/book[last()-1] :表示选择bookstore的倒数第二个book子元素。
  • /bookstore/book[position()<3] :表示选择bookstore的前两个book子元素。
  • //title[@lang] :表示选择所有具有lang属性的title节点。
  • //title[@lang='eng'] :表示选择所有lang属性的值等于"eng"的title节点。

二、python中能够进行网络爬虫的库

1. 安装方式

python中安装包或者模块的方式一般有以下两种:

(1)pip install xxx(xxx表示模块名字)

pip install lxml/numpy/pandas/scrapy/requests

(2)进入到python库网站下载所需模块,然后使用pip install xxx.whl安装即可

pip install lxml‑3.7.3‑cp27‑cp27m‑win_amd64.whl

2. requests

import requests
#我的简书主页
r = requests.get('http://www.jianshu.com/u/95f3a80fac3e')
# r本身是一个reponse对象,需要通过content来返回其内容
print r.content
#其实上面通过content之后得到就是一个完整的html文档,之后可以直接使用lxml等工具直接对其进行解析,下一小节会讲到lxml
print r.status_code
print r.encoding #html的编码方式,一般是UTF-8
print r.cookies

3. lxml

lxml中集成了刚才讲述的xpath这种路径查询语言;例如我们首先创建一个html文档如下

html= """
<!DOCTYPE html>
<html>
 <head lang='en'>
    <meta charest='utf-8'>
    <title></title>
 </head>
 <body>
    <div id="test-1">需要的内容1</div>
    <div id="test-2">需要的内容2</div>
    <div id="testfault">需要的内容3</div>
 </body>
</html>
"""

然后使用lxml对我们想要的内容进行爬取

from lxml import etree
selector = etree.HTML(html)
content = selector.xptah('path') #此处的path指的就是需要爬虫的文件路径
for item in content:
    print item

前面讲到的requests通常也是配合lxml使用,首先使用requests获取到网页内容,即html文档,然后使用lxml中的xpath爬取我们所需要的内容。例子如下:

#爬取豆瓣电影top250,并将电影名称和评分打印出来
import requests
from lxml import etree

s = requests.Session() #开启一个requests会话
for id in range(0, 251, 25):
    url = 'https://movie.douban.com/top250/?start-' + str(id)
    r = s.get(url) #返回是一个reponse对象
    r.encoding = 'utf-8'
    root = etree.HTML(r.content)
    items = root.xpath('//ol/li/div[@class="item"]')
    # print(len(items))
    for item in items:
        title = item.xpath('./div[@class="info"]//a/span[@class="title"]/text()')
        name = title[0].encode('gb2312', 'ignore').decode('gb2312')
        # rank = item.xpath('./div[@class="pic"]/em/text()')[0]
        rating = item.xpath('.//div[@class="bd"]//span[@class="rating_num"]/text()')[0]
        print(name, rating)

4. 大杀器scrapy

scrapy是一个为了爬取网站数据,提取结构性数据而编写的应用框架。 可以应用在包括数据挖掘,信息处理或存储历史数据等一系列的程序中。我们需要知道的是,scrapy是一种集成框架,类似于request和xpath这些方法在scrapy都有集成。

(1)scrapy安装

安装scrapy前,需要先安装lxml模块,然后按照之前说的方法进行安装就可以(我的电脑安装的是anaconda集成环境)

(2)scrapy的结构

Paste_Image.png

其中,不同模块负责不同的任务分工。首先Scheduler发出请求(Requests),Downloader负责从互联网上下载内容,将下载好的内容(Responses)交给Spiders进行解析,解析完成后将内容Item返回,当然其中可能会涉及到对于解析后数据的进一步处理,这个任务是在Pipeline中完成的。

(3)scrapy通常有两种使用方式:

  • 直接在python脚本里定义一个爬取数据的类(参见爬虫案例1,2,3)
  • 创建完整的scrapy项目(参见爬虫案例4);创建命令为
    scrapy startproject xxx

创建完scrapy项目后,会在对应的文件下生成如下结构:

YLA943`5WMORH9SNVD~D}EA.png

其中,
spiders文件夹下面是真正爬虫的代码;
items之前提到过,是定义需要爬取的内容;
pipelines是对爬取到的数据进行的进一步处理;
settings中主要是一些环境变量和配置。
另外,scrapy有提供两个xpath选择器,HtmlXPathSelector和XmlXPathSelector,一个用于html,一个用于xml,xpath选择器有三个方法:

  • select(xpath): 返回一个相对于当前选中节点的选择器列表(一个xpath可能选到多个节点)
  • extract(): 返回选择器(列表)对应的节点的字符串(列表,类型就是python中的list)
    -_ re(regex)_: 返回正则表达式匹配的字符串(分组匹配)列表

需要注意的是,如果爬取的页面相对简单,爬取内容较少,且不用对爬取到的数据做过多的后期处理,使用第一种,反之,使用第二种,当然建立完整的scrapy项目肯定可以处理简单任务。

三、爬虫案例

1. 想要爬取得内容都在同一页面

本实例中,我们通过scrapy爬取七月在线课程信息。

#scrapy runspider spider.py –o xxx.json(运行时可以在命令窗口进行)
import scrapy 
class JulyeduSpider(scrapy.Spider):
    name = "julyedu"
    start_urls = ['https://www.julyedu.com/category/index'] #开始爬取网址,是一个list列表

    # 定义解析页面函数
    def parse(self, response):
        for julyedu_class in response.xpath('//div[@class="course_info_box"]'):
            print julyedu_class.xpath('a/h4/text()').extract_first()
            print julyedu_class.xpath('a/p[@class="course-info-tip"][1]/text()').extract_first()
            print julyedu_class.xpath('a/p[@class="course-info-tip"][2]/text()').extract_first()
            print response.urljoin(julyedu_class.xpath('a/img[1]/@src').extract_first())
            print "\n"

            # 返回函数值
            yield {
                'title':julyedu_class.xpath('a/h4/text()').extract_first(),
                'desc': julyedu_class.xpath('a/p[@class="course-info-tip"][1]/text()').extract_first(),
                'time': julyedu_class.xpath('a/p[@class="course-info-tip"][2]/text()').extract_first(),
                'img_url': response.urljoin(julyedu_class.xpath('a/img[1]/@src').extract_first())
            }

2. 想要爬去的内容在多页,不同页之间可以进行人为拼接构成(例如博客园

本实例中,我们利用scrapy爬取博客园中的信息,爬取内容包括每条博文的标题、链接、作者、评论等信息

class CnBlogSpider(scrapy.Spider):
    name = "cnblogs"
    allowed_domains = ["cnblogs.com"]
    start_urls = [ 'http://www.cnblogs.com/pick/#p%s' % p for p in xrange(1, 11)        ]

    #定义解析函数
    def parse(self, response):
        for article in response.xpath('//div[@class="post_item"]'):
            print article.xpath('div[@class="post_item_body"]/h3/a/text()').extract_first().strip()
            print response.urljoin(article.xpath('div[@class="post_item_body"]/h3/a/@href').extract_first()).strip()
            print article.xpath('div[@class="post_item_body"]/p/text()').extract_first().strip()
            print article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/a/text()').extract_first().strip()
            print response.urljoin(article.xpath('div[@class="post_item_body"]/div/a/@href').extract_first()).strip()
            print article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/span[@class="article_comment"]/a/text()').extract_first().strip()
            print article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/span[@class="article_view"]/a/text()').extract_first().strip()
            print ""

            yield {
                'title': article.xpath('div[@class="post_item_body"]/h3/a/text()').extract_first().strip(),
                'link': response.urljoin(article.xpath('div[@class="post_item_body"]/h3/a/@href').extract_first()).strip(),
                'summary': article.xpath('div[@class="post_item_body"]/p/text()').extract_first().strip(),
                'author': article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/a/text()').extract_first().strip(),
                'author_link': response.urljoin(article.xpath('div[@class="post_item_body"]/div/a/@href').extract_first()).strip(),
                'comment': article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/span[@class="article_comment"]/a/text()').extract_first().strip(),
                'view': article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/span[@class="article_view"]/a/text()').extract_first().strip(),
            }

3. 想要爬取的内容存在页面跳转(例如腾讯社会新闻

class QQNewsSpider(scrapy.Spider):
    name = 'qqnews'
    start_urls = ['http://news.qq.com/society_index.shtml']

    def parse(self, response):
        for href in response.xpath('//*[@id="news"]/div/div/div/div/em/a/@href'):
            full_url = response.urljoin(href.extract())
            yield scrapy.Request(full_url, callback=self.parse_question) #调用scrapy中Request方法,对合并后的网址进行分析,调用函数是parse_question

    #真正意义上解析页面的函数
    def parse_question(self, response):
        print response.xpath('//div[@class="qq_article"]/div/h1/text()').extract_first()
        print response.xpath('//span[@class="a_time"]/text()').extract_first()
        print response.xpath('//span[@class="a_catalog"]/a/text()').extract_first()
        print "\n".join(response.xpath('//div[@id="Cnt-Main-Article-QQ"]/p[@class="text"]/text()').extract())
        print ""
        yield {
            'title': response.xpath('//div[@class="qq_article"]/div/h1/text()').extract_first(),
            'content': "\n".join(response.xpath('//div[@id="Cnt-Main-Article-QQ"]/p[@class="text"]/text()').extract()),
            'time': response.xpath('//span[@class="a_time"]/text()').extract_first(),
            'cate': response.xpath('//span[@class="a_catalog"]/a/text()').extract_first(),
        }

4. 通过创建scrapy工程的方式爬取全国34个省、市所属的2290个地区的历史天气预报数据,网址请戳这里,并将其保存为json格式

之前跟大家分享的案例大多是参照网络视频资源和相关博客资料,这两天由于项目需要,爬取全国34个省、市所属的2290个地区的历史天气预报数据,真正动手才发现“纸上得来终觉浅,绝知此事要躬行”的道理所在,整个过程碰到很多坑,也请教了一些牛人,终于将数据成功爬取到本地,在此记录下整个爬取过程。

1. 多级页面跳转问题

涉及到多级页面跳转才能爬取到数据的场景,有两个方面是需要特别注意的,第一是确保整个页面跳转过程的逻辑正确,第二是跳转到某个具体页面使用xpath进行路经查询时,要保证xpath的写的没有问题。

2. Scrapy终端(Scrapy shell)

Scrapy终端是一个交互终端,供您在未启动spider的情况下尝试及调试您的爬取代码。 其本意是用来测试提取数据的代码,不过您可以将其作为正常的Python终端,在上面测试任何的Python代码。
对于检查xpath路径查询语言是否正确非常,举个例子,我想要爬取某个城市2017年一月的天气中的日期数据,

Paste_Image.png

根据html中提供的路径,我写了如下的xpath表达式

day = sel.xpath('//div[@class="tqtongji2"]/ul[position()>1]/li[1]/a/text()').extract()

具体对不对,能不能爬取到相对应的日期数据,我们就可以利用scrapy shell进行检查。

(1)首先在命令窗口启动scrapy shell,启动语句也很简单,如下

scrapy shell url
其中url表示想测试的网页链接。

Paste_Image.png

(2)输入需要检查的xpath语句

Paste_Image.png

可以看到输出结果中已经正确提取出我们需要的日期数据,从而也说明我们写的xpath路径没有问题。

3. 爬虫正式开始

(1)建立scrapy工程

利用前面讲到的方法创建相应的scrapy工程,在item.py中创建需要爬去的数据如下:

Paste_Image.png

(2)爬虫spider脚本

我们打开需要爬取页面的首页如下,http://lishi.tianqi.com/

Paste_Image.png

从页面中我们可以看到,全国各县区的城市按照A,B,C...Z顺序排列,每个字母下面包含很多城市,比如字母A下面包含阿城等地方。且每一类下面的第一个li标签是不需要的,因为第一个li标签表示的是字母A,B,C...Z等,如下图

Paste_Image.png

分析到这一步,我们可以写出一级解析函数parse来获取所有城市—链接和城市名的xpath路径查询语言,

链接如下:

sel = Selector(response)
country_urls = sel.xpath('//ul[@class="bcity"]/li[position()>1]/a/@href').extract() 

城市名如下:

sel = Selector(response)
country_urls = sel.xpath('//ul[@class="bcity"]/li[position()>1]/a/text()').extract() 

接下来就是每个城市(链接)进行for循环遍历,将每个城市的链接url和城市名城保存到item中,保存file_name目的在于为了待会儿写入数据时方便。将所有获取到的城市链接和城市名保存到items列表中,然后使用for循环对每个城市的进行二级页面解析,调用的是scrapy的Request方法,Request中的回调函数callback就是二级页面解析函数second_parse。一级解析函数parse的完整代码如下:

Paste_Image.png

既然说到二级页面,我们还是首先观察下二级页面的特点,以澳门历史天气详情为例

Paste_Image.png

可以看到,澳门所有历史数据(按月)都在div class = "tqtongji" 标签下面,每年的数据又被一个ul标签包围,每个ul标签下面拥有很多个li标签,分别表示一年的各个月份数据,分析到此,我们同样可以在二级解析页面函数中写出获取每个城市每个月链接和月份名称的xpath路径

链接如下:

sel = Selector(response)
month_urls = sel.xpath('//div[@class="tqtongji1"]/ul/li/a/@href').extract()

城市名如下:

sel = Selector(response)
month_titles = sel.xpath('//div[@class="tqtongji1"]/ul/li/a/text()').extract()

同样,获取到每个城市每个月份的链接后,在二级解析函数里面对每个月份进行遍历,最后仍然使用scrapy.Request对每个获取的月份链接进行解析,回调函数是三级页面解析函数detail_parse。
以下是二级页面解析函数的脚本

Paste_Image.png

最后跳转到我们最终要爬取数据的页面了,到了这一页面之后(如下图),便能够很方便的爬取到我们需要的数据。

Paste_Image.png

直接贴上最终三级页面解析函数

Paste_Image.png

到此,爬取数据的spider脚本已经全部开发完成,不过为了将数据按照城市分别保存到本地,还需要在pipeline中进行相应设置如下:

Paste_Image.png

最终爬取的效果图如下图:

Paste_Image.png
Paste_Image.png

喜欢的朋友请小小的点个赞,你的肯定会让我更有动力!!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容