:requests+parsel抓取招聘數(shù)據(jù)并寫入CSV)
簡介這份資源面向具備Python基礎(chǔ)、希望入門網(wǎng)絡(luò)爬蟲的開發(fā)者與數(shù)據(jù)分析初學(xué)者以獵聘網(wǎng)為例完整演示如何抓取招聘崗位信息并落地為CSV文件。內(nèi)容圍繞Requests發(fā)送GET請求、Parsel結(jié)合CSS選擇器解析頁面、請求頭偽裝、字典組織字段、csv模塊追加寫入等環(huán)節(jié)展開同時涉及異常處理與robots協(xié)議等合規(guī)要點幫助讀者理解從請求到存儲的完整鏈路。資源包共1個docx文檔約18KB以圖文筆記形式呈現(xiàn)關(guān)鍵代碼與解析思路便于對照練習(xí)。目前已有4903人學(xué)習(xí)下載說明該案例在爬蟲入門場景中具有較高的參考價值。讀者可借此掌握職位標題、城市、薪資、經(jīng)驗學(xué)歷、公司標簽等字段的提取方法并理解反爬與數(shù)據(jù)清洗的常見處理方向為后續(xù)采集其他站點或做崗位數(shù)據(jù)分析打下基礎(chǔ)。1. 從一份招聘 CSV 說起為什么我還在用 requests parsel 這套老組合上周幫一個做數(shù)據(jù)分析的朋友跑崗位薪酬分布他丟過來一份 CSV字段整整齊齊標題、城市、薪資、經(jīng)驗要求、學(xué)歷要求、職位標簽、公司名稱、公司標簽、詳情頁。我問他怎么弄的他說照著一段 Python 爬蟲代碼改的核心就三樣?xùn)|西——requests 發(fā)請求、parsel 解析頁面、csv 落盤。這套組合放到今天不算新甚至有人覺得過時但真到要快速抓一批結(jié)構(gòu)化崗位數(shù)據(jù)、還要能直接喂給 Excel 或 pandas 的場景它依然是最短路徑。這份資源解決的就是這件事以招聘網(wǎng)站為例用 GET 請求拿到搜索頁 HTML用 CSS 選擇器把列表項里的字段一個個摳出來再以追加模式寫進 CSV。適合兩類人——剛學(xué)完 requests 和選擇器、想找一個完整閉環(huán)練手的以及手頭有臨時數(shù)據(jù)需求、不想上 Scrapy 這種重框架的從業(yè)者。下面我按能跑起來的順序拆一遍重點講參數(shù)怎么定、選擇器為什么這么寫、以及那些不跑一次根本不知道的坑。2. 請求層拆解URL、params 與 headers 到底怎么定2.1 先搞清楚請求方式和參數(shù)從哪來很多人卡在第一步不是不會寫代碼而是不知道該往params里塞什么。正確做法是打開目標搜索頁按 F12 進開發(fā)者工具切到 Network 面板篩選 Doc 類型請求然后刷新頁面。在請求列表里找那個返回 HTML 的文檔請求點進去看兩個地方Headers 里的 Request Method以及 Payload 里的查詢參數(shù)。以招聘搜索頁為例請求方式是 GET參數(shù)掛在 URL 問號后面形如?inputFromwww_indexworkYearCode0keypythonsceneinputckIdxxx。這些就是你要放進params字典的東西。注意key就是搜索關(guān)鍵詞改它就能換崗位ckId這類看著像會話標識的字段有時不帶也能返回數(shù)據(jù)有時會觸發(fā)校驗先原樣帶上最穩(wěn)。import requests url https://www.liepin.com/zhaopin/ # params 對應(yīng) URL 問號后的查詢參數(shù)key 控制搜索的崗位關(guān)鍵詞 data { inputFrom: www_index, workYearCode: 0, key: python, scene: input, ckId: kfpt1emhvkrshc9o4h2xquxd74pfihmb, } # User-Agent 必須偽裝成真實瀏覽器否則容易被直接拒絕 headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36 Edg/99.0.1150.46 } response requests.get(urlurl, headersheaders, paramsdata) print(response.status_code)邏輯說明requests.get會把params自動拼接到 URL 上等價于你手動拼字符串但更安全會自動做 URL 編碼。headers里最關(guān)鍵的是User-Agent它告訴服務(wù)器我是瀏覽器。參數(shù)說明workYearCode0一般表示不限工作年限sceneinput表示來自搜索框輸入這些值直接照抄抓包結(jié)果即可不要自己臆造。2.2 判斷請求是否真的成功寫完上面幾行別急著往下解析。先看response.status_code是不是 200再看len(response.text)有沒有幾萬字符。如果返回的是幾百字符的短頁面八成是被攔了或者參數(shù)不對。我一般會順手把response.text存成debug.html用瀏覽器打開肉眼確認列表項在不在。# 快速自檢狀態(tài)碼 頁面長度 是否含目標關(guān)鍵詞 print(response.status_code) print(len(response.text)) print(job-list-item in response.text) # 返回 True 說明列表結(jié)構(gòu)在如果job-list-item不在返回內(nèi)容里先別改選擇器問題多半在請求層——要么 UA 被識別要么參數(shù)缺了關(guān)鍵項要么該頁面本身需要先訪問首頁拿 Cookie。常見做法是先用requests.Session()訪問一次首頁再帶著同一個 session 去請求搜索頁。3. 解析層實戰(zhàn)CSS 選擇器逐字段提取與容錯3.1 用 parsel 定位列表容器拿到 HTML 后用parsel.Selector包一層就能用 CSS 或 XPath 選擇器提取。核心思路是先抓大容器再在容器內(nèi)逐字段摳這樣比全局選擇器穩(wěn)得多因為列表項之間的結(jié)構(gòu)是重復(fù)的。import parsel selector parsel.Selector(response.text) # 先定位每一個職位卡片這是最外層重復(fù)單元 lis selector.css(.left-list-box .job-list-item) print(抓到卡片數(shù), len(lis))邏輯說明.left-list-box .job-list-item是后代選擇器意思是left-list-box 里面所有 job-list-item。參數(shù)說明類名必須和抓包看到的 DOM 完全一致差一個字符就是空列表。如果len(lis)為 0回到上一步確認 HTML 里到底有沒有這個類名。3.2 逐字段提取與列表拼接每個卡片內(nèi)部再分別取標題、城市、薪資、標簽、公司信息、詳情頁鏈接。這里有個高頻坑標簽是一個列表前兩個是經(jīng)驗和學(xué)歷剩下的才是技能標簽需要用切片和join處理。for li in lis: title li.css(.job-title-box div:nth-child(1)::attr(title)).get() city li.css(.job-dq-box .ellipsis-1::text).get() money li.css(.job-detail-header-box .job-salary::text).get() # 標簽是列表前兩項固定為經(jīng)驗和學(xué)歷其余合并成技能標簽 tag_list li.css(.job-labels-box .labels-tag::text).getall() exp tag_list[0] if len(tag_list) 0 else edu tag_list[1] if len(tag_list) 1 else tag ,.join(tag_list[2:]) company_name li.css(.company-name::text).get() company_list li.css(.company-tags-box span::text).getall() company_tag ,.join(company_list) href li.css(.job-detail-box a:nth-child(1)::attr(href)).get() print(title, city, money, exp, edu, tag, company_name, company_tag, href)邏輯說明::text取文本::attr(title)取屬性值.get()取第一個匹配.getall()取全部匹配返回列表。參數(shù)說明tag_list[0]和[1]前加了長度判斷這是血淚經(jīng)驗——不是每條崗位都帶滿標簽直接索引會拋IndexError讓整個循環(huán)崩掉。,.join()把列表拼成逗號分隔字符串方便寫進 CSV 單格。提示選擇器里的nth-child(1)依賴 DOM 順序網(wǎng)站改版時最容易失效。如果某天標題全空了優(yōu)先懷疑這里而不是懷疑請求。4. 落盤與工程化CSV 寫入、翻頁與異常處理4.1 DictWriter 寫 CSV 的正確姿勢用csv.DictWriter比手拼字符串靠譜字段順序由fieldnames決定字典的鍵必須和它對齊否則會報ValueError。打開文件時newline不能省否則 Windows 下每行之間會多出空行encodingutf-8保證中文不亂碼。import csv f open(招聘.csv, modea, encodingutf-8, newline) csv_write csv.DictWriter(f, fieldnames[ 標題, 城市, 薪資, 經(jīng)驗要求, 學(xué)歷要求, 職位標簽, 公司名稱, 公司標簽, 詳情頁, ]) # 首次寫入時寫表頭重復(fù)運行會重復(fù)寫建議加判斷 csv_write.writeheader() dit { 標題: title, 城市: city, 薪資: money, 經(jīng)驗要求: exp, 學(xué)歷要求: edu, 職位標簽: tag, 公司名稱: company_name, 公司標簽: company_tag, 詳情頁: href, } csv_write.writerow(dit) f.close()邏輯說明modea是追加模式多次運行會往同一個文件后面加數(shù)據(jù)適合邊抓邊存。參數(shù)說明writeheader()只在文件為空時調(diào)用一次比較穩(wěn)妥否則每次運行都會插一行表頭。writerow(dit)按fieldnames的順序把字典值寫進去鍵名對不上會直接報錯這其實是好事——能幫你早發(fā)現(xiàn)字段拼寫問題。4.2 翻頁與請求節(jié)奏單頁數(shù)據(jù)量有限要抓多頁就得改params里的分頁參數(shù)常見是curPage或page循環(huán)遞增。但別裸奔式狂請求加個time.sleep控制節(jié)奏既降低被封風(fēng)險也更接近真人行為。import time for page in range(1, 6): data[curPage] str(page) # 分頁參數(shù)名以實際抓包為準 resp requests.get(urlurl, headersheaders, paramsdata) # ...解析與寫入邏輯... time.sleep(2) # 每頁間隔 2 秒邏輯說明把分頁參數(shù)放進循環(huán)每輪重新請求。參數(shù)說明curPage只是常見命名實際字段名必須看你抓包結(jié)果寫錯會導(dǎo)致每頁返回相同內(nèi)容。time.sleep(2)是經(jīng)驗值太短容易觸發(fā)限制太長效率低。4.3 異常處理別省網(wǎng)絡(luò)請求和解析都可能出意外一個try-except能讓程序在個別頁面失敗時繼續(xù)跑而不是整體崩掉。for li in lis: try: title li.css(.job-title-box div:nth-child(1)::attr(title)).get() # ...其余字段提取... csv_write.writerow(dit) except Exception as e: print(單條解析失敗, e) continue邏輯說明把單條記錄的提取和寫入包在try里出錯就打印并跳過不影響后續(xù)記錄。參數(shù)說明捕獲Exception是粗粒度做法生產(chǎn)環(huán)境可以細化到IndexError、AttributeError但練手階段先保證不中斷更重要。5. 避坑排查那些不跑一次根本不知道的問題5.1 現(xiàn)象返回內(nèi)容里沒有列表項選擇器全空原因請求被識別為非瀏覽器或缺少必要 Cookie/參數(shù)。解決補全User-Agent用requests.Session()先訪問首頁再請求搜索頁必要時把抓包看到的Referer也帶上。5.2 現(xiàn)象標簽字段報 IndexError程序中斷原因部分崗位沒有經(jīng)驗或?qū)W歷標簽tag_list長度不足 2。解決索引前判斷長度用if len(tag_list) 0 else 兜底別假設(shè)每條數(shù)據(jù)都完整。5.3 現(xiàn)象CSV 用 Excel 打開中文亂碼原因Excel 默認按本地編碼解析不認 UTF-8。解決寫入時用encodingutf-8-sig它會帶 BOM 頭Excel 就能正確識別中文。5.4 現(xiàn)象多次運行后 CSV 里出現(xiàn)多行表頭原因writeheader()每次運行都執(zhí)行。解決先判斷文件是否為空或是否已存在只在首次寫入時調(diào)用。5.5 現(xiàn)象翻頁后每頁數(shù)據(jù)一模一樣原因分頁參數(shù)名寫錯或參數(shù)沒真正生效。解決回到 Network 面板對比第一頁和第二頁請求的 Payload 差異找到真正變化的那一項。注意抓取前先看目標站點的 robots.txt控制請求頻率只取公開的崗位信息不要碰任何需要登錄才能看到的個人數(shù)據(jù)。6. 進階技巧把 CSV 變成能直接分析的數(shù)據(jù)跑通基礎(chǔ)版之后真正拉開差距的是數(shù)據(jù)清洗。原始薪資字段往往是15-25k·13薪這種字符串直接做統(tǒng)計沒法用。我一般會寫個小函數(shù)把它拆成最低、最高兩個數(shù)值列再算個中位數(shù)。import re def parse_salary(text): 把 15-25k·13薪 解析成 (15, 25)解析失敗返回 (None, None) if not text: return None, None nums re.findall(r(\d), text) if len(nums) 2: return int(nums[0]), int(nums[1]) return None, None # 用法讀回 CSV 后逐行處理 import csv with open(招聘.csv, encodingutf-8-sig) as f: rows list(csv.DictReader(f)) for r in rows: low, high parse_salary(r[薪資]) r[薪資下限] low r[薪資上限] high邏輯說明re.findall抓出字符串里所有數(shù)字前兩個當作區(qū)間上下限。參數(shù)說明正則(\d)匹配連續(xù)數(shù)字遇到面議這類無數(shù)字的返回(None, None)后續(xù)統(tǒng)計時過濾掉即可。這樣處理后用 pandas 一行df.groupby(城市)[薪資上限].mean()就能出城市薪酬對比。另一個實用技巧是去重。同一崗位可能在不同頁重復(fù)出現(xiàn)用詳情頁鏈接做唯一鍵去重最可靠seen set() unique_rows [] for r in rows: key r[詳情頁] if key and key not in seen: seen.add(key) unique_rows.append(r) print(f去重前 {len(rows)} 條去重后 {len(unique_rows)} 條)邏輯說明用集合記錄已出現(xiàn)的詳情頁鏈接重復(fù)的跳過。參數(shù)說明key為空時也跳過避免把無效記錄算進去。從那以后我每次抓完數(shù)據(jù)都強制先跑一遍去重和薪資解析再談分析——不然拿著帶重復(fù)、帶字符串薪資的原始表做圖結(jié)論全是錯的。這套 requests parsel csv 的組合不花哨但把請求、解析、落盤、清洗四步走完一份能直接用的崗位數(shù)據(jù)集就到手了。希望幫到你。本文還有配套的精品資源點擊獲取