python 爬蟲 學習筆記(一)Scrapy框架入門

沉迷於經過高效算法及經典數據結構來優化程序的時候並不理解,爲何多線程能夠優化爬蟲運行速度?原來是程序特性所決定的:傳統算法的程序複雜度主要來源於計算,但網絡程序的計算時間能夠忽略不計,網絡程序所面臨的挑戰打開不少很慢的連接,或者說,是如何有效的等待大量網絡事件。css

(1)簡單的socket爬蟲:

直接下載一個頁面html

import socket


def threaded_method():
    sock = socket.socket()
    sock.connect(('xkcd.com', 80))
    request = 'GET /353/ HTTP/1.0\r\nHost: xkcd.com\r\n\r\n'
    sock.send(request.encode('ascii'))
    response = b''
    chunk = sock.recv(4096)
    while chunk:
        response += chunk
        chunk = sock.recv(4096)
        #print(chunk)

    print(response)

threaded_method()

 每次經過recv向字節流讀取4096字節的數據。固然默認狀況下recv和connect都是阻塞的。這樣咱們每下載一個頁面就須要一個線程。線程開銷是昂貴的,極可能在頁面處理完以前就用光了線程。這是裸的套接字寫法,爲了接下來的編寫和學習舒服一點,果真仍是來用幾個python包吧。java

(2)scrapy框架

官方文檔:Scrapy框架簡單介紹node

這裏記錄下scrapy的基本操做python

建立一個scrapy項目:scrapy startproject tutorial

在你的工做文件夾下輸入此命令,tutorial文件夾就出現了。第一感受文件不多很清爽。web

定義item:vi items

編輯tutorial中的items文件。items是你保存爬取數據的容器。咱們須要根據你程序獲取的內容對item進行建模,好比咱們須要title,link,desc這三個字段算法

import scrapy

class DmozItem(scrapy.Item):
    title = scrapy.Field()
    link = scrapy.Field()
    desc = scrapy.Field()

 普通爬取

在tutorial/spiders目錄下建立dmoz_spider.py,好比代碼寫成這樣sql

import scrapy

class DmozSpider(scrapy.Spider):
    name = "dmoz"
    allowed_domains = ["dmoz.org"]
    start_urls = [
       "https://www.amazon.cn/%E6%96%87%E5%AD%A6%E5%9B%BE%E4%B9%A6/b/ref=sa_menu_books_l2_b658394051?ie=UTF8&node=658394051"
    ]

    def parse(self, response):
        filename = response.url.split("/")[-2]
        with open(filename, 'wb') as f:
            f.write(response.body)

 你必須繼承自scrapy.Spider,並定義以下三個屬性chrome

  • name: 用於區別Spider。 該名字必須是惟一的,您不能夠爲不一樣的Spider設定相同的名字。shell

  • start_urls: 包含了Spider在啓動時進行爬取的url列表。 所以,第一個被獲取到的頁面將是其中之一。 後續的URL則從初始的URL獲取到的數據中提取。

  • parse() 是spider的一個方法。 被調用時,每一個初始URL完成下載後生成的 Response 對象將會做爲惟一的參數傳遞給該函數。 該方法負責解析返回的數據(response data),提取數據(生成item)以及生成須要進一步處理的URL的 Request 對象

  進入項目根目錄執行命令啓動爬蟲:scripy crawl dmoz  

  你也能夠加個scripy crawl dmoz  --nolog關閉日誌信息,這樣輸出頁面就變得清爽了

(python是講究格式的,start_url列表裏格式裏有坑)

(3)爬蟲、網頁分析解析輔助工具 

Xpath-helper

谷歌瀏覽器插件:xpath-helper

安裝好以後,咱們從新打開瀏覽器,按ctrl+shift+x就能調出xpath-helper框了。

若是咱們要查找某一個、或者某一塊元素的xpath路徑,能夠按住shift,並移動到這一塊中,上面的框就會顯示這個元素的xpath路徑,右邊則會顯示解析出的文本內容,而且咱們能夠本身改動xpath路徑,程序也會自動的顯示對應的位置,能夠很方便的幫助咱們判斷咱們的xpath語句是否書寫正確。

啊忘記說了scrapy的選擇器是基於css+xpath的,可能在應用scrapy的時候大部分時間都用在xpath的構建上。而這個插件頗有用

