摘要:所以說,我們所看到的微博頁面的真實數據并不是最原始的頁面返回的,而是后來執行后再次向后臺發送了請求,拿到數據后再進一步渲染出來的。結果提取仍然是拿微博為例,我們接下來用來模擬這些請求,把馬云發過的微博爬取下來。
上一篇文章:Python3網絡爬蟲實戰---34、數據存儲:非關系型數據庫存儲:Redis
下一篇文章:Python3網絡爬蟲實戰---36、分析Ajax爬取今日頭條街拍美圖
有時候我們在用 Requests 抓取頁面的時候,得到的結果可能和在瀏覽器中看到的是不一樣的,在瀏覽器中可以看到正常顯示的頁面數據,但是使用 Requests 得到的結果并沒有,這其中的原因是 Requests 獲取的都是原始的 HTML 文檔,而瀏覽器中的頁面則是頁面又經過 JavaScript 處理數據后生成的結果,這些數據的來源有多種,可能是通過 Ajax 加載的,可能是包含在了 HTML 文檔中的,也可能是經過 JavaScript 經過特定算法計算后生成的。
對于第一種情況,數據的加載是一種異步加載方式,原始的頁面最初不會包含某些數據,原始頁面加載完后會會再向服務器請求某個接口獲取數據,然后數據再被處理才呈現到網頁上,這其實就是發送了一個 Ajax 請求。
照 Web 發展的趨勢來看,這種形式的頁面越來越多,網頁原始 HTML 文檔不會包含任何數據,數據都是通過 Ajax 來統一加載然后再呈現出來,這樣在 Web 開發上可以做到前后端分離,而且降低服務器直接渲染頁面帶來的壓力。
所以如果我們遇到這樣的頁面,如果我們再利用 Requests 等庫來抓取原始頁面是無法獲取到有效數據的,這時我們需要做的就是分析網頁的后臺向接口發送的 Ajax 請求,如果我們可以用 Requests 來模擬 Ajax 請求,那就可以成功抓取了。
所以本章我們的主要目的是了解什么是 Ajax 以及如何去分析和抓取 Ajax 請求。
1、什么是AjaxAjax,全稱為 Asynchronous JavaScript and XML,即異步的 JavaScript 和 XML。
Ajax 不是一門編程語言,而是利用 JavaScript 在保證頁面不被刷新、頁面鏈接不改變的情況下與服務器交換數據并更新部分網頁的技術。
對于傳統的網頁,如果想更新其內容,那么必須要刷新整個頁面,但有了 Ajax,我們便可以實現在頁面不被全部刷新的情況下更新其內容。在這個過程中,頁面實際是在后臺與服務器進行了數據交互,獲取到數據之后,再利用 JavaScript 改變網頁,這樣網頁內容就會更新了。
可以到 W3School 上體驗幾個 Demo 來感受一下:http://www.w3school.com.cn/aj...。
我們在瀏覽網頁的時候會發現很多網頁都有上滑查看更多的選項,比如拿微博來說,我們以馬云的主頁為例:https://m.weibo.cn/u/2145291155,切換到微博頁面,一直下滑,可以發現下滑幾個微博之后,再向下就沒有了,轉而會出現一個加載的動畫,不一會兒下方就繼續出現了新的微博內容,那么這個過程其實就是 Ajax 加載的過程,如圖 6-1 所示:
evernotecid://D603D29C-DFBA-4C04-85E9-CCA3C33763F6/appyinxiangcom/23852268/ENResource/p142
圖 6-1 頁面加載過程
我們注意到頁面其實并沒有整個刷新,也就意味著頁面的鏈接是沒有變化的,但是這個過程網頁中卻又多了新的內容,也就是后面刷出來的新的微博。這就是通過 Ajax 獲取新的數據并呈現而實現的過程。
初步了解了 Ajax 之后我們再來詳細了解一下它的基本原理,發送 Ajax 請求到網頁更新的這個過程可以簡單分為三步:
發送請求解析內容渲染網頁下面我們分別來詳細介紹一下這幾個過程。
我們知道 JavaScript 可以實現頁面的各種交互功能,那么 Ajax 也不例外,它也是由 JavaScript 來實現的,實際它就是執行了類似如下的代碼:
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("POST","/ajax/",true); xmlhttp.send();
這是 JavaScript 對 Ajax 最底層的實現,實際上就是新建了 XMLHttpRequest 對象,然后調用了onreadystatechange 屬性設置了監聽,然后調用 open() 和 send() 方法向某個鏈接也就是服務器發送了一個請求,我們在前面用 Python 實現請求發送之后是可以得到響應結果的,只不過在這里請求的發送變成了 JavaScript 來完成,由于設置了監聽,所以當服務器返回響應時,onreadystatechange 對應的方法便會被觸發,然后在這個方法里面解析響應內容即可。
解析內容得到響應之后,onreadystatechange 屬性對應的方法便會被觸發,此時利用 xmlhttp 的 responseText 屬性便可以取到響應的內容。這也就是類似于 Python 中利用 Requests 向服務器發起了一個請求,然后得到響應的過程。那么返回內容可能是 HTML,可能是 Json,接下來只需要在方法中用 JavaScript 進一步處理即可。比如如果是 Json 的話,可以進行解析和轉化。
渲染網頁JavaScript 有改變網頁內容的能力,解析完響應內容之后,就可以調用 JavaScript 來針對解析完的內容對網頁進行下一步的處理了。比如通過document.getElementById().innerHTML 這樣的操作便可以對某個元素內的源代碼進行更改,這樣網頁顯示的內容就改變了,這樣的操作也被稱作 DOM 操作,即對 Document網頁文檔進行操作,如更改、刪除等。
如上例中,document.getElementById("myDiv").innerHTML=xmlhttp.responseText 便將 ID 為 myDiv 的節點內部的 HTML 代碼更改為服務器返回的內容,這樣 myDiv 元素內部便會呈現出服務器返回的新數據,網頁的部分內容看上去就更新了。
以上就是Ajax的三個步驟。
我們觀察到,以上的步驟其實都是由 JavaScript 來完成的,它完成了整個請求、解析、渲染的過程。
所以再回想微博的下拉刷新,這其實就是 JavaScript 向服務器發送了一個 Ajax 請求,然后獲取新的微博數據,將其解析,并將其渲染在網頁中。
因此我們可以知道,真實的數據其實都是一次次 Ajax 請求得到的,我們如果想要抓取這些數據,就需要知道這些請求到底是怎么發送的,發往哪里,發了哪些參數。如果我們知道了這些,不就可以用 Python 來模擬這個發送操作,獲取到這其中的結果了嗎?
所以在下一節我們就來了解下到哪可以看到這些后臺 Ajax 操作,去了解它到底是怎么發送的,發送了什么參數。
還是以上文中的微博為例,我們已經知道了拖動刷新的內容是由 Ajax 加載的,而且頁面的 URL 沒有變化,那么我們應該到哪去查看這些 Ajax 請求呢?
1. 查看請求在這里我們還是需要借助于瀏覽器的開發者工具,我們以 Chrome 瀏覽器為例來看一下怎樣操作。
首先用 Chrome 瀏覽器打開微博的鏈接:https://m.weibo.cn/u/2145291155,隨后在頁面中點擊鼠標右鍵,會出現一個檢查的選項,點擊它便會彈出開發者工具,如圖 6-2 所示:
圖 6-2 開發者工具
那么在 Elements 選項卡便會觀察到網頁的源代碼,右側便是節點的樣式。
不過這不是我們想要尋找的內容,我們切換到 Network 選項卡,隨后重新刷新頁面,可以發現在這里出現了非常多的條目,如圖 6-3 所示:
圖 6-3 Network 面板結果
前文我們也提到過,這里其實就是在頁面加載過程中瀏覽器與服務器之間發送 Request 和接收 Response 的所有記錄。
Ajax其實有其特殊的請求類型,它叫做 xhr,在上圖中我們可以發現一個名稱為 getIndex 開頭的請求,其 Type 為 xhr,這就是一個 Ajax 請求,我們鼠標點擊這個請求,可以查看這個請求的詳細信息,如圖 6-4 所示:
圖 6-4 詳細信息
我們在右側可以觀察到其 Request Headers、URL 和 Response Headers 等信息,如圖 6-5 所示:
圖 6-5 詳細信息
其中 Request Headers 中有一個信息為 X-Requested-With:XMLHttpRequest,這就標記了此請求是 Ajax 請求。
隨后我們點擊一下 Preview,即可看到響應的內容,響應內容是 Json 格式,在這里 Chrome 為我們自動做了解析,我們可以點擊箭頭來展開和收起相應內容,如圖 6-6 所示:
圖 6-6 Json 結果
觀察可以發現,這里的返回結果是馬云的個人信息,如昵稱、簡介、頭像等等,這也就是用來渲染個人主頁所使用的數據,JavaScript 接收到這些數據之后,再執行相應的渲染方法,整個頁面就被渲染出來了。
另外也可以切換到 Response 選項卡,可以觀察到真實的返回數據,如圖 6-7 所示:
圖 6-7 Response 內容
接下來我們切回到第一個請求,觀察一下它的 Response 是什么,如圖 6-8 所示:
圖 6-8 Response 內容
這是最原始的鏈接 https://m.weibo.cn/u/2145291155 返回的結果,其代碼只有五十行,結構也非常簡單,只是執行了一些 JavaScript。
所以說,我們所看到的微博頁面的真實數據并不是最原始的頁面返回的,而是后來執行 JavaScript 后再次向后臺發送了 Ajax 請求,拿到數據后再進一步渲染出來的。
接下來我們再利用 Chrome 開發者工具的篩選功能篩選出所有的 Ajax 請求,在請求的上方有一層篩選欄,我們可以點擊 XHR,這樣在下方顯示的所有請求便都是 Ajax 請求了,如圖 6-9 所示:
圖 6-9 Ajax 請求
再接下來我們我們不斷滑動頁面,可以看到在頁面底部有一條條新的微博被刷出,而開發者工具下方也一個個地出現 Ajax 請求,這樣我們就可以捕獲到所有的 Ajax 請求了。
隨意點開一個條目都可以清楚地看到其 Request URL、Request Headers、Response Headers、Response Body等內容,想要模擬請求和提取就非常簡單了。
如圖所示內容便是馬云某一頁微博的列表信息,如圖 6-10 所示:
圖 6-10 微博列表信息
3. 結語到現在為止我們已經可以分析出來 Ajax 請求的一些詳細信息了,接下來我們只需要用程序來模擬這些 Ajax 請求就可以輕松提取我們所需要的信息了。
所以在下一節我們來用 Python 實現 Ajax 請求的模擬,從而實現數據的抓取。
仍然是拿微博為例,我們接下來用 Python 來模擬這些 Ajax 請求,把馬云發過的微博爬取下來。
1. 分析請求我們打開 Ajax 的 XHR 過濾器,然后一直滑動頁面加載新的微博內容,可以看到會不斷有Ajax請求發出。
我們選定其中一個請求來分析一下它的參數信息,點擊該請求進入詳情頁面,如圖 6-11 所示:
圖 6-11 詳情頁面
可以發現這是一個 GET 類型的請求,請求鏈接為:https://m.weibo.cn/api/contai...;value=2145291155&containerid=1076032145291155&page=2,請求的參數有四個:type、value、containerid、page。
隨后我們再看一下其他的請求,觀察一下這些請求,發現它們的 type、value、containerid 始終如一。type 始終為 uid,value 的值就是頁面的鏈接中的數字,其實這就是用戶的 id,另外還有一個 containerid,經過觀察發現它就是 107603 然后加上用戶 id。所以改變的值就是 page,很明顯這個參數就是用來控制分頁的,page=1 代表第一頁,page=2 代表第二頁,以此類推。
以上的推斷過程可以實際觀察參數的規律即可得出。
隨后我們觀察一下這個請求的響應內容,如圖 6-12 所示:
圖 6-12 響應內容
它是一個 Json 格式,瀏覽器開發者工具自動為做了解析方便我們查看,可以看到最關鍵的兩部分信息就是 cardlistInfo 和 cards,將二者展開,cardlistInfo 里面包含了一個比較重要的信息就是 total,經過觀察后發現其實它是微博的總數量,我們可以根據這個數字來估算出分頁的數目。
cards 則是一個列表,它包含了 10 個元素,我們展開其中一個來看一下,如圖 6-13 所示:
圖 6-13 列表內容
發現它又有一個比較重要的字段,叫做 mblog,繼續把它展開,發現它包含的正是微博的一些信息。比如 attitudes_count 贊數目、comments_count 評論數目、reposts_count 轉發數目、created_at 發布時間、text 微博正文等等,得來全不費功夫,而且都是一些格式化的內容,所以我們提取信息也更加方便了。
這樣我們可以請求一個接口就得到 10 條微博,而且請求的時候只需要改變 page 參數即可,目前總共 138 條微博那么只需要請求 14 次即可,也就是 page 最大可以設置為14。
這樣我們只需要簡單做一個循環就可以獲取到所有的微博了。
在這里我們就開始用程序來模擬這些 Ajax 請求,將馬云的所有微博全部爬取下來。
首先我們定義一個方法,來獲取每次請求的結果,在請求時page 是一個可變參數,所以我們將它作為方法的參數傳遞進來,代碼如下:
from urllib.parse import urlencode import requests base_url = "https://m.weibo.cn/api/container/getIndex?" headers = { ??? "Host": "m.weibo.cn", ??? "Referer": "https://m.weibo.cn/u/2145291155", ??? "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", ??? "X-Requested-With": "XMLHttpRequest", } def get_page(page): ??? params = { ??????? "type": "uid", ??????? "value": "2145291155", ??????? "containerid": "1076032145291155", ??????? "page": page ??? } ??? url = base_url + urlencode(params) ??? try: ??????? response = requests.get(url, headers=headers) ??????? if response.status_code == 200: ??????????? return response.json() ??? except requests.ConnectionError as e: ??????? print("Error", e.args)
首先在這里我們定義了一個 base_url 來表示請求的 URL 的前半部分,接下來構造了一個參數字典,其中 type、value、containerid 是固定的參數,只有 page 是可變參數,接下來我們調用了 urlencode() 方法將參數轉化為 URL 的 GET請求參數,即類似于type=uid&value=2145291155&containerid=1076032145291155&page=2 這樣的形式,隨后 base_url 與參數拼合形成一個新的 URL,然后我們用 Requests 請求這個鏈接,加入 headers 參數,然后判斷響應的狀態碼,如果是200,則直接調用 json() 方法將內容解析為 Json 返回,否則不返回任何信息,如果出現異常則捕獲并輸出其異常信息。
隨后我們需要定義一個解析方法,用來從結果中提取我們想要的信息,比如我們這次想保存微博的 id、正文、贊數、評論數、轉發數這幾個內容,那可以先將 cards 遍歷,然后獲取 mblog 中的各個信息,賦值為一個新的字典返回即可。
from pyquery import PyQuery as pq def parse_page(json): ??? if json: ??????? items = json.get("cards") ??????? for item in items: ??????????? item = item.get("mblog") ??????????? weibo = {} ??????????? weibo["id"] = item.get("id") ??????????? weibo["text"] = pq(item.get("text")).text() ??????????? weibo["attitudes"] = item.get("attitudes_count") ??????????? weibo["comments"] = item.get("comments_count") ??????????? weibo["reposts"] = item.get("reposts_count") ??????????? yield weibo
在這里我們借助于 PyQuery 將正文中的 HTML 標簽去除掉。
最后我們遍歷一下 page,一共 14 頁,將提取到的結果打印輸出即可。
if __name__ == "__main__": ??? for page in range(1, 15): ??????? json = get_page(page) ??????? results = parse_page(json) ??????? for result in results: ??????????? print(result)
另外我們還可以加一個方法將結果保存到 MongoDB 數據庫。
from pymongo import MongoClient client = MongoClient() db = client["weibo"] collection = db["weibo"] def save_to_mongo(result): ??? if collection.insert(result): ??????? print("Saved to Mongo")
最后整理一下,最后的程序如下:
import requests from urllib.parse import urlencode from pyquery import PyQuery as pq from pymongo import MongoClient base_url = "https://m.weibo.cn/api/container/getIndex?" headers = { ??? "Host": "m.weibo.cn", ??? "Referer": "https://m.weibo.cn/u/2145291155", ??? "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", ??? "X-Requested-With": "XMLHttpRequest", } client = MongoClient() db = client["weibo"] collection = db["weibo"] max_page = 14 def get_page(page): ??? params = { ??????? "type": "uid", ??????? "value": "2145291155", ??????? "containerid": "1076032145291155", ??????? "page": page ??? } ??? url = base_url + urlencode(params) ??? try: ??????? response = requests.get(url, headers=headers) ??????? if response.status_code == 200: ??????????? return response.json() ??? except requests.ConnectionError as e: ??????? print("Error", e.args) def parse_page(json): ??? if json: ??????? items = json.get("cards") ??????? for item in items: ??????????? item = item.get("mblog") ??????????? weibo = {} ??????????? weibo["id"] = item.get("id") ??????????? weibo["text"] = pq(item.get("text")).text() ??????????? weibo["attitudes"] = item.get("attitudes_count") ??????????? weibo["comments"] = item.get("comments_count") ??????????? weibo["reposts"] = item.get("reposts_count") ??????????? yield weibo def save_to_mongo(result): ??? if collection.insert(result): ??????? print("Saved to Mongo") if __name__ == "__main__": ??? for page in range(1, max_page + 1): ??????? json = get_page(page) ??????? results = parse_page(json) ??????? for result in results: ??????????? print(result) ??????????? save_to_mongo(result)
運行程序后樣例輸出結果如下:
{"id": "3938863363932540", "text": "我們也許不能解決所有的問題,但我們可以盡自己的力量去解決一些問題。移動互聯網不能只是讓留守孩子多了一個隔空說話的手機,移動互聯網是要讓父母和孩子一直在一起。過年了,回家吧…… 農村淘寶2016團圓賀歲片《福與李》 ", "attitudes": 21785, "comments": 40232, "reposts": 2561} Saved to Mongo {"id": "3932968885900763", "text": "跟來自陜甘寧云貴川六省的100位優秀鄉村教師共度了難忘的兩天,接下來我又得出遠門了。。。為了4000萬就讀于鄉村學校的孩子,所以有了這么一群堅毅可愛的老師,有了這么多關注鄉村教育的各界人士,這兩天感動、欣喜、振奮!我們在各自的領域里,一直堅持和努力吧!", "attitudes": 32057, "comments": 7916, "reposts": 2332} Saved to Mongo
查看一下 MongoDB,相應的數據也被保存到 MongoDB,如圖 6-14 所示:
圖 6-14 保存結果
4. 本節代碼本節代碼地址:https://github.com/oldmarkfac...
5. 結語本節實例的目的是為了演示 Ajax 的模擬請求過程,爬取的結果不是重點,該程序仍有很多可以完善的地方,如頁碼的動態計算、微博查看全文等,如感興趣可以嘗試一下。
通過這個實例我們主要是為了學會怎樣去分析 Ajax 請求,怎樣用程序來模擬抓取 Ajax 請求,了解了相關抓取原理之后,下一節的 Ajax 實戰演練會更加得心應手。
上一篇文章:Python3網絡爬蟲實戰---34、數據存儲:非關系型數據庫存儲:Redis
下一篇文章:Python3網絡爬蟲實戰---36、分析Ajax爬取今日頭條街拍美圖
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/44091.html
摘要:上一篇文章網絡爬蟲實戰數據爬取下一篇文章網絡爬蟲實戰動態渲染頁面抓取本節我們以今日頭條為例來嘗試通過分析請求來抓取網頁數據的方法,我們這次要抓取的目標是今日頭條的街拍美圖,抓取完成之后將每組圖片分文件夾下載到本地保存下來。 上一篇文章:Python3網絡爬蟲實戰---35、 Ajax數據爬取下一篇文章:Python3網絡爬蟲實戰---37、動態渲染頁面抓取:Selenium 本節我們...
摘要:在前面我們講到了和的概念,我們向網站的服務器發送一個,返回的的便是網頁源代碼。渲染頁面有時候我們在用或抓取網頁時,得到的源代碼實際和瀏覽器中看到的是不一樣的。所以使用基本請求庫得到的結果源代碼可能跟瀏覽器中的頁面源代碼不太一樣。 上一篇文章:Python3網絡爬蟲實戰---16、Web網頁基礎下一篇文章:Python3網絡爬蟲實戰---18、Session和Cookies 爬蟲,即網...
摘要:,引言注釋上一篇爬蟲實戰安居客房產經紀人信息采集,訪問的網頁是靜態網頁,有朋友模仿那個實戰來采集動態加載豆瓣小組的網頁,結果不成功。 showImg(https://segmentfault.com/img/bVzdNZ); 1, 引言 注釋:上一篇《Python爬蟲實戰(3):安居客房產經紀人信息采集》,訪問的網頁是靜態網頁,有朋友模仿那個實戰來采集動態加載豆瓣小組的網頁,結果不成功...
摘要:為了使用各種應用場景,該項目的整個網絡爬蟲產品線包含了四類產品,如下圖所示本實戰是上圖中的獨立爬蟲的一個實例,以采集安居客房產經紀人信息為例,記錄整個采集流程,包括和依賴庫的安裝,即便是初學者,也可以跟著文章內容成功地完成運行。 showImg(https://segmentfault.com/img/bVy2Iy); 1, 引言 Python開源網絡爬蟲項目啟動之初,我們就把網絡爬蟲...
摘要:時間永遠都過得那么快,一晃從年注冊,到現在已經過去了年那些被我藏在收藏夾吃灰的文章,已經太多了,是時候把他們整理一下了。那是因為收藏夾太亂,橡皮擦給設置私密了,不收拾不好看呀。 ...
閱讀 1659·2021-11-16 11:41
閱讀 2456·2021-11-08 13:14
閱讀 3106·2019-08-29 17:16
閱讀 3079·2019-08-29 16:30
閱讀 1843·2019-08-29 13:51
閱讀 356·2019-08-23 18:38
閱讀 3223·2019-08-23 17:14
閱讀 630·2019-08-23 15:09