網路資料抓取-JS動態生成資料-Python-爬蟲
ofollow,noindex">智慧決策上手系列教程索引
前面三篇文章介紹瞭如何利用Headers模擬瀏覽器請求,如何巢狀For迴圈抓取二級頁面。但針對的都是Html檔案資料,這一篇我們來看一下另外一種情況的資料以及更加複雜的Headers模擬。
案例是拉勾網(一個招聘網站)抓取某個公司全部招聘資訊,然後分析中大型人工智慧公司的人才需求分佈情況。
這次我們使用Anaconda的Jupyter Notebook。
1. 理解頁面
開啟 這個頁面 ,這是思必馳科技(一家專注於人工智慧語音技術的科技公司)在拉勾網的全部招聘職位列表。

思必馳招聘職位
我們可以看到共有47個招聘職位。但是,如果我們【右擊-檢視網頁原始碼】,然後【Ctrl+F】搜尋第一個職位的名稱“運維技術專家”卻什麼也搜不到,實際上整個頁面只有600行左右,並沒有包含任何職位資訊。
資料不在請求的Html檔案裡面,資料在哪?
這幾年的網站很多都採用了類似遊戲的模式:你開啟遊戲軟體的時候,本機電腦裡面沒有任何玩家資訊,但是遊戲軟體啟動後會向伺服器請求資料(而不是Html檔案),拿到這些資料之後,遊戲軟體就把各種線上玩家資料顯示在螢幕上,讓你能夠看到他們。
換成網頁就是:你剛開啟網頁的時候,請求的Html檔案沒有資料,但是網頁在瀏覽器執行之後,網頁自己就會向伺服器請求資料,網頁拿到資料之後,它就會把各種資料填充到頁面上,你就看到了這些資料,——但這些資料並不是像以前那樣直接寫在html檔案裡的。

動態填充資料頁面流程
這些能夠動態請求資料和填充資料的程式碼就是Html網頁內執行的JavaScript指令碼程式碼,它們可以做各種事情,尤其善於玩弄資料。
JS(JavaScript)從伺服器獲取的資料大多是json格式的,類似下面這種物件(Python裡面也叫dict字典),也有xml格式的,這裡暫時用不到就不介紹了。
data={ 'title':'內容標題', 'text':'文字內容' }
這個格式看上去比html一堆尖括號標記看上去舒服多了。但如何拿到這個資料呢?
2. 理解資料請求Request
我們知道Elements面板顯示了所有標記元素,而Network面板顯示了所有瀏覽器發出的請求Request,既然JS是向伺服器發出請求的,那麼就一定會在Network面板留下痕跡。
還是 剛才的頁面 ,【右鍵-檢查】切換到Network面板,點選紅色小按鈕清空,然後點選上面的第2頁按鈕,檢視Network裡面的變化。

Network檢視JS的xhr請求
我們注意到 searchPosition.json
這行,它的型別(Type)是 xhr
,資料請求都是這個型別的。
點選 searchPosition.json
可以看到這個請求的詳細資訊。

Headers詳細資訊
?aaa=xxx&bbb=yyy
這類結尾了),但是多了
Form Data
表單資料,其實和Parameters作用相同,就是向伺服器說明你要哪個公司(
companyId
)的資料、第幾頁(
pageNo
)、每頁多少個職位(
pageSize
)等等。
再點選上面的【preview】預覽,可以看到這個請求實際獲得了什麼資料:

資料結構預覽
如圖,小三角一路點下去,就能看到這個資料實際和頁面展示的職位列表是一一對應的。所以我們只要拿到這個資料就OK了!
3. 傳送資料請求
上面看到,我們需要的資料都在 searchPosition.json
這個Request請求裡面,【右鍵-Copy-Copy link address】複製請求地址。

複製請求地址
開啟Notebook,新建Python 3檔案,貼上過去。
#單元1 url='https://www.lagou.com/gongsi/searchPosition.json'
向這個地址傳送請求:
#單元2 import requests jsonData=requests.get(url) print(jsonData.text)
全部執行後得到下圖結果,我們的爬蟲請求被伺服器識別了!

