《網路爬蟲開發實戰案例》筆記

行雲tack發表於2020-08-10

轉自行雲部落格https://www.xy586.top/

爬蟲基礎

1.HTTP基本原理

URI和URL

URI: 統一資源標誌符

URL: 統一資源定位符

HTTP和HTTPS

HTTP: 超文字傳輸協議,用於從網路傳輸超文字資料到本地瀏覽器的傳輸協議,它能夠保證高效準確的傳輸文字文件

HTTPS: 以安全為目標的HTTP通道,簡單來講是HTTP的安全版,即HTTP下加入SSL,所簡稱為HTTPS

HTTPS的安全基礎是SSL,因此通過它傳輸的內容都是經過SSL加密的,它的主要作用可以分為兩種:

  1. 建立一個資訊保安通道來保證資料傳輸的安全
  2. 確認網站的安全性,凡是使用HTTPS的網站,都可以通過點選瀏覽器位址列的鎖頭標誌來檢視網站認證的真實資訊

HTTP請求過程

  • 第一列name: 請求的名稱,一般會將URL的最後一部分當作名稱

  • 第二列status: 響應的狀態碼

  • 第三列type: 請求的文件資訊

  • 第四列initiator: 請求源,用來標記請求是由哪個物件或者程式發起的

  • 第五列size: 從伺服器下載的檔案和請求的資源大小,如果是從快取中取得的資源,則該列會顯示 from cache

  • 第六列time: 發起請求到獲取響應所用的總時間

  • 第七列waterfall: 網路請求的視覺化瀑布流

請求

請求,由客戶端向服務端發出,可以分為四個部分內容:請求方法、請求的網址,請求頭,請求體

  1. 請求方法: 常見的請求方法有兩種:GET 和 POST

    GET 和POST 請求方法有如下區別:

    • GET請求的引數包含在URL裡面,資料可以在URL中看到,而POST請求的URL不會包含這些資料,資料都是通過表單形式傳輸的,會包含在請求體中
    • GET請求提交的資料最多隻有1024位元組,而POST方式沒有限制
  2. 請求的網址: URL

  3. 請求頭:

    • Accept: 請求報頭域,用於指定客戶端可接受哪些型別的資訊

    • Accept-Language: 指定客戶端可接受的語言型別

    • Accept-Encoding: 指定客戶端可接受的語言型別

    • Host: 用於指定請求資源的主機 IP 和 埠號,其內容為請求URL 的原始伺服器或者閘道器的位置

    • Cookie: 這是網站為了辨別使用者進行會話跟蹤而儲存在使用者本地的資料,他的主要功能就是維持當前訪問會話,例如,我們輸入使用者名稱和密碼成功登入某個網 站後,伺服器會用會話儲存登入狀態資訊,後面我們每次重新整理或請求該站點的其他頁面時, 會發現都是登入狀態,這就是 Cookies 的功勞。Cookies 裡有資訊標識了我們所對應的伺服器 的會話,每次瀏覽器在請求該站點的頁面時,都會在請求頭中加上 Cookies 並將其傳送給服

      務器,伺服器通過 Cookies 識別出是我們自己,並且查出當前狀態是登入狀態,所以返回結 果就是登入之後才能看到的網頁內容

    • Referer: 此內容來標識這個請求是從哪個頁面發過來的,伺服器可以拿到這一資訊並做相應的處理,如做來源統計、防盜鏈處理

    • User-Agent: 簡稱UA ,他是一個特殊的字串,可以使伺服器識別客戶使用的作業系統及版本,瀏覽器即版本資訊,在做爬蟲時加上此資訊,可以偽裝為瀏覽器,如果不加,很可能會被識別為爬蟲

    • Content-Type: 也叫網際網路媒體型別,在HTTP協議訊息頭中,它用來表示具體請求的媒體類資訊,例如text/html 代表HTML格式 、application/json 代表 JSON 型別

      因此,請求頭是請求的重要組成部分,再寫爬蟲時,大部分情況下都需要設定請求頭

  4. 請求體

    請求體一般承載的內容是POST請求中的表單資料,而對於GET請求,請求體為空

    在爬蟲中,如果要構造POST請求需要使用正確的Content-Type。並瞭解各種請求庫的各個引數設定時使用的是哪種Content-Type,不然可能會導致POSt提交後無法正常響應

響應

響應,由伺服器端返回給客戶端,可以分為但部分:響應狀態碼、響應頭、響應體

  1. 響應狀態碼

    狀態碼描述
    200伺服器正常響應
    404頁面未找到
    500伺服器內部發生錯誤
  2. 響應頭

    響應頭包含了伺服器對請求的應答資訊,如Content-Type、Server、Set-Cookie

    • Date: 標識響應產生的時間
    • Last-Modified: 指定資源的最後修改時間
    • Content-Encoding: 指定響應內容的編碼
    • Server: 包含伺服器的資訊,比如名稱、版本號
    • Content-Type: 文件型別,代表返回的資料型別是什麼
    • Set-Cookie: 設定Cookies 響應頭中的Set-Cookie 告訴瀏覽器需要將此內容放在Cookies中,下次請求攜帶Cookies
    • expires: 指定響應的過期時間,可以使代理伺服器或瀏覽器將載入的內容跟新到快取中,如果再次訪問時,就可以直接從快取中載入,降低伺服器負載,縮短載入時間
  3. 響應體

    最重要的就是響應體,響應的正文資料都在響應體中

2.網頁基礎

網頁的組成

  • HTML: 超文字標記語言

  • CSS: 層疊樣式表,樣式指網頁中文字大小、顏色、元素間距、排列等格式

  • JavaScript: 指令碼語言,時使用者與資訊之間不只是一種瀏覽與顯示的關係,而是實現了一種實時、動態、互動的頁面功能

    綜上所述,HTML定義了網頁的內容和結構,CSS描述了網頁的佈局,JS定義了網頁的行為

網頁的結構

  • 節點樹及節點間的關係:

    在HTML中,所有標籤定義的內容都是節點,他們構成了一個HTML DOM 樹

    DOM: 文件物件模型,它允許程式和指令碼動態的訪問和更新文件的內容、結構、樣式

  • 選擇器:

爬蟲的基本原理

​ 我們可以把網際網路比作一張大網,而爬蟲便是在網上爬行的蜘蛛,把網的節點比作一個個網頁,爬蟲爬到這就相當於訪問了該頁面,獲取了其資訊,可以把節點之間的連線比作網頁與網頁之間的連線關係,這樣蜘蛛通過一個節點,可以順著節點連線繼續爬到下一個節點,網站的資料就可以被抓取下來了

  • 爬蟲概述:

    1. 獲取網頁
    2. 提取資訊
    3. 儲存資料
    4. 自動化程式
  • JavaScript渲染頁面:

    有時候我們在用urllib 或 requests 抓取網頁時,得到的原始碼實際和瀏覽器中看到的不一樣,但是在用 lib request 等庫請求當前頁面時,我們得到的只是這個 HTML 碼,它不會幫助 我們去繼續載入這個 JavaScript 檔案,這樣也就看不到瀏覽器中的內容了,因此,使用基本 HTTP 請求庫得到的原始碼可能跟瀏覽器中的頁面原始碼不太一樣 對於這樣的情 況,我們可以分析其後臺 Ajax 介面,也可使用 Se nium Splash 這樣的庫來實現模擬 JavaScript 渲染

會話的基本原理

  • 無狀態HTTP

    HTTP協議對事務處理是沒有記憶功能的,也就是說伺服器不知道客戶端是什麼狀態。

    這時兩個用於保持 HTTP 接狀態的技術就出現了,它 分別是會話和 Cookies 會話在服務端。

    也就是網站的伺服器,用來儲存使用者的會話資訊; Cookies 在客戶端,也可以理解為瀏覽器端,有

    Cookies ,瀏覽器在下次訪問網頁時會自動附帶上它傳送給伺服器,伺服器通過識別 Cookjes 並鑑定出

    是哪個使用者,然後再判斷使用者是否是登入狀態,然後返回對應的響應。

    我們可以理解為 Cookies 裡面儲存了登入的憑證,有了它,只需要在下次請求攜帶 Cookies 傳送

    請求而不必重新輸入使用者名稱、密碼等資訊重新登入了。

    因此在爬蟲中,有 候處理需要登入才能訪問的頁面時,我們一般會直接將登入成功後獲取的

    Cookies 放在請求頭裡面直接請求,而不必重新模擬登入。

  • 屬性結構

    • name: Cookie的名稱,一旦建立,該名稱便不可更改
    • value: 改Cookie的值,如果值為Unicode字元,需要為字元編碼。如果值為二進位制資料,則需要使用BASE64 編碼
    • domain: 可以訪問改Cookie的域名,例如,如果設定為.zhihu.com ,則所有以 zhihu.com 結尾的域名都可以訪問該Cookie
    • Max Age: 該Cookie失效的時間
    • path: 該Cookie 的使用路徑,如果設定為 /,則本域名下的所有頁面都可以訪問該Cookie
  • 會話 Cookie 和持久 Cookie

    從表面意思來說,會話 Cookie 就是把 Cookie 放在瀏覽器記憶體裡,瀏覽器在關閉之後該 Cookie 失效 持久 Cookie 會儲存到客戶端的硬碟中,下次還可以繼續使用,用於長久保持使用者登入狀態 其實嚴格來說,沒有會話 Cookie 和持久 Cookie 分,只是由 ookie Max Age Expires 欄位 決定了過期的時間

