前言
刀槍劍戟,斧鉞鉤叉,镋鐮槊棒,鞭锏錘抓。
神兵在手,妖魔不怕,劈荊斬棘,濺血生花。
行走江湖,誰沒有件趁手的兵器。
但是,兵器有帶楞的,有帶刃兒的,有帶戎繩的,有帶鎖鏈兒的,五花八門,對于新手來說,真的是“亂花漸欲迷人眼”。
不過,古有江湖百曉生,今有 Python 百媚生。百曉生所著的《兵器譜》讓江湖血雨腥風,這百媚生也編纂了一部 Python 《神兵譜》,不知能否讓 Python 江湖掀起什么暴雨狂風?
我們今天就來講講這《神兵譜》的“數據分析”篇。這“數據分析”篇又分為上、中、下三篇,分別針對數據分析的數據采集、數據處理及數據可視化三個方面。
本文不光是神兵的展示,更要教會大家簡單的使用,能夠幫助大家挑選合適趁手的兵器,才能在刀光劍影的江湖,立于不敗之地。
話不多說,直入主題。
上篇:數據采集
說到數據采集,那最大名鼎鼎的方式就是“爬蟲”啦,讓我們來看看百媚生帶給我們的“爬蟲”利器吧,是不是真如傳言的“見血封喉”呢?
Requests
啥?為什么 requests 是“爬蟲”?
可不要小瞧了它!雖說 requests 是網絡請求庫,但它卻如高手手中的「木劍」一般,用好了,一樣招招致命。
使用 requests 發(fā)起攻擊(請求),猶如疾風般迅速,猶如落葉般輕盈。
>>> 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()
# 遍歷節(jié)點
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 更是支持強大的 xpath 和 xlst 語法(語法文檔詳見參考)。
# 使用 xpath 語法快速定位節(jié)點,提取數據
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 文檔!這下就需要 BeautifulSoup 或 lxml 解析器出馬了。
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,分分鐘讓我們全副武裝。
# 創(chuàng)建一個項目
scrapy startproject tutorial
cd tutorial
# 創(chuàng)建一個爬蟲
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
這還沒有發(fā)揮 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 方法即是跟隨鏈接,快速生成對應的請求對象)即繼續(xù)爬取。
"""
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 還提供了多種數據采集需要用到的功能。
- 強大的擴展能力,快速編寫擴展和中間件。
- 靈活的配置,并發(fā)控制,限速控制等。
- 自定義的爬取對象處理流水線。
- 自定義的爬取對象存儲。
- 自動統(tǒng)計數據。
- 整合郵件。
- Telnet 控制臺等等。
這只是核心功能,還沒見到它的社區(qū)能力呢!
- Scrapyd:工程化部署爬蟲。
- Scrapy-Splash:為 Scrapy 提供了 JS 渲染能力。
- Scrapy Jsonrpc:Json RPC 服務控制爬蟲。
- Gerapy:Web 爬蟲管理平臺。
- ScrapyWeb:另一個 Web 爬蟲管理平臺。
- ScrapyKeeper:還是一個 Web 爬蟲管理平臺。
- Portia:無需編碼的交互式爬蟲平臺。
這些就不再展開了。
快速而又強大的數據采集利器,當屬 Scrapy!
Pyspider
強大的瑞士軍刀 —— Pyspider。
Pyspider 可不得了,它提供了一整套完整的數據采集解決方案,堪稱爬蟲界的“瑞士軍刀”。
- 原生提供 Web 管理界面,支持任務監(jiān)控、項目管理、結果查看等等。
- 原生支持眾多的數據庫后端,如 MySQL、MongoDB、SQLite、Elasticsearch、Postgresql。
- 原生支持多種消息隊列,如 RabbitMQ,Beanstalk、Redis、Kombu。
- 支持任務優(yōu)先級、自動重試、定時任務、支持 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()]
}
還能執(zhí)行一段 JS 代碼,來獲取那些動態(tài)生成的網頁內容。
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')]
}
好了,接下來我知道,問題就是 Pyspider 和 Scrapy 選哪個?
簡單說下它們的對比。
Scrapy 有更強大的擴展能力,社區(qū)更活躍,周邊更豐富。而 Pyspider 本身功能更全,但擴展能力較弱。許多 Scrapy 需要擴展實現(xiàn)的功能,如 Web 界面、JS 渲染等,Pyspider 原生都提供了。
Pyspider 的整套生態(tài)上手更容易,實現(xiàn)更快速。Scrapy 對復雜的場景有更多的選擇余地,更靈活。
所以,諸位選哪款?
成年人需要做選擇嗎?
后記
此上篇介紹了數據采集領域的三款神兵。
- 樸實而又神奇的“接骨木劍” —— Requests
- 快速而又強大的“百變神兵” —— Scrapy
- 簡單而又全能的“瑞士軍刀” —— Pyspider
有此三款神兵在手,不信你不能馳騁“爬蟲”的江湖!
百媚生 Python《神兵譜》之數據分析-上篇,如果覺得有用,請點贊關注收藏哦!
來自 知乎專欄。