直接發起資料請求失敗
並不是所有資料請求都會被識別,拉勾網伺服器做了這方面的檢測,有些網站就沒有檢測機制,可以直接獲取有效資料。
回顧上面截圖的瀏覽器Request請求的Headers資訊,實際上瀏覽器傳送請求的時候還攜帶了很多Request headers(包含Cookie),以及Form data資料(對應我們以前提到過的Parameters資訊)。
4. 新增params和headers
params
就是 Form Data
,從瀏覽器的 Network
面板直接手工複製,然後修改成為Python的字典物件(就是大括號包含的一些屬性資料),注意都要加上引號,每行結尾有逗號。
header
可以用右鍵 searchPosition.json
然後【Copy-Copy Request headers】複製到,但是注意這個字元很多而且換行,所以要用三個單引號才能包括起來。
注意!這裡我刪除了其中一行 Content-Length: 86
,因為在傳送Request請求的時候Python會自動計算生成 Content-Length
數值(不一定是86)。如果這裡不刪除就會導致重複引發錯誤。
修改單元1的程式碼:
#單元1 url='https://www.lagou.com/gongsi/searchPosition.json' params={ 'companyId': '94', 'positionFirstType': '全部', 'schoolJob': 'false', 'pageNo': '2', 'pageSize': '10' } headers=''' POST /gongsi/searchPosition.json HTTP/1.1 Host: www.lagou.com Connection: keep-alive Origin: https://www.lagou.com X-Anit-Forge-Code: 38405859 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Accept: application/json, text/javascript, */*; q=0.01 X-Requested-With: XMLHttpRequest X-Anit-Forge-Token: fcd0cae2-af8a-44b7-ae08-6cc103677fc1 Referer: https://www.lagou.com/gongsi/j94.html Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7 Cookie: JSESSIONID=ABAAABAAAGFRGDA8929AE8AEDDF675B0A416152D50F1155; user_trace_token=20180914214240-a4f27a86-ee75-49d4-a447-7d7ec6386510; _ga=GA1.2.764376373.1536932562; LGUID=20180914214241-0d64224c-b824-11e8-b93f-6544005c3644; WEBTJ-ID=20180917170602-165e6c78d78209-0f57b51c336360b-3461790f-1296000-165e6c78d7953; __utmc=14951595; __utmz=14951595.1537175176.1.1.utmcsr=m_cf_cpt_sogou_pc|utmccn=(not%20set)|utmcmd=(not%20set); X_HTTP_TOKEN=b53ce1f559f492d4aa675d08aaffa8d93; _putrc=67FE3A6CCEBE7074123F83D1B170EADC; login=true; hasDeliver=0; index_location_city=%E5%85%A8%E5%9B%BD; unick=%E6%8B%89%E5%8B%BE%E7%94%A8%E6%88%B75537; showExpriedIndex=1; showExpriedCompanyHome=1; showExpriedMyPublish=1; Hm_lvt_4233e74dff0ae5bd0a3d81c6ccf677e6=1536922564,1537493466; TG-TRACK-CODE=hpage_code; _gid=GA1.2.969240417.1537831173; gate_login_token=2b25e668e5c44f984fa699aa1142cccd6a9c3d914111e874bf297af1b325c383; __utma=14951595.764376373.1536932562.1537589263.1537831174.12; Hm_lpvt_4233e74dff0ae5bd0a3d81c6ccf756e6=1537831174; LGRID=20180925071933-4b7c4ce8-c3330-11e8-bb1c-5254005c3644 '''
注意,不要直接複製使用上面的headers程式碼,其中的資訊涉及到我的個人隱私,所以都被修改過了,不能正常使用。必須自己複製你的瀏覽器裡面 searchPosition.json
的真實 Request Headers
5. 把headers轉化為字典物件
headers是一長串字元,不符合Python要使用的字典物件格式 {'key':'value'}
的格式,我們必須轉化它一下。
你可以像處理 params
那樣手工加引號加逗號,也可以使用下面這個程式碼實現自動轉化,有興趣的話可以參考程式碼裡的註釋理解,或者不管什麼意思直接使用也行。
#單元1.5 def str2obj(s,s1=';',s2='='): li=s.split(s1) res={} for kv in li: li2=kv.split(s2) if len(li2)>1: res[li2[0]]=li2[1] return res headers=str2obj(headers,'\n',': ') print(headers)
把這個放在緊跟單元1後面,然後全部執行,可以看到輸出的結果大致如下:

轉化後的header
6. 重新發送請求
這次我們模擬瀏覽器,攜帶我們複製來的Headers和Form Data資料重新發送請求,檢視輸出結果:
#單元2 import requests jsonData=requests.get(url,params=params,headers=headers) print(jsonData.text)
我們執行全部程式碼,可以看到正常輸出的結果資料:

獲取資料成功
7. 解析json資料
json
資料格式其實和我們一直用的字典物件幾乎是一樣的,類似這樣:
zidian={ 'a':'1', 'b':{ 'b1':'2-1', 'b2':'2-2' } }
json資料和字典物件都是可以一層層巢狀的(上面b1就是巢狀在b物件裡面的)。如果我們要獲取b2的值就可以 print(zidian['b']['b2'])
,它會輸出 '2-1'
我們可以用下面的程式碼把剛才Request獲得的很多json資料整齊的顯示出來:
import json import requests jsonData=requests.get(url,params=params,headers=headers) data=json.loads(jsonData.text) print(json.dumps(data,indent=2,ensure_ascii=False))
這裡我們import引入了json功能模組,然後使用 data=json.loads(jsonData.text)
的 loads
方法把Request獲得的字串資料轉換為正式的json物件格式, dumps
方法就是把json物件再變為字串輸出。是的,loads和dumps是相反的功能,但是我們的dumps加了 indent=2,ensure_ascii=False
就能讓輸出的字串顯示的很整齊了,如下圖:

整齊顯示的json物件
這樣,我們就可以從圖中的層級一層層找到需要的資料資訊了,比如 data['content']['data']['page']['result']
就是我們需要的職位的列表物件,我們可以用for迴圈輸出這個列表的每一項:
import json import requests jsonData=requests.get(url,params=params,headers=headers) data=json.loads(jsonData.text) #print(json.dumps(data,indent=2,ensure_ascii=False)) jobs=data['content']['data']['page']['result'] for job in jobs: print(job['positionName'])
得到的結果是:

輸出職位名稱
8. 輸出資料到Excel
我們只要針對每個job進行詳細的處理,就可以輸出更多內容了:
import json import requests import time hud=['職位','薪酬','學歷','經驗'] print('\t'.join(hud)) for i in range(1,6): params['pageNo']=i jsonData=requests.get(url,params=params,headers=headers) data=json.loads(jsonData.text) jobs=data['content']['data']['page']['result'] for job in jobs: jobli=[] jobli.append(job['positionName']) jobli.append(job['salary']) jobli.append(job['education']) jobli.append(job['workYear']) print('\t'.join(jobli)) time.sleep(1)
從瀏覽器可以看到總共有47個職位,每頁10個共5頁,所以這裡都抓取了:

最終輸出資料
直接滑鼠選中,然後複製,開啟Excel表格新建,選擇足夠大區域,右鍵,選擇性貼上,選擇Unicode,就能得到資料表格了。
10. 抓取二級職位詳情頁面
最後附上抓取職位詳情頁面的程式碼,綜合了我們這幾節前面使用的很多內容,僅供參考和理解:
#cell-1 url='https://www.lagou.com/gongsi/searchPosition.json' params={ 'companyId': '94', 'positionFirstType': '全部', 'schoolJob': 'true', 'pageNo': '1', 'pageSize': '10' } headers=''' POST /gongsi/searchPosition.json HTTP/1.1 Host: www.lagou.com ... LGRID=20180925071933-4b7c4ce8-c050-11e8-bb5c-5254005c3644 ''' jobheaders=''' Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8 ... User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 '''
這裡的 jobheader
是給二級頁面使用的。你必須複製自己瀏覽器 https://www.lagou.com/jobs/5151679.html?source=pl&i=pl-6
頁面的Request請求 5151679.html?source=pl&i=pl-6
的資訊header資訊,我這裡只是示意,不能直接複製使用。
#cell-2 def str2obj(s,s1=';',s2='='): li=s.split(s1) res={} for kv in li: li2=kv.split(s2) if len(li2)>1: res[li2[0]]=li2[1] return res headers=str2obj(headers,'\n',': ') jobheaders=str2obj(jobheaders,'\n',': ')
這裡只是最後一行,也轉化jobheaders物件。
#cell-3 import json import requests import time from bs4 import BeautifulSoup hud=['頁數','職位','薪酬','學歷','經驗','描述'] def getJobs(compId=94,school='true',pageCount=1): for i in range(1,1+pageCount): params['pageNo']=str(i) params['companyId']=compId params['schoolJob']=school params['pageNo']=i jsonData=requests.get(url,params=params,headers=headers) data=json.loads(jsonData.text) #print(json.dumps(data,indent=2,ensure_ascii=False)) jobs=data['content']['data']['page']['result'] for job in jobs: jobli=[str(i)] jobli.append(job['positionName']) jobli.append(job['salary']) jobli.append(job['education']) jobli.append(job['workYear']) #請求二級詳情頁面 pid=job['positionId'] joburl='https://www.lagou.com/jobs/'+str(pid)+'.html' jobhtml=requests.get(joburl,headers=jobheaders) jobsoup= BeautifulSoup(jobhtml.text, 'html.parser') desc=jobsoup.find('dd','job_bt').div.text desc=desc.replace('\n','') jobli.append(desc) time.sleep(1) print('\t'.join(jobli)) time.sleep(1)
這裡沒有直接使用,而是def了一個函式getJobs,帶有三個引數compId公司序號,school是否社招,pageCount一共有多少頁。
#cell-4 print('\t'.join(hud)) getJobs(94,'false',5)
啟動。
本篇小節
- 頁面可以不直接包含資料,而是通過執行JavaScript程式碼,從伺服器重新獲取資料,再填充到頁面上。
- 任何向伺服器發起的請求都可以在Network面板找到資訊,帶了哪些引數params(Form Data),帶了什麼樣的headers,等等
- json資料和字典物件用起來一樣,從Request獲取的文字text資料需要用json.load轉換一下,然後就可以用shuju['aa']['bb']的方法一層層找到我們需要的資訊
智慧決策上手系列教程索引
每個人的智慧決策新時代
如果您發現文章錯誤,請不吝留言指正;
如果您覺得有用,請點喜歡;
如果您覺得很有用,歡迎轉載~
END