代理的基本原理

我們在做爬蟲的過程巾經常會遇到這樣的情況 最初爬蟲正常執行,正常抓取資料,一切看起來 都是那麼美好,然 杯茶的功夫可能就 出現錯誤,比如 403 Forbidden 這時候開啟網頁一看 ,可 能會看到“您的 IP 訪問頻率太高”這樣的提示 出現這種現象的原因是網站採取了一些反爬蟲措施 比如,伺服器會檢測某個 IP 在單位時間內的請求次數,如果超過了這個闊值,就會直接拒絕服務,返 問一些錯誤資訊,這種情況可以稱為封 IP,既然服務 檢測的是某個 IP 單位時間的請求次數,那麼藉助某種方式來偽裝我們的 IP ,讓服 器識別不出是由我們本機發起的請求

  • 代理的作用:

    1. 突破自身IP的訪問限制,訪問一些平時不能訪問的站點
    2. 訪問一些單位或團體內部資源:比如使用教育網內地址段免費代理伺服器,就可以用於對教育網開放的各類FTP下載上傳,以及各類資料查詢共享服務
    3. 提高訪問速度,通常代理伺服器都設定一個較大的硬碟緩衝區,當外界的資訊通過時,同時也將其儲存到緩衝區中,當其他使用者在訪問相同的資訊時,則直接由緩衝區取出資訊,傳給使用者,以提高訪問速度
    4. 隱藏真實的IP,上網者也可以通過這種方式隱藏自己的IP,免受攻擊,對於爬蟲來說,我們用代理就是為了隱藏自身IP,防止自身的IP被封鎖
  • 爬蟲代理

    對於爬蟲來說,由於爬蟲爬取速度過快,在爬取過程中可能遇到同 IP 訪問過於頻繁的問題,

    此時網站就會讓我們輸入驗證碼登入或者直接封鎖 ,這樣會給爬取帶來極大的不便

    使用代理隱藏真實的 IP ,讓伺服器誤以為是代理伺服器在請求向自己 這樣在爬取過程中通過不斷

    更換代理,就不會被封鎖,可以達到很好的爬取效果

  • 代理分類

    1. 根據協議區分
      • FTP代理伺服器,主要用於訪問FTP伺服器,一般有上傳、下載、快取功能,埠一般為 21 、2121 等
      • HTTP 代理伺服器:主要用於訪問網頁,一般有內容過濾和快取功能,埠一般為 80 、8080、3128
      • SSL/TLS:主要用於訪問加密網站,一般有SSL 或者 TLS 加密功能(最高支援128位加密強度),埠一般為443
      • RTSP代理:主要用於訪問Real流媒體伺服器,一般有快取功能,一般埠為554
      • Telnet代理:主要用於telnet 遠端控制(黑客入侵計算機時常用於隱藏身份),埠一般為23
      • POP3/SMTOP代理:主要用於POP3/SMTP方式收發郵件,一般有快取功能,埠一般為110、25
      • SOCKS代理:只是單純傳遞資料包。不關心具體協議和用法,所有速度很快,埠一般為1080
    2. 根據匿名程度區分
      • 高度匿名代理:會將資料包原封不動的轉發,在伺服器看來就好像是一個真正的普通客戶端在訪問,而記錄的IP是代理伺服器的IP
      • 普通匿名代理:會在資料包上做一些改動,服務端有可能發現這是個代理伺服器,也有一定機率追查到客戶端的真實IP
      • 透明代理:不但改動了資料包,還會告訴伺服器客戶端的真實IP,這種代理除了能用快取技術提高瀏覽速度,能用內容過濾提高安全效能之外,並無其他顯著作用,最常見的例子就是內網中的硬體防火牆
  • 常見的代理設定

    • 使用網上的免費代理: 最好使用高匿名代理,另外可用的代理不多,需要在使用前篩選一下可用代理,也可以進一步維護一個代理池
    • 使用付費代理服務: 質量比免費代理還很多
    • ADSL撥號: 播一次號換一次IP ,穩定性高,也是一種比較有效的解決方案

3.requests庫的基本使用

