Scrapy 爬取七麦 app数据排行榜

前言

熟悉Scrapy之后,本篇文章带大家爬取七麦数据(https://www.qimai.cn/rank )的ios appstore付费应用排行榜前100名应用。

爬取内容包括app在列表中的下标,app图标地址,app的名称信息,app的类型,在分类中的排行,开发者,详情等。

考虑的问题:

  • Forbidden by robots.txt的错误
  • 网页返回403
  • 页面通过动态渲染,普通的请求url,在页面渲染之前已经返回response,解析没有数据
  • 列表一页20个app,想要拿到前100个需要翻页,但是翻页没有更改url,而是通过js动态加载
  • ...
1563269958.jpg

创建项目

在需要放置项目的目录下,

> scrapy startproject qimairank

回车即可创建默认的Scrapy项目架构。

1563271101.jpg

创建Item

创建Item来存储我们爬取的app在列表中的下标,app图标地址,app的名称信息,app的类型,在分类中的排行,开发者,详情。
修改items.py,在下面增加

class RankItem(scrapy.Item):
    # 下标
    index = scrapy.Field()
    # 图标地址
    src = scrapy.Field()
    # app标题信息
    title = scrapy.Field()
    # app类型
    type = scrapy.Field()
    # 分类中的排行
    type_rank = scrapy.Field()
    # 开发者
    company = scrapy.Field()
    # 详情信息
    info = scrapy.Field()

创建Spider

spiders目录下创建RankSpider.py,并创建class RankSpider,继承于scrapy.Spider。

import scrapy

class RankSpider(scrapy.Spider):
    name = "RankSpider"
    start_urls = ["https://www.qimai.cn/rank"]

    def parse(self, response):
       pass
  • name:用于区别Spider,该名字必须是唯一的。
  • start_urls:Spider在启动时进行爬取的url列表,首先会爬取第一个。
  • def parse(self, response):得到url的response信息后的解析方法。

解析付费榜

解析用的Selectors选择器有多种方法:

  • xpath(): 传入xpath表达式,返回该表达式所对应的所有节点的selector list列表 。
  • css(): 传入CSS表达式,返回该表达式所对应的所有节点的selector list列表.
  • extract(): 序列化该节点为unicode字符串并返回list。
  • re(): 根据传入的正则表达式对数据进行提取,返回unicode字符串list列表。

下面我们用xpath()选择节点,xpath的语法可参考w3c的http://www.w3school.com.cn/xpath/xpath_nodes.asp 学习,需要熟悉语法、运算符、函数等。

def parse(self, response):
    base = response.xpath(
        "//div[@class='ivu-row rank-all-item']/div[@class='ivu-col ivu-col-span-8'][2]//ul/li[@class='child-item']/div[@class='ivu-row']")
    for box in base:
        # 创建实例
        rankItem = RankItem()
        # 下标
        rankItem['index'] = \
            box.xpath(".//div[@class='ivu-col ivu-col-span-3 left-item']/span/text()").extract()[0]
        # 图标地址
        rankItem['src'] = box.xpath(".//img/@src").extract()[0]
        # app名称信息
        rankItem['title'] = box.xpath(".//div[@class='info-content']//a/text()").extract()[0]
        # app类型
        rankItem['type'] = box.xpath(".//div[@class='info-content']//p[@class='small-txt']/text()").extract()[0]
        # 分类中的排行
        rankItem['type_rank'] = box.xpath(
            ".//div[@class='info-content']//p[@class='small-txt']//span[@class='rank-item']/text()").extract()[
            0]
        # 开发者
        rankItem['company'] = box.xpath(
            ".//div[@class='info-content']//p[@class='small-txt']//span[@class='company-item']/text()").extract()[
            0]
        # 详情页地址
        infoUrl = "https://www.qimai.cn" + box.xpath(".//div[@class='info-content']//a/@href").extract()[0]
        yield rankItem

运行爬取初始app列表

直接运行

qimairank>scrapy crawl RankSpider -o data.json

你会发现窗口没有item输出,data.json中也没有数据,是我们写错了吗?

1563358018.jpg

scrapy默认遵守robot协议的,在访问网址前会先访问robot.txt来查看自己是否有权限访问。如果网站不允许被爬,就不能访问。
怎么样不遵守协议呢?

settings.py

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

再次运行仍然失败,我们来看下具体原因:

1563358364.jpg

因为七麦网站对请求的User-Agent做了校验,解决办法是在配置文件

settings.py

# Enable or disable downloader middlewares
# See https://doc.scrapy.org/en/latest/topics/downloader-middleware.html
DOWNLOADER_MIDDLEWARES = {
    #    'qimairank.middlewares.QimairankDownloaderMiddleware': 543,
    'qimairank.middlewares.RandomUserAgent': 1,
}

USER_AGENTS = [
    "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; AcooBrowser; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Acoo Browser; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.0.04506)",
    "Mozilla/4.0 (compatible; MSIE 7.0; AOL 9.5; AOLBuild 4337.35; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727)",
    "Mozilla/5.0 (Windows; U; MSIE 9.0; Windows NT 9.0; en-US)",
    "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 2.0.50727; Media Center PC 6.0)",
    "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.0; Trident/4.0; WOW64; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET CLR 1.0.3705; .NET CLR 1.1.4322)",
    "Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 5.2; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.04506.30)",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)",
    "Mozilla/5.0 (X11; U; Linux; en-US) AppleWebKit/527+ (KHTML, like Gecko, Safari/419.3) Arora/0.6",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.2pre) Gecko/20070215 K-Ninja/2.1.1",
    "Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9) Gecko/20080705 Firefox/3.0 Kapiko/3.0",
    "Mozilla/5.0 (X11; Linux i686; U;) Gecko/20070322 Kazehakase/0.4.5",
    "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.8) Gecko Fedora/1.9.0.8-1.fc10 Kazehakase/0.5.6",
    "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/535.20 (KHTML, like Gecko) Chrome/19.0.1036.7 Safari/535.20",
    "Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; fr) Presto/2.9.168 Version/11.52",
]

