《Learning Scrapy》(中文版)第9章 使用Pipelines


序言
第1章 Scrapy介紹
第2章 理解HTML和XPath
第3章 爬蟲基礎(chǔ)
第4章 從Scrapy到移動應(yīng)用
第5章 快速構(gòu)建爬蟲
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy編程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和實(shí)時(shí)分析


在上一章,我們學(xué)習(xí)了如何辨析Scrapy中間件。在本章中,我們通過實(shí)例學(xué)習(xí)編寫pipelines,包括使用REST APIs、連接數(shù)據(jù)庫、處理CPU密集型任務(wù)、與老技術(shù)結(jié)合。

我們在本章中會使用集中新的數(shù)據(jù)庫,列在下圖的右邊:

Vagrant已經(jīng)配置好了數(shù)據(jù)庫,我們可以從開發(fā)機(jī)向其發(fā)送ping,例如ping es或ping mysql。讓我們先來學(xué)習(xí)REST APIs。

使用REST APIs

REST是用來一套創(chuàng)建網(wǎng)絡(luò)服務(wù)的技術(shù)集合。它的主要優(yōu)點(diǎn)是,比起SOAP和專有web服務(wù),REST更簡單和輕量。軟件開發(fā)者注意到了web服務(wù)的CRUD(Create、Read、Update、Delete)和HTTP操作(GET、POST、PUT、DELETE)的相似性。它們還注意到傳統(tǒng)web服務(wù)調(diào)用需要的信息可以再URL源進(jìn)行壓縮。例如,http://api.mysite.com/customer/john是一個(gè)URL源,它可以讓我們分辨目標(biāo)服務(wù)器,,更具體的,名字是john的服務(wù)器(行的主鍵)。它與其它技術(shù)結(jié)合時(shí),比如安全認(rèn)證、無狀態(tài)服務(wù)、緩存、輸出XML或JSON時(shí),可以提供一個(gè)強(qiáng)大但簡單的跨平臺服務(wù)。REST席卷軟件行業(yè)并不奇怪。

Scrapy pipeline的功能可以用REST API來做。接下來,我們來學(xué)習(xí)它。

使用treq

treq是一個(gè)Python包,它在Twisted應(yīng)用中和Python的requests包相似。它可以讓我們做出GET、POST、和其它HTTP請求。可以使用pip install treq安裝,開發(fā)機(jī)中已經(jīng)安裝好了。

比起Scrapy的Request/crawler.engine.download() API,我們使用treq,因?yàn)楹笳呔哂行阅軆?yōu)勢,詳見第10章。

一個(gè)寫入Elasticsearch的pipeline

我們從一個(gè)向ES服務(wù)器(Elasticsearch)寫入Items的爬蟲開始。你可能覺得從ES開始,而不是MySQL,有點(diǎn)奇怪,但實(shí)際上ES是最容易的。ES可以是無模式的,意味著我們可以不用配置就使用它。treq也足以應(yīng)付需要。如果想使用更高級的ES功能,我們應(yīng)該使用txes2和其它Python/Twisted ES包。

有了Vagrant,我們已經(jīng)有個(gè)一個(gè)運(yùn)行的ES服務(wù)器。登錄開發(fā)機(jī),驗(yàn)證ES是否運(yùn)行:

$ curl http://es:9200
{
  "name" : "Living Brain",
  "cluster_name" : "elasticsearch",
  "version" : { ... },
  "tagline" : "You Know, for Search"
}

在瀏覽器中登錄http://localhost:9200也可以看到相同的結(jié)果。如果訪問http://localhost:9200/properties/property/_search,我們可以看到一個(gè)響應(yīng),說ES已經(jīng)進(jìn)行了全局嘗試,但是沒有找到索引頁。

筆記:在本章中,我們會在項(xiàng)集合中插入新的項(xiàng),如果你想恢復(fù)原始狀態(tài)的話,可以用下面的命令:

$ curl -XDELETE http://es:9200/properties

本章中的pipeline完整代碼還有錯(cuò)誤處理的功能,但我盡量讓這里的代碼簡短,以突出重點(diǎn)。

提示:本章位于目錄ch09,這個(gè)例子位于ch09/properties/properties/pipelines/es.py。

本質(zhì)上,這個(gè)爬蟲只有四行:

@defer.inlineCallbacks
def process_item(self, item, spider):
    data = json.dumps(dict(item), ensure_ascii=False).encode("utf- 8")
    yield treq.post(self.es_url, data)

前兩行定義了一個(gè)標(biāo)準(zhǔn)process_item()方法,它可以產(chǎn)生延遲項(xiàng)。(參考第8章)

第三行準(zhǔn)備了插入的data。ensure_ascii=False可使結(jié)果壓縮,并且沒有跳過非ASCII字符。我們?nèi)缓髮SON字符串轉(zhuǎn)化為JSON標(biāo)準(zhǔn)的默認(rèn)編碼UTF-8。

最后一行使用了treq的post()方法,模擬一個(gè)POST請求,將我們的文檔插入ElasticSearch。es_url,例如http://es:9200/properties/property存在settings.py文件中(ES_PIPELINE_URL設(shè)置),它提供重要的信息,例如我們想要寫入的ES的IP和端口(es:9200)、集合名(properties)和對象類型(property)。

為了是pipeline生效,我們要在settings.py中設(shè)置ITEM_PIPELINES,并啟動ES_PIPELINE_URL設(shè)置:

ITEM_PIPELINES = {
    'properties.pipelines.tidyup.TidyUp': 100,
    'properties.pipelines.es.EsWriter': 800,
}
ES_PIPELINE_URL = 'http://es:9200/properties/property'

這么做完之后,我們前往相應(yīng)的目錄:

$ pwd
/root/book/ch09/properties
$ ls
properties  scrapy.cfg

然后運(yùn)行爬蟲:

$ scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=90
...
INFO: Enabled item pipelines: EsWriter...
INFO: Closing spider (closespider_itemcount)...
   'item_scraped_count': 106,

如果現(xiàn)在訪問http://localhost:9200/properties/property/_search,除了前10條結(jié)果,我們可以在響應(yīng)的hits/total字段看到插入的文件數(shù)。我們還可以添加參數(shù)?size=100以看到更多的結(jié)果。通過添加q= URL搜索中的參數(shù),我們可以在全域或特定字段搜索關(guān)鍵詞。相關(guān)性最強(qiáng)的結(jié)果會首先顯示出來。例如,http://localhost:9200/properties/property/_search?q=title:london,可以讓標(biāo)題變?yōu)長ondon。對于更復(fù)雜的查詢,可以在https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html查詢ES文檔。

