實戰(zhàn)|Python爬取字節(jié)跳動1W+招聘信息
擊上方“Python爬蟲與數(shù)據(jù)挖掘”,進行關(guān)注
回復(fù)“書籍”即可獲贈Python從入門到進階共10本電子書
歡迎加入菜J學(xué)Python交流群與數(shù)據(jù)專家交流。
作者簡介:Pandas數(shù)據(jù)處理專家,10余年編碼經(jīng)驗,至今已幫助過成千上萬名數(shù)據(jù)從業(yè)者解決工作實際遇到的問題,現(xiàn)為菜J學(xué)Python核心技術(shù)團隊成員之一。

我們打開開發(fā)者工具并訪問:
https://jobs.bytedance.com/experienced/position?keywords=&category=&location=&project=&type=&job_hot_flag=¤t=1&limit=10
這次訪問監(jiān)控到的數(shù)據(jù)很多,其中這個posts接口才有我們需要的json數(shù)據(jù):

觀察響應(yīng)頭發(fā)現(xiàn)一個重要參數(shù)csrf:

說明字節(jié)跳動的網(wǎng)站具備csrf校驗的功能,后文將再介紹如何獲取到這個csrf的token。
查看請求參數(shù):

參數(shù)包裝函數(shù)
為了正常爬取時的方便,我們需要先將上面需要的參數(shù),組織成python能夠識別的字典形式。直接復(fù)制粘貼有很多需要加雙引號的地方,但我們可以編程解決這個問題。
首先,定義一個處理函數(shù):
import re
def warp_heareder(s):
print("{")
lines = s.splitlines()
for i, line in enumerate(lines):
k, v = line.split(": ")
if re.search("[a-zA-Z]", k):
k = f'"{k}"'
if re.search("[a-zA-Z]", v):
v = f'"{v}"'
print(f" {k}: {v},")
print("}")
處理請求頭:

處理post請求數(shù)據(jù):

首先,清空cookie:

然后刷新頁面,查看網(wǎng)絡(luò)請求的抓包情況:

找啊找,終于找到了一個set-cookie的響應(yīng)頭,而且這個設(shè)置cookie參數(shù)包括了csrf的設(shè)置。那么這個接口我們就可以用來作為獲取csrf校驗值的接口。
使用session保存響應(yīng)頭設(shè)置的cookie:
import requests
session = requests.session()
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36',
'Origin': 'https://jobs.bytedance.com',
'Referer': f'https://jobs.bytedance.com/experienced/position?keywords=&category=&location=&project=&type=&job_hot_flag=¤t=1&limit=10'
}
data = {
"portal_entrance": 1
}
url = "https://jobs.bytedance.com/api/v1/csrf/token"
r = session.post(url, headers=headers, data=data)
r
結(jié)果:
查看獲取到的cookie:
cookies = session.cookies.get_dict()
cookies
結(jié)果:
{'atsx-csrf-token': 'RDTEznQqdr3O3h9PjRdWjfkSRW79K_G16g85FrXNxm0%3D'}
顯然這個token相對真實需要的存在url編碼,現(xiàn)在對它進行url解碼:
from urllib.parse import unquote
unquote(cookies['atsx-csrf-token'])
結(jié)果:
'RDTEznQqdr3O3h9PjRdWjfkSRW79K_G16g85FrXNxm0='開始爬取第一頁的數(shù)據(jù)
有了token我們就可以順利的直接訪問接口了:
import requests
import json
headers = {
"Accept": "application/json, text/plain, */*",
"Host": "jobs.bytedance.com",
"Origin": "https://jobs.bytedance.com",
"Referer": "https://jobs.bytedance.com/experienced/position?keywords=&category=&location=&project=&type=&job_hot_flag=¤t=1&limit=10",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",
"x-csrf-token": unquote(cookies['atsx-csrf-token']),
}
data = {
"job_category_id_list": [],
"keyword": "",
"limit": 10,
"location_code_list": [],
"offset": 0,
"portal_entrance": 1,
"portal_type": 2,
"recruitment_id_list": [],
"subject_id_list": []
}
url = "https://jobs.bytedance.com/api/v1/search/job/posts"
r = session.post(url, headers=headers, data=json.dumps(data))
r
結(jié)果:
響應(yīng)碼是200,說明已經(jīng)順利通過了校驗,現(xiàn)在查看一下數(shù)據(jù)結(jié)構(gòu):
r.json()
結(jié)果:

使用Pandas對json數(shù)據(jù)進行處理
import pandas as pd
df = pd.DataFrame(r.json()['data']['job_post_list'])
df.head(3)
結(jié)果:

然后我們對各列提取出我們需要的數(shù)據(jù):
df.city_info = df.city_info.str['name']
df.recruit_type = df.recruit_type.str['parent'].str['name']
tmp = []
for x in df.job_category.values:
if x['parent']:
tmp.append(f"{x['parent']['name']}-{x['name']}")
else:
tmp.append(x['name'])
df.job_category = tmp
df.publish_time = df.publish_time.apply(lambda x: pd.Timestamp(x, unit="ms"))
df.head(2)
結(jié)果:

再刪除一些,明顯沒有任何用的列:
df.drop(columns=['sub_title', 'job_hot_flag', 'job_subject'], inplace=True)
df.head()
結(jié)果:

爬取字節(jié)跳動全部職位信息
有了上面的測試基礎(chǔ),我們就可以組織一下完整的爬取代碼:
import requests
from urllib.parse import unquote
import pandas as pd
import time
import os
session = requests.session()
page = 1500
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36',
'Origin': 'https://jobs.bytedance.com',
'Referer': f'https://jobs.bytedance.com/experienced/position?keywords=&category=&location=&project=&type=&job_hot_flag=¤t=1&limit={page}'
}
data = {
"portal_entrance": 1
}
url = "https://jobs.bytedance.com/api/v1/csrf/token"
r = session.post(url, headers=headers, data=data)
cookies = session.cookies.get_dict()
url = "https://jobs.bytedance.com/api/v1/search/job/posts"
headers["x-csrf-token"] = unquote(cookies["atsx-csrf-token"])
data = {
"job_category_id_list": [],
"keyword": "",
"limit": page,
"location_code_list": [],
"offset": 0,
"portal_entrance": 1,
"portal_type": 2,
"recruitment_id_list": [],
"subject_id_list": []
}
for i in range(11):
print(f"準備爬取第{i}頁")
data["offset"] = i*page
r = None
while not r:
try:
r = session.post(url, headers=headers,
data=json.dumps(data), timeout=3)
except Exception as e:
print("訪問超時!等待5s", e)
time.sleep(5)
df = pd.DataFrame(r.json()['data']['job_post_list'])
if df.shape[0] == 0:
print("爬取完畢?。?!")
break
df.city_info = df.city_info.str['name']
df.recruit_type = df.recruit_type.str['parent'].str['name']
tmp = []
for x in df.job_category.values:
if x['parent']:
tmp.append(f"{x['parent']['name']}-{x['name']}")
else:
tmp.append(x['name'])
df.job_category = tmp
df.publish_time = df.publish_time.apply(
lambda x: pd.Timestamp(x, unit="ms"))
df.drop(columns=['sub_title', 'job_hot_flag', 'job_subject'], inplace=True)
df.to_csv("bytedance_jobs.csv", mode="a", header=not os.path.exists("bytedance_jobs.csv"), index=False)
print(",".join(df.title.head(10)))
# 對結(jié)果去重
df = pd.read_csv("bytedance_jobs.csv")
df.drop_duplicates(inplace=True)
df.to_csv("bytedance_jobs.csv", index=False)
print("共爬取", df.shape[0], "行無重復(fù)數(shù)據(jù)")
結(jié)果:

僅7.3秒爬完了字節(jié)跳動1W+以上的職位信息。
可以讀取看看:
import pandas as pd
df = pd.read_csv("bytedance_jobs.csv")
df
結(jié)果:

有1萬個以上的職位信息。
補充資料
CSRF的含義
CSRF(Cross-site request forgery)也被稱為 one-click attack或者 session riding,中文全稱是叫「跨站請求偽造」。一般來說,攻擊者通過偽造用戶的瀏覽器的請求,向訪問一個用戶自己曾經(jīng)認證訪問過的網(wǎng)站發(fā)送出去,使目標網(wǎng)站接收并誤以為是用戶的真實操作而去執(zhí)行命令。常用于盜取賬號、轉(zhuǎn)賬、發(fā)送虛假消息等。攻擊者利用網(wǎng)站對請求的驗證漏洞而實現(xiàn)這樣的攻擊行為,網(wǎng)站能夠確認請求來源于用戶的瀏覽器,卻不能驗證請求是否源于用戶的真實意愿下的操作行為。

CSRF的攻擊原理
比如,博客網(wǎng)站A的后臺存在一個添加文章的功能,為方便說明,假設(shè)它是個get請求,如/admin/add?title=標題&body=內(nèi)容。要提交這個請求時,會判斷用戶是否已經(jīng)登錄,如果沒登錄則會自動跳轉(zhuǎn)到登錄頁面,只有管理員有權(quán)限登錄。所以,攻擊者即使知道該請求路徑,也過不了登錄這關(guān)。
但是攻擊者在自己的網(wǎng)站或支持富文本編輯的論壇網(wǎng)站B上評論如下的內(nèi)容:
當某個用戶打開網(wǎng)站B時,如果對于網(wǎng)站A的登錄后臺的session還有效,那么他就會自動向博客網(wǎng)站A后臺發(fā)送添加文章的請求,完成攻擊者的目的。這個過程中,攻擊者不需要拿到用戶的cookie就可以完成攻擊。
當然博客網(wǎng)站A可以把校驗改成post請求來避免來著img標簽帶來的攻擊,但仍然無法避免通過javascript模擬post請求帶來的攻擊(將上面html代碼改成JavaScript代碼即可)。
防范CSRF攻擊的方法
開啟token驗證:CSRF 攻擊之所以能夠成功,是因為黑客可以完全偽造用戶的請求,該請求中所有的用戶驗證信息都是存在于cookie中,因此黑客可以在不知道這些驗證信息的情況下直接利用用戶自己的cookie 來通過安全驗證。要抵御 CSRF,關(guān)鍵在于在請求中放入黑客所不能偽造的信息,并且該信息不存在于 cookie 之中??梢栽?HTTP 請求中以參數(shù)的形式加入一個隨機產(chǎn)生的 token,并在服務(wù)器端建立一個攔截器來驗證這個 token,如果請求中沒有token或者 token 內(nèi)容不正確,則認為可能是 CSRF 攻擊而拒絕該請求。token 在用戶登陸后產(chǎn)生并放于session之中,然后在每次請求時把token 從 session 中拿出,與請求中的 token 進行比對。
一些問題的解釋
字節(jié)職位的服務(wù)本身并不需要防范CSRF攻擊,只是因為框架默認開啟了這項認證,我們也就只需根據(jù)規(guī)則完成這個認證,證明我跟上一訪問是同一個人即可。
為了使python的訪問能緩存cooike相關(guān)的信息,所以我使用了session會話,響應(yīng)頭設(shè)置的cookie都會保留下來。
我使用r = session.post(url, headers=headers, data=json.dumps(data))而不是直接使用r = session.post(url, headers=headers, data=data)的原因是字節(jié)跳動nginx服務(wù)器json文本校驗的原因,requests庫內(nèi)部將字典對象轉(zhuǎn)為json文本的結(jié)果無法被nginx解析,但直接使用json庫將字典對象轉(zhuǎn)換成功的json文本卻可以被nginx服務(wù)器解析通過(不信可以自己嘗試)。
-------------------?End?-------------------
往期精彩文章推薦:

歡迎大家點贊,留言,轉(zhuǎn)發(fā),轉(zhuǎn)載,感謝大家的相伴與支持
想加入Python學(xué)習(xí)群請在后臺回復(fù)【入群】
萬水千山總是情,點個【在看】行不行
/今日留言主題/
隨便說一兩句吧~~