并在middlewares.py中创建RandomUserAgent

import random

class RandomUserAgent(object):
    """
    随机获取settings.py中配置的USER_AGENTS设置'User-Agent'
    """

    def __init__(self, agents):
        self.agents = agents

    @classmethod
    def from_crawler(cls, crawler):
        return cls(crawler.settings.getlist('USER_AGENTS'))

    def process_request(self, request, spider):
        request.headers.setdefault('User-Agent', random.choice(self.agents))

再次运行,没有报错,但是没有数据,是我们的xpath写错啦?我们在parse中增加输出body的信息

1563428566.jpg

可以看到body为空,没有我们需要的列表数据,这是因为七麦数据是通过js动态渲染的,在渲染完成前,我们的response已经返回,那么怎么样才能等一等呀,等到渲染完成才返回呢?

爬取动态渲染的方式,我知道是通过Splash或者Selenium,像我们的桌面版系统可以选择用Selenium,操作可以设置可视化,所有界面操作都能看见,Splash依赖于Docker,无界面。

安装Selenium包:

pip install selenium

使用前需要安装驱动,配置详情点击

驱动安装完成,在middlewares.py中创建 SeleniumMiddleware

class SeleniumMiddleware(object):
    def __init__(self):
        self.timeout = 50
        # 2.Firefox---------------------------------
        # 实例化参数对象
        options = webdriver.FirefoxOptions()
        # 无界面
        # options.add_argument('--headless')
        # 关闭浏览器弹窗
        options.set_preference('dom.webnotifications.enabled', False)
        options.set_preference('dom.push.enabled', False)
        # 打开浏览器
        self.browser = webdriver.Firefox(firefox_options=options)
        # 指定浏览器窗口大小
        self.browser.set_window_size(1400, 700)
        # 设置页面加载超时时间
        self.browser.set_page_load_timeout(self.timeout)
        self.wait = WebDriverWait(self.browser, self.timeout)

    def process_request(self, request, spider):
        # 当请求的页面不是当前页面时
        if self.browser.current_url != request.url:
            # 获取页面
            self.browser.get(request.url)
            time.sleep(5)
        else:
            pass
        # 返回页面的response
        return HtmlResponse(url=self.browser.current_url, body=self.browser.page_source,
                            encoding="utf-8", request=request)

    def spider_closed(self):
        # 爬虫结束 关闭窗口
        self.browser.close()
        pass

    @classmethod
    def from_crawler(cls, crawler):
        # 设置爬虫结束的回调监听
        s = cls()
        crawler.signals.connect(s.spider_closed, signal=signals.spider_closed)
        return s

在settins.py中配置

# Enable or disable downloader middlewares
DOWNLOADER_MIDDLEWARES = {
    #    'qimairank.middlewares.QimairankDownloaderMiddleware': 543,
    'qimairank.middlewares.RandomUserAgent': 1,
    'qimairank.middlewares.SeleniumMiddleware': 10,
}

再次运行scrapy crawl RankSpider -o data.json,啦啦啦~这回有数据啦。

Selenium调用JS脚本

观察爬取出来的data.json,发现怎么肥四,只有20条数据,而且除了前6个的app图标都是七麦的默认图标。

1563434696.jpg