基本用法

  • GET請求:

    import requests
    r = requests.get(’http://httpbin.org/get‘)
    
    """
    輸出結果:
    {
      "args": {}, 
      "headers": {
        "Accept": "*/*", 
        "Accept-Encoding": "gzip, deflate", 
        "Host": "httpbin.org", 
        "User-Agent": "python-requests/2.22.0"
      }, 
      "origin": "220.202.133.149, 220.202.133.149", 
      "url": "https://httpbin.org/get"
    }
    """
    

    r . json()方法:將返回的結果是JSON格式的字串轉為一個字典格式

    一般要在headers上加上User-Agent資訊

    headers = {
                  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36"
              }
    
  • POST請求:

    import requests
    data = {
        'name':'donmo',
        'age' : 20
    }
    r = requests.post('http://httpbin.org/post',data = data)
    print(r.text)
    
    """
    {
      "args": {}, 
      "data": "", 
      "files": {}, 
      "form": {
        "age": "20", 
        "name": "donmo"
      }, 
      "headers": {
        "Accept": "*/*", 
        "Accept-Encoding": "gzip, deflate", 
        "Content-Length": "17", 
        "Content-Type": "application/x-www-form-urlencoded", 
        "Host": "httpbin.org", 
        "User-Agent": "python-requests/2.22.0"
      }, 
      "json": null, 
      "origin": "220.202.133.149, 220.202.133.149", 
      "url": "https://httpbin.org/post"
    }
    我們可以成功獲取返回結果,其中form部分就是提交的資料,這就證明了post請求成功傳送了
    """
    
    
  • 響應

    import requests
    headers = {
                  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36"
              }
    r = requests.get('https://www.jianshu.com/',headers = headers)
    print(type(r.status_code))
    print(type(r.headers))
    print(type(r.cookies))
    print(type(r.url))
    print(type(r.history))
    
    """
    <class 'int'>
    <class 'requests.structures.CaseInsensitiveDict'>
    <class 'requests.cookies.RequestsCookieJar'>
    <class 'str'>
    <class 'list'>
    """
    

高階用法

  1. 檔案上傳:

    我們可以知道requests可以模擬提交一些資料,假如有的網站需要上傳檔案,我們也可以用它來實現

    import requests
    files = {
        'file':open('爬取的資源/favicon.ico','rb')
    }
    r = requests.post('http://httpbin.org/post',files = files)
    print(r.text)
    
    """
    {
      "args": {}, 
      "data": "", 
      "files": {
        "file": "data:application/octet-........
      }, 
      "form": {}, 
      "headers": {
        "Accept": "*/*", 
        "Accept-Encoding": "gzip, deflate", 
        "Content-Length": "6665", 
        "Content-Type": "multipart/form-data; boundary=bcf321b8c23ed3ffec5c8c2ea30f3c40", 
        "Host": "httpbin.org", 
        "User-Agent": "python-requests/2.22.0"
      }, 
      "json": null, 
      "origin": "220.202.133.149, 220.202.133.149", 
      "url": "https://httpbin.org/post"
    }
    裡面包含 files 這個欄位,而 form 欄位是 的,這證明檔案上傳部分會單獨有一個 files 欄位來標識
    
    """
    
  2. Cookies

    import requests
    r = requests.get('https://www.baidu.com')
    print(r.cookies)
    for key,value in r.cookies.items():
        print(key + "=" + value)
        
    """
    <RequestsCookieJar[<Cookie BDORZ=27315 for .baidu.com/>]>
    BDORZ=27315
    
    這裡我們首先呼叫 Cookies 屬性即可成功得到 Cookies 可以發現它 RequestCookiJar 型別
    然後 items()方法將其轉為元組組成的列表  遍歷輸出每一個 Cookie 名稱和值  實現Cookie
    的遍歷解析
    """
    
  3. 會話維持

  4. 代理設定

正規表示式

模式描述
\w匹配字母,數字及下劃線
\W匹配不是字母,數字及下劃線
\s匹配任意空白字元,等價於【\t \n \r\ \f】
\S匹配任意非空字元
\d匹配任意數子 等價於【0-9】
\D匹配任意非數字的字元
\A匹配字串開頭
\Z匹配字串結尾,如果存在換行,只匹配換行前的結束字元
\z匹配字串結尾,如果存在換行,同時還會匹配換行符
\G匹配最後完成匹配的位置
\n匹配一個換行符
\t匹配一個製表符
^匹配一行字串的開頭
$匹配一行字串的結尾
.匹配任意字元,除了換行
[…]用來表示一組字元,單獨列出,比如[amk] 匹配a、m、或 k
[^…]不在[ ]中的字元
*匹配0 個 或多個表示式
+匹配一個或多個表示式
匹配 0 個或 1 個前面的正規表示式定義的片段,非貪婪方式
{n}精確匹配 n 個前面的表示式
{n,m}匹配 n 到 m 次由前面正規表示式定義的片段,貪婪方式
a|b匹配 a 或 b
( )匹配括號內的表示式,也表示一個組
  • match(): 向他傳入要匹配的字串以及正規表示式,從字串的開頭開始匹配
  • 貪婪匹配:儘可能匹配多得字元
  • 非貪婪匹配:儘可能匹配少的字元
  • search(): 掃描整個字串,然後返回第一個成功匹配的結果
  • findall(): 該方法會搜尋整個字串,然後返回匹配正規表示式的所有內容,有結果就是列表型別
  • sub(): 可以藉助他來修改文字,第一個引數傳入匹配的內容,第二個引數為替換的字串,第三引數是原字串
  • compile(): 將正則字串編譯成正規表示式物件,以便在後面的匹配中複用

4.解析庫的使用

使用XPath

XPath:全稱XML Path Language ,即XML 路徑語言,是一門在XML 文件中查詢資訊的語言

官網:[https://www.w3.org/TR/xpath/]( XPath 官方文件)

  1. XPath 概覽: 幾乎所有我們想要定位的節點,都可以用XPath 來選擇

  2. XPath 常用規則:

    表示式描述
    nodename選取此節點的所有子節點
    /從當前節點選取直接子節點
    //從當前節點選取子孫節點
    .選取當前節點
    選取當前節點的父節點
    @選取屬性
  3. XPath 常用匹配規則:

    //title[@lang = ‘eng’] : 代表選擇所有名稱為 title,同時屬性 lang 的值為 eng 的節點

  4. 例項引入:

    from lxml import etree
    text = ' html .....'
    html = etree.HTML(text)
    result = etree.tostring(html)  # 這結果是btyes 型別 可以利用decode('utf-8') 轉成str 型別
    
    • # 也可以直接讀取文字檔案進行解析
      from lxml import etree
      html = etree.parse('./test.html',etree.HTMLParse())
      result = etree.tostring(html)
      
  5. 文字的獲取: text()

  6. 屬性值多匹配: contains() 函式

    from lxml import etree
    text = '<li class = "li li-list">...</li>'
    html = etree.HTML(text)
    result = html.xpath('//li[contains(@class,"li")]')
    
  7. 多屬性匹配: 根據多個屬性確定一個節點,這時就需要同時匹配多個屬性,此時可以使用 and 來連線

    from lxml import etree
    text = '<li class = "li li-list" name = "item">...</li>'
    html = etree.HTML(text)
    result = html.xpath('//li[contains(@class,"li") and @name = "item"]')
    
  8. 按序選擇: 我們在選擇的時候可能同時匹配了多個節點,但只想要其中的某個節點

    result1 = html.xpath('//li[1]')  #選取第一個li 節點    li[1]
    result2 = html.xpath('//li[last()]')  #選取最後一個 li 節點   last()
    result3 = html.xpath('//li[position()<3]')  #選取位置小於3 的li 節點 也就1 2 節點  position()<3
    result4 = html.xpath('//li[last() - 2]')  #選取倒數第三個 li 節點  last() -2
    
  9. 節點軸選擇:

    軸名稱描述
    //li[1]/ancestor:?匹配第一個li 節點的所有祖先節點
    //li[1]/ancestor::div只有div 這個祖先節點
    //li[1]/attribute:?第一個li 節點的所有屬性值
    //li[1]/child::a[@href = “link.html”]獲取 href 屬性為link.html 的所有直接a 子節點
    //li[1]/descendant::span獲取所有的子孫節點 所有的span節點
    //li[1]/following:?[2]獲取當前節點之後的所有節點 這裡只獲取第二個後續節點
    //li[1]/following - sibling:: *獲取當前結點之後的所有同級節點

使用Beautiful Soup

Beautiful Soup : 藉助網頁的結構和屬性等特性來解析網頁

  1. 解析器:

    Beautiful Soup 在解析式 實際上依賴解析器,lxml 解析器有解析 HTML 和 XML 的功能, 而且速度快,容錯能力強,所以推薦使用它

  2. 基本用法:

    import requests
    from bs4 import BeautifulSoup
    
    response = requests.get('https://zhidao.baidu.com/question/268587066.html')
    response.encoding = 'gbk'
    html = response.text
    soup = BeautifulSoup(html,'lxml')
    print(soup.prettify())
    print(soup.title.string)
    
    # prettify()方法: 把要解析的字串以標準的縮排格式輸出
    # soup.title.string:  輸出 HTML 中 title 節點的文字內容
    
  3. 節點選擇器: 直接呼叫節點的名稱就可以選擇節點元素,在呼叫string屬性就可以得到節點的文字內容

    • 選擇元素

      print(soup.title)
      print(type(soup.title))
      print(soup.title.string)
      
      '''
      輸出:
      
      <title>什麼是子節點_百度知道</title>
      <class 'bs4.element.Tag'>
      什麼是子節點_百度知道
      '''
      
    • 提取資訊

      1. 獲取名稱:利用name 屬性來獲取節點的名稱

        print(soup.title.name)    # title
        
      2. 獲取屬性 : 呼叫 attrs 獲取所有屬性

        print(soup.p.attrs)
        print(soup.p.attrs['name'])
        
        '''
        輸出:
        {'class':['title'],name = 'dromouse'}
        dromouse
        '''
        
      3. 獲取內容

        print(soup.p.string)
        
        #注意: 這裡選擇到的 p 節點是第一個 p 節點,獲取文字也是第一個 p 節點裡面的文字
        
    • 巢狀選擇:

      html = '''<html>
      			<title>
      				The Dromouse's stroy
      			</title>
      		</html> '''
      from bs4 import BeautifulSoup
      soup = BeautifulSoup(html,'lxml')
      print(soup.html.title)
      print(soup.html.title.string)
      
      '''
      輸出:
      <title>The Dromouse's stroy</title>
      The Dromouse's stroy
      '''
      
    • 關聯選擇:

      1. 子節點和子孫節點 :

        • 選取節點元素之後,如果想要獲取它的直接子節點,可以呼叫 contents 屬性

        返回的結果是列表形式,包含文字 又包含節點,列表中的每個元素都是p 節點的直接子節點,所以說,contents 屬性得到的結果是直接子節點的列表,

        • 同樣的 可以呼叫 children 屬性得到相應的結果, 但返回的結果是生成器型別
        • 如果要得到所有的子孫節點的話,可以呼叫 descendants 屬性 返回結果是生成器,遞迴查詢所有的子節點,得到所有的子孫節點
      2. 父節點和祖先節點

        如果獲取某個節點元素的父節點,可以呼叫 parent 屬性 ,注意: 這裡得到的只是直接父節點,而沒有再向外尋找父節點的祖先節點,如果想要獲取所有的祖先節點, 可以呼叫 parents屬性

      3. 兄弟節點

        • next_sibling ; 獲取節點的下一個兄弟元素
        • previous_sibling : 獲取節點的上一個兄弟元素
        • next_siblings : 獲取節點的下面所有兄弟元素
        • previous_siblings : 獲取節點的上面所有兄弟元素
      4. 提取資訊 :string 、attrs 屬性 獲得 其文字和屬性

      5. 方法選擇器

        • find_all() :

          傳入一些屬性 或 文字 ,就可以得到符合條件的元素

          1. soup.find_all(name = ‘li’)

          2. soup.find_all( attrs = {‘id’: ‘list-1’ }) 引數為字典的格式

            一些常用的屬性,可以不用 attrs 來傳參 soup.find_all( id = ‘list1’) soup.find_all( class_ = ‘elemen’)

        • find(): 返回單個元素,也就是第一個匹配的元素

        • find_parents() 和 find_parent()

        • find_next_siblings() 和 find_next_sibling()

        • find_previous_siblings() 和 find_previous_sibling()

    • CSS 選擇器

      只需要呼叫select()方法 ,傳入相應的CSS 選擇器即可

使用pyquery

  1. 初始化: 它的初始化有多種,傳入字串、URL、檔名、等等

  2. 基本CSS 選擇器

  3. 查詢節點:

    這些函式和jQuery 中函式的用法完全相同

    • 子節點: 用法哦find() 方法,傳入的引數是 CSS 選擇器 ,查詢的是所有的子孫節點

      ​ 用 children()方法 只查詢子節點 ,可以傳入CSS 選擇器

    • 父節點: 用parent()方法獲取某個節點的父節點,直接fujiedian

      用parents() 方法可以獲得節點的祖先節點

    • 兄弟節點: 使用siblings()方法 獲取兄弟節點

  4. 遍歷:

    pyquery 的選擇結果可能是多個節點,也可能是單個節點,並沒有返回像Beautiful Soup 那樣的列表

    lis = doc('li').items()
    for li in lis:
        print(li)
        
    # 呼叫items()方法後,會得到一個生成器
    
  5. 獲取資訊:

    • 獲取屬性:提取到某個 pyquery 型別的節點後,就可以呼叫attr()方法來獲取屬性

      ​ print(a.attr.href) print(a…attr(‘href’))

      當返回結果包含多個節點時,呼叫 attr()方法,只會得到第一個節點的屬性,那麼這個情況,如果想要獲取所有的節點的屬性,那麼就要遍歷了

    • 獲取文字: 呼叫 text()方法來實現節點內部的文字資訊,返回純文字內容

      ​ html()方法 會返回節點的所有HTML文字

      ​ 但如果返回多個節點的時候想要獲取文字的時候,text()直接返回多個文字資訊,中間用空格隔開,而html() 則需要遍歷才能拿到所有的文字資訊

  6. 節點操作:

    pyquery提供了一系列方法來對節點進行動態修改,比如為某個節點新增一個class ,移除某個節點等,這些操作回味提取資訊帶來極大的方便

    • addClass 和 removeClass : 動態改變節點的 class屬性

    • attr、text 和 html:attr() 對屬性進行操作,

      text():節點內的全部文字改為傳入的字串文字

      html():節點內的全部文字變成傳入的HTML 文字

      所以說 ,attr(),text(),html()只傳入一個引數 就是獲取這個引數的屬性值,傳入兩個引數,就可以來修改內容

    • remove :remove()就是移除

  7. 偽類選擇器:

5.資料儲存

檔案儲存

TXT文件儲存

open():讀一個引數為目標檔案的名稱,第二個引數為開啟方式,第三個引數可以設定為字元編碼

open(‘info.txt’,‘a’,‘utf-8’)

  1. 檔案開啟方式 :

    • r :以只讀的方式開啟檔案,檔案的指標會放在檔案的開頭,這是預設模式
    • rb : 以二進位制只讀方式開啟一個檔案
    • r+ :以讀寫方式開啟一個檔案,檔案指標放在檔案的開頭
    • rb+ : 以二進位制讀寫方式開啟一個檔案
    • w :以寫入方式開啟一個檔案,如果該檔案已存在,則將其覆蓋,如不存在將建立其檔案
    • wb :以二進位制寫入方式開啟一個檔案
    • w+ :以讀寫方式開啟一個檔案,如果該檔案已存在,則將其覆蓋,如不存在將建立其檔案
    • wb+ :以二進位制讀寫方式開啟一個檔案
    • a :以追加方式開啟一個檔案,檔案指標放在檔案的末尾,檔案如果不存在將會建立新的檔案來寫入
    • ab :以二進位制追加方式開啟一個檔案
    • a+ : 以讀寫方式開啟一個檔案,檔案指標放在檔案的末尾
    • ab+ :以二進位制追加方式開啟一個檔案
  2. 簡化寫法

    with open('info','a',encoding = 'utf-8') as file:
        file.write('hello word')
    

JSON檔案儲存

​ JSON:JavaScript 物件標記

  • 物件:在JavaScript中是使用 {} 包裹起來的內容,資料結構為:{key1:value1,key2:value2,…} 的鍵值對結構,在物件導向的語言中,key 為物件的屬性,value 為對應的值

  • 陣列:陣列在JavaScript 中是 [] 包裹起來的內容,資料結構 [“java”,“JavaScript”,…] 的索引結構

    import json
    str = '''
        [{
            "name":"Bob",
            "age":20,
            "sex":"男"
        },
        {
            "name":"Rose",
            "age":"18",
            "sex":"女"
        }
        ]
    '''
    data  = json.loads(str)
    print(data)
    
    # 輸出
    [{'name': 'Bob', 'age': 20, 'sex': '男'}, {'name': 'Rose', 'age': '18', 'sex': '女'}]
    

注意: JSON字串的表示需要用雙引號,否則loads()方法會解析失敗

從JSON文字中讀取內容:

with open("resource/info.json",'r',encoding='utf-8') as file:
    str = file.read()
    data = json.loads(str)
    print(data)

輸出JSON:

​ 呼叫dumps()方法將JSON物件轉化為字串

import json
str = [{
        "name":"Bob",
        "age":20,
        "sex":"男"
    },
    {
        "名字":"東魔",
        "年齡":"20",
        "性別":"男"
    }
    ]

with open('resource/info.json','w',encoding='utf-8') as file:
    file.write(json.dumps(str,ensure_ascii=False,indent = 2))
    print('寫入成功')
    
# 注意:在檔案開啟 open() 時加上以 'utf-8'編碼,在dump()時也要加上 ensure_ascii = False
# 不然會變成ascii 碼寫到json 檔案中
# indent引數:  代表縮排字元的個數 

這裡dumps()是將 dict 轉換成 str 格式 , loads() 是將str 轉換成 dict 格式

CSV檔案儲存

CSV:Comma-Separated Values ,中文叫做 逗號分隔值,其檔案以純文字形式儲存資料

import csv
with open('resource/data.csv','w') as csvfile:
    writer = csv.writer(csvfile)
    writer.writerow(['id','name','age'])
    writer.writerow(['1001','Mike',20])
    writer.writerow(['1002','Bob',18])
    writer.writerow(['1003','rose',15])

如果想修改列與列之間的分隔符,可以傳入delimiter引數 delimiter = ’ ’

字典的方式寫入

import csv
with open('resource/data.csv','w') as csvfile:
    fieldnames = ['id','name','age']
    writer = csv.DictWriter(csvfile,fieldnames = fieldnames)
    writer.writeheader()
    writer.writerow({'id':'1001','name':'xiaoming','age':'18'})
    writer.writerow({'id':'1002','name':'xiaohua','age':'20'})
    writer.writerow({'id':'1003','name':'xiaowang','age':'22'})

讀取:

import csv
with open('resource/data.csv','r',encoding='utf-8') as csvfile:
    reader = csv.reader(csvfile)
    for row in reader:
        print(row)
  • pandas 中的read_csv() 方法讀取csv檔案

    import pandas as pd
    
    df = pd.read_csv('resource/data.csv')
    print(df)
    

MySQL的儲存

  1. 連線內網伺服器上寶塔上的MySQL資料庫

    import pymysql
    db = pymysql.connect(host='172.20.32.111',user='donmo',password='47AdmG3hke7PTkzc',port=3306)
    cursor = db.cursor()
    cursor.execute('SELECT VERSION()')
    data = cursor.fetchone()
    print(data)
    
  2. 建立表

    import pymysql
    db = pymysql.connect(host='172.20.32.111',user='donmo',password='47AdmG3hke7PTkzc',port=3306,db='donmo')
    cursor = db.cursor()
    # 建立表
    sql = '''create table if not exists students(
                id varchar(255) not null ,
                name varchar(255) not null ,
                age int not null ,
                primary key (id)
    )'''
    cursor.execute(sql)
    db.close()
    
  3. 插入資料

    # 插入資料
    id = '201706030120'
    name = '東魔'
    age = 20
    sql = 'insert into students(id,name ,age) values (%s,%s,%s)'
    try:
        cursor.execute(sql,(id,name,age))
        db.commit()
    except:
        db.rollback()
    
    db.close()
    
  4. 事務的4個屬性

    屬性解釋
    原子性(atomicity)事務是不可分割的工作單位,事務包括的諸多操作要麼都做,要麼都不做
    一致性(consistency)事務必須使用資料庫從一個一致性狀態變到另一個一致性狀態,一致性與原子性是密切相關的
    隔離性(isolation)一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的資料對併發的其他事務是隔離的,併發執行的各個事務之間互不干擾
    永續性一個事務一旦提交,他對資料庫的改變應該是永久性的,接下來的其他操作或者故障不因該對其有任何影響
  5. 動態插入資料

    很多情況下,我們要達到的效果就是插入方法無需改動,做成一個通用的方法,只需要傳入一個動態的字典就好了

    # 動態字典插入資料
    data = {
        'id':'201706030102',
        'name':'劉湘雲',
        'age':20
    }
    table = 'students'
    keys = ','.join(data.keys())
    values = ','.join(['%s']*len(data))
    sql = f'insert into {table}({keys}) values ({values})'
    
    try:
        if cursor.execute(sql,tuple(data.values())):
            print("success")
            db.commit()
    except:
        print('failed')
        db.rollback()
    
    db.close()
    

    INSERT INTO students (id,name,age)VALUES (%s,%s,%s)

    因此我們就達到實現傳入一個字典來插入資料的方法,不需要再去修改SQL語句和插入操作了

  6. 更新資料

    • # 更新資料
      sql = 'update students set age=%s where name =%s'
      try:
          if cursor.execute(sql,(18,'東魔')):
              print('sucess')
              db.commit()
      except:
          print('filed')
          db.rollback()
      
      
    • 實際情況下,我們關心的是會不會出現重複的資料,如果出現了,我們希望的是更新資料,而不是重複儲存一次,所以可以實現一種去重的方法

      # 去重更新資料
      data = {
          'id':'201706030120',
          'name':'donmo',
          'age':20
      }
      table = 'students'
      keys = ','.join(data.keys())
      values = ','.join(['%s']*len(data))
      sql = f'insert into {table}({keys}) values ({values}) on duplicate key update '
      update = ','.join([f"{key} = %s" for key in data])
      sql += update
      
      try:
          if cursor.execute(sql,tuple(data.values())*2):
              print('success')
              db.commit()
      except:
          print('field')
          db.rollback()
      

      insert into students(id,name,age) values (%s,%s,%s) on duplicate key update id = %s,name = %s,age = %s

      ON DUPLICATE KEY UPDATE :如果主鍵已存在,就執行更新操作,如此一來,就可以實現主鍵不存在便插入資料,主鍵存在就更新資料

  7. 刪除資料

    # 刪除資料
    table = 'students'
    condition = 'age<20'
    sql = f'delete from {table} where {condition}'
    try:
        cursor.execute(sql)
        db.commit()
    except:
        db.rollback()
    
  8. 查詢資料

    • sql = 'select * from students'
      try:
          cursor.execute(sql)
          print('count:',cursor.rowcount)  # 獲取查詢結果的條數
          one = cursor.fetchone()  #  返回結果的第一條資料
          print('one:',one)
          result = cursor.fetchall() #  得到結果的所有資料   結果為 二重元組
          print('all:',result)
          for row in result:
              print(row)
      except:
          print('error')
          
      # fetchall()  內部實現有一個偏移指標來指向查詢結果,去一次後就往下移一位,因此用了一次fetchone()  再用fetchall() 資料會少一條
      
    • 如果資料量很大,那麼fetchall()方法佔用的開銷會非常高,因此推薦如下方法逐條取資料

      # 逐條查詢資料
      sql = 'select * from students'
      try:
          cursor.execute(sql)
          print('count:',cursor.rowcount)
          row = cursor.fetchone()
          while row:
              print('Row:',row)
              row = cursor.fetchone()
      except:
          print('error')
      

非關係型資料庫儲存

MongoDB儲存:

Mongodb: 由C++ 語言編寫的非關係型資料庫,是一個基於分散式檔案儲存的開源資料庫系統,其內容儲存形式類似 JSON 物件,它的欄位值可以包含其他文件,陣列,及文件陣列。非常靈活

  1. 連線MongoDB

    ​ 用到pyMongo 庫裡面的 MongoClient ,一般來說,傳入MongoDB 的IP 及埠即可

    import pymongo
    # 連線MongoDB
    client = pymongo.MongoClient(host='172.20.32.111',port=27017)
    
    # 指定資料庫
    db = client.mongo_donmo
    # db = client['donmo'] 方法二
    
    # 指定集合  類似於關係型資料庫中的表
    collection = db.students
    # collection = db['students']   方法二
    
    
  2. 插入資料

    官方推薦使用inert_one 和 insert_many 方法來插入單條記錄和多條記錄

    • import pymongo
      # 連線MongoDB
      client = pymongo.MongoClient(host='172.20.32.111',port=27017)
      
      # 指定資料庫
      db = client.mongo_donmo
      # db = client['donmo'] 方法二
      
      # 指定集合  類似於關係型資料庫中的表
      collection = db.students
      # collection = db['students']   方法二
      
      # 插入單條資料
      student = {
          'id': '201706030120',
          'name':'東魔',
          'age':20,
          'gender':'male'
      }
      result = collection.insert_one(student)
      print(result)
      print(result.inserted_id)
      
    # 插入多條資料
    student = {
        'id': '201706030124',
        'name':'黃家偉',
        'age':20,
        'gender':'male'
    }
    student1 = {
        'id': '201706030105',
        'name':'老蔡',
        'age':20,
        'gender':'male'
    }
    result = collection.insert_many([student,student1])
    print(result)
    print(result.inserted_ids)
    
    # <pymongo.results.InsertManyResult object at 0x038F3800>
    # [ObjectId('5ddf48b7587b3e553dec7735'), ObjectId('5ddf48b7587b3e553dec7736')]
    
  3. 查詢

    我們可以利用find_one 或者find 方法來進行查詢,find_one 查詢得到的是單個結果,find 方法返回的是一個生成器物件

    • # 查詢資料
      result  = collection.find_one({'name':"趙波"})
      print(type(result))
      print(result)
      
      # <class 'dict'>
      # {'_id': ObjectId('5ddf46993c11ac435633f3d2'), 'id': '201706030134', 'name': '趙波', 'age': 20, 'gender': 'male'}
      # 這裡的 _id 屬性,是MongoDB 在插入過程中自動新增的
      
    results = collection.find({'age':20})
    print(results)
    for result in results:
        print(result)
        
    <pymongo.cursor.Cursor object at 0x03E7C4B0>  # 返回的結果是 Cursor  相當於一個生成器
    # {'_id': ObjectId('5ddf46993c11ac435633f3d2'), 'id': '201706030134', 'name': '趙波', 'age': 20, 'gender': 'male'}
    # {'_id': ObjectId('5ddf47b6bf9cb333f31b4408'), 'id': '201706030120', 'name': '東魔', 'age': 20, 'gender': 'male'}
    # {'_id': ObjectId('5ddf48a86269c96348b19455'), 'id': '201706030124', 'name': '黃家偉', 'age': 20, 'gender': 'male'}
    
    
    • 比較符號

      符號含義示例
      $lt小於{‘age’:{’$lt’:20}}
      $gt大於{‘age’:{’$gt’:20}}
      $lte小於等於{‘age’:{’$lte’:20}}
      $gte大於等於{‘age’:{’$gte’:20}}
      $ne不等於{‘age’;{’$ne’:20}}
      $in在範圍內{‘age’:{’$in’:[20,30]}}
      $nin不在範圍內{‘age’:{’$nin’:[20,30]}}
    • 正則查詢

      符號含義示例示例含義
      $regex匹配正規表示式{‘name’:{’$regex’:’^M,*’}}name 以M開頭
      $exists屬性是否存在{‘name’:{’$exists’:True}}name 屬性存在
      $type型別判斷{‘age’:{’$type’:‘int’}}age 的型別為 int
      $mod數字模操作{‘age’:{’$mod’:[5,0]}}age 取模5 為 0
      $text文字查詢{‘KaTeX parse error: Expected '}', got 'EOF' at end of input: text':{'search’:‘Mike’}}text 型別的屬性包含Mike字串
      $where高階條件查詢{’$where’:‘obj.fans_count == obj.follows_count’}自身粉絲數等於關注數

      MongoDB 官方文件:https://docs.mongodb.com/manual/reference/operator/query/

  4. 計數

    要統計查詢結果有多少條資料,可以呼叫count() 方法

    count = collection.find().count()
    print(count)
    
  5. 排序

    直接呼叫sort() 方法,在其中傳入排序的欄位及升序的標誌即可

    pymongo.ASCENDING: 指定升序排列

    pymongo.DESCENDING:指定降序排列

    # 排序
    results = collection.find().sort('id',pymongo.ASCENDING)
    print([result['name'] for result in results])
    
    # ['老蔡', '東魔', '黃家偉', '趙波']
    
  6. 偏移

    在某些情況下,我們可能只想取某幾個元素,這時就可以利用 skip() 方法偏移幾個位置

    results = collection.find().sort('id',pymongo.ASCENDING).skip(2)
    print([result['id'] for result in results])
    
    # ['黃家偉', '趙波']
    
    # limit() 方法指定要取結果的個數
    results = collection.find().sort('id',pymongo.ASCENDING).skip(2).limit(1)
    print([result['id'] for result in results])
    
    # ['黃家偉']
    

    注意: 在資料庫數量非常龐大的時候,最好不要用偏移量來查詢資料,因為這樣可能導致記憶體溢位

    from bso.objectid import ObjectId
    results = collection.find({'_id':{'$gt':ObjectId('5ddf46993c11ac435633f3d2')}})
    
    # 這是需要記錄好上次查詢的Id 
    
  7. 更新

    對於資料更新,我們可以使用update()方法,指定更新的條件和更新後的資料即可

    matched_count: 獲得匹配的條數

    modified_count:獲得影響的條數

    • condition = {'age':{'$in':[20]}}
      result = collection.update_many(condition,{'$inc':{'age':18}})
      print(result)
      print(result.matched_count,result.modified_count)
      
      # 年齡為20的 每條資料都加18
      
    condition = {'name': '老蔡'}
    student = collection.find_one(condition)
    student['name'] = '戴徐坤'
    result = collection.update_one(condition,{'$set':student})
    print(result)
    print(result.matched_count,result.modified_count)
    
  8. 刪除

    直接呼叫 remove() 方法指定刪除條件即可,符合條件的所有資料均會被刪除

    官方推薦:

    delete_one : 刪除第一條符合條件的資料

    delete_many: 刪除所有符合條件的資料

    result = collection.delete_one({'name':'huanxi'})
    print(result)
    
  9. 其他操作

    另外,PyMongo 還提供了一些組合方法,find_one_and_delete()、find_one_and_replace()、find_one_and_update()

    PyMongo 的詳細用法文件

Redis儲存:

Redis: 是一個基於記憶體的高效鍵值對型非關係型資料庫,存取效率極高,而且支援多種儲存資料結構,使用也非常簡單

  1. Redis 和 StrictRedis

    redis-py 庫提供這兩個類來實現Redistribution 的命令操作

  2. 連線Redis

    from redis import StrictRedis
    redis = StrictRedis(host='170.20.32.111',port=6379)
    
  3. 鍵操作

    方法作用引數說明結果
    exists(name)判斷一個鍵是否存在name:鍵名True
    delete(name)刪除一個鍵name:鍵名
    type(name)判斷鍵型別name:鍵名b’string’
    keys(pattern)獲取所有符合規則的鍵pattern:匹配規則[b’name’]
    randomkey()獲取隨機的一個鍵b’name’
    rename(src,dst)重新命名鍵src:原鍵名 dst:新鍵名True
    dbsize()獲取當前資料庫中鍵的數目
    expire(name,time)設定鍵的過期時間name:鍵名
    ttl(name)獲取鍵的過期時間name:鍵名
    move(name,db)將鍵移動到其他資料庫name:鍵名 db:資料庫代號
    flushdb()刪除當前資料庫所有的鍵
    flushall()刪除所有資料庫中所有的鍵
  4. 字串操作

    Redis 支援最基本的鍵值對形式儲存

    方法作用引數說明結果
    set(name,value)給資料庫中鍵為name的string賦予值valuename:鍵名 value:值True
    get(name)返回資料庫中鍵為name的string的valuename:鍵名b’Bob’
    getset(name,value)給資料庫中鍵為name的string賦予值value並返回上次的valuename:鍵名 value:新值b’Bob’
    mget(keys,*args)返回多個鍵對應的valuekeys:鍵的列表
    setnx(name,value)如果不存在這個鍵值對,則更新vallue,否則不變name:鍵名True/False
    setex(name,time,value)設定可以對應的值為string型別的value,並指定此鍵對應的有效期name:鍵名 time:有效期 value:值True
    setrange(name,offset,value)設定指定鍵的value值的子字串name:鍵名 offset:偏移量 value:值字串長度
    mset(mapping)批量賦值mapping:字典True
    msetnx(mapping)鍵均不存在時才批量賦值mapping:字典True
    incr(age,amount=1)鍵為age的value增值操作,預設為1,鍵不存在則被建立並設為amountname:鍵名 amount:增長值修改後的值
    decr(name,amount)減值操作
    append(key,value)鍵為name的string的值追加valuekey:鍵名修改的字串長度
    substr(name,start,end=-1)返回鍵為name 的string子字串name:鍵名 start:起始所以 end:終止索引b’ello’
    getrange(key,start,end)獲取鍵的value值從start到end的子字串key:鍵名 start:起始值 end:終止索引b’ello’
  5. 列表操作

    Redis 還提供了列表儲存,列表內的元素可以重複,而且可以從兩端儲存

    方法作用引數說明結果
    rpush(name,*values)在鍵為name的列表末尾新增值為value的元素name:鍵名 value:值列表的大小
    lpush(name,*values)在鍵為name的列表頭新增值為value的元素name:鍵名 value:值列表的大小
    llen(name)返回鍵為name的列表的長度name:鍵名列表長度
    lrange(name,start,end)返回鍵為name的列表中start至end之間的元素name:鍵名 start:起始索引 end:終止索引[ ]
    ltrim(name,start,end)擷取鍵為name的列表保留索引為start 到end的內容name:鍵名 start:起始索引 end:終止索引[ ]
    lindex(name,index)返回鍵為name的列表中index位置元素name:鍵名 index:索引[ ]
    lset(name,index,value)給鍵為name的列表中index位置元素賦值,越界則報錯name:鍵名 index:索引值 value:值True
    lrem(name,count,value)將鍵為name的列表中刪除count個值為value的元素name:鍵名 count:刪除個數 value:值刪除的個數
    lpop(name)返回並刪除鍵為name的列表中的首元素name:鍵名刪除的元素
    rpop(name)返回並刪除鍵為name的列表的尾元素name:鍵名刪除的元素
  6. 集合操作

    方法作用引數說明結果
    sadd(name,*values)向鍵為name的集合中新增元素插入資料的個數
    srem(name,*values)從鍵為name的集合中刪除元素刪除的個數
    spop(name)隨機返回並刪除鍵為name的集合中的一個元素刪除元素
    smove(src,dst,value)從src對應的集合中移除元素並將其新增到dst對應的集合中src:源集合 dst:目標集合 value:值True
    scatd(name)返回鍵為name的集合的元素個數
    sismember(name,value)測試value是否是鍵為name的集合的元素
    sinter(keys,*args)返回所有給定鍵的集合的交集
  7. RedisDump

    RedisDump 提供了強大的Redis 資料的匯入和匯出功能

    RedisDump 提供了兩個可執行命令:

    ​ redis-dump:匯出資料

    ​ redis-load: 用於匯入資料

6.Ajax資料爬取

對於一種情況,資料載入是一種非同步載入方式,原始的頁面最初不會包含某些資料,原始頁面載入完畢後,會向伺服器請求某個介面獲取資料,然後資料才會被處理從而顯示到網頁上,這其實即使傳送了一個Ajax

大多數網頁的原始HTML 文件不會包含任何資料,資料都是通過Ajax統一載入後再呈現出來的,這樣在web開發上可以做到前後端分離,而且降低伺服器直接渲染頁面帶來的壓力

所以如果遇到這樣的頁面,直接利用requests 等庫來抓取頁面,是無法獲取有效的資料的,這時需要分析網頁後臺像介面傳送Ajax 請求,如果可以利用requests 來模擬Ajax 請求,那麼就i可以成功抓取了

  1. 什麼是Ajax

    Ajax:全稱 Asynchronous Javascript and XML ,非同步的Javascript 和 XML,不是一門程式語言,而是利用Javascript 在保證頁面不被重新整理,頁面連結不改變的情況下與伺服器交換資料並更新部分網頁的技術

    W3school教程:

    1. 建立XMLHttpRequest物件

      XMLHttpRequest 用於在後臺與伺服器交換資料,意味著可以在不重新載入整個網頁的情況下,對網頁的某些部分進行更新

      variable = new XMLHttpRequest();
      
    2. XHR 請求

      如需將請求傳送到伺服器,我們使用XMLHttpRequest 物件的open() 和 send()方法

      xmlhttp.open("GET","test1.txt",true);
      xmlhttp.send();
      
      方法描述
      open(method,url,async)規定請求的型別,URL,以及是否非同步處理
      method:請求的型別;GET 或 POST
      url: 檔案在伺服器上的位置
      async: true(非同步) 或 false (同步)
      send(string)將請求傳送到伺服器
      string:僅限於POST 請求

      GET 還是 POST?

      在以下情況中,請使用POST 請求:

      • 無法使用快取檔案(更新伺服器上的檔案或資料)

      • 向伺服器傳送大量資料(POST 沒有資料量限定)

      • 傳送包含未知字元的使用者輸入( POST 比 GET 更穩定也更可靠)


    3. XHR 響應

      屬性描述
      responseText獲得字串形式的響應資料
      document.getElementById(“myDiv”).innerHTML=xmlhttp.responseText’
      responseXML獲得XML形式的響應資料
    4. XHR readystatechange 事件

      當請求被髮送到伺服器時,我們需要執行一些基於響應的任務

      每當readystate 改變時,就會觸發onreadystatechange 事件

      readystate 屬性存有 XMLHttpRequest的狀態資訊

      屬性描述
      onreadystatechange儲存函式,每當readystate屬性改變時,就會呼叫該函式
      readystate存有XMLHttpRequest 的狀態。從 0 到 4 發生變化
      0:請求未初始化
      1:伺服器連線已建立
      2:請求已連線
      3:請求處理中
      4:請求已完成,且響應已就緒
      status200: ok
      404: 未找到頁面

      .

  2. 基本原理

    傳送Ajax 請求到網頁更新的這個過程可以簡單的分為三步:

    1. 傳送請求
    2. 解析內容
    3. 渲染頁面
    • 傳送請求

      function loadXMLDoc()
      {
      var xmlhttp;
      if (window.XMLHttpRequest)
        {// code for IE7+, Firefox, Chrome, Opera, Safari
        xmlhttp=new XMLHttpRequest();
        }
      else
        {// code for IE6, IE5
        xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
        }
      xmlhttp.onreadystatechange=function()
        {
        if (xmlhttp.readyState==4 && xmlhttp.status==200)
          {
          document.getElementById("myDiv").innerHTML=xmlhttp.responseText;
          }
        }
      xmlhttp.open("GET","/ajax/test1.txt",true);
      xmlhttp.send();
      }
      
    • 解析內容

      返回的內容可能時HTML,可能是JSON ,接下來只需要在方法中用 JavaScript 進一步處理即可

    • 渲染網頁

      我們知道,真實的資料其實都是一次次Ajax 請求得到的,如果想要抓取這些資料,需要知道這些請求是怎麼樣傳送的,發往哪裡,發了那些引數

  3. Ajax 分析方法

    1. 檢視請求

      [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-bAm3wz8Q-1597060868055)(C:\Users\23977\AppData\Roaming\Typora\typora-user-images\1575095638127.png)]

      [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-Y559b52C-1597060868060)(C:\Users\23977\AppData\Roaming\Typora\typora-user-images\1575096228712.png)]

    2. 過濾請求

      [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-1kNCgKR7-1597060868064)(C:\Users\23977\AppData\Roaming\Typora\typora-user-images\1575096788093.png)]

  4. Ajax 結果提取

    base_url 來表示請求的URL 的前半部分,接下來構造引數字典

    urlencode() 方法將引數轉化為 URL 的 GET 請求引數 ,類似於 type=uid&page=3

    直接呼叫 json() 方法將內容解析為 JSON 返回

