Python 神兵谱之数据分析-上篇:数据采集

前言

刀枪剑戟,斧钺钩叉,镋镰槊棒,鞭锏锤抓。

神兵在手,妖魔不怕,劈荆斩棘,溅血生花。

行走江湖,谁没有件趁手的兵器。

但是,兵器有带楞的,有带刃儿的,有带戎绳的,有带锁链儿的,五花八门,对于新手来说,真的是“乱花渐欲迷人眼”。

不过,古有江湖百晓生,今有 Python 百媚生。百晓生所著的《兵器谱》让江湖血雨腥风,这百媚生也编纂了一部 Python 《神兵谱》,不知能否让 Python 江湖掀起什么暴雨狂风?

我们今天就来讲讲这《神兵谱》的“数据分析”篇。这“数据分析”篇又分为上、中、下三篇,分别针对数据分析的数据采集、数据处理及数据可视化三个方面。

本文不光是神兵的展示,更要教会大家简单的使用,能够帮助大家挑选合适趁手的兵器,才能在刀光剑影的江湖,立于不败之地。

话不多说,直入主题。

上篇:数据采集

说到数据采集,那最大名鼎鼎的方式就是“爬虫”啦,让我们来看看百媚生带给我们的“爬虫”利器吧,是不是真如传言的“见血封喉”呢?

Requests

啥?为什么 requests 是“爬虫”?

可不要小瞧了它!虽说 requests 是网络请求库,但它却如高手手中的「木剑」一般,用好了,一样招招致命。

使用 requests 发起攻击(请求),犹如疾风般迅速,犹如落叶般轻盈。

>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200
>>> r.headers['content-type']
'application/json; charset=utf8'
>>> r.encoding
'utf-8'
>>> r.text
'{"type":"User"...'
>>> r.json()
{'private_gists': 419, 'total_private_repos': 77, ...}

这就完了?

如果对方是返回 Json 格式的 API 服务,是的,这就完了。我们已经拿到数据了。

如果对方是返回 XML 格式的 API 服务,那么,我们再搭配上原生的 xml 或者 lxml 解析器,灭敌于百步之外。

"""
content 是 xml 格式的字符串,即 r.text
例如
<?xml version="1.0"?>
<data>
    <country name="a"></country>
    <country name="b"></country>
    <country name="c"></country>
</data>
"""
import xml.etree.ElementTree as ET

tree = ET.parse(content)
root = tree.getroot()
# 遍历节点
for child in root:
    print(child.tag, child.attrib)

lxml 更快更凶残。

from lxml import etree

root = etree.XML(content)
for element in root.iter():
    print("%s - %s" % (element.tag, element.text))

lxml 更是支持强大的 xpathxlst 语法(语法文档详见参考)。

# 使用 xpath 语法快速定位节点,提取数据
r = root.xpath('country')
text = root.xpath('country/text()')

xlst 进行快速转换。

xslt_root = etree.XML('''\
    <xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="/">
        <foo><xsl:value-of select="/a/b/text()" /></foo>
    </xsl:template>
    </xsl:stylesheet>''')
transform = etree.XSLT(xslt_root)
f = StringIO('<a><b>Text</b></a>')
doc = etree.parse(f)
result_tree = transform(doc)

对手更凶残了,是 HTML 文档!这下就需要 BeautifulSouplxml 解析器出马了。

BeautifulSoup 虽然速度不快,好在利于理解。

from bs4 import BeautifulSoup

# content 即 html 字符串, requests 返回的文本 text
soup = BeautifulSoup(content, 'html.parser')

print(soup.title)
print(soup.title.name)
print(soup.find_all('a'))
print(soup.find(id="link3"))
for link in soup.find_all('a'):
    print(link.get('href'))

上房揭瓦(解析网页),那是手到擒来。

而用 lxml 还是那么干净利落。

html = etree.HTML(content)
result = etree.tostring(html, pretty_print=True, method="html")
print(result)
# 接下来就是 xpath 的表演时间

可见,木剑虽朴实,在高手手中,也能变化无穷。如果是“接骨木”,那更是了不得。最快速便捷的数据采集神兵,非 requests 莫属!

Scrapy

接下来让我们看看数据采集的百变神兵 —— Scrapy,分分钟让我们全副武装。

# 创建一个项目
scrapy startproject tutorial
cd tutorial
# 创建一个爬虫
scrapy genspider quotes quotes.toscrape.com

然后编辑项目下 spiders/quotes.py 爬虫文件。

import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def start_requests(self):
        """
        生成初始请求。
        """
        urls = [
            'http://quotes.toscrape.com/page/1/',
            'http://quotes.toscrape.com/page/2/',
        ]
        for url in urls:
            yield scrapy.Request(url=url, callback=self.parse)

    def parse(self, response):
        """
        处理请求返回的响应。
        """
        page = response.url.split("/")[-2]
        filename = 'quotes-%s.html' % page
        with open(filename, 'wb') as f:
            f.write(response.body)
        self.log('Saved file %s' % filename)

然后就是启动爬虫。

scrapy crawl quotes

这还没有发挥 Scrapy 的能力呢!

解析网页

# CSS 解析
response.css('title::text').getall()
# xpath 解析
response.css('//title/text()').getall()

自动生成结果文件

