JB的Python之旅-爬蟲篇--requests&Scrapy

jb發表於2018-06-08

前序
本來說好隨緣更新的,因為想去寫測試相關的文件,但想想,既然第一篇爬蟲文章都發布吧,就繼續完善吧~

上一章回顧:JB的Python之旅-爬蟲篇--urllib和Beautiful Soup

看回之前寫的爬蟲計劃:

JB的Python之旅-爬蟲篇--requests&Scrapy

關於後續爬蟲的計劃:
目前還處於初級的定向指令碼編寫,本文內容主要介紹requests庫跟Scrapy框架;
計劃下一篇是Selenium,再下一篇是分散式爬蟲,後面加點實戰,再看看怎麼更新吧~

本文介紹:

上一篇主要介紹了urllib 跟 BeautifuSoap,練手的專案有小說網站及百度圖片的爬取,對於日常工作來說,感覺是夠了~

但只要有瞭解過爬蟲,肯定聽過requests跟Scrapy,這兩個又是什麼?
簡單介紹,requests是一個第三方庫,正如其名,就是處理請求相關內容,比起Python自帶的urllib庫,用起來相對的方便;

Scrapy呢,度娘給的介紹是:

Python開發的一個快速、高層次的螢幕抓取和web抓取框架,用於抓取web站點並從頁面中提取結構化的資料;
Scrapy吸引人的地方在於它是一個框架,任何人都可以根據需求方便的修改。它也提供了多種型別爬蟲的基類,如BaseSpider、sitemap爬蟲等;
複製程式碼

那,request是跟scrapy區別在哪裡?
從使用層面來說,單純爬取一個網頁的話,兩者區別不大,本質是一樣的,但scrapy是一個專業爬蟲框架,假如需要派去1W個網站的時候,差異就出現了,需要做併發,監控儲存,scrapy是可以做,但request這塊貌似做不到;

從程式碼層面,scrapy裡面可以使用requests的內容,會有較多二次封裝,在使用上更加方便~

更多的內容就度娘吧,這裡不展開了,留個大概印象就行了~

另外,本文還會使用另外一個網頁解析方法--xpath,上篇文章我們學會了用beautifulsoup,那為什麼要用xpath?
beautifulsoup是先把整個網頁結構解析,然後再查詢相關內容,而xpath是直接查詢,不需要做額外的解析操作,
因此可以得出,在效能效率上,xpath是灰常灰常的快的~

requests庫

本小節主要介紹下requests的庫常見使用,以及會介紹一個內容實戰~

1.簡介

官方中文文件

Requests 是用Python語言編寫,基於 urllib,採用 Apache2 Licensed 開源協議的 HTTP 庫。
它比 urllib 更加方便,可以節約我們大量的工作,

看了下官方文件,有以下特性:
Keep-Alive & 連線池
國際化域名和 URL
帶持久 Cookie 的會話
瀏覽器式的 SSL 認證
自動內容解碼
基本/摘要式的身份認證
優雅的 key/value Cookie
自動解壓
Unicode 響應體
HTTP(S) 代理支援
檔案分塊上傳
流下載
連線超時
分塊請求
支援 .netrc
Requests 支援 Python 2.6—2.7以及3.3—3.7,而且能在 PyPy 下完美執行。

看到就會覺得很厲害了,不明覺厲啊~
萬事開頭難,先安裝走一波把 pip install requests,安裝完就可以用了~

2.教程

直接上例子:
傳送請求

import requests