雖然這個小插件使用很是方便,但它也不是萬能的,有兩個問題:

  1.XPath Helper 自動提取的 XPath 都是從根路徑開始的,這幾乎必然致使 XPath 過長,不利於維護;

  2.當提取循環的列表數據時,XPath Helper 是使用的下標來分別提取的列表中的每一條數據,這樣並不適合程序批量處理,仍是須要人爲修改一些相似於*標記等。

 Scrapy shell

scrapy shell 「https://www.amazon.cn/%E6%96%87%E5%AD%A6%E5%9B%BE%E4%B9%A6/b/ref=sa_menu_books_l2_b658394051?ie=UTF8&node=658394051」
以上命令執行後,會使用Scrapy downloader下載指定url的頁面數據,而且打印出可用的對象和函數列表

這對於想要練習xpath使用的人來講實在是棒得不行,不須要每次浪費時間爬一次才發現xpath沒寫對,你能夠充分的驗證你xpath的正確性而後再開啓爬蟲顯然省了不少時間

xpath編寫實例

到底怎麼編寫xpath纔好呢我也思索了半天。谷歌瀏覽器有一個copy xpath功能(在f12模式下選中元素右鍵能夠看到),多玩了幾回發現它老是優先找路徑上是否有Id選擇器,顯然copy xpath的製做者利用了一個html頁面的設計規則--id選擇器的屬性是惟一對應的,找指定元素優先找id選擇器是很是方便的。好比

import scrapy
from tutorial.items import TutorialItem

class DmozSpider(scrapy.Spider):
    name = "amoz"
    allowed_domains = ["amazon.cn"]
    start_urls = [
        "https://www.amazon.cn/%E6%96%87%E5%AD%A6%E5%9B%BE%E4%B9%A6/b/ref=sa_menu_books_l2_b658394051?ie=UTF8&node=658394051"
    ]

    def parse(self, response):
        for sel in response.xpath('//*[@id="a-page"]/div[4]/div/div[2]/div/div[1]/div[1]/ul[2]'):
            item = TutorialItem()

            item['link'] = sel.xpath('li[2]/a/@href').extract()
            #link = sel.xpath('a/@href').extract()
            #print title,price,desc
            #print item['title'][0],item['price'][0],item['desc'][0]
            print item['link'][0]
            #yield item

 

 這裏的path就是谷歌瀏覽器拷下來的一個路徑,而後xpath嵌套使用從而提取路徑。

可是顯然咱們只抓一個指定元素這種狀況用得不多,而後再嘗試抓取一組類似的目標。這裏選取了亞馬孫文學圖書頁面,試圖抓取標題,價格,評論數。

而後發現這些元素的屬性會隨着分類變化,好比電子書跟實體書標題屬性不同,折扣商品的價格屬性又和不折扣的商品不同(我真是曰了苟了這源碼這麼亂的嗎,彷佛只有分類討論來解決?這樣的話又會寫不少,暫時沒有想到好方法)

import scrapy
from tutorial.items import TutorialItem
from scrapy.http import Request

class DmozSpider(scrapy.Spider):
    name = "amoz"
    allowed_domains = ["amazon.cn"]
    start_urls = [
        "https://www.amazon.cn/b/ref=amb_link_9?ie=UTF8&node=659379051&pf_rd_m=A1AJ19PSB66TGU&pf_rd_s=merchandised-search-leftnav&pf_rd_r=04N93678VRYT3BZZZWFA&pf_rd_r=04N93678VRYT3BZZZWFA&pf_rd_t=101&pf_rd_p=4b8b99d4-2fd8-44c5-96ff-dbc77bd19498&pf_rd_p=4b8b99d4-2fd8-44c5-96ff-dbc77bd19498&pf_rd_i=658394051"
    ]

    def parse(self, response):
        #item['link'] = response.xpath('//a[@target="_blank"]/@href').extract()
        url_list = response.xpath('//a[@target="_blank"]/@href').extract()
        for url in url_list:
            yield Request(url,callback=self.parse_name)
            #print title,price,desc
            #print item['title'][0],item['price'][0],item['desc'][0]
        for i in range(2,75):
            page_url = 'https://www.amazon.cn/s/ref=lp_659379051_pg_{}?rh=n%3A658390051%2Cn%3A%21658391051%2Cn%3A658394051%2Cn%3A658511051%2Cn%3A659379051&page=2&ie=UTF8&qid=1497358151&spIA=B01FTXJZV2'.format(i)
            yield Request(page_url, callback=self.parse_name)
    def parse_name(self,response):
        item = TutorialItem()
        item['title'] = response.xpath('//*[@id="productTitle" or @id="ebooksProductTitle"]/text()').extract()
        item['descnum'] = response.xpath('//*[@id="acrCustomerReviewText"]/text()').extract()
        item['price'] = response.xpath('//span[@class="a-size-medium a-color-price inlineBlock-display offer-price a-text-normal price3P" or @class="a-color-price a-size-medium a-align-bottom"]/text()').extract()
        item['link'] = response.url
        yield item

  