这是因为七麦数据的列表默认每页20条,而且默认渲染前6个的图标,其余的页需要触发滑动事件加载,而且滑动到的图标才开始渲染。这样怎么办呢?我们只需要滑动到可以加载的按钮就可以啦,检查发现在三个列表的外层标签有一个class为cm-explain-bottom的标签


1563442615.jpg

我们用Selenium调用js脚本,滑动到这个标签就可以啦,在中间件process_request方法更改

def process_request(self, request, spider):
    # 当请求的页面不是当前页面时
    if self.browser.current_url != request.url:
        # 获取页面
        self.browser.get(request.url)
        time.sleep(5)
        # 请求的url开始为https://www.qimai.cn/rank/时,调用滑动界面,每页20个,滑动4次
        if request.url.startswith("https://www.qimai.cn/rank"):
            try:
                for i in (0, 1, 2, 3):
                    self.browser.execute_script(
                        "document.getElementsByClassName('cm-explain-bottom')[0].scrollIntoView(true)")
                    time.sleep(4)
            except JavascriptException as e:
                pass
            except Exception as e:
                pass

再次执行scrapy crawl RankSpider -o data1.json,则可看见已经生成data1.json里面有100个item。

获取app详情

详情页需要跟进url,我们在RankSpider#parse方法中,不用yield Item,而是yield Request就可以跟进。

# 详情页地址
infoUrl = "https://www.qimai.cn" + box.xpath(".//div[@class='info-content']//a/@href").extract()[0]
# yield rankItem
yield Request(infoUrl.replace("rank", "baseinfo"), self.parseInfo,
              meta={'rankItem': dict(rankItem).copy()}, dont_filter=True)

解析的infoUrl替换"rank"字符串为"baseinfo"就可以访问app应用信息页,用meta传递item到下一个解析方法中,用软拷贝的方式,避免Item因为地址相同,内容覆盖。

self.parseInfo为指定这次请求的解析方法,

def parseInfo(self, response):
    print("基地址:" + response.url)
    if response.status != 200:
        return

    rankItem = response.meta['rankItem']

    info = dict()
    base = response.xpath("//div[@id='app-container']")
    if base.extract():
        # try:
        # 描述
        try:
            info['desc'] = base.xpath(
                ".//div[@class='app-header']//div[@class='app-subtitle']/text()").extract()[0]
        except Exception as e:
            print("无描述")
        # 开发商
        info['auther'] = base.xpath(
            ".//div[@class='app-header']//div[@class='auther']//div[@class='value']/text()").extract()[0]
        # 分类
        info['classify'] = base.xpath(
            ".//div[@class='app-header']//div[@class='genre']//div[@class='value']/a/text()").extract()[0]
        # appid
        info['appid'] = base.xpath(
            ".//div[@class='app-header']//div[@class='appid']//div[@class='value']/a/text()").extract()[0]
        # appstore地址
        info['appstorelink'] = base.xpath(
            ".//div[@class='app-header']//div[@class='appid']//div[@class='value']/a/@href").extract()[0]
        # 价格
        info['price'] = base.xpath(
            ".//div[@class='app-header']//div[@class='price']//div[@class='value']/text()").extract()[0]
        # 最新版本
        info['version'] = base.xpath(
            ".//div[@class='app-header']//div[@class='version']//div[@class='value']/text()").extract()[0]
        # 应用截图
        info['screenshot'] = base.xpath(
            ".//div[@class='router-wrapper']//div[@class='app-screenshot']//div[@class='screenshot-box']//img/@src").extract()
        # 应用描述
        info['desc'] = base.xpath(
            ".//div[@class='router-wrapper']//div[@class='app-describe']//div[@class='description']").extract()[
            0]
        # 应用基本信息
        info['baseinfo'] = []
        for infoBase in base.xpath(
                ".//div[@class='router-wrapper']//div[@class='app-baseinfo']//ul[@class='baseinfo-list']/li"):
            # print(info['baseinfo'])
            try:
                info['baseinfo'].append(dict(type=infoBase.xpath(".//*[@class='type']/text()").extract()[0],
                                             info=infoBase.xpath(".//*[@class='info-txt']/text()").extract()[0]))
            except Exception as e:
                pass

        rankItem['info'] = info
        # 替换图标 列表加载为默认图标
        rankItem['src'] = \
            response.xpath("//*[@id='app-side-bar']//div[@class='logo-wrap']/img/@src").extract()[
                0]
        yield rankItem

再次执行scrapy crawl RankSpider -o data1.json,则可看见已经生成data2.json,但是生成的列表不是排行的列表,甚至是乱序的,原因是因为我们使用了url跟进返回,每个页面的请求返回的速度不一样,需要排序的话就写个小脚本按照index排个序。

项目源码

原文链接

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

推荐阅读更多精彩内容