r = requests.get(url='http://www.baidu.com')    # 最基本的GET請求
r = requests.get(url='http://xxct.baidu.com/s', params={'wd':'python'})   #帶引數的GET請求
r = requests.post(url='http://xx', data={'wd':'python'})   #帶body的POST請求
r = requests.post('https://x', data=json.dumps({'wd': 'python'}))   #帶JSON的POST
#其他請求方式的用法
requests.get(‘https://XX’) #GET請求
requests.post(“http://XX”) #POST請求
requests.put(“http://XX”) #PUT請求
requests.delete(“http://XX”) #DELETE請求
requests.head(“http://XX”) #HEAD請求
requests.options(“http://XX”) #OPTIONS請求
複製程式碼

響應內容
請求返回的值是一個Response 物件,Response 物件是對 HTTP 協議中服務端返回給瀏覽器的響應資料的封裝,響應的中的主要元素包括:狀態碼、原因短語、響應首部、響應體等等,這些屬性都封裝在Response 物件中。

# 狀態碼
response.status_code
200

# 原因短語
response.reason
'OK'

# 響應首部
for name,value in response.headers.items():
    print("%s:%s" % (name, value))

Content-Encoding:gzip
Server:nginx/1.10.2
Date:Thu, 06 Apr 2017 16:28:01 GMT

# 響應內容
response.content
複製程式碼

查詢引數
很多URL都帶有很長一串引數,我們稱這些引數為URL的查詢引數,用"?"附加在URL連結後面,多個引數之間用"&"隔開,比如:http://www.baidu.com/?wd=python&key=20 ,現在你可以用字典來構建查詢引數:

args = {"wd": python, "time": 20}
response = requests.get("http://www.baidu.com", params = args)
response.url
'http://www.baidu.com/?wd=python&time=20'
複製程式碼

請求首部
requests 可以很簡單地指定請求首部欄位 Headers,比如有時要指定 User-Agent 偽裝成瀏覽器傳送請求,以此來矇騙伺服器。直接傳遞一個字典物件給引數 headers 即可。

r = requests.get(url, headers={'user-agent': 'Mozilla/5.0'})
複製程式碼

請求體
requests 可以非常靈活地構建 POST 請求需要的資料,
如果伺服器要求傳送的資料是表單資料,則可以指定關鍵字引數 data,
如果要求傳遞 json 格式字串引數,則可以使用json關鍵字引數,引數的值都可以字典的形式傳過去。

作為表單資料傳輸給伺服器

payload = {'key1': 'value1', 'key2': 'value2'}
r = requests.post("http://www.baidu.com", data=payload)
複製程式碼

作為 json 格式的字串格式傳輸給伺服器

import json
url = 'http://www.baidu.com'
payload = {'some': 'data'}
r = requests.post(url, json=payload)
複製程式碼

響應內容
響應體在 requests 中處理非常靈活,
與響應體相關的屬性有:content、text、json()。

content 是 byte 型別,適合直接將內容儲存到檔案系統或者傳輸到網路中

r = requests.get("https://pic1.zhimg.com/v2-2e92ebadb4a967829dcd7d05908ccab0_b.jpg")
type(r.content)
<class 'bytes'>
# 另存為 test.jpg
with open("test.jpg", "wb") as f:
    f.write(r.content)
複製程式碼

text 是 str 型別,比如一個普通的 HTML 頁面,需要對文字進一步分析時,使用 text。

r = requests.get("https://www.baidu.com")
type(r.text)
<class 'str'>
re.compile('xxx').findall(r.text)
複製程式碼

如果使用第三方開放平臺或者API介面爬取資料時,返回的內容是json格式的資料時,那麼可以直接使用json()方法返回一個經過json.loads()處理後的物件。

>>> r = requests.get('https://www.baidu.com')
>>> r.json()
複製程式碼

代理設定
當爬蟲頻繁地對伺服器進行抓取內容時,很容易被伺服器遮蔽掉,因此要想繼續順利的進行爬取資料,使用代理是明智的選擇。如果你想爬取牆外的資料,同樣設定代理可以解決問題,requests 完美支援代理。

import requests

proxies = {
  'http': 'http://XX',
  'https': 'https://XX',
}

requests.get('https://foofish.net', proxies=proxies, timeout=5)
複製程式碼

超時設定
requests 傳送請求時,預設請求下執行緒一直阻塞,直到有響應返回才處理後面的邏輯。
如果遇到伺服器沒有響應的情況時,問題就變得很嚴重了,它將導致整個應用程式一直處於阻塞狀態而沒法處理其他請求。 r = requests.get("http://www.google.coma", timeout=5)

3.實戰1 爬取青春妹子網站

這次爬取的網站是:http://www.mmjpg.com/,直接F12看了下,所有的資訊都集中在ul區塊下的img區塊裡:

JB的Python之旅-爬蟲篇--requests&Scrapy

分析下網頁的結構,基本是,這樣的:
1)每一頁會有15個圖集
2)每一個圖集裡有N張圖片

而我們需要的就是下載圖片,那就是需要先獲取網頁分頁->圖集分頁->圖片下載頁,爬取的思路:
1)獲取當前網址的圖集地址
2)獲取當前網站的下載地址
3)下載圖片

先以首頁為例子,獲取首頁的所有圖集地址:

import requests
from lxml import html

def Get_Page_Number():
    url = 'http://www.mmjpg.com'
    response = requests.get(url).content
    #呼叫requests庫,獲取二進位制的相應內容。
    #注意,這裡使用.text方法的話,下面的html解析會報錯.這裡涉及到.content和.text的區別了。簡單說,如果是處理文字、連結等內容,建議使用.text,處理視訊、音訊、圖片等二進位制內容,建議使用.content。
    selector = html.fromstring(response)
    #使用lxml.html模組構建選擇器,主要功能是將二進位制的伺服器相應內容response轉化為可讀取的元素樹(element tree)。lxml中就有etree模組,是構建元素樹用的。如果是將html字串轉化為可讀取的元素樹,就建議使用lxml.html.fromstring,畢竟這幾個名字應該能大致說明功能了吧。
    urls = []
    #準備容器
    for i in selector.xpath("//ul/li/a/@href"):
    #利用xpath定位到所有的套圖的詳細地址
        urls.append(i)
        #遍歷所有地址,新增到容器中
    return urls
複製程式碼

這裡的urls,就是圖集的地址,接下來就是獲取套圖地址的標題
這裡有同學有疑問,為什麼用xpatch,而不是用beautifulsoup?
單一例子來首,兩者都是用來解析網頁的,差別不大,主要是想熟悉瞭解下,關於xpath的內容,scrapy下面有詳細介紹~

JB的Python之旅-爬蟲篇--requests&Scrapy
結構跟首頁一致但我們獲取的是標題,直接獲取h2的title即可,如下:

def Get_Image_Title():
# 現在進入到套圖的詳情頁面了,現在要把套圖的標題和圖片總數提取出來
url = "http://www.mmjpg.com/mm/1367"
response = requests.get(url).content
selector = html.fromstring(response)
image_title = selector.xpath("//div[@class='article']/h2/text()")[0]
# 需要注意的是,xpath返回的結果都是序列,所以需要使用[0]進行定位
return image_title
複製程式碼

接下來獲取圖片的數量:

JB的Python之旅-爬蟲篇--requests&Scrapy
同樣的套圖頁,直接點位到座標處,會發現a標籤的倒數第二個區塊就是圖集的最後一頁,直接取數就行,程式碼如下:

def Get_Image_Count():
    url = "http://www.mmjpg.com/mm/1367"
    response = requests.get(url).content
    selector = html.fromstring(response)
    image_count = selector.xpath("//div[@class='page']/a[last()-1]/text()")[0]
    return image_count
複製程式碼

接下來就是獲取圖片的下載地址:

JB的Python之旅-爬蟲篇--requests&Scrapy
並且點選不同頁碼的圖片,變化的是最後一位: http://www.mmjpg.com/mm/1365/XX

因此想獲取圖集下的所有圖片連結,就是先獲取有多少頁(上面的方法就可以啦),然後找到img獲取下載連結,程式碼如下:

def Get_Image_Url():
    url = "http://www.mmjpg.com/mm/1367"
    response = requests.get(url).content
    selector = html.fromstring(response)
    image_links = []
    image_aount = selector.xpath("//div[@class='page']/a[last()-1]/text()")[0]

    for i in range(int(image_aount)):
        image_url = "http://www.mmjpg.com/mm/1367/"+str(i+1)
        response = requests.get(image_url).content
        sel = html.fromstring(response)
        image_download_link = sel.xpath("//div[@class='content']/a/img/@src")[0]
        # 這裡是單張圖片的最終下載地址
        image_links.append(str(image_download_link))
    return image_links
複製程式碼

最後,就是下載檔案啦~

def Download_Image(image_title,image_links):
    num = 1
    amount = len(image_links)

    for i in image_links:
        filename = "%s%s.jpg" % (image_title,num)
        print('正在下載圖片:%s第%s/%s張,' % (image_title, num, amount))
        #用於在cmd視窗上輸出提示,感覺可以增加一個容錯函式,沒想好怎麼寫
        urllib.request.urlretrieve(requests.get(i,headers=header), "%s%s%s.jpg" % (dir, image_title, num))
        num += 1
複製程式碼

跑起來後發現,爬下來的圖都是這樣的,但是把url資訊輸出看了下,連結都沒問題啊,奇怪了~

JB的Python之旅-爬蟲篇--requests&Scrapy

然後用PC玩了下,連結跟爬取的都沒問題,這種情況只有網站做了反爬蟲了;
既然如此,那我們修改下策略,上面在下載檔案的時候,用的是urlretrieve,但是現在需要在下載圖片時請求下,而且urlretrieve沒有可以設定請求的引數,因此不適用本場景;

urlretrieve(url, filename=None, reporthook=None, data=None)
引數 finename 指定了儲存本地路徑(如果引數未指定,urllib會生成一個臨時檔案儲存資料。)
引數 reporthook 是一個回撥函式,當連線上伺服器、以及相應的資料塊傳輸完畢時會觸發該回撥,我們可以利用這個回撥函式來顯示當前的下載進度。
引數 data 指 post 到伺服器的資料,該方法返回一個包含兩個元素的(filename, headers)元組,filename 表示儲存到本地的路徑,header 表示伺服器的響應頭。
複製程式碼

那我們就改成用wirte的方式去寫入圖片:

    with open(dir+filename, 'wb') as f:
        #以二進位制寫入的模式在本地構建新檔案
        header = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.32 Safari/537.36'}
        f.write(requests.get(i,headers=header).content)
複製程式碼

執行後發現,依然是下載同一張圖片,那說明靠UA還不夠,再看下請求頭資訊:

JB的Python之旅-爬蟲篇--requests&Scrapy
也沒有什麼特別的,既然如此,就把整個頭部模擬一模一樣,最後發現,還需要Referer這個引數:對應的值,貌似就是上面的i,因此修改成這樣:

   for i in image_links:
    filename = "%s%s.jpg" % (image_title, num)
    print('正在下載圖片:%s第%s/%s張,' % (image_title, num, amount))
    # 用於在cmd視窗上輸出提示,感覺可以增加一個容錯函式,沒想好怎麼寫

    with open(dir+filename, 'wb') as f:
    #以二進位制寫入的模式在本地構建新檔案
        header = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.32 Safari/537.36',
            'Referer':i}
        f.write(requests.get(i,headers=header).content)