ES不需要配置,因?yàn)樗鶕?jù)提供的第一個(gè)文件,進(jìn)行模式(字段類型)自動檢測的。通過訪問http://localhost:9200/properties/,我們可以看到它自動檢測的映射。

再次運(yùn)行crawl easy -s CLOSESPIDER_ITEMCOUNT=1000。因?yàn)閜ipelines的平均時(shí)間從0.12變?yōu)?.15秒,平均延遲從0.78變?yōu)?.81秒。吞吐量仍保持每秒約25項(xiàng)。

筆記:用pipelines向數(shù)據(jù)庫插入Items是個(gè)好方法嗎?答案是否定的。通常來講,數(shù)據(jù)庫更簡單的方法以大量插入數(shù)據(jù),我們應(yīng)該使用這些方法大量批次插入數(shù)據(jù),或抓取完畢之后進(jìn)行后處理。我們會在最后一章看到這些方法。然后,還是有很多人使用pipelines向數(shù)據(jù)庫插入文件,相應(yīng)的就要使用Twisted APIs。

pipeline使用Google Geocoding API進(jìn)行地理編碼

我們的房子有各自所在的區(qū)域,我們還想對它們進(jìn)行地理編碼,即找到相應(yīng)的坐標(biāo)(經(jīng)度、緯度)。我們可以將坐標(biāo)顯示在地圖上,或計(jì)算距離。建這樣的數(shù)據(jù)庫需要復(fù)雜的數(shù)據(jù)庫、復(fù)雜的文本匹配,還有復(fù)雜的空間計(jì)算。使用Google Geocoding API,我們可以避免這些。在瀏覽器中打開它,或使用curl取回以下URL的數(shù)據(jù):

$ curl "https://maps.googleapis.com/maps/api/geocode/json?sensor=false&ad
dress=london"
{
   "results" : [
         ...
         "formatted_address" : "London, UK",
         "geometry" : {
            ...
            "location" : {
               "lat" : 51.5073509,
               "lng" : -0.1277583
          },
            "location_type" : "APPROXIMATE",
            ...
   ],
   "status" : "OK"
}

我們看到一個(gè)JSON對象,如果搜索一個(gè)location,我們可以快速獲取倫敦中心的坐標(biāo)。如果繼續(xù)搜索,我們可以看到相同文件中海油其它地點(diǎn)。第一個(gè)是相關(guān)度最高的。因此如果存在results[0].geometry.location的話,它就是我們要的結(jié)果。

可以用前面的方法(treq)使用Google Geocoding API。只需要幾行,我們就可以找到一個(gè)地址的坐標(biāo)(目錄pipelines中的geo.py),如下所示:

@defer.inlineCallbacks
def geocode(self, address):
   endpoint = 'http://web:9312/maps/api/geocode/json'
   parms = [('address', address), ('sensor', 'false')]
   response = yield treq.get(endpoint, params=parms)
   content = yield response.json()
   geo = content['results'][0]["geometry"]["location"]
   defer.returnValue({"lat": geo["lat"], "lon": geo["lng"]})

這個(gè)函數(shù)做出了一條URL,但我們讓它指向一個(gè)可以離線快速運(yùn)行的假程序。你可以使用endpoint = 'https://maps.googleapis.com/maps/api/geocode/json'連接Google服務(wù)器,但要記住它對請求的限制很嚴(yán)格。address和sensor的值是URL自動編碼的,使用treq的方法get()的參數(shù)params。對于第二個(gè)yield,即response.json(),我們必須等待響應(yīng)主題完全加載完畢對解析為Python對象。此時(shí),我們就可以找到第一個(gè)結(jié)果的地理信息,格式設(shè)為dict,使用defer.returnValue()返回,它使用了inlineCallbacks。如果發(fā)生錯(cuò)誤,這個(gè)方法會扔出例外,Scrapy會向我們報(bào)告。

通過使用geocode(),process_item()變成了一行語句:

item["location"] = yield self.geocode(item["address"][0])

設(shè)置讓pipeline生效,將它添加到ITEM_PIPELINES,并設(shè)定優(yōu)先數(shù)值,該數(shù)值要小于ES的,以讓ES獲取坐標(biāo)值:

ITEM_PIPELINES = {
    ...
    'properties.pipelines.geo.GeoPipeline': 400,

開啟數(shù)據(jù)調(diào)試,然后運(yùn)行:

$ scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=90 -L DEBUG
...
{'address': [u'Greenwich, London'],
...
 'image_URL': [u'http://web:9312/images/i06.jpg'],
 'location': {'lat': 51.482577, 'lon': -0.007659},
 'price': [1030.0],
...

我們現(xiàn)在可以看到Items里的location字段。如果使用真正的Google API的URL運(yùn)行,會得到例外:

File "pipelines/geo.py" in geocode (content['status'], address))
Exception: Unexpected status="OVER_QUERY_LIMIT" for  
address="*London"

這是為了檢查我們在完整代碼中插入了地點(diǎn),以確保Geocoding API響應(yīng)的status字段有OK值。除非是OK,否則我們?nèi)』氐臄?shù)據(jù)不會有設(shè)定好的格式,進(jìn)而不能使用。對于這種情況,我們會得到OVER_QUERY_LIMIT狀態(tài),它指明我們在某處做錯(cuò)了。這個(gè)問題很重要,也很常見。應(yīng)用Scrapy的高性能引擎,進(jìn)行緩存、限制請求就很必要了。

我們可以在Geocoder API的文檔,查看它的限制,“每24小時(shí),免費(fèi)用戶可以進(jìn)行2500次請求,每秒5次請求”。即使我們使用付費(fèi)版本,仍有每秒10次請求的限制,所以這里的分析是有意義的。

筆記:后面的代碼看起來可能有些復(fù)雜,復(fù)雜度還要取決于實(shí)際情況。在多線程環(huán)境中創(chuàng)建這樣的組件,需要線程池和同步,這樣代碼就會變復(fù)雜。

這是一個(gè)簡易的運(yùn)用Twisted技術(shù)的限制引擎:

class Throttler(object):
    def __init__(self, rate):
        self.queue = []
        self.looping_call = task.LoopingCall(self._allow_one)
        self.looping_call.start(1. / float(rate))
    def stop(self):
        self.looping_call.stop()
    def throttle(self):
        d = defer.Deferred()
        self.queue.append(d)
        return d
    def _allow_one(self):
        if self.queue:
            self.queue.pop(0).callback(None)

這可以讓延遲項(xiàng)在一個(gè)列表中排隊(duì),逐個(gè)觸發(fā),調(diào)用_allow_one();_allow_one()檢查隊(duì)列是否為空,如果不是,它會調(diào)用第一個(gè)延遲項(xiàng)的callback()。我們使用Twisted的task.LoopingCall() API,周期性調(diào)用_allow_one()。使用Throttler很容易。我們在pipeline的init初始化它,當(dāng)爬蟲停止時(shí)清空它:

class GeoPipeline(object):
    def __init__(self, stats):
        self.throttler = Throttler(5)  # 5 Requests per second
    def close_spider(self, spider):
        self.throttler.stop()

在使用限定源之前,我們的例子是在process_item()中調(diào)用geocode(),必須yield限制器的throttle()方法:

yield self.throttler.throttle()
item["location"] = yield self.geocode(item["address"][0])

對于第一個(gè)yield,代碼會暫停一下,一段時(shí)間之后,會繼續(xù)運(yùn)行。例如,當(dāng)某時(shí)有11個(gè)延遲項(xiàng)時(shí),限制是每秒5次請求,即時(shí)間為11/5=2.2秒之后,隊(duì)列變空,代碼會繼續(xù)。

使用Throttler,不再有錯(cuò)誤,但是爬蟲會變慢。我們看到示例中的房子只有幾個(gè)不同的地址。這時(shí)使用緩存非常好。我們使用一個(gè)簡單的Python dict來做,但這么可能會有競爭條件,這樣會造成偽造的API請求。下面是一個(gè)沒有此類問題的緩存方法,展示了Python和Twisted的特點(diǎn):

class DeferredCache(object):
    def __init__(self, key_not_found_callback):
        self.records = {}
        self.deferreds_waiting = {}
        self.key_not_found_callback = key_not_found_callback
    @defer.inlineCallbacks
    def find(self, key):
        rv = defer.Deferred()
        if key in self.deferreds_waiting:
            self.deferreds_waiting[key].append(rv)
        else:
            self.deferreds_waiting[key] = [rv]
            if not key in self.records:
                try:
                    value = yield self.key_not_found_callback(key)
                    self.records[key] = lambda d: d.callback(value)
                except Exception as e:
                    self.records[key] = lambda d: d.errback(e)
            action = self.records[key]
            for d in self.deferreds_waiting.pop(key):
                reactor.callFromThread(action, d)
        value = yield rv
        defer.returnValue(value)

這個(gè)緩存看起來有些不同,它包含兩個(gè)組件:

  • self.deferreds_waiting:這是一個(gè)延遲項(xiàng)的隊(duì)列,等待給鍵賦值
  • self.records:這是鍵值對中出現(xiàn)過的dict

在find()方法的中間,如果沒有在self.records找到一個(gè)鍵,我們會調(diào)用預(yù)先定義的callback函數(shù),以取回丟失的值(yield self.key_not_found_callback(key))。這個(gè)調(diào)回函數(shù)可能會扔出一個(gè)例外。如何在Python中壓縮存儲值或例外呢?因?yàn)镻ython是一種函數(shù)語言,根據(jù)是否有例外,我們在self.records中保存小函數(shù)(lambdas),調(diào)用callback或errback。lambda函數(shù)定義時(shí),就將值或例外附著在上面。將變量附著在函數(shù)上稱為閉包,閉包是函數(shù)語言最重要的特性之一。

筆記:緩存例外有點(diǎn)不常見,但它意味著首次查找key時(shí),key_not_found_callback(key)返回了一個(gè)例外。當(dāng)后續(xù)查找還找這個(gè)key時(shí),就免去了調(diào)用,再次返回這個(gè)例外。

find()方法其余的部分提供了一個(gè)避免競爭條件的機(jī)制。如果查找某個(gè)鍵已經(jīng)在進(jìn)程中,會在self.deferreds_waiting dict中有記錄。這時(shí),我們不在向key_not_found_callback()發(fā)起另一個(gè)調(diào)用,只是在延遲項(xiàng)的等待列表添加這個(gè)項(xiàng)。當(dāng)key_not_found_callback()返回時(shí),鍵有了值,我們觸發(fā)所有的等待這個(gè)鍵的延遲項(xiàng)。我們可以直接發(fā)起action(d),而不用reactor.callFromThread(),但需要處理每個(gè)扔給下游的例外,我們必須創(chuàng)建不必要的很長的延遲項(xiàng)鏈。

使用這個(gè)緩存很容易。我們在init()對其初始化,設(shè)定調(diào)回函數(shù)為API調(diào)用。在process_item()中,使用緩存查找的方法如下:

def __init__(self, stats):
    self.cache = DeferredCache(self.cache_key_not_found_callback)
@defer.inlineCallbacks
def cache_key_not_found_callback(self, address):
    yield self.throttler.enqueue()
    value = yield self.geocode(address)
    defer.returnValue(value)
@defer.inlineCallbacks
def process_item(self, item, spider):
    item["location"] = yield self.cache.find(item["address"][0])
    defer.returnValue(item)

提示:完整代碼位于ch09/properties/properties/pipelines/geo2.py。

為了使pipeline生效,我們使前一個(gè)方法無效,并添加當(dāng)前的到settings.py的ITEM_PIPELINES:

ITEM_PIPELINES = {
    'properties.pipelines.tidyup.TidyUp': 100,
    'properties.pipelines.es.EsWriter': 800,
    # DISABLE 'properties.pipelines.geo.GeoPipeline': 400,
    'properties.pipelines.geo2.GeoPipeline': 400,
}

運(yùn)行爬蟲,用如下代碼:

$ scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=1000
...
Scraped... 15.8 items/s, avg latency: 1.74 s and avg time in pipelines: 
0.94 s
Scraped... 32.2 items/s, avg latency: 1.76 s and avg time in pipelines: 
0.97 s
Scraped... 25.6 items/s, avg latency: 0.76 s and avg time in pipelines: 
0.14 s
...
: Dumping Scrapy stats:...
   'geo_pipeline/misses': 35,
   'item_scraped_count': 1019,

