據(jù)爬取與可視化分析實(shí)戰(zhàn):從爬蟲到Flask展示全流程)
開篇這個(gè)題目到底在做什么值不值得做圖書數(shù)據(jù)爬取和可視化分析是近幾年課程設(shè)計(jì)和畢業(yè)設(shè)計(jì)里出現(xiàn)頻率特別高的一類題目。它考察的東西非常綜合網(wǎng)絡(luò)請(qǐng)求、HTML解析、數(shù)據(jù)清洗、數(shù)據(jù)庫存儲(chǔ)、圖表展示再加上一個(gè)簡單的Web界面把這些串起來基本上就把Python數(shù)據(jù)分析的完整鏈路走了一遍。這也是為什么很多老師愿意出這個(gè)題因?yàn)槟隳茉谶@個(gè)項(xiàng)目里展示的技能點(diǎn)足夠多寫進(jìn)簡歷也拿得出手。這個(gè)系統(tǒng)實(shí)際能做的事情也很直觀從圖書網(wǎng)站上抓取一批圖書信息比如書名、作者、出版社、價(jià)格、評(píng)分、評(píng)論數(shù)然后存下來再從不同維度做統(tǒng)計(jì)分析和可視化展示比如價(jià)格分布、出版社排行、出版年份趨勢(shì)、評(píng)分區(qū)間占比等等。最終交付的形態(tài)通常是一個(gè)Flask或者Django搭建的本地Web頁面里面嵌入了各種交互式圖表用戶可以在頁面上直觀地看到分析結(jié)果。如果你正在準(zhǔn)備課程設(shè)計(jì)、畢設(shè)或者想用一個(gè)小項(xiàng)目把爬蟲到可視化的技能串起來這篇內(nèi)容就是給你寫的。我不打算泛泛地講概念而是按照一個(gè)真實(shí)可落地的方案把系統(tǒng)怎么拆、代碼怎么寫、坑在哪里、答辯可能會(huì)被問到什么一條條給你捋清楚。1. 系統(tǒng)整體設(shè)計(jì)與架構(gòu)拆解先想明白再動(dòng)手很多人拿到這個(gè)題目就開始寫爬蟲寫完爬蟲再想可視化最后發(fā)現(xiàn)數(shù)據(jù)和圖表對(duì)不上或者頁面展示邏輯一團(tuán)糟。這個(gè)項(xiàng)目最忌諱的就是“邊寫邊想”因?yàn)樗哪K跨度大每個(gè)環(huán)節(jié)的產(chǎn)出都會(huì)影響下一個(gè)環(huán)節(jié)必須先把架構(gòu)想清楚。1.1 核心功能模塊拆解按照數(shù)據(jù)流動(dòng)的方向這個(gè)系統(tǒng)可以清晰地拆成四個(gè)模塊數(shù)據(jù)采集模塊負(fù)責(zé)向目標(biāo)網(wǎng)站發(fā)起HTTP請(qǐng)求獲取HTML頁面解析出結(jié)構(gòu)化字段如書名、作者、出版社、價(jià)格、出版日期、評(píng)分、評(píng)論數(shù)、分類等。數(shù)據(jù)存儲(chǔ)模塊將解析后的數(shù)據(jù)清洗、去重然后寫入本地存儲(chǔ)。一般用CSV配合SQLite或MySQLCSV方便直接查看和論文展示數(shù)據(jù)庫方便后續(xù)做查詢統(tǒng)計(jì)。數(shù)據(jù)分析與可視化模塊從存儲(chǔ)中讀取數(shù)據(jù)通過統(tǒng)計(jì)聚合生成圖表配置輸出HTML格式的交互式圖表。Web展示模塊使用Flask搭建輕量級(jí)Web應(yīng)用將圖表嵌入頁面形成完整的可視化系統(tǒng)界面。這四個(gè)模塊之間的依賴關(guān)系非常明確上一層的輸出就是下一層的輸入。所以在一開始就應(yīng)該把數(shù)據(jù)字段定義好比如你打算爬哪幾個(gè)字段、每個(gè)字段的類型是什么、缺失值怎么處理這些一旦確定后面所有的模塊都圍繞它展開。1.2 技術(shù)選型背后的取舍邏輯技術(shù)選型是這個(gè)項(xiàng)目里最能體現(xiàn)水平的地方也是答辯時(shí)老師最喜歡問的環(huán)節(jié)。我直接說結(jié)論再講為什么。爬蟲框架requests BeautifulSoup而不是Scrapy。雖然Scrapy的性能和架構(gòu)更好但課程設(shè)計(jì)場(chǎng)景要的是“看得懂、能講清、好維護(hù)”用Scrapy寫出來的分布式爬蟲反而容易把自己繞暈。requests BeautifulSoup足夠完成幾千條數(shù)據(jù)的采集而且代碼直觀每一行都好解釋。當(dāng)然如果你爬取的數(shù)據(jù)量非常大或者想展示異步處理能力Scrapy也是加分項(xiàng)只是復(fù)雜度會(huì)明顯上升。數(shù)據(jù)存儲(chǔ)CSV SQLite。CSV文件可以直接打開看也方便在論文里截圖展示數(shù)據(jù)樣本。SQLite是一個(gè)單文件數(shù)據(jù)庫不需要安裝數(shù)據(jù)庫服務(wù)對(duì)本地項(xiàng)目非常友好也能順便體現(xiàn)你懂SQL。不建議一上來就上MySQL沒必要??梢暬痯yecharts而不是matplotlib。matplotlib生成的圖片是靜態(tài)的交互性差而且默認(rèn)樣式比較“學(xué)術(shù)”。pyecharts生成的是基于ECharts的交互式HTML圖表有懸停提示、縮放、圖例篩選這些功能放在頁面上看起來專業(yè)很多而且可以直接嵌入Flask頁面。用過的學(xué)生基本都說好。Web框架Flask。它足夠輕量一個(gè)文件就能啟動(dòng)服務(wù)模板渲染和靜態(tài)資源管理也簡單。Django那種重型框架對(duì)這個(gè)項(xiàng)目來說過于龐大了。表格總結(jié)一下選型對(duì)比模塊推薦選擇備選方案推薦理由爬蟲requests BeautifulSoupScrapy、httpx簡單直觀易解釋易維護(hù)存儲(chǔ)CSV SQLiteMySQL、MongoDB零配置方便展示和查詢可視化pyechartsmatplotlib、Plotly交互式圖表樣式現(xiàn)代Web展示FlaskDjango、FastAPI輕量靈活適合小項(xiàng)目這一步其實(shí)就是整個(gè)系統(tǒng)的設(shè)計(jì)藍(lán)圖定下來之后后面的工作就是按部就班地填代碼。2. 爬蟲模塊詳解從頁面分析到數(shù)據(jù)落庫爬蟲是整個(gè)系統(tǒng)的數(shù)據(jù)源頭也是技術(shù)含量最集中的地方。很多同學(xué)的爬蟲寫得能用但很脆弱換一個(gè)頁面結(jié)構(gòu)就掛了。核心問題在于沒有做好頁面分析和異常處理。這一節(jié)我把完整流程走一遍包括目標(biāo)站點(diǎn)分析、請(qǐng)求解析翻頁三步走、數(shù)據(jù)清洗與存儲(chǔ)。2.1 目標(biāo)站點(diǎn)分析與數(shù)據(jù)字段規(guī)劃我用的例子是當(dāng)當(dāng)網(wǎng)的圖書頻道因?yàn)樗捻撁娼Y(jié)構(gòu)相對(duì)規(guī)整反爬強(qiáng)度適中也不要求登錄非常適合做教學(xué)案例。相比之下豆瓣的反爬比較嚴(yán)格頻繁請(qǐng)求容易觸發(fā)驗(yàn)證碼京東的反爬機(jī)制也更復(fù)雜。課程設(shè)計(jì)階段能穩(wěn)定跑通比挑戰(zhàn)高難度更重要。打開當(dāng)當(dāng)網(wǎng)圖書頻道后按F12進(jìn)入開發(fā)者工具用元素選擇器選中一本書的標(biāo)題你會(huì)看到類似這樣的結(jié)構(gòu)div classcon p classname a title書名 href... target_blank書名/a /p p classauthor作者/p p classpublisher出版社/p p classprice span classprice_n¥28.40/span /p p classstar span classlevel stylewidth:80%;/span span classstar_count1342條評(píng)論/span /p /div當(dāng)然不同的圖書列表頁結(jié)構(gòu)會(huì)有差異你以實(shí)際的頁面為準(zhǔn)。分析的目的是確認(rèn)你要爬的字段能否在HTML里找到以及選擇器怎么寫才穩(wěn)定。我建議優(yōu)先基于class屬性和標(biāo)簽層級(jí)來定位盡量少用絕對(duì)路徑因?yàn)轫撁娓陌鏁r(shí)絕對(duì)路徑最容易失效。根據(jù)分析結(jié)果我規(guī)劃了這些字段書名后續(xù)分析的核心維度必須非空作者可能為空出版社用于出版社排行統(tǒng)計(jì)出版日期解析字符串中的年份用于年份趨勢(shì)分析價(jià)格需要從“¥28.40”這樣的字符串中提取數(shù)字然后轉(zhuǎn)成float評(píng)分用于評(píng)分區(qū)間分析取值范圍0到5可能為空評(píng)論數(shù)從“1342條評(píng)論”中提取數(shù)字轉(zhuǎn)為int分類從頁面分類導(dǎo)航讀取用于分類占比統(tǒng)計(jì)把字段規(guī)劃清楚之后后面寫代碼就變成體力活了。2.2 請(qǐng)求、解析、翻頁三步走的完整實(shí)現(xiàn)爬蟲的核心邏輯其實(shí)就是三步請(qǐng)求頁面、解析HTML、翻頁循環(huán)。用代碼寫出來大概是這樣的import requests from bs4 import BeautifulSoup import time import random def get_page_html(url, headers): 請(qǐng)求頁面并返回HTML文本帶超時(shí)和重試 for attempt in range(3): try: resp requests.get(url, headersheaders, timeout10) resp.raise_for_status() resp.encoding resp.apparent_encoding return resp.text except Exception as e: print(f第{attempt 1}次請(qǐng)求失敗: {e}) time.sleep(2) return None def parse_book_list(html): 從HTML中解析圖書信息 soup BeautifulSoup(html, html.parser) books [] items soup.select(ul.bigimg li) for item in items: name_node item.select_one(.name a) if not name_node: continue book { title: name_node.get(title) or name_node.get_text(stripTrue), author: item.select_one(.author) .get_text(stripTrue) if item.select_one(.author) else , publisher: item.select_one(.publisher) .get_text(stripTrue) if item.select_one(.publisher) else , price: parse_price(item), comment_count: parse_comment_count(item), publish_date: parse_publish_date(item), } books.append(book) return books def parse_price(item): 從價(jià)格節(jié)點(diǎn)提取浮點(diǎn)數(shù) price_node item.select_one(.price_n) if not price_node: return None text price_node.get_text(stripTrue) return float(text.replace(¥, ).replace(元, ).strip()) def parse_comment_count(item): 從評(píng)論節(jié)點(diǎn)提取數(shù)字 comment_node item.select_one(.star_count) if not comment_node: return 0 text comment_node.get_text(stripTrue) return int(.join(filter(str.isdigit, text))) if any(c.isdigit() for c in text) else 0這里有兩個(gè)關(guān)鍵細(xì)節(jié)值得展開講第一是resp.encoding resp.apparent_encoding。中文網(wǎng)頁最常見的亂碼問題就是因?yàn)檎?qǐng)求頭里聲明的編碼和實(shí)際返回的編碼不一致。apparent_encoding是從頁面內(nèi)容里檢測(cè)編碼能有效避免“錕斤拷”這類亂碼。但要注意它并不是100%準(zhǔn)確如果發(fā)現(xiàn)解析出來的字符串還是有亂碼可以手動(dòng)指定為resp.encoding gbk因?yàn)椴簧匐娚叹W(wǎng)站用的還是GBK編碼。第二是選擇器.name a這種鏈?zhǔn)綄懛?。它表示“class為name的節(jié)點(diǎn)內(nèi)部的a標(biāo)簽”比直接寫a.title更保險(xiǎn)因?yàn)橛行﹫D書沒有副標(biāo)題或者頁面結(jié)構(gòu)微調(diào)會(huì)讓屬性丟失。寫選擇器的時(shí)候盡量考慮最穩(wěn)定的路徑。翻頁邏輯也很簡單當(dāng)當(dāng)?shù)膱D書列表頁URL有規(guī)律可循h(huán)ttp://category.dangdang.com/pg{頁碼}-cp01.54.00.00.00.00.html。所以只需要在循環(huán)里拼接頁碼即可def crawl_pages(max_pages20): base_url http://category.dangdang.com/pg{page}-cp01.54.00.00.00.00.html headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: http://category.dangdang.com/ } all_books [] for page in range(1, max_pages 1): url base_url.format(pagepage) print(f正在爬取第{page}頁...) html get_page_html(url, headers) if not html: continue books parse_book_list(html) print(f第{page}頁解析到{len(books)}本書) all_books.extend(books) time.sleep(random.uniform(2, 4)) # 隨機(jī)延時(shí)防止請(qǐng)求過快 return all_books這里我特意加了time.sleep(random.uniform(2, 4))隨機(jī)延時(shí)2到4秒。這個(gè)操作有兩個(gè)作用一是降低對(duì)目標(biāo)站點(diǎn)的請(qǐng)求壓力避免被封IP二是體現(xiàn)你了解爬蟲的規(guī)范邊界這在答辯時(shí)是加分項(xiàng)。隨機(jī)比固定延時(shí)更好因?yàn)楣潭ǖ拈g隔反而容易被反爬策略識(shí)別。2.3 數(shù)據(jù)清洗與存儲(chǔ)別把臟數(shù)據(jù)丟進(jìn)數(shù)據(jù)庫爬下來的數(shù)據(jù)不能直接入庫必須先清洗。根據(jù)我的經(jīng)驗(yàn)圖書數(shù)據(jù)最常見的臟數(shù)據(jù)問題就幾類價(jià)格異常有的節(jié)點(diǎn)提取出來是空字符串有的是“暫無報(bào)價(jià)”直接轉(zhuǎn)float會(huì)報(bào)錯(cuò)評(píng)論數(shù)異常有的圖書沒有評(píng)論節(jié)點(diǎn)有的評(píng)論數(shù)節(jié)點(diǎn)解析出來是空串作者和出版社字段有些節(jié)點(diǎn)存在但內(nèi)容為空重復(fù)數(shù)據(jù)翻頁循環(huán)中偶爾會(huì)重復(fù)采集到同一本書我的處理方式是寫一個(gè)清洗函數(shù)對(duì)每條記錄做類型轉(zhuǎn)換和空值處理def clean_books(books): cleaned [] seen_titles set() for book in books: title book.get(title, ).strip() if not title or title in seen_titles: continue seen_titles.add(title) # 價(jià)格清洗空值填充為None后續(xù)分析時(shí)排除 if book.get(price) is not None: book[price] round(float(book[price]), 2) # 評(píng)論數(shù)字段強(qiáng)制轉(zhuǎn)為int book[comment_count] int(book.get(comment_count) or 0) # 出版年份提取比如從2023年01月中提取2023 publish_date book.get(publish_date) or match re.search(r(19|20)\d{2}, publish_date) book[publish_year] match.group(0) if match else None cleaned.append(book) return cleaned存儲(chǔ)方面我一般先存CSV再導(dǎo)入SQLite。CSV的好處是任何人都能用Excel打開也方便論文里放截圖。SQLite的建表和插入語句也很簡單import sqlite3 import csv def save_to_csv(books, filenamebooks.csv): with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[title, author, publisher, price, comment_count, publish_year, publish_date]) writer.writeheader() writer.writerows(books) def save_to_sqlite(books, db_namebooks.db): conn sqlite3.connect(db_name) cursor conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, author TEXT, publisher TEXT, price REAL, comment_count INTEGER, publish_year TEXT, publish_date TEXT ) ) cursor.executemany( INSERT INTO books (title, author, publisher, price, comment_count, publish_year, publish_date) VALUES (:title, :author, :publisher, :price, :comment_count, :publish_year, :publish_date) , books) conn.commit() conn.close()注意CSV寫入時(shí)用了encodingutf-8-sig這個(gè)編碼比utf-8多了一個(gè)BOM頭Excel打開才不會(huì)中文亂碼。這種細(xì)節(jié)都是實(shí)操中踩過坑才知道的。3. 可視化分析與展示讓圖表自己會(huì)說話數(shù)據(jù)有了接下來要做的是把冷冰冰的數(shù)字變成讓老師和答辯評(píng)委眼前一亮的圖表。這一節(jié)講清楚分析維度怎么定、圖表怎么配、頁面怎么搭建。3.1 分析維度設(shè)計(jì)從數(shù)據(jù)中挖掘什么結(jié)論可視化的目的不是把所有字段都畫一遍圖表而是圍繞“讀者關(guān)心的問題”來設(shè)計(jì)分析維度。我做圖書分析時(shí)主要圍繞這幾個(gè)問題展開價(jià)格分布大部分圖書的價(jià)格集中在什么區(qū)間這直接決定價(jià)格直方圖和區(qū)間占比餅圖。出版社集中度哪些出版社出版的圖書數(shù)量最多用條形圖或者詞云展示Top10出版社。出版年份趨勢(shì)近幾年出版圖書的數(shù)量走勢(shì)如何用折線圖展示可以反映數(shù)據(jù)的時(shí)間分布特征。評(píng)分分布圖書評(píng)分的整體水平如何集中在高分還是低分用餅圖或直方圖展示。評(píng)論數(shù)與價(jià)格的關(guān)系評(píng)論數(shù)高的書價(jià)格是否一定更低用散點(diǎn)圖可以嘗試探索相關(guān)性。設(shè)計(jì)分析維度時(shí)有一個(gè)原則每個(gè)圖表都要能講出一個(gè)結(jié)論。比如“評(píng)分分布餅圖”能得出結(jié)論“大部分圖書評(píng)分集中在4到5分之間”這就是一個(gè)有意義的發(fā)現(xiàn)。相反如果畫一個(gè)“作者名字長度分布”的直方圖就沒什么實(shí)際價(jià)值老師看了只會(huì)覺得你在湊數(shù)。3.2 pyecharts圖表配置與數(shù)據(jù)對(duì)接pyecharts 最方便的地方在于它是鏈?zhǔn)秸{(diào)用代碼寫起來很順暢。下面是我常用的三個(gè)圖表的完整示例包括價(jià)格分布直方圖、出版社Top10條形圖和出版年份折線圖。先準(zhǔn)備好從數(shù)據(jù)庫讀取數(shù)據(jù)的輔助函數(shù)import sqlite3 import pandas as pd def load_data(db_namebooks.db): conn sqlite3.connect(db_name) df pd.read_sql_query(SELECT * FROM books, conn) conn.close() return df df load_data()然后分別創(chuàng)建圖表。第一個(gè)是價(jià)格分布直方圖from pyecharts import options as opts from pyecharts.charts import Bar def create_price_histogram(df): # 將價(jià)格分成0-20, 20-40, 40-60, 60-80, 80-100五檔 bins [0, 20, 40, 60, 80, 100, 200] labels [0-20, 20-40, 40-60, 60-80, 80-100, 100] df[price_range] pd.cut(df[price], binsbins, labelslabels, rightFalse) price_counts df[price_range].value_counts().sort_index() bar ( Bar() .add_xaxis(price_counts.index.tolist()) .add_yaxis(圖書數(shù)量, price_counts.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(title圖書價(jià)格分布), xaxis_optsopts.AxisOpts(name價(jià)格區(qū)間(元)), yaxis_optsopts.AxisOpts(name圖書數(shù)量), ) ) return bar第二個(gè)是出版社Top10條形圖def create_publisher_bar(df): publisher_counts df[publisher].value_counts().nlargest(10) bar ( Bar() .add_xaxis(publisher_counts.index.tolist()) .add_yaxis(圖書數(shù)量, publisher_counts.values.tolist()) .reversal_axis() # 橫向條形圖出版社名字太長時(shí)更好看 .set_global_opts( title_optsopts.TitleOpts(title出版社圖書數(shù)量Top10), xaxis_optsopts.AxisOpts(name圖書數(shù)量), yaxis_optsopts.AxisOpts(name出版社), ) ) return bar第三個(gè)是出版年份折線圖from pyecharts.charts import Line def create_year_trend(df): year_counts df.dropna(subset[publish_year]) year_counts year_counts[publish_year].value_counts().sort_index() line ( Line() .add_xaxis(year_counts.index.tolist()) .add_yaxis(圖書數(shù)量, year_counts.values.tolist()) .set_global_opts( title_optsopts.TitleOpts(title出版年份趨勢(shì)), xaxis_optsopts.AxisOpts(name年份), yaxis_optsopts.AxisOpts(name圖書數(shù)量), ) ) return line這些圖表生成之后可以用.render(templates/chart.html)輸出為獨(dú)立的HTML文件。pyecharts生成的文件自帶完整的ECharts腳本雙擊就能打開。在寫論文或者準(zhǔn)備答辯的時(shí)候可以先截幾張圖表的效果圖放進(jìn)文檔非常方便。這里必須提醒一個(gè)坑pyecharts生成圖表時(shí)如果不設(shè)置高度和寬度默認(rèn)可能只占頁面的一小塊區(qū)域。建議在生成圖表前統(tǒng)一設(shè)置一下bar Bar(init_optsopts.InitOpts(width900px, height500px))另外圖表標(biāo)題、坐標(biāo)軸名稱這些不要偷懶一定要配置完整。答辯展示時(shí)老師第一眼看到的就是圖表的完整度和美觀度這直接影響第一印象。3.3 Flask搭建展示頁面把圖表串成完整系統(tǒng)圖表一個(gè)個(gè)單獨(dú)看還行但真正像一個(gè)“系統(tǒng)”肯定是要有一個(gè)統(tǒng)一的Web頁面把所有圖表整合起來。Flask在這一步顯得特別順手。我在項(xiàng)目里通常這樣組織文件結(jié)構(gòu)project/ ├── app.py # Flask主程序 ├── books.db # SQLite數(shù)據(jù)庫 ├── spider.py # 爬蟲腳本 ├── analysis.py # 數(shù)據(jù)分析和圖表生成 ├── templates/ │ ├── index.html # 總覽頁面 │ └── charts.html # 圖表詳情頁面 └── static/ └── images/ # 存放圖表的備份圖片app.py的核心代碼很簡單from flask import Flask, render_template import pandas as pd import sqlite3 from analysis import create_price_histogram, create_publisher_bar, create_year_trend app Flask(__name__) def get_data(): conn sqlite3.connect(books.db) df pd.read_sql_query(SELECT * FROM books, conn) conn.close() return df app.route(/) def index(): df get_data() total_count len(df) avg_price round(df[price].mean(), 2) if df[price].notna().any() else 0 top_publisher df[publisher].value_counts().index[0] if len(df) else 暫無數(shù)據(jù) return render_template( index.html, total_counttotal_count, avg_priceavg_price, top_publishertop_publisher, ) app.route(/charts) def charts(): df get_data() price_chart create_price_histogram(df).render_embed() publisher_chart create_publisher_bar(df).render_embed() year_chart create_year_trend(df).render_embed() return render_template( charts.html, price_chartprice_chart, publisher_chartpublisher_chart, year_chartyear_chart, ) if __name__ __main__: app.run(debugTrue)注意render_embed()這個(gè)方法它會(huì)把圖表所需的所有JS和HTML全部內(nèi)嵌進(jìn)返回結(jié)果不像render()那樣生成獨(dú)立文件。這樣在Flask的模板里就能直接用一個(gè)變量承載整個(gè)圖表不需要額外處理靜態(tài)文件路由對(duì)新手來說是最省事的方案。模板里接收?qǐng)D表變量后在HTML中直接用{{ price_chart|safe }}輸出即可。safe過濾器是必須的否則Jinja2會(huì)轉(zhuǎn)義HTML標(biāo)簽圖表就顯示不出來。在index.html里我還會(huì)放一個(gè)簡單的統(tǒng)計(jì)卡片區(qū)把數(shù)據(jù)總量、平均價(jià)格、Top1出版社這些指標(biāo)展示出來讓總覽頁面看起來更飽滿。這樣整個(gè)系統(tǒng)就分成了“數(shù)據(jù)總覽”和“圖表分析”兩個(gè)頁面邏輯清晰演示的時(shí)候也方便引導(dǎo)老師的視線。4. 常見問題與排查技巧實(shí)錄這一節(jié)的內(nèi)容全部來自我實(shí)操過程中踩過的坑。網(wǎng)上很多教程只講正確的路不講彎路但恰恰是彎路能幫你省最多時(shí)間。4.1 高頻報(bào)錯(cuò)與解決方案速查表我把這個(gè)項(xiàng)目里最常見的問題整理成了一張表你可以直接對(duì)照排查報(bào)錯(cuò)信息或現(xiàn)象常見原因解決方案requests.exceptions.ConnectionError目標(biāo)網(wǎng)站拒絕連接或IP被臨時(shí)限制檢查網(wǎng)絡(luò)降低請(qǐng)求頻率換User-Agent增加延時(shí)UnicodeDecodeError: gbk codec cant decode編碼判斷失誤設(shè)置resp.encoding resp.apparent_encoding或手動(dòng)指定utf-8/gbk解析結(jié)果為空列表選擇器寫錯(cuò)或頁面結(jié)構(gòu)動(dòng)態(tài)加載用開發(fā)者工具重新定位選擇器檢查是否登錄才能看到內(nèi)容ValueError: could not convert string to float價(jià)格字段包含非數(shù)字字符清洗時(shí)用正則提取數(shù)字先re.sub再floatpandasKeyError字段名不匹配檢查SQLite表字段名和DataFrame列名是否一致pyecharts圖表在頁面中不顯示模板中忘了 safe 過濾器SQLite數(shù)據(jù)庫文件無法打開表結(jié)構(gòu)創(chuàng)建失敗或數(shù)據(jù)沒提交確認(rèn)執(zhí)行了conn.commit()檢查CREATE TABLE是否在插入之前執(zhí)行最容易被忽略的是“解析結(jié)果為空列表”這個(gè)問題。有些圖書列表頁看起來是靜態(tài)HTML實(shí)際上部分字段是通過JS異步加載的直接拿requests請(qǐng)求下來的HTML里根本沒有這些內(nèi)容。遇到這種情況一個(gè)簡單的驗(yàn)證方法在瀏覽器中右鍵查看網(wǎng)頁源代碼按CtrlF搜索你想要的字段值如果搜不到說明內(nèi)容是JS動(dòng)態(tài)渲染的需要考慮用Selenium模擬瀏覽器或者分析Network面板里的XHR接口直接請(qǐng)求數(shù)據(jù)接口。課程設(shè)計(jì)階段優(yōu)先推薦后者更輕量也不容易觸發(fā)反爬。4.2 反爬機(jī)制的合理應(yīng)對(duì)與邊界問題圖書網(wǎng)站的常規(guī)反爬手段主要是User-Agent檢測(cè)、請(qǐng)求頻率限制、IP封禁、驗(yàn)證碼。前兩種是家常便飯應(yīng)對(duì)也很成熟維護(hù)一個(gè)User-Agent池每次請(qǐng)求隨機(jī)取一個(gè)隨機(jī)延時(shí)而不是固定延時(shí)如果遇到503或者頻繁的驗(yàn)證碼立刻停止爬取等幾小時(shí)再繼續(xù)IP封禁這個(gè)層面一般不需要?jiǎng)佑么沓匾驗(yàn)槟菢訒?huì)涉及一些灰色工具。我做這個(gè)項(xiàng)目時(shí)的經(jīng)驗(yàn)是控制總量和頻率才是根本。幾百本到幾千本的數(shù)據(jù)規(guī)模完全可以在不驚動(dòng)對(duì)方的情況下拿到。合規(guī)層面我有幾條自己一直遵守的原則只爬公開可訪問的數(shù)據(jù)不碰需要登錄、有權(quán)限控制的內(nèi)容嚴(yán)格遵守robots協(xié)議如果站點(diǎn)聲明不允許爬取就換一個(gè)公開數(shù)據(jù)源爬取頻率控制在人類正常瀏覽速度之內(nèi)不給目標(biāo)服務(wù)器制造壓力爬取的公開數(shù)據(jù)僅用于個(gè)人學(xué)習(xí)、學(xué)術(shù)研究不用于商業(yè)用途不要把爬蟲腳本用于任何惡意用途比如DDoS、數(shù)據(jù)倒賣、批量注冊(cè)說實(shí)話這些原則不僅是底線問題也是答辯時(shí)的加分項(xiàng)。當(dāng)老師問“你怎么保證你的爬蟲合規(guī)”時(shí)你能說出robots協(xié)議、請(qǐng)求頻率控制、數(shù)據(jù)僅用于學(xué)習(xí)這幾個(gè)關(guān)鍵點(diǎn)比支支吾吾解釋半天要強(qiáng)得多。4.3 答辯高頻問題與思路參考這幾類問題基本每次答辯都會(huì)被問到提前準(zhǔn)備好現(xiàn)場(chǎng)就不慌。問題一為什么選這個(gè)數(shù)據(jù)源數(shù)據(jù)是怎么來的回答思路先說明數(shù)據(jù)源是公開可訪問的圖書網(wǎng)站頁面然后講你已經(jīng)分析了頁面的HTML結(jié)構(gòu)確認(rèn)所需字段都存在于靜態(tài)HTML中所以決定用requestsBeautifulSoup做靜態(tài)解析。再提一下你對(duì)robots協(xié)議和反爬策略的了解說明自己設(shè)置了合理的請(qǐng)求頻率。問題二如果數(shù)據(jù)量擴(kuò)大100倍架構(gòu)上怎么擴(kuò)展回答思路把requests替換成Scrapy框架利用它的并發(fā)能力和去重機(jī)制存儲(chǔ)層從SQLite升級(jí)到MySQL或PostgreSQL爬蟲節(jié)點(diǎn)可以分布式部署用消息隊(duì)列來調(diào)度任務(wù)。這部分不需要真的實(shí)現(xiàn)但能講出思路就能展示你思考過系統(tǒng)的擴(kuò)展性。問題三怎么保證爬取數(shù)據(jù)的準(zhǔn)確性回答思路從三層說第一層是請(qǐng)求層校驗(yàn)響應(yīng)狀態(tài)碼和頁面編碼第二層是解析層字段提取后立即做類型轉(zhuǎn)換和空值處理第三層是存儲(chǔ)層在寫入前做去重和清洗。如果有多頁數(shù)據(jù)還可以做重復(fù)檢查比如按書名去重、按ISBN去重。5. 最后補(bǔ)幾句做這個(gè)項(xiàng)目的心得很多人以為這個(gè)系統(tǒng)的難點(diǎn)在爬蟲實(shí)際上我做完一遍下來的體會(huì)是爬蟲只占三分之一的時(shí)間數(shù)據(jù)清洗和調(diào)試占了另外三分一剩下的時(shí)間全都花在“讓圖表和頁面看起來更完整”上了。所以如果你現(xiàn)在正準(zhǔn)備動(dòng)手我建議你把時(shí)間分配表放在案頭別在爬蟲上戀戰(zhàn)。另外有個(gè)非常實(shí)用的建議在寫報(bào)告和準(zhǔn)備答辯前一定把所有圖表用render()生成一份獨(dú)立的HTML文件存到項(xiàng)目里。因?yàn)榇疝q現(xiàn)場(chǎng)的投影環(huán)境偶爾會(huì)出問題Web服務(wù)可能因?yàn)槎丝谡加弥惖男」收掀鸩粊磉@時(shí)直接雙擊提前渲染好的圖表HTML文件至少能保證核心內(nèi)容正常展示。這種細(xì)節(jié)看似不起眼但在現(xiàn)場(chǎng)就是救命的稻草。最后再分享一個(gè)數(shù)據(jù)源的小技巧如果當(dāng)當(dāng)或者其他站點(diǎn)的頁面結(jié)構(gòu)改版導(dǎo)致選擇器失效不用慌張。網(wǎng)格上的頁面無論怎么變書名、作者、價(jià)格這類信息幾乎一定會(huì)出現(xiàn)在HTML里只是class名變了。花幾分鐘用開發(fā)者工具重新定位一下改一下選擇器即可代碼邏輯本身不需要?jiǎng)?。這類“小修小補(bǔ)”的能力恰恰是爬蟲項(xiàng)目里最值錢的動(dòng)手能力。