複製程式碼

執行後發現,每張圖片大小都不一樣,嗯,終於成功啦~

JB的Python之旅-爬蟲篇--requests&Scrapy

整體程式碼如下:

import requests
from lxml import html
import os
import urllib

dir = "mmjpg/"


def Get_Page_Number(num):
    if (int(num) < 2):
        url = 'http://www.mmjpg.com'
    else:
        url = 'http://www.mmjpg.com/home/' + num
    response = requests.get(url).content
    # 呼叫requests庫,獲取二進位制的相應內容。
    # 注意,這裡使用.text方法的話,下面的html解析會報錯.這裡涉及到.content和.text的區別了。簡單說,如果是處理文字、連結等內容,建議使用.text,處理視訊、音訊、圖片等二進位制內容,建議使用.content。
    selector = html.fromstring(response)
    # 使用lxml.html模組構建選擇器,主要功能是將二進位制的伺服器相應內容response轉化為可讀取的元素樹(element tree)。lxml中就有etree模組,是構建元素樹用的。如果是將html字串轉化為可讀取的元素樹,就建議使用lxml.html.fromstring,畢竟這幾個名字應該能大致說明功能了吧。
    urls = []
    # 準備容器
    for i in selector.xpath("//ul/li/a/@href"):
        # 利用xpath定位到所有的套圖的詳細地址
        urls.append(i)
        # 遍歷所有地址,新增到容器中
    return urls


def Get_Image_Title(url):
    # 現在進入到套圖的詳情頁面了,現在要把套圖的標題和圖片總數提取出來
    response = requests.get(url).content
    selector = html.fromstring(response)
    image_title = selector.xpath("//div[@class='article']/h2/text()")[0]
    # 需要注意的是,xpath返回的結果都是序列,所以需要使用[0]進行定位
    return image_title


def Get_Image_Count(url):
    response = requests.get(url).content
    selector = html.fromstring(response)
    image_count = selector.xpath("//div[@class='page']/a[last()-1]/text()")[0]
    return image_count


def Get_Image_Url(url):
    response = requests.get(url).content
    selector = html.fromstring(response)
    image_links = []
    image_aount = selector.xpath("//div[@class='page']/a[last()-1]/text()")[0]

    for i in range(int(image_aount)):
        image_url = url + "/" + str(i + 1)
        response = requests.get(image_url).content
        sel = html.fromstring(response)
        image_download_link = sel.xpath("//div[@class='content']/a/img/@src")[0]
        # 這裡是單張圖片的最終下載地址
        image_links.append(str(image_download_link))
    return image_links