當(dāng)填充緩存時(shí),我們看到抓取的延遲變高。緩存結(jié)束時(shí),延遲降低。數(shù)據(jù)還顯示有35個(gè)遺漏,正好是數(shù)據(jù)集中不同地點(diǎn)的數(shù)目。很明顯,上例中一共有1019 - 35= 984次API請求。如果我們使用真正的Google API,并提高每秒的API請求數(shù),例如通過改變Throttler(5)到Throttler(10),使從5提高到10,我們可以將重試添加到geo_pipeline/retries stat記錄中。如果有錯(cuò)誤的話,例如,使用API找不到某個(gè)地點(diǎn),會扔出一個(gè)例外,這會被geo_pipeline/errors stat記錄。如果地點(diǎn)通過什么方式已經(jīng)存在了,會在geo_pipeline/already_set stat中指明。最后,如果我們訪問http://localhost:9200/properties/property/_search,以檢查ES中的房子,我們可以看到包括地點(diǎn)的記錄,例如{..."location": {"lat": 51.5269736, "lon": -0.0667204}...}。(運(yùn)行前確保清空集合,去除舊的值)

在Elasticsearch進(jìn)行地理索引

我們已經(jīng)有了地點(diǎn),我們可以將它們按距離排序。下面是一個(gè)HTTP POST請求,返回標(biāo)題中包含Angel的房子,按照離點(diǎn){51.54, -0.19}的距離進(jìn)行排序:

$ curl http://es:9200/properties/property/_search -d '{
    "query" : {"term" : { "title" : "angel" } },
    "sort": [{"_geo_distance": {
        "location":      {"lat":  51.54, "lon": -0.19},
        "order":         "asc",
        "unit":          "km", 
        "distance_type": "plane" 
}}]}'

唯一的問題是如果我們運(yùn)行它,我們會看到一個(gè)錯(cuò)誤信息"failed to find mapper for [location] for geo distance based sort"。它指出,我們的location字段沒有正確的空間計(jì)算的格式。為了設(shè)定正確的格式,我們要手動覆蓋默認(rèn)格式。首先,我們將自動檢測的映射保存起來,將它作為起點(diǎn):

$ curl 'http://es:9200/properties/_mapping/property' > property.txt

然后,我們?nèi)缦滤揪庉媝roperty.txt:

"location":{"properties":{"lat":{"type":"double"},"lon":{"type":"double"}}}

我們將這行代碼替換為:

"location": {"type": "geo_point"}

我們還在文件最后刪除了{(lán)"properties":{"mappings": and two }}。文件現(xiàn)在就處理完了。我們現(xiàn)在可以刪除舊的類型,并用下面的schema建立新的類型:

$ curl -XDELETE 'http://es:9200/properties'
$ curl -XPUT 'http://es:9200/properties'
$ curl -XPUT 'http://es:9200/properties/_mapping/property' --data  
@property.txt

我們現(xiàn)在可以用之前的命令,進(jìn)行一個(gè)快速抓取,將結(jié)果按距離排序。我們的搜索返回的是房子的JSONs對象,其中包括一個(gè)額外的sort字段,顯示房子離某個(gè)點(diǎn)的距離。

連接數(shù)據(jù)庫與Python客戶端

可以連接Python Database API 2.0的數(shù)據(jù)庫有許多種,包括MySQL、PostgreSQL、Oracle、Microsoft、SQL Server和SQLite。它們的驅(qū)動通常很復(fù)雜且進(jìn)行過測試,為Twisted再進(jìn)行適配會浪費(fèi)很多時(shí)間。可以在Twisted應(yīng)用中使用數(shù)據(jù)庫客戶端,例如,Scrapy可以使用twisted.enterprise.adbapi庫。我們使用MySQL作為例子,說明用法,原則也適用于其他數(shù)據(jù)庫。

用pipeline寫入MySQL

MySQL是一個(gè)好用又流行的數(shù)據(jù)庫。我們來寫一個(gè)pipeline,來向其中寫入文件。我們的虛擬環(huán)境中,已經(jīng)有了一個(gè)MySQL實(shí)例。我們用MySQL命令行來做一些基本的管理操作,命令行工具已經(jīng)在開發(fā)機(jī)中預(yù)先安裝了:

$ mysql -h mysql -uroot -ppass

mysql>提示MySQL已經(jīng)運(yùn)行,我們可以建立一個(gè)簡單的含有幾個(gè)字段的數(shù)據(jù)表,如下所示:

mysql> create database properties;
mysql> use properties
mysql> CREATE TABLE properties (
  url varchar(100) NOT NULL,
  title varchar(30),
  price DOUBLE,
  description varchar(30),
  PRIMARY KEY (url)
);
mysql> SELECT * FROM properties LIMIT 10;
Empty set (0.00 sec)

很好,現(xiàn)在已經(jīng)建好了一個(gè)包含幾個(gè)字段的MySQL數(shù)據(jù)表,它的名字是properties,可以開始寫pipeline了。保持MySQL控制臺打開,我們過一會兒會返回查看是否有差入值。輸入exit,就可以退出。

筆記:在這一部分中,我們會向MySQL數(shù)據(jù)庫插入properties。如果你想刪除,使用以下命令:

mysql> DELETE FROM properties;

我們使用MySQL的Python客戶端。我們還要安裝一個(gè)叫做dj-database-url的小功能模塊(它可以幫我們設(shè)置不同的IP、端口、密碼等等)。我們可以用pip install dj-database-url MySQL-python,安裝這兩項(xiàng)。我們的開發(fā)機(jī)上已經(jīng)安裝好了。我們的MySQL pipeline很簡單,如下所示:

from twisted.enterprise import adbapi
...
class MysqlWriter(object):
    ...
    def __init__(self, mysql_url):
        conn_kwargs = MysqlWriter.parse_mysql_url(mysql_url)
        self.dbpool = adbapi.ConnectionPool('MySQLdb',
                                            charset='utf8',
                                            use_unicode=True,
                                            connect_timeout=5,
                                            **conn_kwargs)
    def close_spider(self, spider):
        self.dbpool.close()
    @defer.inlineCallbacks
    def process_item(self, item, spider):
        try:
            yield self.dbpool.runInteraction(self.do_replace, item)
        except:
            print traceback.format_exc()
        defer.returnValue(item)
    @staticmethod
    def do_replace(tx, item):
        sql = """REPLACE INTO properties (url, title, price, description) VALUES (%s,%s,%s,%s)"""
        args = (
            item["url"][0][:100],
            item["title"][0][:30],
            item["price"][0],
            item["description"][0].replace("\r\n", " ")[:30]
        )
        tx.execute(sql, args)

