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