7.動態渲染頁面爬取

很多網站不知Ajax 動態渲染這一種,甚至Ajax介面含有很多加密引數,我們難以直接找出其規律

為了解決這些問題,我們可以直接使用模擬瀏覽器執行的方式來實現,這樣就可以做到在瀏覽器中看到什麼樣,抓取的原始碼就是什麼樣,也就是可見即可爬

python 提供了許多模擬瀏覽器執行的庫。Selenium、Splash、PyV8、Ghost等

Selenium 的使用

  1. 在Windows下,建議直接將chromdriver.exe 檔案拖到 Python 的 Scripts 目錄下

  2. 基本使用

    from selenium import webdriver
    from selenium.webdriver.common.by import By
    from selenium.webdriver.common.keys import Keys
    from selenium.webdriver.support import expected_conditions as EC
    from selenium.webdriver.support.wait import WebDriverWait
    
    browser = webdriver.Chrome()
    try:
        browser.get('https://www.baidu.com')
        input = browser.find_element_by_id('kw')
        input.send_keys('菜鳥教程')
        input.send_keys(Keys.ENTER)
        wait = WebDriverWait(browser,10)
        wait.until(EC.presence_of_element_located((By.ID,'content_left')))
        print(browser.current_url)
        print(browser.get_cookies())
        print(browser.page_source)
    finally:
        browser.close()
    

    這裡我們得到的當前URL、Cookies 和原始碼都是瀏覽器中的真實內容

    所以說,如果用Selenium 來驅動瀏覽器載入網頁的話,就可以直接拿到 JavaScript渲染的結果了,不用擔心使用的是什麼加密系統

  3. 宣告瀏覽器物件

    from selenium import webdriver
    
    browser = webdriver.Chrome()  # 谷歌瀏覽器
    browser = webdriver.Firefox()  # 火狐瀏覽器
    
    # 這樣完成了瀏覽器物件的初始化並將其賦值為 browser 物件,我們接下來要做的就是呼叫 browser物件,讓其執行各個動作來模擬瀏覽器操作
    
  4. 訪問頁面

    from selenium import webdriver
    
    browser = webdriver.Chrome()
    browser.get('https://www.taobao.com')
    print(browser.page_source)
    
    # 通過這幾行程式碼 ,我們就可以實現瀏覽器的驅動並獲取網頁原始碼
    
  5. 查詢結點

    Selenium 可以驅動瀏覽器完成各種操作,比如填充表單、模擬點選,因而我們要找出這些節點所在的位置

    • 單個節點 find_element()

      from selenium import webdriver
      
      browser = webdriver.Chrome()
      browser.get('https://www.taobao.com')
      # print(browser.page_source)
      
      input_first = browser.find_element_by_id('q')
      input_second = browser.find_element_by_css_selector('#q')
      input_third = browser.find_element_by_xpath('//*[@id="q"]')
      print(input_first)
      print(input_second)
      print(input_third)
      browser.close()
      
      # 這裡我們使用了 3種方式獲取輸入框,分別是ID、CSS、XPath 獲取  返回的都是 WebElement型別
      

      ​ 這裡列出了所有獲取單個節點的方法:

      • browser.find_element_by_id()
        browser.find_element_by_name()
        browser.find_element_by_xpath()
        browser.find_element_by_link_text()
        browser.find_element_by_partial_link_text()
        browser.find_element_by_tag_name()
        browser.find_element_by_class_name()
        browser.find_element_by_css_selector()
        

        Selenium 還提供了通用方法 find_element(),他需要傳入兩個函式,查詢方式 By 和 值,實際上,它就是 find_element_by_id() 這種方式的通用函式版本

        find_element_by_id(id) 等價於 find_element(By.ID , id ) 兩者得到的結果完全一致

        這樣引數更加靈活了

    • 多個節點 find_elements()

      from selenium import webdriver
      
      browser = webdriver.Chrome()
      browser.get('https://www.taobao.com')
      # 查詢多個節點
      lis = browser.find_elements_by_css_selector('.service-bd li')
      for li in lis:
          print(li)
          
      # lis = browser.find_elements(By.CSS_SELECTOR,'.service-bd li')  一樣的結果
      
  6. 節點互動
    讓瀏覽器模擬執行一些動作,輸入文字時用 send——keys()方法、清空文字時用clear() 方法、點選按鈕時用click() 方法

    import time
    from selenium import webdriver
    
    browser = webdriver.Chrome()
    browser.get('https://www.taobao.com')
    # 節點互動
    input = browser.find_element_by_id('q')
    input.send_keys('傘')
    time.sleep(3)
    input.clear()
    input.send_keys('帽子')
    button = browser.find_element_by_class_name('btn-search')
    button.click()
    

    互動式官方文件:

    https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.remote.webelement

  7. 動作鏈
    滑鼠拖拽,鍵盤按鍵等,這些動作用一種方式來執行,這就是動作鏈

    from selenium import webdriver
    from selenium.webdriver import ActionChains
    
    # 動作鏈
    browser = webdriver.Chrome()
    url = 'https://www.runoob.com/try/try.php?filename=jqueryui-api-droppable'
    browser.get(url)
    browser.switch_to.frame('iframeResult')
    source = browser.find_element_by_css_selector('#draggable')
    target = browser.find_element_by_css_selector('#droppable')
    actions = ActionChains(browser)
    actions.drag_and_drop(source,target)
    actions.perform()
    

    動作鏈文件:

    https://selenium-python.readthedocs.io/api.html#module-selenium.%20webdriver.common.action-chains

  8. 執行 JavaScript
    對於某些操作, Selenuim API 沒有提供,比如下拉進度條,所以我們可以直接模擬執行 JavaScript

    使用 execute_script() 方法即可實現

  9. 獲取節點資訊

    • 獲取屬性 get_attribute()

    • 獲取文字值 : 每個節點都有text屬性

      相當於 BeautifulSoup 的 get_text() 方法 、 pyquery 的 text() 方法

    • 獲取 id、位置、標籤名和大小

      id:獲取節點的id

      location:獲取節點在頁面中的相對位置

      tag_name: 獲取標籤名稱

      size:獲取節點的大小,也就是寬高

  10. 切換 Frame

    我們知道網頁中有一種節點叫做 iframe ,也就是子Frame。相當於頁面的子頁面

    Selenium開啟頁面,預設是在父級 Frame裡面操作,要想獲得zi Frame 裡面的節點

    這個時候就需要使用 switch_to_frame()方法來切換

  11. 延時等待
    Selenium 中,get() 方法會在網頁載入後結束執行,可能某些頁面還有額外的 Ajax 請求,所以,這裡需要延遲等待一定的時間,確保節點已經載入出來

    等待方式有兩種:隱式等待 和 顯示等待

    • 隱式等待: implicitly_wait()

      如果Selenium 沒有在DOM 中找到節點,將繼續等待,超出設定時間後,則丟擲找不到節點異常,預設時間是 0

    • 顯示等待:

      隱式等待效果沒那好,可能頁面載入會受到網路條件的影響

      指定一個最長等待時間,如果在規定時間內載入出來了這個節點,就返回查詢的節點,如果到了規定時間依然沒有載入出該節點,則丟擲異常

      wait = WebDriverWait(browser,10)
      input = wait.until(EC.presence_of_element_located(By.ID,'q'))
      
      # EC.presence_of_element_located 這個條件,代表節點出現的意思
      
      等待條件含義
      title_is標題是某內容
      title_contains標題包含某內容
      presence_of_element_located節點載入出來,傳入定位元組
      visibility_of_element_located節點可見,傳入定位元組
      presence_of_all_elements_located所有節點載入出來
      text_to_be_present_in_element某個節點包含某文字
      text_to_be_present_in_element_value某個節點值包含某文字
      element_to_be_clickable節點可點選
      staleness_of判斷一個節點是否仍在DOM ,可判斷頁面是否重新整理
      alert_is_present是否出現警告

      等待條件參考文件:

      https://selenium-python.readthedocs.io/api.html#module-selenium.webdriver.support.expectedcoditions

  12. 前進和後退

    forward() 和 back()

  13. Cookies

    對 Cookies 進行操作,例如獲取、新增、刪除Cookoes

    get_cookies() : 獲取所有的 Cookies

    add_cookie() : 新增一個 Cookie

    delete_all_cookies() : 刪除所有的 Cookies

  14. 異常處理

    try except 來捕獲各類異樣