提示:完整代碼位于ch09/properties/properties/pipelines/mysql.py。

本質(zhì)上,這段代碼的大部分都很普通。為了簡潔而省略的代碼將一條保存在MYSQL_PIPELINE_URL、格式是mysql://user:pass@ip/database的URL,解析成了獨(dú)立的參數(shù)。在爬蟲的init()中,將它們傳遞到adbapi.ConnectionPool(),它使用adbapi的底層結(jié)構(gòu),初始化MySQL連接池。第一個(gè)參數(shù)是我們想要引入的模塊的名字。對于我們的MySQL,它是MySQLdb。我們?yōu)镸ySQL客戶端另設(shè)了幾個(gè)參數(shù),以便正確處理Unicode和超時(shí)。每當(dāng)adbapi打開新連接時(shí),所有這些參數(shù)都要進(jìn)入底層的MySQLdb.connect()函數(shù)。爬蟲關(guān)閉時(shí),我們調(diào)用連接池的close()方法。

我們的process_item()方法包裝了dbpool.runInteraction()。這個(gè)方法給調(diào)回方法排隊(duì),會在當(dāng)連接池中一個(gè)連接的Transaction對象變?yōu)榭捎脮r(shí)被調(diào)用。這個(gè)Transaction對象有一個(gè)和DB-API指針相似的API。在我們的例子中,調(diào)回方法是do_replace(),它定義在后面幾行。@staticmethod是說這個(gè)方法關(guān)聯(lián)的是類而不是具體的類實(shí)例,因此,我們可以忽略通常的self參數(shù)。如果方法不使用成員的話,最好設(shè)其為靜態(tài),如果你忘了設(shè)為靜態(tài)也不要緊。這個(gè)方法準(zhǔn)備了一個(gè)SQL字符串、幾個(gè)參數(shù),并調(diào)用Transaction的execute()函數(shù),以進(jìn)行插入。我們的SQL使用REPLACE INTO,而不用更常見的INSERT INTO,來替換鍵相同的項(xiàng)。這可以讓我們的案例簡化。如果我們相擁SQL返回?cái)?shù)據(jù),例如SELECT聲明,我們使用dbpool.runQuery(),我們可能還需要改變默認(rèn)指針,方法是設(shè)置adbapi.ConnectionPool()的參數(shù)cursorclass為cursorclass=MySQLdb.cursors,這樣取回?cái)?shù)據(jù)更為簡便。

使用這個(gè)pipeline,我們要在settings.py的ITEM_PIPELINES添加它,還要設(shè)置一下MYSQL_PIPELINE_URL:

ITEM_PIPELINES = { ...
    'properties.pipelines.mysql.MysqlWriter': 700,
...
MYSQL_PIPELINE_URL = 'mysql://root:pass@mysql/properties'

執(zhí)行以下命令:

scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=1000

運(yùn)行這條命令后,返回MySQL控制臺,可以看到如下記錄:

mysql> SELECT COUNT(*) FROM properties;
+----------+
|     1006 |
+----------+
mysql> SELECT * FROM properties LIMIT 4;
+------------------+--------------------------+--------+-----------+
| url              | title                    | price  | description
+------------------+--------------------------+--------+-----------+
| http://...0.html | Set Unique Family Well   | 334.39 | website c
| http://...1.html | Belsize Marylebone Shopp | 388.03 | features                       
| http://...2.html | Bathroom Fully Jubilee S | 365.85 | vibrant own
| http://...3.html | Residential Brentford Ot | 238.71 | go court
+------------------+--------------------------+--------+-----------+
4 rows in set (0.00 sec)

延遲和吞吐量的性能和之前相同。結(jié)果讓人印象深刻。

使用Twisted 特定客戶端連接服務(wù)

目前為止,我們學(xué)習(xí)了如何用treq使用類REST APIs。Scrapy可以用Twisted特定客戶端連接許多其它服務(wù)。例如,如果我們想連接MongoDB,通過搜索“MongoDB Python”,我們可以找到PyMongo,它是阻塞/同步的,除非我們使用pipeline處理阻塞操作中的線程,我們不能在Twisted中使用PyMongo。如果我們搜索“MongoDB Twisted Python”,可以找到txmongo,它可以完美適用于Twisted和Scrapy。通常的,Twisted客戶端群體很小,但使用它比起自己寫一個(gè)客戶端還是要方便。下面,我們就使用這樣一個(gè)Twisted特定客戶端連接Redis鍵值對存儲。

用pipeline讀寫Redis

Google Geocoding API是按照每個(gè)IP進(jìn)行限制的。如果可以接入多個(gè)IPs(例如,多臺服務(wù)器),當(dāng)一個(gè)地址已經(jīng)被另一臺機(jī)器做過地理編碼,就要設(shè)法避免對發(fā)出重復(fù)的請求。如果一個(gè)地址之前已經(jīng)被查閱過,也要避免再次查閱。我們不想浪費(fèi)限制的額度。

筆記:與API商家聯(lián)系,以確保這符合規(guī)定。你可能,必須每幾分鐘/小時(shí),就要清空緩存記錄,或者根本就不能緩存。

我們可以使用Redis鍵值緩存作為分布式dict。Vagrant環(huán)境中已經(jīng)有了一個(gè)Redis實(shí)例,我們現(xiàn)在可以連接它,用redis-cli作一些基本操作:

$ redis-cli -h redis
redis:6379> info keyspace
# Keyspace
redis:6379> set key value
OK
redis:6379> info keyspace
# Keyspace
db0:keys=1,expires=0,avg_ttl=0
redis:6379> FLUSHALL
OK
redis:6379> info keyspace
# Keyspace
redis:6379> exit

通過搜索“Redis Twisted”,我們找到一個(gè)txredisapi庫。它最大的不同是,它不僅是一個(gè)Python的同步封裝,還是一個(gè)Twisted庫,可以通過reactor.connectTCP(),執(zhí)行Twisted協(xié)議,連接Redis。其它庫也有類似用法,但是txredisapi對于Twisted效率更高。我們可以通過安裝庫dj_redis_url可以安裝它,這個(gè)庫通過pip可以解析Redis配置URL(sudo pip install txredisapi dj_redis_url)。和以前一樣,開發(fā)機(jī)中已經(jīng)安裝好了。

我們?nèi)缦聠覴edisCache pipeline:

from txredisapi import lazyConnectionPool
class RedisCache(object):
...
    def __init__(self, crawler, redis_url, redis_nm):
        self.redis_url = redis_url
        self.redis_nm = redis_nm
        args = RedisCache.parse_redis_url(redis_url)
        self.connection = lazyConnectionPool(connectTimeout=5,
                                             replyTimeout=5,
                                             **args)
        crawler.signals.connect(
                self.item_scraped,signal=signals.item_scraped)

這個(gè)pipeline比較簡單。為了連接Redis服務(wù)器,我們需要主機(jī)、端口等等,它們?nèi)加肬RL格式存儲。我們用parse_redis_url()方法解析這個(gè)格式。使用命名空間做鍵的前綴很普遍,在我們的例子中,我們存儲在redis_nm。我們?nèi)缓笫褂胻xredisapi的lazyConnectionPool()打開一個(gè)數(shù)據(jù)庫連接。

最后一行有一個(gè)有趣的函數(shù)。我們是想用pipeline封裝geo-pipeline。如果在Redis中沒有某個(gè)值,我們不會設(shè)定這個(gè)值,geo-pipeline會用API像之前一樣將地址進(jìn)行地理編碼。完畢之后,我們必須要在Redis中緩存鍵值對,我們是通過連接signals.item_scraped信號來做的。我們定義的調(diào)回(即item_scraped()方法,馬上會講)只有在最后才會被調(diào)用,那時(shí),地址就設(shè)置好了。

提示:完整代碼位于ch09/properties/properties/pipelines/redis.py。

我們簡化緩存,只尋找和存儲每個(gè)Item的地址和地點(diǎn)。這對Redis來說是合理的,因?yàn)樗ǔJ沁\(yùn)行在單一服務(wù)器上的,這可以讓它很快。如果不是這樣的話,可以加入一個(gè)dict結(jié)構(gòu)的緩存,它與我們在geo-pipeline中用到的相似。以下是我們?nèi)绾翁幚砣霂斓腎tems:

process incoming Items:
@defer.inlineCallbacks
def process_item(self, item, spider):
    address = item["address"][0]
    key = self.redis_nm + ":" + address
    value = yield self.connection.get(key)
    if value:
        item["location"] = json.loads(value)
    defer.returnValue(item)

和預(yù)期的相同。我們得到了地址,給它添加前綴,然后使用txredisapi connection的get()在Redis進(jìn)行查找。我們將JSON編碼的對象在Redis中保存成值。如果一個(gè)值設(shè)定了,我們就使用JSON解碼,然后將其設(shè)為地點(diǎn)。

當(dāng)一個(gè)Item到達(dá)pipelines的末端時(shí),我們重新取得它,將其保存為Redis中的地點(diǎn)值。以下是我們的做法:

    from txredisapi import ConnectionError
    def item_scraped(self, item, spider):
        try:
            location = item["location"]
            value = json.dumps(location, ensure_ascii=False)
        except KeyError:
            return
        address = item["address"][0]
        key = self.redis_nm + ":" + address
        quiet = lambda failure: failure.trap(ConnectionError)
        return self.connection.set(key, value).addErrback(quiet)

如果我們找到了一個(gè)地點(diǎn),我們就取得了地址,添加前綴,然后使用它作為txredisapi連接的set()方法的鍵值對。set()方法沒有使用@defer.inlineCallbacks,因?yàn)樘幚韘ignals.item_scraped時(shí),它不被支持。這意味著,我們不能對connection.set()使用yield,但是我們可以返回一個(gè)延遲項(xiàng),Scrapy可以在它后面排上其它信號對象。任何情況下,如果Redis的連接不能使用用connection.set(),它就會拋出一個(gè)例外。在這個(gè)錯(cuò)誤處理中,我們把傳遞的錯(cuò)誤當(dāng)做參數(shù),我們讓它trap()任何ConnectionError。這是Twisted的延遲API的優(yōu)點(diǎn)之一。通過用trap()捕獲錯(cuò)誤項(xiàng),我們可以輕易忽略它們。

使這個(gè)pipeline生效,我們要做的是在settings.py的ITEM_PIPELINES中添加它,并提供一個(gè)REDIS_PIPELINE_URL。必須要讓它的優(yōu)先級比geo-pipeline高,以免太晚就不能使用了:

ITEM_PIPELINES = { ...
    'properties.pipelines.redis.RedisCache': 300,
    'properties.pipelines.geo.GeoPipeline': 400,
...
REDIS_PIPELINE_URL = 'redis://redis:6379'

像之前一樣運(yùn)行。第一次運(yùn)行時(shí)和以前很像,但隨后的運(yùn)行結(jié)果如下:

$ scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=100
...
INFO: Enabled item pipelines: TidyUp, RedisCache, GeoPipeline, 
MysqlWriter, EsWriter
...
Scraped... 0.0 items/s, avg latency: 0.00 s, time in pipelines: 0.00 s
Scraped... 21.2 items/s, avg latency: 0.78 s, time in pipelines: 0.15 s
Scraped... 24.2 items/s, avg latency: 0.82 s, time in pipelines: 0.16 s
...
INFO: Dumping Scrapy stats: {...
   'geo_pipeline/already_set': 106,
   'item_scraped_count': 106,

我們看到GeoPipeline和RedisCache都生效了,RedisCache第一個(gè)輸出。還注意到在統(tǒng)計(jì)中g(shù)eo_pipeline/already_set: 106。這是GeoPipeline發(fā)現(xiàn)的Redis緩存中填充的數(shù)目,它不調(diào)用Google API。如果Redis緩存是空的,你會看到Google API處理了一些鍵。從性能上來講,我們看到GeoPipeline引發(fā)的初始行為消失了。事實(shí)上,當(dāng)我們開始使用內(nèi)存,我們繞過了每秒只有5次請求的API限制。如果我們使用Redis,應(yīng)該考慮使用過期鍵,讓系統(tǒng)周期刷新緩存數(shù)據(jù)。

連接CPU密集型、阻塞或舊方法

最后一部分講連接非Twisted的工作。盡管異步程序的優(yōu)點(diǎn)很多,并不是所有庫都專門為Twisted和Scrapy寫的。使用Twisted的線程池和reactor.spawnProcess()方法,我們可以使用任何Python庫和任何語言寫的編碼。

pipeline進(jìn)行CPU密集型和阻塞操作

我們在第8章中強(qiáng)調(diào),反應(yīng)器適合簡短非阻塞的任務(wù)。如果我們不得不要處理復(fù)雜和阻塞的任務(wù),又該怎么做呢?Twisted提供了線程池,有了它可以使用reactor.callInThread() API在分線程而不是主線程中執(zhí)行慢操作。這意味著,反應(yīng)器可以一直運(yùn)行并對事件反饋,而不中斷計(jì)算。但要記住,在線程池中運(yùn)行并不安全,當(dāng)你使用全局模式時(shí),會有多線程的同步問題。讓我們從一個(gè)簡單的pipeline開始,逐漸做出完整的代碼:

class UsingBlocking(object):
    @defer.inlineCallbacks
    def process_item(self, item, spider):
        price = item["price"][0]
        out = defer.Deferred()
        reactor.callInThread(self._do_calculation, price, out)
    item["price"][0] = yield out
        defer.returnValue(item)
    def _do_calculation(self, price, out):
        new_price = price + 1
        time.sleep(0.10)
        reactor.callFromThread(out.callback, new_price)

在前面的pipeline中,我們看到了一些基本用法。對于每個(gè)Item,我們提取出價(jià)格,我們相用_do_calucation()方法處理它。這個(gè)方法使用time.sleep(),一個(gè)阻塞操作。我們用reactor.callInThread()調(diào)用,讓它在另一個(gè)線程中運(yùn)行。顯然,我們傳遞價(jià)格,我們還創(chuàng)建和傳遞了一個(gè)名為out的延遲項(xiàng)。當(dāng)_do_calucation()完成了計(jì)算,我們使用out調(diào)回值。下一步,我們執(zhí)行延遲項(xiàng),并未價(jià)格設(shè)新的值,最后返回Item。

在_do_calucation()中,有一個(gè)細(xì)微之處,價(jià)格增加了1,進(jìn)而睡了100ms。這個(gè)時(shí)間很多,如果調(diào)用進(jìn)反應(yīng)器主線程,每秒就不能抓取10頁了。通過在另一個(gè)線程中運(yùn)行,就不會再有這個(gè)問題。任務(wù)會在線程池中排隊(duì),每次處理耗時(shí)100ms。最后一步是觸發(fā)調(diào)回。一般的,我們可以使用out.callback(new_price),但是因?yàn)槲覀儸F(xiàn)在是在另一個(gè)線程,這么做不安全。如果這么做的話,延遲項(xiàng)的代碼會被從另一個(gè)線程調(diào)用,這樣遲早會產(chǎn)生錯(cuò)誤的數(shù)據(jù)。不這樣做,轉(zhuǎn)而使用reactor.callFromThread(),它也可以將函數(shù)當(dāng)做參數(shù),將任意其余參數(shù)傳遞到函數(shù)。這個(gè)函數(shù)會排隊(duì)并被調(diào)回主線程,主進(jìn)程反過來會打開process_item()對象yield,并繼續(xù)Item的操作。

如果我們用全局模式,例如計(jì)數(shù)器、滑動平均,又該怎么使用_do_calucation()呢?例如,添加兩個(gè)變量,beta和delta,如下所示:

class UsingBlocking(object):
    def __init__(self):
        self.beta, self.delta = 0, 0
    ...
    def _do_calculation(self, price, out):
        self.beta += 1
        time.sleep(0.001)
self.delta += 1
        new_price = price + self.beta - self.delta + 1
        assert abs(new_price-price-1) < 0.01
        time.sleep(0.10)...

這段代碼是斷言失敗錯(cuò)誤。這是因?yàn)槿绻粋€(gè)線程在self.beta和self.delta間切換,另一個(gè)線程繼續(xù)計(jì)算使用beta/delta計(jì)算價(jià)格,它會發(fā)現(xiàn)它們狀態(tài)不一致(beta大于delta),因此,計(jì)算出錯(cuò)誤的結(jié)果。短暫的睡眠可能會造成競爭條件。為了不發(fā)生這些狀況,我們要使一個(gè)鎖,例如Python的threading.RLock()遞歸鎖。使用它,可以確保沒有兩個(gè)線程在同一時(shí)間操作被保護(hù)代碼:

class UsingBlocking(object):
    def __init__(self):
        ...
        self.lock = threading.RLock()
    ...
    def _do_calculation(self, price, out):
        with self.lock:
            self.beta += 1
            ...
            new_price = price + self.beta - self.delta + 1
        assert abs(new_price-price-1) < 0.01 ...

代碼現(xiàn)在就正確了。記住,我們不需要保護(hù)整段代碼,就足以處理全局模式。

提示:完整代碼位于ch09/properties/properties/pipelines/computation.py。

要使用這個(gè)pipeline,我們需要把它添加到settings.py的ITEM_PIPELINES中。如下所示:

ITEM_PIPELINES = { ...
    'properties.pipelines.computation.UsingBlocking': 500,

像之前一樣運(yùn)行爬蟲,pipeline延遲達(dá)到了100ms,但吞吐量沒有發(fā)生變化,大概每秒25個(gè)items。

pipeline使用二進(jìn)制和腳本

最麻煩的借口當(dāng)屬獨(dú)立可執(zhí)行文件和腳本。打開需要幾秒(例如,從數(shù)據(jù)庫加載數(shù)據(jù)),但是后面處理數(shù)值的延遲很小。即便是這種情況,Twisted也預(yù)料到了。我們可以使用reactor.spawnProcess() API和相關(guān)的protocol.ProcessProtocol來運(yùn)行任何執(zhí)行文件。讓我們來看一個(gè)例子。腳本如下:

#!/bin/bash
trap "" SIGINT
sleep 3
while read line
do
    # 4 per second
    sleep 0.25
    awk "BEGIN {print 1.20 * $line}"
done

這是一個(gè)簡單的bash腳本。它運(yùn)行時(shí),會使Ctrl + C 無效。這是為了避免系統(tǒng)的一個(gè)奇怪的錯(cuò)誤,將Ctrl + C增值到子流程并過早結(jié)束,導(dǎo)致Scrapy強(qiáng)制等待流程結(jié)果。在使Ctrl + C無效之后,它睡眠三秒,模擬啟動時(shí)間。然后,它閱讀輸入的代碼語句,等待250ms,然后返回結(jié)果價(jià)格,價(jià)格的值乘以了1.20,由Linux的awk命令計(jì)算而得。這段腳本的最大吞吐量為每秒1/250ms=4個(gè)Items。用一個(gè)短session檢測:

$ properties/pipelines/legacy.sh 
12 <- If you type this quickly you will wait ~3 seconds to get results
14.40
13 <- For further numbers you will notice just a slight delay
15.60

因?yàn)镃trl + C失效了,我們用Ctrl + D必須結(jié)束session。我們該如何讓Scrapy使用這個(gè)腳本呢?再一次,我們從一個(gè)簡化版開始:

class CommandSlot(protocol.ProcessProtocol):
    def __init__(self, args):
      self._queue = []
        reactor.spawnProcess(self, args[0], args)
    def legacy_calculate(self, price):
        d = defer.Deferred()
        self._queue.append(d)
        self.transport.write("%f\n" % price)
        return d
    # Overriding from protocol.ProcessProtocol
    def outReceived(self, data):
        """Called when new output is received"""
        self._queue.pop(0).callback(float(data))
class Pricing(object):
    def __init__(self):
        self.slot = CommandSlot(['properties/pipelines/legacy.sh'])
    @defer.inlineCallbacks
    def process_item(self, item, spider):
        item["price"][0] = yield self.slot.legacy_calculate(item["price"][0])
       defer.returnValue(item)

我們在這里找到了一個(gè)名為CommandSlot的ProcessProtocol和Pricing爬蟲。在init()中,我們創(chuàng)建了新的CommandSlot,它新建了一個(gè)空的隊(duì)列,并用reactor.spawnProcess()開啟了一個(gè)新進(jìn)程。它調(diào)用收發(fā)數(shù)據(jù)的ProcessProtocol作為第一個(gè)參數(shù)。在這個(gè)例子中,是self的原因是spawnProcess()是被從類protocol調(diào)用的。第二個(gè)參數(shù)是可執(zhí)行文件的名字,第三個(gè)參數(shù)args,讓二進(jìn)制命令行參數(shù)成為字符串序列。

在pipeline的process_item()中,我們用CommandSlot的legacy_calculate()方法代表所有工作,CommandSlot可以返回產(chǎn)生的延遲項(xiàng)。legacy_calculate()創(chuàng)建延遲項(xiàng),將其排隊(duì),用transport.write()將價(jià)格寫入進(jìn)程。ProcessProtocol提供了transport,可以讓我們與進(jìn)程溝通。無論何時(shí)我們從進(jìn)程收到數(shù)據(jù), outReceived()就會被調(diào)用。通過延遲項(xiàng),進(jìn)程依次執(zhí)行,我們可以彈出最老的延遲項(xiàng),用收到的值觸發(fā)它。全過程就是這樣。我們可以讓這個(gè)pipeline生效,通過將它添加到ITEM_PIPELINES:

ITEM_PIPELINES = {...
    'properties.pipelines.legacy.Pricing': 600,

如果運(yùn)行的話,我們會看到性能很差。進(jìn)程變成了瓶頸,限制了吞吐量。為了提高性能,我們需要修改pipeline,允許多個(gè)進(jìn)程并行運(yùn)行,如下所示:

class Pricing(object):
    def __init__(self):
        self.concurrency = 16
        args = ['properties/pipelines/legacy.sh']
        self.slots = [CommandSlot(args) 
                      for i in xrange(self.concurrency)]
        self.rr = 0
    @defer.inlineCallbacks
    def process_item(self, item, spider):
        slot = self.slots[self.rr]
        self.rr = (self.rr + 1) % self.concurrency
        item["price"][0] = yield
                         slot.legacy_calculate(item["price"][0])
        defer.returnValue(item)

這無非是開啟16個(gè)實(shí)例,將價(jià)格以輪轉(zhuǎn)的方式發(fā)出。這個(gè)pipeline的吞吐量是每秒16*4 = 64。我們可以用下面的爬蟲進(jìn)行驗(yàn)證:

 $ scrapy crawl easy -s CLOSESPIDER_ITEMCOUNT=1000
...
Scraped... 0.0 items/s, avg latency: 0.00 s and avg time in pipelines: 
0.00 s
Scraped... 21.0 items/s, avg latency: 2.20 s and avg time in pipelines: 
1.48 s
Scraped... 24.2 items/s, avg latency: 1.16 s and avg time in pipelines: 
0.52 s

延遲增加了250 ms,但吞吐量仍然是每秒25。
請記住前面的方法使用了transport.write()讓所有的價(jià)格在腳本shell中排隊(duì)。這個(gè)可能對你的應(yīng)用不適用,,尤其是當(dāng)數(shù)據(jù)量很大時(shí)。Git的完整代碼讓值和調(diào)回都進(jìn)行了排隊(duì),不想腳本發(fā)送值,除非收到前一項(xiàng)的結(jié)果。這種方法可能看起來更友好,但是會增加代碼復(fù)雜度。

總結(jié)

你剛剛學(xué)習(xí)了復(fù)雜的Scrapy pipelines。目前為止,你應(yīng)該就掌握了所有和Twisted編程相關(guān)的知識。并且你學(xué)會了如何在進(jìn)程中執(zhí)行復(fù)雜的功能,用Item Processing Pipelines存儲Items。我們看到了添加pipelines對延遲和吞吐量的影響。通常,延遲和吞吐量是成反比的。但是,這是在恒定并發(fā)數(shù)的前提下(例如,一定數(shù)量的線程)。在我們的例子中,我們一開始的并發(fā)數(shù)為N=ST=250.77?19,添加pipelines之后,并發(fā)數(shù)為N=25*3.33?83,并沒有引起性能的變化。這就是Twisted的強(qiáng)大之處!下面學(xué)習(xí)第10章,Scrapy的性能。


序言
第1章 Scrapy介紹
第2章 理解HTML和XPath
第3章 爬蟲基礎(chǔ)
第4章 從Scrapy到移動應(yīng)用
第5章 快速構(gòu)建爬蟲
第6章 Scrapinghub部署
第7章 配置和管理
第8章 Scrapy編程
第9章 使用Pipeline
第10章 理解Scrapy的性能
第11章(完) Scrapyd分布式抓取和實(shí)時(shí)分析


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,750評論 1 375
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內(nèi)容