def Download_Image(image_title, image_links):
    num = 1
    amount = len(image_links)

    if not os.path.exists(dir):
        os.makedirs(dir)
    for i in image_links:
        if not os.path.exists(dir+image_title):
            os.makedirs(dir+image_title)
        print('正在下載圖片:%s第%s/%s張,' % (image_title, num, amount))
        # 用於在cmd視窗上輸出提示,感覺可以增加一個容錯函式,沒想好怎麼寫
        filename = image_title+"/"+str(num)+".jpg"
        #建立檔名
        with open(dir+filename, 'wb') as f:
        #以二進位制寫入的模式在本地構建新檔案
            header = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.32 Safari/537.36',
                'Referer':i}
            f.write(requests.get(i,headers=header).content)
        # urllib.request.urlretrieve(requests.get(i,headers=header), "%s%s%s.jpg" % (dir, image_title, num))
        #如果使用這種方式爬,網站會返回防盜連結,爬的圖片都一樣,因此需要爬的時候UA做下處理,而urlretrieve並沒有設定請求頭的方式,因此不適用本案例
        num += 1



if __name__ == '__main__':
    page_number = input('請輸入需要爬取的頁碼:')
    for link in Get_Page_Number(page_number):
        Download_Image(Get_Image_Title(link), Get_Image_Url(link))
複製程式碼

整體程式碼與演示程式碼有點出入,主要是整理了一下,主體思路不變~效果如下:

JB的Python之旅-爬蟲篇--requests&Scrapy

JB的Python之旅-爬蟲篇--requests&Scrapy

該實戰完畢,主要還是能整理出思路,問題則不大~

Scrapy

自己單獨寫爬蟲的話,會用很多重複的程式碼,比如設定headers,代理IP,檔案儲存,雖然每次手動寫,都覺得好爽的,但今天來介紹一個出名的爬蟲框架--Scrapy

1.為什麼要用爬蟲框架?

如果你對爬蟲的基礎知識有了一定了解的話,那麼是時候該瞭解一下爬蟲框架了。那麼為什麼要使用爬蟲框架?
1)學習框架的根本是學習一種程式設計思想,而不應該僅僅侷限於是如何使用它。從瞭解到掌握一種框架,其實是對一種思想理解的過程。
2)框架也給我們的開發帶來了極大的方便。許多條條框框都已經是寫好了的,並不需要我們重複造輪子,我們只需要根據自己的需求定製自己要實現的功能就好了,大大減少了工作量。 。

2.簡介

官網文件:http://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/overview.html

Scrapy是一個為了爬取網站資料,提取結構性資料而編寫的應用框架。
可以應用在包括資料探勘,資訊處理或儲存歷史資料等一系列的程式中。

其最初是為了 頁面抓取 (更確切來說, 網路抓取 )所設計的,
也可以應用在獲取API所返回的資料(例如 Amazon Associates Web Services ) 或者通用的網路爬蟲。

3.scrapy特點:

1)scrapy基於事件的機制,利用twisted的設計實現了非阻塞的非同步操作。
這相比於傳統的阻塞式請求,極大的提高了CPU的使用率,以及爬取效率。 2)配置簡單,可以簡單的通過設定一行程式碼實現複雜功能。 3)可擴充,外掛豐富,比如分散式scrapy + redis、爬蟲視覺化等外掛。 4)解析方便易用,scrapy封裝了xpath等解析器,提供了更方便更高階的selector構造器,可有效的處理破損的HTML程式碼和編碼。

4.scrapy安裝

pip install scrapy
複製程式碼

Windows上安裝時可能會出現錯誤,提示找不到Microsoft Visual C++。這時候我們需要到它提示的網站visual-cpp-build-tools下載VC++ 14編譯器,安裝完成之後再次執行命令即可成功安裝Scrapy。

JB的Python之旅-爬蟲篇--requests&Scrapy

官網連結:http://landinghub.visualstudio.com/visual-cpp-build-tools

JB的Python之旅-爬蟲篇--requests&Scrapy

但實際電腦還是會一直報找不到Microsoft Visual C++,後來網上查詢後,使用Anaconda安裝就好,安裝包如下:

連結:https://pan.baidu.com/s/1a-VxwaR56iQQu108wqz2zA,密碼:r1oz

下載完後直接安裝,安裝完成後直接CMD命令列輸入conda install scrapy,安裝完成後是這樣的:

JB的Python之旅-爬蟲篇--requests&Scrapy

驗證的話,直接輸入scrapy -h,能顯示內容即可~

JB的Python之旅-爬蟲篇--requests&Scrapy

Ubuntu安裝的時候,中途出現一個錯誤:fatal error: 'Python.h' file not found 需要另外安裝python-dev,該庫中包含Python的標頭檔案與靜態庫包, 要根據自己的Python版本進行安裝:

sudo apt-get install python3.4-dev
複製程式碼

5. Scrapy框架路過了解下

架構圖

JB的Python之旅-爬蟲篇--requests&Scrapy

模組介紹:
1)Engine。引擎,處理整個系統的資料流處理、觸發事務,是整個框架的核心。

2)Item。專案,它定義了爬取結果的資料結構,爬取的資料會被賦值成該Item物件。

3)Scheduler。排程器,接受引擎發過來的請求並將其加入佇列中,在引擎再次請求的時候將請求提供給引擎。

4)Downloader。下載器,下載網頁內容,並將網頁內容返回給蜘蛛。Spiders。蜘蛛,其內定義了爬取的邏輯和網頁的解析規則,它主要負責解析響應並生成提取結果和新的請求。

5)Item Pipeline。專案管道,負責處理由蜘蛛從網頁中抽取的專案,它的主要任務是清洗、驗證和儲存資料。

6)Downloader Middlewares。下載器中介軟體,位於引擎和下載器之間的鉤子框架,主要處理引擎與下載器之間的請求及響應。

7)Spider Middlewares。蜘蛛中介軟體,位於引擎和蜘蛛之間的鉤子框架,主要處理蜘蛛輸入的響應和輸出的結果及新的請求。


執行過程

1)引擎首先開啟一個網站,找到處理該網站的Spider,並向該Spider請求第一個要爬取的URL。

