摘要: 在如今互連網環境下,網路上的各種商務資料,如新聞,社交網站,交易類資料等各種各樣的資料越來越多被套用到企業的資料運營中,這些資料一般都資料量巨大,是最適合用MaxCompute來進行剖析和加工的一類資料,尤其可以利用MaxCompute的機器學習服務能力來完成一些資料採礦的商務場景,本文就介紹如何利用開源的Scrapy爬蟲架構來爬取新聞網站的資料到MaxCompute中。
在如今互連網環境下,網路上的各種商務資料,如新聞,社交網站,交易,政府公用資料,氣象資料等各種各樣的資料越來越多被套用到企業的資料運營中,以打通外部資料與內部資料的通道,使得兩者激情碰撞出熱烈的火花。這些資料一般都資料量巨大,是最適合用MaxCompute來進行剖析和加工的一類資料,尤其可以利用MaxCompute的機器學習服務能力來完成一些資料採礦的商務場景,本文就介紹如何利用開源的Scrapy爬蟲架構來爬取新聞網站的資料到MaxCompute中。
一、Scrapy簡單介紹
Scrapy是一個用Python 寫的Crawler Framework ,簡單輕巧,並且非常方便。
Scrapy 使用Twisted 這個非同步網路程式庫來處理網路通訊,架構清晰,並且包含了各種中介軟體介面,可以靈活的完成各種需求。整體架構如下圖所示:
綠線是資料流向,首先從初始URL 開始,Scheduler會將其交給Downloader 進行下載,下載之後會交給Spider 進行剖析,Spider剖析出來的結果有兩種:一種是需要進一步搜耙的連結,例如之前剖析的“下一頁”的連結,這些東西會被傳回Scheduler ;另一種是需要儲存的資料,它們則被送到Item Pipeline 那裡,那是對資料進行後期處理(詳細剖析、遮罩、隱藏等)的地方。另外,在資料流動的通道裡還可以安裝各種中介軟體,進行必要的處理。
二、Scrapy環境安裝
系統內容要求:
Linux
軟體環境要求:
- 已安裝:Python 2.7 ( 下載位址:https://www.python.org/ftp/python/2.7.13/Python-2.7.13.tgz)
- 已安裝:pip (可參考:https://pip.pypa.io/en/stable/installing/ 進行安裝
Scrapy安裝
執行安裝指令:
pip install Scrapy
Scrapy校驗
執行指令:
scrapy
執行結果:
ODPS Python安裝
執行安裝指令:
pip install pyodps
ODPS Python校驗
執行指令:
python -c "from odps import ODPS"
執行結果:無報錯,即為安裝成功
三、建立Scrapy項目
在你想要建立Scrapy項目的目錄下,執行:
scrapy startproject hr_scrapy_demo
看一下Scrapy建立項目後的目錄結構:
hr_scrapy_demo /
scrapy.cfg#全域設定檔
hr_scrapy_demo /#項目下的Python模組,你可以從這裡參考該Python模組
__init__.py
items.py#自訂的Items
pipelines.py#自訂的Pipelines
settings.py#自訂的項目級設定資訊
spiders/#自訂的spiders
__init__.py
四、建立OdpsPipelines
在hr_scrapy_demo/pipelines.py中,我們可以自訂我們的資料處理pipelines,以下是我之前編寫好的一個OdpsPipeline,該Pipeline可以用於將我們採集到的item儲存到ODPS中,但也有幾點需要說明:
1. ODPS中的表必須已經提前建立好。
2. Spider中採集到的item必須包含該表的所有欄位,且名字必須一致,否則會拋出異常。
3. 支援磁碟分割表格和無磁碟分割表格。
將下面代碼取代掉你項目中的pipelines.py
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
from odps import ODPS
import logging
logger = logging.getLogger('OdpsPipeline')
class OdpsPipeline(object):
collection_name = 'odps'
records = []
def __init__(self, odps_endpoint, odps_project,accessid,accesskey,odps_table,odps_partition=None,buffer=1000):
self.odps_endpoint = odps_endpoint
self.odps_project = odps_project
self.accessid = accessid
self.accesskey = accesskey
self.odps_table = odps_table
self.odps_partition = odps_partition
self.buffer = buffer
@classmethod
def from_crawler(cls, crawler):
return cls(
odps_endpoint=crawler.settings.get('ODPS_ENDPOINT'),
odps_project=crawler.settings.get('ODPS_PROJECT'),
accessid=crawler.settings.get('ODPS_ACCESSID'),
accesskey=crawler.settings.get('ODPS_ACCESSKEY'),
odps_table=crawler.settings.get('ODPS_TABLE'),
odps_partition=crawler.settings.get('ODPS_PARTITION'),
buffer=crawler.settings.get('WRITE_BUFFER')
)
def open_spider(self, spider):
self.odps = ODPS(self.accessid,self.accesskey,project=self.odps_project,endpoint=self.odps_endpoint)
self.table = self.odps.get_table(self.odps_table)
if(self.odps_partition is not None and self.odps_partition != ""):
self.table.create_partition(self.odps_partition,if_not_exists=True)
def close_spider(self, spider):
self.write_to_odps()
'''
將資料寫入odps
'''
def write_to_odps(self):
if(len(self.records) is None or len(self.records) == 0):
return
if(self.odps_partition is None or self.odps_partition == ""):
with self.table.open_writer() as writer:
writer.write(self.records)
logger.info("write to odps {0} records. ".format(len(self.records)))
self.records = []
else:
with self.table.open_writer(partition=self.odps_partition) as writer:
writer.write(self.records)
logger.info("write to odps {0} records. ".format(len(self.records)))
self.records = []
def isPartition(self,name):
for pt in self.table.schema.partitions:
if(pt.name == name):
return True
return False
def process_item(self, item, spider):
cols = []
for col in self.table.schema.columns:
if(self.isPartition(col.name)):
continue
c = None
for key in item.keys():
if(col.name == key):
c = item[key]
break
if(c is None):
raise Exception("{0} column not found in item.".format(col.name))
cols.append(c)
self.records.append(self.table.new_record(cols))
#logger.info("records={0} : buffer={1}".format(len(self.records),self.buffer))
if( len(self.records) >= int(self.buffer)):
self.write_to_odps()
return item
註冊Pipeline到hr_scrapy_demo/setting.py,修改ITEM_PIPELINES的值為:
# Configure item pipelines
# See http://scrapy.readthedocs.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
'hr_scrapy_demo.pipelines.OdpsPipeline': 300,
}
#300代表Pipeline的優先順序,可以同時存在多個pipeline,依據該數值從小到大依次執行pipeline
五、設定ODPS基本資料
hr_scrapy_demo/setting.py中,新增參數如下:
ODPS_PROJECT = 'your odps project name'
ODPS_ACCESSID = 'accessid'
ODPS_ACCESSKEY = 'accesskey'
ODPS_ENDPOINT = 'http://service.odps.aliyun.com/api'
#注:如果爬蟲執行在ECS上,可將ODPS_ENDPOINT修改為內網位址:
#ODPS_ENDPOINT = 'http:// odps-ext.aliyun-inc.com/api'
六、建立自己的Spiders
Spider主要用於採集網站資料,並解析網站資料轉換為相應的items,再交由Pipelines進行處理。針對每個需要採集的網站,我們都需要單獨建立對應的Spider。
以下是一個Spider樣本,以採集南方新聞網的要聞資訊為依據。
# -*- coding:utf-8 -*-
import scrapy
import logging
logger = logging.getLogger('NanfangSpider')
class NanfangSpider(scrapy.Spider):
name = "nanfang"
'''
設定你要採集的其實網址,可以是多個.
此處以南方新聞網-要聞-首頁為例.
'''
start_urls = [
'http://www.southcn.com/pc2016/yw/node_346416.htm'
]
'''
[ODPS設定資訊]
ODPS_TABLE:ODPS表名
ODPS_PARTITION:ODPS表的分區值(可選)
WRITE_BUFFER:寫入快取(預設1000條)
'''
custom_settings = {
'ODPS_TABLE':'hr_scrapy_nanfang_news',
#'ODPS_PARTITION':'pt=20170209',
'WRITE_BUFFER':'1000'
}
'''
ODPS Demo DDL:
drop table if exists hr_scrapy_nanfang_news;
create table hr_scrapy_nanfang_news
(
title string,
source string,
times string,
url string,
editor string,
content string
);
'''
'''
對start_urls的url的解析方法,返回結果為item.
關於具體解析API可參考:https://doc.scrapy.org/en/latest/intro/tutorial.html
'''
def parse(self, response):
#尋找網頁中DIV元素,且其class=j-link,並對其進行遍歷
for quote in response.css("div.j-link"):
#尋找該DIV中的所有<a>超連結,並追蹤其href
href = quote.css("a::attr('href')").extract_first()
#進入該href連結,此處跳躍到方法:parse_details,對其返回HTML進行再次處理。
yield scrapy.Request(response.urljoin(href),callback=self.parse_details)
#尋找下一頁的串連,此處用xpath方式追蹤,因css文法簡單,無法追蹤
nexthref = response.xpath(u'//div[@id="displaypagenum"]//center/a[last()][text()="u4e0bu4e00u9875"]/@href').extract_first()
#如找到下一頁,則跳躍到下一頁,並繼續由parse對返回HTML進行處理。
if(nexthref is not None):
yield scrapy.Request(response.urljoin(nexthref),callback=self.parse)
'''
新聞詳情頁處理方法
'''
def parse_details(self, response):
#找到本文
main_div = response.css("div.main")
#因新聞詳情也可能有分頁,追蹤下一頁的連結
next_href = main_div.xpath(u'//div[@id="displaypagenum"]/center/a[last()][text()="u4e0bu4e00u9875"]/@href').extract_first()
#追蹤本文內容,僅取DIV內所有<p>元素下的文字。
content = main_div.xpath('//div[@class="content"]//p//text()').extract()
content = "
".join(content)
if(next_href is None):
#最後一頁,則追蹤所有內容,返回item
title = main_div.css('div.m-article h2::text').extract_first()
source = main_div.css('div.meta span[id="pubtime_baidu"]::text').extract_first()
times = main_div.css('div.meta span[id="source_baidu"]::text').extract_first()
url = response.url
editor = main_div.css('div.m-editor::text').extract_first()
item = {}
if('item' in response.meta):
item = response.meta['item']
item['title'] = title
item['source'] = source
item['times'] = times
item['url'] = url
item['editor'] = editor
if('content' in item):
item['content'] += '
'+content
else:
item['content'] = content
yield item
else:
#非最後一頁,則取出本期頁content,並拼接,然後跳躍到下一頁
request = scrapy.Request(response.urljoin(next_href),
callback=self.parse_details)
item = {}
if('item' in response.meta and 'content' in response.meta['item']):
item = response.meta['item']
item['content'] += '
'+content
else:
item['content'] = content
request.meta['item'] = item
yield request
七、執行Scrapy
切換到你的工程目錄下,執行以下指令:
Scrapy crawl nanfang –loglevel INFO
執行結果如下圖所示:
八、驗證爬取結果
待資料擷取完成之後,登陸DATA IDE查看採集內容:
本文示範僅為一個簡單的案例,實際生產還需考慮多執行緒,網站校驗,分散式爬取等。
相關產品:
- 巨量資料計算服務(MaxCompute)
- 雲端服務器ECS