import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
        'http://quotes.toscrape.com/page/2/',
    ]

    def parse(self, response):
        # parse 函数直接返回字典或者 Item 对象。
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('small.author::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

在爬取的命令上加上 -o 参数,即可快速将结果保存到文件,支持多种格式(csv,json,json lines,xml),也可方便地扩展自己的格式。

scrapy crawl quotes -o quotes.json

数据分页了,还有下一页怎么办?抛出请求,让 Scrapy 自己去处理。



class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = [
        'http://quotes.toscrape.com/page/1/',
    ]

    def parse(self, response):
        """
        parse 函数 yield 字典或者 Item 对象,则视为结果,
        yield 请求对象(follow 方法即是跟随链接,快速生成对应的请求对象)即继续爬取。
        """
        for quote in response.css('div.quote'):
            yield {
                'text': quote.css('span.text::text').get(),
                'author': quote.css('span small::text').get(),
                'tags': quote.css('div.tags a.tag::text').getall(),
            }

        next_page = response.css('li.next a').get()
        if next_page is not None:
            yield response.follow(next_page, callback=self.parse)

这就完了吗?当然不会,Scrapy 还提供了多种数据采集需要用到的功能。

  • 强大的扩展能力,快速编写扩展和中间件。
  • 灵活的配置,并发控制,限速控制等。
  • 自定义的爬取对象处理流水线。
  • 自定义的爬取对象存储。
  • 自动统计数据。
  • 整合邮件。
  • Telnet 控制台等等。

这只是核心功能,还没见到它的社区能力呢!

这些就不再展开了。

快速而又强大的数据采集利器,当属 Scrapy

Pyspider

强大的瑞士军刀 —— Pyspider。

Pyspider 可不得了,它提供了一整套完整的数据采集解决方案,堪称爬虫界的“瑞士军刀”。

  • 原生提供 Web 管理界面,支持任务监控、项目管理、结果查看等等。
  • 原生支持众多的数据库后端,如 MySQL、MongoDB、SQLite、Elasticsearch、Postgresql。
  • 原生支持多种消息队列,如 RabbitMQ,Beanstalk、Redis、Kombu。
  • 支持任务优先级、自动重试、定时任务、支持 JS 渲染等功能。
  • 分布式架构。

爬虫,就是这么简单!

from pyspider.libs.base_handler import *

class Handler(BaseHandler):
    crawl_config = {
    }

    @every(minutes=24 * 60)
    def on_start(self):
        self.crawl('http://scrapy.org/', callback=self.index_page)

    @config(age=10 * 24 * 60 * 60)
    def index_page(self, response):
        for each in response.doc('a[href^="http"]').items():
            self.crawl(each.attr.href, callback=self.detail_page)

    def detail_page(self, response):
        return {
            "url": response.url,
            "title": response.doc('title').text(),
        }

启动爬虫框架。

pyspider

然后,我们就可以通过 http://localhost:5000/ 进行爬虫的管理和运行了。

我们可以使用 css 选择器快速提取网页信息。

    def index_page(self, response):
        for each in response.doc('a[href^="http"]').items():
            if re.match("http://www.imdb.com/title/tt\d+/$", each.attr.href):
                self.crawl(each.attr.href, callback=self.detail_page)
        self.crawl(response.doc('#right a').attr.href, callback=self.index_page)
        
    def detail_page(self, response):
        return {
            "url": response.url,
            "title": response.doc('.header > [itemprop="name"]').text(),
            "rating": response.doc('.star-box-giga-star').text(),
            "director": [x.text() for x in response.doc('[itemprop="director"] span').items()],
        }

启用 PhantomJS 来渲染网页上的 JS。

pyspider phantomjs

使用 fetch_type='js'

class Handler(BaseHandler):
    def on_start(self):
        self.crawl('http://www.twitch.tv/directory/game/Dota%202',
                   fetch_type='js', callback=self.index_page)

    def index_page(self, response):
        return {
            "url": response.url,
            "channels": [{
                "title": x('.title').text(),
                "viewers": x('.info').contents()[2],
                "name": x('.info a').text(),
            } for x in response.doc('.stream.item').items()]
        }

还能执行一段 JS 代码,来获取那些动态生成的网页内容。

class Handler(BaseHandler):
    def on_start(self):
        self.crawl('http://www.pinterest.com/categories/popular/',
                   fetch_type='js', js_script="""
                   function() {
                       window.scrollTo(0,document.body.scrollHeight);
                   }
                   """, callback=self.index_page)

    def index_page(self, response):
        return {
            "url": response.url,
            "images": [{
                "title": x('.richPinGridTitle').text(),
                "img": x('.pinImg').attr('src'),
                "author": x('.creditName').text(),
            } for x in response.doc('.item').items() if x('.pinImg')]
        }

好了,接下来我知道,问题就是 PyspiderScrapy 选哪个?

简单说下它们的对比。

Scrapy 有更强大的扩展能力,社区更活跃,周边更丰富。而 Pyspider 本身功能更全,但扩展能力较弱。许多 Scrapy 需要扩展实现的功能,如 Web 界面、JS 渲染等,Pyspider 原生都提供了。

Pyspider 的整套生态上手更容易,实现更快速。Scrapy 对复杂的场景有更多的选择余地,更灵活。

所以,诸位选哪款?

成年人需要做选择吗?

后记

此上篇介绍了数据采集领域的三款神兵。

  • 朴实而又神奇的“接骨木剑” —— Requests
  • 快速而又强大的“百变神兵” —— Scrapy
  • 简单而又全能的“瑞士军刀” —— Pyspider

有此三款神兵在手,不信你不能驰骋“爬虫”的江湖!

百媚生 Python《神兵谱》之数据分析-上篇,如果觉得有用,请点赞关注收藏哦!

来自 知乎专栏

参考

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