2)引擎從Spider中獲取到第一個要爬取的URL,並通過Scheduler以Request的形式排程。

3)引擎向Scheduler請求下一個要爬取的URL。

4)Scheduler返回下一個要爬取的URL給引擎,引擎將URL通下載器中介軟體轉發給Downloader下載。


5)一旦頁面下載完畢,Downloader生成該頁面的Response,並將其通過下載器中介軟體傳送給引擎。

6)引擎從下載器中接收到Response,並將其通過Spider Middlewares傳送給Spider處理。

7)Spider處理Response,並返回爬取到的Item及新的Request給引擎。

8)引擎將Spider返回的Item給Item Pipeline,將新的Request給Scheduler。

9)重複第二步到最後一步,直到Scheduler中沒有更多的Request,引擎關閉該網站,爬取結束。

6.專案結構

新建的專案結構如下:

ScrapyStudy/
    scrapy.cfg            # 專案的配置檔案
    ScrapyStudy/          # 該專案的python模組,程式碼都加在裡面
        __init__.py
        items.py          # 專案中的item檔案
        pipelines.py      # 專案中pipelines檔案
        settings.py       # 專案的設定檔案
        spiders/          # 方式spider程式碼的目錄
            __init__.py
複製程式碼

檔案描述如下:
1)scrapy.cfg:它是Scrapy專案的配置檔案,其內定義了專案的配置檔案路徑、部署相關資訊等內容。
2)items.py:它定義Item資料結構,所有的Item的定義都可以放這裡。
3)pipelines.py:它定義Item Pipeline的實現,所有的Item Pipeline的實現都可以放這裡。
4)settings.py:它定義專案的全域性配置。
5)middlewares.py:它定義Spider Middlewares和Downloader Middlewares的實現。
6)spiders:其內包含一個個Spider的實現,每個Spider都有一個檔案

7.製作爬蟲步驟

1)新建專案(scrapy startproject xxx): 新建一個新的爬蟲專案
2)明確目標(編寫items.py): 明確你想要抓取的目標
3)製作爬蟲(spiders/xxsp der.py): 製作爬蟲開始爬取網頁
4)儲存內容(pipelines.py): 設計管道儲存爬取內容

8.實戰1 使用scrapy輸出廖雪峰官網的title

根據7得知,製作爬蟲需要4個步驟,那現在就以實戰來介紹這4個步驟;

1)建立專案:

scrapy startproject 專案名
複製程式碼

這樣就代表新建完成了

JB的Python之旅-爬蟲篇--requests&Scrapy

然後用pycharm開啟了建立的專案後,就開始體驗啦~

以廖雪峰python官網為例子,輸出title資訊;
再spiders目錄下新增一個檔案,比如liaoxuefeng.py,程式碼如下:

import scrapy

class LiaoxuefengSpider(scrapy.Spider):
    # 這裡是將爬蟲定義為scrapy.Spider這個類下的一個例項。
    # Spider這個類定義了爬蟲的很多基本功能,我們直接例項化就好,
    # 省卻了很多重寫方法的麻煩。
    name = 'lxf'
    #這是爬蟲的名字,這個非常重要。

    start_urls = ['http://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000']
    #這是爬蟲開始幹活的地址,必須是一個可迭代物件。

    def parse(self, response):
    #爬蟲收到上面的地址後,就會傳送requests請求,在收到伺服器返回的內容後,就將內容傳遞給parse函式。在這裡我們重寫函式,達到我們想要的功能。
        titles = response.xpath("//ul[@class='uk-nav uk-nav-side']//a/text()").extract()
        #這是廖雪峰老師python教程的標題列表。我們利用xpath解析器對收到的response進行分析,從而提取出我們需要的資料。//XXX表示任何任何目錄下的XXX區塊,/XXX表示子目錄下的XXX區塊,XXX[@class=abc]表示帶有class=abc屬性值的XXX區塊,/text()表示獲取該區塊的文字。最後加上.extract()表示將內容提取出來。
        for title in titles:
            print (title)
        #這個沒什麼說的了,直接遍歷,然後列印標題。
複製程式碼

然後進入cmd,在專案的根目錄下執行scrapy crawl lxf(這個lxf就是剛才liaoxuefeng.py檔案中的name欄位,千萬不要弄錯了),執行成功,當觀察發現,並沒有所需要的內容,直接提示“503 Service Unavailable”,

根據經驗,這是因為沒有設定請求頭導致的;

那在setting.py,找到USER_AGENT這個引數,預設是註釋的,取消註釋後,提供value,比如: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.104 Safari/537.36 Core/1.53.4843.400 QQBrowser/9.7.13021.400

spider必須定義三個屬性

-name: 用於區別Spider。 該名字必須是唯一的,您不可以為不同的Spider設定相同的名字。
-start_urls: 包含了Spider在啟動時進行爬取的url列表。 因此,第一個被獲取到的頁面將是其中之一。 後續的URL則從初始的URL獲取到的資料中提取。
-parse() 是spider的一個方法。 被呼叫時,每個初始URL完成下載後生成的 Response 物件將會作為唯一的引數傳遞給該函式。 該方法負責解析返回的資料,提取資料(生成item)以及生成需要進一步處理的URL的 Request 物件。

然後再次執行scrapy crawl lxf,就會列印當前頁面所有的目錄名稱:

JB的Python之旅-爬蟲篇--requests&Scrapy

上面提及到一個知識點:取出網頁中想要的資訊
Scrapy中使用一種基於XPath和CSSDE表示式機制:Scrapy Selectors 來提取出網頁中我們所需的資料。

Selector是一個選擇,有四個基本方法:
1)xpath():傳入xpath表示式,返回該表示式對應的所有節點的selector list列表;
2)css():傳入CSS表示式,返回該表示式對應的所有及誒點的selector list列表;
3)extract():序列化該節點為unicode字串並返回list;
4)re():根據傳入的正規表示式對資料進行提取,返回unicode字串list列表;