Splash 的使用

Splash 是一個 JavaScript 渲染服務,是一個帶有 HTTP API 的輕量級瀏覽器,同時 對接了 Python 中的 Twisted 和 QT 庫,利用他,我們同樣可以實現動態頁面的抓取

  1. 功能介紹

    • 非同步方式處理多個網頁渲染過程
    • 獲取渲染後的頁面的原始碼或截圖
    • 通過關閉圖片渲染或者使用 Adblock 規則來加快頁面渲染速度
    • 可執行特定的 JavaScript 指令碼
    • 可通過 Lua 指令碼來控制頁面渲染過程
    • 獲取渲染的詳細過程並通過 HAR 格式呈現
  2. Splash 的安裝

    在 docker 下安裝:

    docker run -d -p 8050:8050 scraping/splash
    
  3. Splash Lua 指令碼

    • 入口及返回值

      function main(splash,args)
          splash:go("http://www.baidu.com")
          splash:wait(0.5)
          local title = splash:evaljs("document.title")
          return {titile = title}
      end
      
      返回結果:
      Splash Response: Object
      titile: "百度一下,你就知道"
      
    • 非同步處理

      function main(splash,args)
          local example_urls = {"www.baidu.com","www.taobao.com","www.jd.com"}
          local urls = args.urls or example_urls
          local results = {}
          for index ,url in ipairs(urls) do
              local ok,reason = splash:go("https//" .. url)
              if ok then
                  splash:wait(2)
                  results[url] = splash:png()
              end
          end
              return results
      end
      
  4. Splash 物件屬性

    • args

      該屬性可以獲取載入時配置的引數,比如 URL,如果為 GET 請求,可以獲得 GET 請求引數,為 POST 請求,它可以獲取表單提交的資料,Splash 也支援使用第二個引數直接作為 args

      function main(splash,args)
          local url = args.url
      end    
      
    • js_enabled

      這個屬性是 Splash 的 JavaScript 執行開關,可以將其配置為 true 或 false 來控制是否執行 JavaScript 程式碼,預設為true

    • resource_timeout

      此屬性可以設定載入的超時時間,單位是秒,如果設定為 0 或 nil(類似於 Python 中的 Nome),代表不檢測超時,此屬性適合在網頁載入速度較慢的情況下設定

    • image_enabled

      此屬性可以設定圖片是否載入,預設情況下是載入的。禁用該屬性後,可以節省網路流量並提高網頁的載入速度,但是需要注意的是,可能會影響 JavaScript 的渲染,因為禁用後,它外層 DOM 節點的高度會受到影響,進而影響 DOM 節點的位置,如果 JavaScript 對圖片節點有操作的話,其執行就會受到影響

    • plugins_enabled

      此屬性可以控制瀏覽器外掛(如 flash 外掛)是否開啟,預設情況下是 false,不開啟

    • scroll_position

      事務之此屬性,我們可以控制頁面上下滾動

      function main(splash,args)
          assert(splash:go("https://www.taobao.com"))
          splash.scroll_position = {y=400}
          return {png = splash.png()}
      end    
      
  5. Splash 物件的方法

    • go()

      該方法用來請求某個連結,可以模仿 GET 和 POST 請求,同時支援傳入請求頭,表單等資料

      ok,reason = splash:go{url,baseurl=nil,headers=nil,http_method="GET",body=nil,formdata=nil}
      
      • url: 請求的URL
      • baseurl : 可選引數,預設為空,表示資源載入相對路徑
      • headers : 可選引數,預設為空,表示請求頭
      • http_method : 可選引數,預設為GET,也支援 POST
      • body :可選引數,預設為空,發 POST請求時,使用的 content-type 為 application/json
      • formdata :可選引數,預設為空,POST 的時候的表單引數,使用 Content-type 為 application/x-www-form-urlencode
    • wait()

      此方法可以控制頁面的等待時間

      ok,reason = splash:wait{time,cancel_on_redirect,cancel_on_error = true}
      
      • time:等待的秒數
      • cancel_on _redirect :可選引數,預設 false ,表示如果發生了重定向就停止等待,並返回重定向結果
      • cancel_on_error :可選引數,預設為false ,表示如果發生了載入錯誤,就停止等待
    • jsfun():此方法可以直接呼叫 Javascript 定義的方法,但是所呼叫的方法需要用雙中括號包圍,相當於實現了 Javascript方法 到 Lua 指令碼的轉換

    • evaljs() :此方法執行 Javascript 程式碼 並返回其結果

    • html() :此方法來獲取網頁的原始碼

    • png() :此方法來獲取 PNG 格式的網頁截圖

    • har() : 此方法來獲取頁面載入過程描述

    • get_cookies() :此方法可以獲取當前頁面的 Cookies

    • select_all():此方法可以選中所有符合條件的節點,其引數時 CSS 選擇器

    • mouse_click() : 此方法可以模擬滑鼠點選操作,傳入的引數為座標 x 和 y,也可以直接選中某個節點

  6. Splash API 呼叫

    • render.html

      此介面用於獲取 JavaScript 渲染的頁面的HML程式碼,介面地址就是 Splash 的執行地址 加此介面名稱

      curl http://172.20.32.111:8050/render.html?url = https://www.baidu.com
      
      
      用Python 實現的話
      import requests
      url = 'http://172.20.32.111:8050/render.html?url = https://www.baidu.com'
      response = requests.get(url)
      print(response.text)
      
    • render.png

      此介面獲取網頁截圖,width 和 height 來控制寬高,返回的時 PNG 格式的 二進位制資料

      curl http://172.20.32.111:8050/render.png?url=https://www.taobao.com&wait=5&width=1000&height=700
      
    • render.json

      此介面包含了前面介面的所有功能,返回結果是 JSON 格式

      curl https://172.20.32.111:8050/render.json?url=https://httpbin.org
      

      我們可以傳入不同的引數控制其返回結果。傳入 html=1,返回結果即會得到原始碼資料,png=1 ,即會得到頁面 PNG截圖資料

    • execute

      此介面才是最強大的介面,此介面可以實現與 Lua指令碼的對接

      前面的render.html 和 render.rng 等介面對於一般的 JavaScript渲染頁面足夠了。但是如果要實現一些互動操作的話,他們還是無能為力,這裡就需要使用 excute介面

      一個簡單的 Lua 指令碼
      function main(splash)
      	return ''hello
      end
      
      然後將此指令碼轉化為 URL 編碼後的字元。拼接到execute介面後面
      curl http://localhost:8050/executelua_source=functio+main%28splash%29%0D%0A++return+%27hello%27%OD%0Aend
      

      用python來實現

      import requests
      from urllib.parse import quote
      
      lua = """
      function main(splash)
      	return 'hello'
      """
      
      url = 'https://172.20.32.111:8050/excute?lua_source=' + quote(lua)
      response = requests.get(url)
      print(response.text)
      
      # quote() 方法 將 指令碼進行 URL 轉碼  ,然後將其作為 lua_source 引數傳遞
      
      # 返回的結果是 JSON 格式
      

      如此一來,我麼之前所說的 Lua 指令碼均可以用此方式與python 進行對接,所有的網頁的動態渲染,模擬點選,表單提交,頁面滑動,延時等待後的一些結果均可以自由控制,獲取頁面原始碼和截圖也都不在話下

相關文章