(4)將爬取的內容存入數據庫

當Item在Spider中被收集以後,它將會被傳遞到Item Pipeline,一些組件會按照必定的順序執行對Item的處理。

每一個item pipeline組件(有時稱之爲「Item Pipeline」)是實現了簡單方法的Python類。他們接收到Item並經過它執行一些行爲,同時也決定此Item是否繼續經過pipeline,或是被丟棄而再也不進行處理。

如下是item pipeline的一些典型應用:

  • 清理HTML數據
  • 驗證爬取的數據(檢查item包含某些字段)
  • 查重(並丟棄)
  • 將爬取結果保存到數據庫中

顯然在pipeline中對數據進行篩選並存儲很是的穩

出於各類考慮(安全性,維護性),咱們通常不將數據庫的信息寫在源碼裏,而是像這樣寫在settings.py裏

#Mysql 配置
MYSQL_HOST = '127.0.0.1'
MYSQL_DBNAME = 'amazon'         #數據庫名字,請修改
MYSQL_USER = 'root'             #數據庫帳號,請修改
MYSQL_PASSWD = '123456'         #數據庫密碼,請修改

MYSQL_PORT = 3306

 而後從setting中加載數據庫信息

# -*- 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 twisted.enterprise import adbapi
from scrapy.exceptions import DropItem
import MySQLdb
import MySQLdb.cursors
import codecs
import json

class TutorialPipeline(object):
    def __init__(self,dbpool):
        self.dbpool=dbpool
        self.ids_seen=set()

    def process_item(self, item, spider):
        #print '----------------'
        #print u'標題' + item['title'][0]
        #print u'評論' + item['descnum'][0]
        #print u'價格' + item['price'][0]
        if item['title'] and item ['descnum'] and item['price']:

            query = self.dbpool.runInteraction(self._conditional_insert, item)  # 調用插入的方法
            query.addErrback(self._handle_error, item, spider)  # 調用異常處理方法
            return item
        else:
            raise DropItem("Missing price in %s" % item)

    @classmethod
    def from_settings(cls, settings):
           #一、@classmethod聲明一個類方法,而對於日常咱們見到的則叫作實例方法。
           #二、類方法的第一個參數cls(class的縮寫,指這個類自己),而實例方法的第一個參數是self,表示該類的一個實例
           #三、能夠經過類來調用,就像C.f(),至關於java中的靜態方法
        dbparams = dict(
            host=settings['MYSQL_HOST'],  # 讀取settings中的配置
            db=settings['MYSQL_DBNAME'],
            user=settings['MYSQL_USER'],
            passwd=settings['MYSQL_PASSWD'],
            charset='utf8',  # 編碼要加上,不然可能出現中文亂碼問題
            cursorclass=MySQLdb.cursors.DictCursor,
            use_unicode=False,
        )
        dbpool = adbapi.ConnectionPool('MySQLdb', **dbparams)  # **表示將字典擴展爲關鍵字參數,至關於host=xxx,db=yyy....
        return cls(dbpool)  # 至關於dbpool付給了這個類,self中能夠獲得

    def _conditional_insert(self, tx, item):
        # print item['name']
        sql = "insert into testtable(title,descnum,price) values(%s,%s,%s)"
        params = (item["title"], item["descnum"],item['price'])
        tx.execute(sql, params)
    def _handle_error(self, failue, item, spider):
        print '--------------database operation exception!!-----------------'
        print '-------------------------------------------------------------'
        print failue

  

如此就成功存儲了

相關文章
相關標籤/搜索