這裡順道學下XPath的基本語法:
首先XPath中的路徑分為絕對路徑與相對路徑
絕對路徑:用**/,表示從根節點開始選取;
相對路徑:用//,表示選擇任意位置的節點,而不考慮他們的位置;
另外可以使用*萬用字元來表示未知的元素;除此之外還有兩個選取節點的:
.:選取當前節點;..:當前節點的父節點;

接著就是選擇分支進行定位了,比如存在多個元素,想唯一定位,
可以使用
[]**中括號來選擇分支,下標是從1開始算的哦!
比如可以有下面這些玩法:

1)/tr/td[1]:取第一個td
2)/tr/td[last()]:取最後一個td
3)/tr/td[last()-1]:取倒數第二個td
4)/tr/td[position()<3]:取第一個和第二個td
5)/tr/td[@class]:選取擁有class屬性的td
6)/tr/td[@class='xxx']:選取擁有class屬性為xxx的td
7)/tr/td[count>10]:選取 price 元素的值大於10的td

然後是選擇屬性,其實就是上面的這個**@** 可以使用多個屬性定位,可以這樣寫:/tr/td[@class='xxx'][@value='yyy'] 或者**/tr/td[@class='xxx' and @value='yyy']**

接著是常用函式:除了上面的last(),position(),外還有: contains(string1,string2):如果前後匹配返回True,不匹配返回False; text():獲取元素的文字內容 start-with():從起始位置匹配字串

回顧

第一個實戰專案就到此結束啦~
是不是很簡單?

遇到一個問題
在用pycharm開啟scrapy專案後,scrapy一直在顯示紅色報錯,當時介意了很久,心想著,本地用都能用,為什麼你這邊報錯?
結果後來發現,原來scrapy不是在pycharm上執行的,報錯也不需要管~本地確認scrapy有安裝就行了~

JB的Python之旅-爬蟲篇--requests&Scrapy

9.實戰2 使用scrapy輸出廖雪峰官網的title

1)建立專案,直接在需要的目錄下執行scrapy startproject 專案名
2)在spiders目錄下新建爬蟲檔案,比如本例叫LiaoxuefengSpider,建立後,需要思考在裡面填寫什麼?
填寫需要爬取的網站;
設定爬蟲名稱,這裡注意,該名稱是全域性唯一的,不允許重複;
介面解析;
內容輸出;
因此不難寫出下面的程式碼;

import scrapy

class LiaoxuefengSpider(scrapy.Spider):
    # 這裡是將爬蟲定義為scrapy.Spider這個類下的一個例項。
    # Spider這個類定義了爬蟲的很多基本功能,我們直接例項化就好,
    # 省卻了很多重寫方法的麻煩。
    name = 'lxf'
    #這是爬蟲的名字,這個非常重要。
    start_urls = ['https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000']
    #這是爬蟲開始幹活的地址,必須是一個可迭代物件。

    def parse(self, response):
    #爬蟲收到上面的地址後,就會傳送requests請求,在收到伺服器返回的內容後,就將內容傳遞給parse函式。在這裡我們重寫函式,達到我們想要的功能。
        titles = response.xpath("//ul[@class='uk-nav uk-nav-side']//a/text()").extract()
        #這是廖雪峰老師python教程的標題列表。我們利用xpath解析器對收到的response進行分析,從而提取出我們需要的資料。//XXX表示任何任何目錄下的XXX區塊,/XXX表示子目錄下的XXX區塊,XXX[@class=abc]表示帶有class=abc屬性值的XXX區塊,/text()表示獲取該區塊的文字。最後加上.extract()表示將內容提取出來。
        for title in titles:
            print (title)
        #這個沒什麼說的了,直接遍歷,然後列印標題。
複製程式碼

編寫後,在專案目錄下執行scrapy crawl 爬蟲名稱,如scrapy crawl lxf,就會執行,但是會發現報錯:

JB的Python之旅-爬蟲篇--requests&Scrapy
從截圖資訊,看到伺服器返回503,根據經驗,這是因為沒有設定請求headers導致,因此有兩種方案:
1)在爬蟲檔案裡設定請求headers
2)開啟專案裡的settings.py,找到USER_AGENT,預設是被註釋的,關閉註釋,並且給一個預設的UA即可,如:
USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36'

再次輸入scrapy crawl爬蟲名稱,發現能正常顯示了,如下:

JB的Python之旅-爬蟲篇--requests&Scrapy

這就說明指令碼能正常執行,同時也說明scrapy第一個例子成功啦~
重要的資訊重複強調,爬蟲名稱是全域性唯一的~

10.實戰3 使用scrapy爬取電影圖片

先建立專案:scrapy startproject 專案名
建立完專案之後,在spiders建立一個檔案,建立後,專案結構如下:

JB的Python之旅-爬蟲篇--requests&Scrapy

想爬的url連結是這個:http://www.id97.com/movie/,先看看我們想爬什麼~

JB的Python之旅-爬蟲篇--requests&Scrapy

從上圖看出,可以爬的東西有,電影名稱,型別,分數以及封面連結
既然都已經決定了,那還記得,哪個檔案是用來存放目標的嗎?沒錯,就是item.py

import scrapy

class MovieScrapyItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    articleUrl = scrapy.Field()
    movieName = scrapy.Field()
    scoreNumber = scrapy.Field()
    style = scrapy.Field()
複製程式碼

不難得出上面的內容,那我們繼續分析網頁結構~

JB的Python之旅-爬蟲篇--requests&Scrapy
直接點選,發現每一部電影的資料都是在獨立的class裡面,這個class叫col-xs-1-5 col-sm-4 col-xs-6 movie-item
這也意味著,上面需要的內容,都可以獨立獲取了,那div這塊可以這樣寫了

    def parse(self,response):
        selector = Selector(response)
        divs = selector.xpath("//div[@class='col-xs-1-5 col-sm-4 col-xs-6 movie-item']")
複製程式碼

