戰(zhàn):打造招聘數(shù)據(jù)可視化大屏)
簡介這是一份高分畢業(yè)設(shè)計項(xiàng)目基于Python爬蟲與MapReduce分析構(gòu)建招聘信息大數(shù)據(jù)可視化系統(tǒng)面向軟件工程、計算機(jī)科學(xué)、人工智能、通信工程等專業(yè)的在校學(xué)生與教師可廣泛應(yīng)用于畢業(yè)設(shè)計、課程設(shè)計、項(xiàng)目初期立項(xiàng)演示及個人學(xué)習(xí)進(jìn)階。資源包含完整源碼、部署文檔與全部數(shù)據(jù)資料共207個文件壓縮包約17.13MB文件類型覆蓋Python后端腳本py、前端頁面html/css/js、界面效果演示gif、持久化數(shù)據(jù)庫sqlite3以及配置說明json/xml等直觀展示從招聘信息爬取、MapReduce分布式處理到可視化大屏呈現(xiàn)的完整流程。目前已有334人學(xué)習(xí)下載項(xiàng)目代碼在macOS、Windows10/11及Linux系統(tǒng)下均測試運(yùn)行成功并經(jīng)導(dǎo)師指導(dǎo)認(rèn)可、答辯評審分達(dá)95分質(zhì)量有保障。讀者既可對照部署文檔快速啟動并復(fù)現(xiàn)整體流程也可基于現(xiàn)有代碼修改擴(kuò)展實(shí)現(xiàn)個性化功能是畢業(yè)設(shè)計與課程設(shè)計不可多得的參考資料。1. 一個Python招聘爬蟲項(xiàng)目把MapReduce用在了實(shí)處做招聘數(shù)據(jù)分析類畢設(shè)最容易被答辯老師追問的問題就是數(shù)據(jù)都存進(jìn) MySQL 了為什么還要繞一圈 MapReduce大多數(shù)及格分的項(xiàng)目到這里就卡住了。這套高分畢業(yè)設(shè)計源碼的價值恰恰在于它沒有把 MapReduce 當(dāng)擺設(shè)而是用兩類 MR 作業(yè)分別處理職位描述文本的關(guān)鍵詞詞頻以及城市、學(xué)歷、薪資區(qū)間這類結(jié)構(gòu)化字段的聚合再把計算結(jié)果回傳給 Flask 和 ECharts 輸出可視化大屏。爬蟲負(fù)責(zé)拿數(shù)據(jù)MapReduce 負(fù)責(zé)算數(shù)據(jù)可視化負(fù)責(zé)講數(shù)據(jù)數(shù)據(jù)鏈路完整閉合。適合正在做大數(shù)據(jù)方向畢設(shè)、課設(shè)或者想搞明白 requests 爬蟲與離線批處理如何銜接的讀者對照源碼逐個模塊拆解。2. 數(shù)據(jù)源頭requests爬蟲的字段設(shè)計、限速與落庫爬蟲部分只做到“能跑”是不夠的關(guān)鍵要能說清楚為什么這么設(shè)計。這套系統(tǒng)在采集層選的是 requests BeautifulSoup 的組合沒有上 scrapy 框架。原因不難理解requests 的代碼對答辯評委來說可讀性更高重試、限速、解析邏輯寫在一個函數(shù)里一眼能看完scrapy 的異步引擎吞吐量確實(shí)大但對單站點(diǎn)、日新增幾千條的招聘數(shù)據(jù)來說屬于過度設(shè)計。真正拉開差距的是下面三個細(xì)節(jié)字段怎么定、請求頻率怎么控、重復(fù)數(shù)據(jù)怎么去。2.1 先定數(shù)據(jù)結(jié)構(gòu)再寫抓取邏輯我拿到爬蟲需求的第一件事永遠(yuǎn)是先把輸出結(jié)構(gòu)定死而不是先寫請求。這套源碼里的崗位核心結(jié)構(gòu)如下后續(xù) MapReduce、可視化所有字段都從這里對齊字段名說明是否必存url_md5職位頁 URL 的 MD5全庫去重鍵是job_title職位名稱是company公司名稱是city工作城市是salary_low薪資區(qū)間下限K是salary_high薪資區(qū)間上限K是education學(xué)歷要求是jd_text職位描述全文是薪資區(qū)間拆成兩個獨(dú)立字段是這套設(shè)計里最值得抄的一個點(diǎn)。很多項(xiàng)目把“10-15K”當(dāng)一個字符串存進(jìn)庫到了 MapReduce 階段還要二次解析白白多寫一個 Mapper。這里爬蟲階段就拆成 salary_low 和 salary_high后面聚合平均薪資時直接取兩字段均值即可數(shù)據(jù)清洗的成本前移到了采集層。2.2 requests 抓取與 BeautifulSoup 解析的骨架代碼核心抓取函數(shù)保持了非常樸素的結(jié)構(gòu)單函數(shù)完成請求、解析、URL 去重鍵生成三件事方便在答辯時逐行講清楚。import hashlib import random import time import requests from bs4 import BeautifulSoup UA_POOL [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0, Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2) AppleWebKit/605.1.15 Safari/605.1.15, ] def fetch_job_list(page: int) - list[dict]: url fhttps://example-job-site.com/jobs?kwpythonpn{page} resp requests.get( url, headers{User-Agent: random.choice(UA_POOL)}, timeout(3, 8), ) if resp.status_code ! 200: resp.raise_for_status() soup BeautifulSoup(resp.text, lxml) items [] for li in soup.select(ul.job-list li): a li.select_one(a.job-title) if a is None: continue title a.get_text(stripTrue) link a.get(href) items.append({ job_title: title, url: link, url_md5: hashlib.md5(link.encode(utf-8)).hexdigest(), }) time.sleep(random.uniform(1.2, 2.4)) return items代碼邏輯并不復(fù)雜但有三個參數(shù)值得在答辯時展開講。timeout 傳的是元組 (3, 8)3 秒是連接超時8 秒是數(shù)據(jù)讀取超時分開設(shè)置比單值更合理因?yàn)檎衅妇W(wǎng)站首屏響應(yīng)通常很快但列表頁偶爾會卡頓。UA_POOL 里放兩個 User-Agent 輪換用于降低單 UA 高頻請求被識別的概率。time.sleep 放在函數(shù)末尾而不是開頭保證無論上一頁請求成功還是拋異常下一次調(diào)用前都會先停 1.2 到 2.4 秒。如果導(dǎo)師追問分布式爬蟲怎么擴(kuò)展在這個函數(shù)基礎(chǔ)上把 sleep 改成從 Redis 里取一個隨機(jī)值再掛幾個不同出口 IP 的節(jié)點(diǎn)即可。2.3 限速與去重控制頻率比寫解析器更影響存活時間把目標(biāo)站點(diǎn)視為一個對外提供數(shù)據(jù)的接口控制請求頻率的優(yōu)先級永遠(yuǎn)高于解析速度。常犯的錯誤是一口氣把列表頁 1 到 200 全部請求完前 50 頁正常第 60 頁開始出現(xiàn) 403。限速參數(shù)建議按下面的表設(shè)置作為初始值足夠穩(wěn)參數(shù)建議值說明timeout(3, 8)連接 3 秒讀取 8 秒sleep 區(qū)間1.2 - 2.4 秒均勻隨機(jī)分布避免固定間隔失敗重試2 次僅在 503/504 時重試403 不重試單次任務(wù)上限500 頁到達(dá)即停止等待下輪增量任務(wù)去重方面這份源碼直接用了 url_md5 做唯一鍵。寫入 MySQL 時用INSERT IGNORE或者先SELECT再INSERT都可以數(shù)據(jù)量在十萬量級時性能差距不大。更穩(wěn)妥的做法是維護(hù)一張已抓取 URL 表每次抓取前 bulk 查詢一次把已經(jīng)存在的 URL 過濾掉這樣斷點(diǎn)續(xù)爬時不會重復(fù)請求列表頁。2.4 為什么先落 MySQL 而不是直接進(jìn) HDFS很多做大數(shù)據(jù)畢設(shè)的同學(xué)會在這里走偏覺得爬完數(shù)據(jù)就應(yīng)該直接寫 HDFS讓整個鏈路都帶“大數(shù)據(jù)”標(biāo)簽。但招聘數(shù)據(jù)采集端的特點(diǎn)是量小、結(jié)構(gòu)雜、字段隨時可能調(diào)整MySQL 的隨意改表和條件查詢在這個階段遠(yuǎn)比 HDFS 里的文本文件方便。常規(guī)做法是爬蟲寫入 MySQL 的原始表跑 MapReduce 之前用 Sqoop 或直接導(dǎo)出 CSV 到 HDFS。這套源碼的部署文檔里也是這條路徑采集層 MySQL、計算層 HDFS、結(jié)果層再回 MySQL。這樣有三層好處原始數(shù)據(jù)可回溯、MR 輸入格式可控、可視化層不需要直接讀 HDFS 里的 part 文件。3. MapReduce分析JD詞頻統(tǒng)計與城市薪資聚合的實(shí)現(xiàn)MapReduce 部分是整個項(xiàng)目的得分核心。如果只用 SQL 的 GROUP BY 完成分析那和普通 Web 課設(shè)沒有區(qū)別。這套源碼的真正價值在于它在 MapReduce 框架里完成了兩類典型作業(yè)一類處理職位描述文本的詞頻統(tǒng)計輸出技能關(guān)鍵詞排名另一類針對結(jié)構(gòu)化字段做多維聚合計算各城市平均薪資和學(xué)歷分布。3.1 先分清兩類分析任務(wù)對應(yīng)兩條 MR 鏈路招聘場景里最常見的兩類需求第一個是文本分析型把 JD 描述中的“Java”“Python”“Hadoop”等技能詞拆出來統(tǒng)計頻次這類任務(wù)適合用 MapReduce 的經(jīng)典詞頻模式因?yàn)?MySQL 里用 LIKE 做文本分詞統(tǒng)計既慢又繞第二個是結(jié)構(gòu)化聚合型按城市、學(xué)歷、工作經(jīng)驗(yàn)分組計算平均薪資這類任務(wù)說穿了就是 GROUP BY但為了讓整條鏈路完整也要走一遍 MR 流程。區(qū)分這兩類任務(wù)的意義在于它們的 Mapper 輸出鍵值類型完全不同一個是文本關(guān)鍵詞到計數(shù)一個是城市名到薪資。整個 mapreduce 工作流程正好用同一套框架覆蓋這兩種場景也方便在答辯時對照說明。3.2 詞頻統(tǒng)計Mapper 切詞Reducer 歸并技能詞頻統(tǒng)計可以直接用 Hadoop 自帶的 WordCount 變形。把 HDFS 里的 JD 文本文件讀進(jìn)來按分隔符切成詞然后輸出詞和計數(shù) 1Reducer 把同一個詞的所有計數(shù)累加。public class SkillFreqMapper extends MapperLongWritable, Text, Text, IntWritable { private static final IntWritable ONE new IntWritable(1); private Text word new Text(); Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { // 每行是一條職位描述先整體轉(zhuǎn)小寫再按非字母字符切分 String line value.toString().toLowerCase(); String[] tokens line.split([^a-z#]); for (String token : tokens) { if (token.length() 2) { continue; // 過濾單字符噪聲 } word.set(token); context.write(word, ONE); } } }Override protected void reduce(Text key, IterableIntWritable values, Context context) throws IOException, InterruptedException { int sum 0; for (IntWritable val : values) { sum val.get(); } context.write(key, new IntWritable(sum)); }這個 Mapper 里的兩個細(xì)節(jié)值得注意轉(zhuǎn)小寫是必須的否則 “Java” 和 “java” 會被統(tǒng)計成兩個詞切分正則[^a-z#]保留字母以及 C、C# 這類帶符號的技能名把中文部分暫時過濾掉。如果想要統(tǒng)計中文技能詞我一般在 Mapper 前先用 HanLP 或 jieba 對 JD 文本做一次分詞再把分詞結(jié)果按空格拼接輸出這樣 Mapper 本身不需要改動只改輸入數(shù)據(jù)的前置格式。Reducer 部分沒有做額外復(fù)雜的邏輯直接累加輸出是標(biāo)準(zhǔn)的 mapreduce 基礎(chǔ)實(shí)戰(zhàn)寫法。3.3 結(jié)構(gòu)化聚合按城市計算平均薪資第二個作業(yè)處理的是結(jié)構(gòu)化字段。從 MySQL 導(dǎo)出的 CSV 文件每行包含城市和薪資上下限Mapper 讀取每一行把城市作為 key平均薪資和計數(shù) 1 作為 value 輸出。Reducer 端對同一城市的所有薪資求平均。from mrjob.job import MRJob class CitySalary(MRJob): def mapper(self, _, line): # CSV 導(dǎo)出格式: job_title,company,city,salary_low,salary_high,education parts line.split(,) if len(parts) 6: return city parts[2].strip() try: salary (float(parts[3]) float(parts[4])) / 2 except ValueError: return # 臟數(shù)據(jù)直接丟棄 yield city, (salary, 1) def reducer(self, city, values): total 0 count 0 for salary, one in values: total salary count one yield city, round(total / count, 2) if __name__ __main__: CitySalary.run()這里我用 mrjob 替代了 Java 實(shí)現(xiàn)原因是這套源碼的部署文檔里同時提供了 Java 和 Python 兩個版本Python 版在本地調(diào)試時不需要啟動 Hadoop 集群對課設(shè)場景更友好。這個作業(yè)的關(guān)鍵點(diǎn)在于 value 是一個 (salary, 1) 的復(fù)合結(jié)構(gòu)而不是只傳 salary這樣 Reducer 才能同時維護(hù)薪資總和與記錄數(shù)。如果只傳 salaryReducer 端還需要額外記錄一個計數(shù)器代碼會變得更繞。還需要提醒的是 CSV 解析不要用逗號直接 splitJD 描述里大概率含逗號常規(guī)做法是在導(dǎo)出時把 jd_text 字段去掉MR 只管結(jié)構(gòu)化字段文本分析走另一個作業(yè)。3.4 運(yùn)行命令與調(diào)優(yōu)參數(shù)無論用 Java 包還是 Streaming 方式跑 MR 作業(yè)時都需要指定輸入輸出目錄和 Reduce 任務(wù)數(shù)。hadoop jar /opt/hadoop/share/hadoop/tools/hadoop-streaming-*.jar \ -files mapper.py,reducer.py \ -mapper python3 mapper.py \ -reducer python3 reducer.py \ -input /recruit/jd_text \ -output /recruit/jd_freq \ -numReduceTasks 4-files 參數(shù)會把當(dāng)前目錄下的 mapper 腳本同步到所有節(jié)點(diǎn)這是 Streaming 方式最容易踩坑的地方漏掉任何依賴文件都會在運(yùn)行時拋 FileNotFoundException。-numReduceTasks 設(shè)置成 4 而不是默認(rèn)的 1是因?yàn)檎衅笖?shù)據(jù)按城市分布后 key 的個數(shù)遠(yuǎn)大于 4多開幾個 Reduce 能讓最終輸出文件分散到多個 part后續(xù)處理可以并行。如果數(shù)據(jù)量就幾萬條Reduce 任務(wù)數(shù)設(shè) 2 就夠了任務(wù)開太多反而增加調(diào)度開銷。參數(shù)建議值適用場景numReduceTasks2 - 4數(shù)據(jù)量在 10 萬條以內(nèi)mapreduce.map.memory.mb1024默認(rèn) 1024JD 文本較長時可調(diào) 2048mapreduce.output.fileoutputformat.compresstrue中間結(jié)果集較大時開啟壓縮dfs.blocksize128MB小文件多時在寫入端合并3.5 沒裝 Hadoop 集群時怎么演示 MR 流程本地開發(fā)環(huán)境不一定有完整集群這套源碼的部署文檔提供了一個很實(shí)用的降級方案用 mrjob 的 inline runner 在本地模擬讓它跑完整個 map 和 reduce 階段輸出結(jié)果和集群版一致。python3 city_salary.py -r inline city_salary_data.csv result.txt這樣做的意義在于MR 的編程模型、數(shù)據(jù)流向、shuffle 過程中的 key 分組邏輯全部保留差異只在并行度和文件系統(tǒng)。答辯時如果被問到“為什么不用 Spark”可以從觸發(fā)時機(jī)和資源成本兩個角度回答這套系統(tǒng)的數(shù)據(jù)量在十萬到百萬級之間MR 的批處理語義已經(jīng)足夠Spark 的引入需要額外的 YARN 資源和內(nèi)存管理屬于復(fù)雜度換不到收益的情況。4. Flask 后端與 ECharts把 MR 計算結(jié)果變成可交互大屏MapReduce 算完的結(jié)果如果只躺在 HDFS 里可視化層就無從下手。這套系統(tǒng)的處理方式是先把 MR 輸出文件中的統(tǒng)計分析結(jié)果回灌到 MySQL 的結(jié)果表里Flask 只負(fù)責(zé)提供 JSON 查詢接口ECharts 在瀏覽器端完成圖表渲染。這樣職責(zé)分離之后可視化層的開發(fā)和調(diào)試都不需要依賴 Hadoop 環(huán)境。4.1 數(shù)據(jù)回灌從 part-r-00000 到 MySQL 結(jié)果表每次 MR 作業(yè)跑完HDFS 的輸出目錄下會產(chǎn)生多個 part 文件文件名形如 part-r-00000、part-r-00001。回灌思路很直接用文件讀取方式將結(jié)果解析成結(jié)構(gòu)化行再批量寫入數(shù)據(jù)庫。import subprocess def load_mr_result_to_mysql(hdfs_output_path: str, table_name: str): # 先合并 HDFS 上的所有 part 文件到本地 subprocess.run( [hadoop, fs, -getmerge, hdfs_output_path, ./mr_result.tsv], checkTrue, ) with open(./mr_result.tsv, r, encodingutf-8) as f: rows [] for line in f: # 每行格式: key \t value key, value line.strip().split(\t, 1) rows.append((key, float(value))) sql fINSERT INTO {table_name} (dim_name, metric_value) VALUES (%s, %s) cursor.executemany(sql, rows) conn.commit()hadoop fs -getmerge是這個流程里最省事的命令它能把一個目錄下的所有 part 文件按順序合并成一個本地文件。split 時指定maxsplit1是為了防止 key 本身包含制表符而導(dǎo)致解包報錯MR 輸出的 key 和 value 之間只用一個制表符分隔。回灌完成后MySQL 里就多了一張 key-value 結(jié)構(gòu)的統(tǒng)計結(jié)果表Flask 查詢時還會按指標(biāo)類型做二次分組。4.2 Flask API 路由設(shè)計與 JSON 響應(yīng)后端接口設(shè)計的核心原則是接口參數(shù)白名單化查詢維度顯式化。下面這個城市薪資排行的接口完整展示了這套源碼的 API 風(fēng)格。from flask import Flask, jsonify, request app Flask(__name__) app.route(/api/v1/city_salary_rank) def city_salary_rank(): limit request.args.get(limit, default15, typeint) limit min(limit, 50) rows db_query( SELECT city, avg_salary, job_count FROM mr_city_salary ORDER BY avg_salary DESC LIMIT %s, (limit,), ) return jsonify({ code: 0, data: rows, }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)注意這里typeint的顯式聲明這樣當(dāng)請求參數(shù)不是數(shù)字時Flask 會直接返回 400 而不是把 TypeError 拋到瀏覽器控制臺。host 設(shè)為 0.0.0.0 而不是 127.0.0.1是為了讓同一局域網(wǎng)內(nèi)的其他終端也能訪問大屏頁面這在答辯演示時非常實(shí)用。debug 模式必須關(guān)掉否則瀏覽器端每個請求都會觸發(fā) Werkzeug 的重載器影響性能且存在安全隱患。4.3 ECharts 圖表配置柱狀圖與折線圖的基礎(chǔ)模板大屏頁面的核心是三張圖各城市平均薪資柱狀圖、技能關(guān)鍵詞排行條形圖、學(xué)歷分布餅圖。這里以城市薪資柱狀圖為例展示前端配置方式。fetch(/api/v1/city_salary_rank?limit15) .then(res res.json()) .then(json { const cities json.data.map(d d.city); const salaries json.data.map(d d.avg_salary); const chart echarts.init(document.getElementById(city-chart)); chart.setOption({ tooltip: { trigger: axis }, grid: { left: 80, right: 30 }, xAxis: { type: category, data: cities, axisLabel: { rotate: 30 } }, yAxis: { type: value, name: 平均薪資K }, series: [{ type: bar, data: salaries, itemStyle: { color: #5470c6 } }] }); });這段代碼的關(guān)鍵點(diǎn)在于 x 軸的城市名稱有 15 個全部橫排會互相擠壓axisLabel.rotate設(shè)為 30 度能讓城市名傾斜顯示而不重疊。yAxis 的 name 字段標(biāo)明了單位避免看圖的人對數(shù)值含義產(chǎn)生歧義。如果需要疊加折線圖展示崗位數(shù)量趨勢常規(guī)做法是在 series 數(shù)組里追加一個{ type: line, yAxisIndex: 1 }的配置項(xiàng)并在 yAxis 里增加一個右側(cè)坐標(biāo)軸。所有數(shù)據(jù)源的字段名必須與后端 JSON 返回的 key 完全對齊前端 map 出來是 undefined 時先打開瀏覽器 Network 面板看接口返回的原始結(jié)構(gòu)。4.4 大屏輪詢與緩存刷新策略大屏頁面如果通過手動刷新來更新數(shù)據(jù)演示效果會打折扣。這里用的是 setInterval 定時器輪詢方案輪詢間隔分兩級原始數(shù)據(jù)表和結(jié)果表更新頻率不同可視化查詢接口只關(guān)注結(jié)果表的變化。場景刷新間隔說明大屏圖表數(shù)據(jù)30 秒避免頻繁請求后端熱門技能詞云60 秒詞頻統(tǒng)計更新較慢抓取任務(wù)狀態(tài)10 秒需要實(shí)時反饋爬蟲進(jìn)度輪詢帶來的一個直接問題是數(shù)據(jù)庫壓力。這套源碼在 Flask 層做了一個內(nèi)存級緩存把最近一分鐘內(nèi)的查詢結(jié)果暫存在字典里減少對結(jié)果表的重復(fù)查詢。簡單實(shí)現(xiàn)方式是加一個裝飾器以請求路徑和參數(shù)為 key緩存過期時間為 60 秒。注意MR 結(jié)果回灌完成后必須主動清理對應(yīng)緩存否則用戶看到的一直是舊數(shù)據(jù)。5. 增量爬取、MR輸出讀取與部署排錯的三組技巧最后這部分分享三個容易被忽略但實(shí)際運(yùn)行過程中一定會遇到的細(xì)節(jié)全部來自我在部署這套系統(tǒng)時反復(fù)踩過的坑。5.1 增量爬取與斷點(diǎn)續(xù)爬的 Redis 寫法全量爬取一次之后后續(xù)每次只需要抓新增職位。這套源碼的增量邏輯很簡單用一個 Redis Set 存儲已抓取 URL 的 MD5每次解析完列表頁后用管道批量判斷哪些 URL 未出現(xiàn)過。import redis r redis.Redis(db0) def extract_new_items(items): pipe r.pipeline() for item in items: pipe.sadd(recruit:visited_urls, item[url_md5]) results pipe.execute() new_items [] for item, added in zip(items, results): if added 1: new_items.append(item) return new_itemssadd返回 1 表示這個 key 之前不存在返回 0 表示已存在這個返回值天然就是去重標(biāo)記。管道模式把多個命令打包成一次網(wǎng)絡(luò)往返比逐條 sadd 快得多。斷點(diǎn)續(xù)爬只需要在爬蟲啟動時判斷 Redis 里有沒有已完成的頁碼標(biāo)記從標(biāo)記處繼續(xù)即可。5.2 MR 輸出文件的中文編碼與換行坑HDFS 上的 part 文件默認(rèn)編碼是 UTF-8但用hadoop fs -cat直接查看時終端可能顯示亂碼那是因?yàn)榻K端本地編碼不是 UTF-8。回灌 MySQL 時最穩(wěn)妥的做法是統(tǒng)一用 UTF-8 讀取并連接數(shù)據(jù)庫conn pymysql.connect( hostlocalhost, userroot, password******, databaserecruit, charsetutf8mb4, )還有一個容易忽略的細(xì)節(jié)Windows 下用文本編輯器打開 part 文件發(fā)現(xiàn)所有行都擠在一起這是因?yàn)?Hadoop 輸出的換行符是\n而 Windows 記事本識別的是\r\n。用 VS Code 或其他支持 LF 換行的編輯器打開即可不影響后續(xù)處理。5.3 一鍵重啟采集分析的腳本整套系統(tǒng)涉及的進(jìn)程比較多爬蟲、Flask、Hadoop 任務(wù)都分開運(yùn)行手工啟動容易漏。我習(xí)慣把整個流程串進(jìn)一個 shell 腳本用nohup把每個服務(wù)丟到后臺日志分別落盤#!/bin/bash # 啟動 Flask 可視化服務(wù) nohup python3 app.py logs/flask.log 21 # 啟動增量爬蟲 nohup python3 crawler.py --incremental logs/crawler.log 21 # 等待爬蟲寫入完成后提交 MR 作業(yè) python3 export_to_hdfs.py hadoop jar /opt/hadoop/contrib/streaming/hadoop-streaming-*.jar \ -files mapper.py,reducer.py \ -mapper python3 mapper.py \ -reducer python3 reducer.py \ -input /recruit/jd_text \ -output /recruit/jd_freq腳本里每個后臺進(jìn)程的日志都單獨(dú)輸出排查時直接tail -f logs/crawler.log看實(shí)時狀態(tài)。驗(yàn)證整套系統(tǒng)是否正常工作時不需要打開瀏覽器直接 curl 一下 Flask 接口即可curl http://localhost:5000/api/v1/city_salary_rank?limit5返回的 JSON 中包含code: 0和五條城市薪資數(shù)據(jù)說明爬蟲、MR、回灌、API 整條鏈路全部打通大屏頁面只是把這組 JSON 換了一種更直觀的呈現(xiàn)方式。本文還有配套的精品資源點(diǎn)擊獲取