回顧上面內容,什麼是Selector?
Scrapy提取資料有自己的一套機制。它們被稱作選擇器(seletors),因為他們通過特定的 XPath 或者 CSS 表示式來“選擇” HTML檔案中的某個部分;

剛剛上面提及到了,所有的電影內容都在不同的class裡面,但都叫col-xs-1-5 col-sm-4 col-xs-6 movie-item,因此只需要直接找這個class, 那返回的就是所有div資訊;

那隻需要寫個for,即可獲取每個div的資訊

        for div in divs:
            yield self.parse_item(div)
複製程式碼

知識點,yield是啥?
yield的作用是把parse函式變成一個發生器(generator),每次函式執行會返回一個迭代物件(iterable 物件)。聽上去是不是跟return一樣好像一樣的感覺?
return返回的是函式返回值,而yield返回的是一個生成器~
關於如何更好理解yield,這邊貼一個外鏈,感興趣的同學可以瞭解:
http://pyzh.readthedocs.io/en/latest/the-python-yield-keyword-explained.html

扯遠了,上面的程式碼,parse_item這個方法不是自帶的,是自己寫的,用來處理的就是針對每個電影div做詳細的資料獲取,那繼續看分析吧~

隨便挑一個開啟,就能看到我們需要的內容了,標題,封面url,分數,型別,其中,標題跟url可以直接獲取到,而分數跟型別還要做下處理~

JB的Python之旅-爬蟲篇--requests&Scrapy

首先,第一步,獲取資料,如下:

item['articleUrl'] = div.xpath('div[@class="movie-item-in"]/a/img/@data-original ').extract_first()
item['movieName'] = div.xpath('div[@class="movie-item-in"]/a/@title').extract_first()
item['scoreNumber']  = div.xpath('div[@class="movie-item-in"]/div[@class="meta"]/h1/em/text()').extract_first()
item['style']= div.xpath(
            'string(div[@class="movie-item-in"]/div[@class="meta"]/div[@class="otherinfo"])').extract_first(
            default="")
複製程式碼

上面這樣獲取是不是就行了?不是的,分數那是不對的,因為通過xpath獲取到的分數是這樣的: - 6.8分,
而我們需要的是6.8,那以意味著分數需要二次處理,也很簡單,直接正則提取就行了,不詳細說明,原始碼如下:

def parse_item(self,div):
    item = MovieScrapyItem()
    #封面url
    item['articleUrl'] = div.xpath('div[@class="movie-item-in"]/a/img/@data-original ').extract_first()
    #電影名稱
    item['movieName'] = div.xpath('div[@class="movie-item-in"]/a/@title').extract_first()
    #分數,但需要處理
    score = div.xpath('div[@class="movie-item-in"]/div[@class="meta"]/h1/em/text()').extract_first()
    item['scoreNumber'] = self.convertScore(score)
    #型別
    style = div.xpath(
        'string(div[@class="movie-item-in"]/div[@class="meta"]/div[@class="otherinfo"])').extract_first(
        default="")
    item['style'] = style


#用來提取分數的
def convertScore(self, str):
    list = re.findall(r"\d+\.?\d*", str)
    if list:
        return list.pop(0)
    else:
        return 0
複製程式碼

有同學會有一個疑問,extract_first()跟extract()區別在哪裡?
extract()返回的是資料,extract_first()返回的是字串,不信?看看下面~

JB的Python之旅-爬蟲篇--requests&Scrapy
直接輸出,系統提示must be str,not list,那行,那我們就str(XX),接下來繼續看~

JB的Python之旅-爬蟲篇--requests&Scrapy
先看右上角,輸出2個引數,一個是extract_first,一個是str(extract),然後再看輸出內容的倒數4行
結果發現,str(extract)輸出的就是一個資料,如果用於拼接,還需要額外處理的" ".join(str(extract))這樣處理,那還不如直接一個是extract_first()返回字串來的快~

那如果我們想翻頁繼續爬呢?當然沒問題啦~
直接點選下一頁,它的結構如下,是在一個叫pager-gb的class裡面,而下一頁的按鈕就在倒數第二個li裡面~

JB的Python之旅-爬蟲篇--requests&Scrapy

#定位到頁數這樣
pages = selector.xpath("body/div/div[@class='pager-bg']/ul/li")

#下一頁
nextPageUrl = self.host + (pages[- 2].xpath('a/@href').extract_first())
複製程式碼

這裡的self.host是檔案開頭定義好的,是"http://www.id97.com",用於拼接的;
這樣就能獲取到下一頁的連結了,那接下來就是再次發起請求即可;
import 下Request,直接執行即可;

yield Request(nextPageUrl,callback=self.parse)
複製程式碼

完整程式碼文末貼出,彆著急=。=

至此,爬蟲部分搞定了,item裡面的內容就是我們要的東西了~

那,能否把這些資訊儲存到csv裡面
當然可以,沒問題,而且還不需要改動程式碼哦~
直接在執行命令的時候,加多幾個引數即可~

scrapy crawl 專案名 -o xx.csv
複製程式碼

這樣就能生成csv檔案~但是開啟後,辣眼睛啊~都亂碼?

JB的Python之旅-爬蟲篇--requests&Scrapy

網上找了下,原因是:
微軟的軟體開啟檔案預設都是 ANSI 編碼(國內就是 GBK),UTF-8 的 csv 檔案在 execl 中開啟時解碼自然就亂碼了~

解決方案呢?技術層面貌似找了好久都沒找到,就是encode啥指定編碼都不行,因此只能這樣: 用記事本開啟剛剛儲存的csv,點選另存為,不再使用utf-8,然後再開啟就好了~

JB的Python之旅-爬蟲篇--requests&Scrapy

既然都有url了,那能存拿url出來進行儲存?
沒問題,都可以滿足~

儲存圖片有2種方式:
1)urlib.request.urlretrieve
2)scrapy內建的ImagePipeline

第一種不需要解釋了,上篇文章已經有說明,使用場景就是在獲取url的時候,直接下載,效率會慢點:

        if item['articleUrl']:
            file_name = "%s.jpg"%(item['movieName'])
            file_path = os.path.join("F:\\pics", file_name)
            urllib.request.urlretrieve(item['articleUrl'], file_path)
複製程式碼

第二種,需要修改pipelines.py這個檔案,直接重寫即可,規則自己定義:

class MovieScrapyPipeline(ImagesPipeline):
    # def process_item(self, item, spider):
    #     return item
    # 在工作流程中可以看到,管道會得到圖片的URL並從專案中下載。
    # # 為了這麼做,你需要重寫 get_media_requests() 方法,並對各個圖片URL返回一個Request:
    def get_media_requests(self, item, info):
        # 這裡把item傳過去,因為後面需要用item裡面的name作為檔名
        yield Request(item['articleUrl'])

    #修改圖片生存名稱規則
    def file_path(self, request, response=None, info=None):
        item = request.meta['item']
        image_guid = request.url.split('/')[-1]  # 倒數第一個元素
        filenames = "full/%s/%s" % (item['name'], image_guid)
        # print(filename)
        return filenames
複製程式碼

修改完pipelinse後,還要修改下settings.py檔案;
找到ITEM_PIPELINES把註釋去掉,啟用pinelines, 把我們自定義的PicPipeLine加上,還有順道設定下下載圖片的存放位置:

ITEM_PIPELINES = {
   'movie_scrapy.pipelines.MovieScrapyPipeline':300,
}
IMAGES_STORE = "F:\pics"
複製程式碼

然後再執行,圖片的嗶哩吧啦的下載啦~

這裡遇到一個問題,自帶的ImagePipeline儲存圖片功能,同一份程式碼,在Linux下可以下載圖片,但是在Windows下就不能下載,
但是程式碼沒報錯,沒找到原因,所以後來才用urlretrieve下載的,不知道有同學知道原因嗎?

原始碼如下: movie_spiders.py

import scrapy
from scrapy.selector import Selector
from scrapy import Request
from movie_scrapy.items import MovieScrapyItem
import re
import os
import urllib

class movie_spiders(scrapy.Spider):
    name = "movie"
    host = "http://www.id97.com"
    start_urls = ["http://www.id97.com/movie/"]

    def parse(self,response):
        selector = Selector(response)
        divs = selector.xpath("//div[@class='col-xs-1-5 col-sm-4 col-xs-6 movie-item']")
        pages = selector.xpath("body/div/div[@class='pager-bg']/ul/li")
        for div in divs:
            yield self.parse_item(div)


        nextPageUrl = self.host + (pages[- 2].xpath('a/@href').extract_first())


        yield Request(nextPageUrl,callback=self.parse)


    def parse_item(self,div):
        item = MovieScrapyItem()
        item['articleUrl'] = div.xpath('div[@class="movie-item-in"]/a/img/@data-original ').extract_first()
        item['movieName'] = div.xpath('div[@class="movie-item-in"]/a/@title').extract_first()

        score = div.xpath('div[@class="movie-item-in"]/div[@class="meta"]/h1/em/text()').extract_first()
        item['scoreNumber'] = self.convertScore(score)

        style = div.xpath(
            'string(div[@class="movie-item-in"]/div[@class="meta"]/div[@class="otherinfo"])').extract_first(
            default="")
        item['style'] = style


        #下載圖片使用
        if item['articleUrl']:
            file_name = "%s.jpg"%(item['movieName'])
            file_path = os.path.join("F:\\pics", file_name)
            urllib.request.urlretrieve(item['articleUrl'], file_path)
        return item


    def convertScore(self, str):
        list = re.findall(r"\d+\.?\d*", str)
        if list:
            return list.pop(0)
        else:
            return 0
複製程式碼

items.py

import scrapy


class MovieScrapyItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    articleUrl = scrapy.Field()
    movieName = scrapy.Field()
    scoreNumber = scrapy.Field()
    style = scrapy.Field()
複製程式碼

pipelines.py

# -*- coding: utf-8 -*-
from scrapy.pipelines.images import ImagesPipeline
from scrapy.http import Request

class MovieScrapyPipeline(ImagesPipeline):
    # def process_item(self, item, spider):
    #     return item
    # 在工作流程中可以看到,管道會得到圖片的URL並從專案中下載。
    # # 為了這麼做,你需要重寫 get_media_requests() 方法,並對各個圖片URL返回一個Request:
    def get_media_requests(self, item, info):
        # 這裡把item傳過去,因為後面需要用item裡面的name作為檔名
        yield Request(item['articleUrl'])

    #修改圖片生存名稱規則
    def file_path(self, request, response=None, info=None):
        item = request.meta['item']
        image_guid = request.url.split('/')[-1]  # 倒數第一個元素
        filenames = "full/%s/%s" % (item['name'], image_guid)
        # print(filename)
        return filenames
複製程式碼

settings.py # -- coding: utf-8 --

BOT_NAME = 'movie_scrapy'

SPIDER_MODULES = ['movie_scrapy.spiders']
NEWSPIDER_MODULE = 'movie_scrapy.spiders'


ROBOTSTXT_OBEY = True



ITEM_PIPELINES = {
   'movie_scrapy.pipelines.MovieScrapyPipeline':300,
}
IMAGES_STORE = "F:\pics"
複製程式碼

小結

本章大方向學習了requests跟scrapy,但細節點還是不少:
xpath,yield,反爬蟲,網頁結構分析,為後續爬蟲做下預熱學習;

下文預告:
本來有些實戰例子是想選擇知乎等平臺的,還是發現登入的時候需要各種驗證碼,比如說選擇倒立的圖片等等,
所以下篇文章就以模擬登入,如何跨過驗證碼為前提去做,初步想法是覆蓋英文/數字驗證碼,滑動驗證碼,倒立點選驗證碼以及12306驗證碼~

碎片時間寫了10天,終於結束了,謝謝大家~

JB的Python之旅-爬蟲篇--requests&Scrapy

相關文章