論情感分析實(shí)戰(zhàn):從爬蟲采集到數(shù)據(jù)清洗全流程指南)
簡(jiǎn)介該壓縮包是一個(gè)面向?yàn)H坊與淄博旅游評(píng)論數(shù)據(jù)的完整爬蟲與情感分析項(xiàng)目適用人群包括Python爬蟲與自然語言處理入門學(xué)習(xí)者、相關(guān)課程設(shè)計(jì)參與者以及需要了解游客反饋的旅游從業(yè)者和決策者。項(xiàng)目從評(píng)論采集到情感傾向判斷形成了一條完整鏈路利用爬蟲抓取約5萬條城市評(píng)論隨后對(duì)數(shù)據(jù)進(jìn)行清洗、預(yù)處理與情感分析最終輸出游客對(duì)兩座城市的整體評(píng)價(jià)、滿意度及不滿意細(xì)節(jié)可直接用于旅游體驗(yàn)調(diào)研或輿情參考。壓縮包共1988個(gè)文件以Python腳本、CSV評(píng)論數(shù)據(jù)、TXT文本和JSON配置文件為主同時(shí)包含JavaScript、類型定義、文檔及少量圖表資源整體約29.59MB目錄結(jié)構(gòu)清晰便于按采集、數(shù)據(jù)、分析、可視化等模塊查閱。已有587人學(xué)習(xí)下載。通過該項(xiàng)目讀者可得到可復(fù)用的爬蟲代碼、分城市存儲(chǔ)的評(píng)論數(shù)據(jù)集、情感分析實(shí)現(xiàn)及結(jié)果圖表并掌握針對(duì)具體城市評(píng)論的文本挖掘思路為后續(xù)擴(kuò)展分析或論文寫作提供扎實(shí)的參考基礎(chǔ)。1. 5萬條城市評(píng)論情感分析先想辦法把數(shù)據(jù)洗干凈再談模型先說結(jié)論這個(gè)項(xiàng)目真正的門檻不在爬蟲也不在情感分析算法而在“5萬條評(píng)論抓回來后你只有大約3萬條能用”。城市評(píng)論這個(gè)場(chǎng)景用戶會(huì)大量寫“哈哈哈哈環(huán)境絕了”“排隊(duì)兩小時(shí)再也不來”這種夾雜語氣詞、表情符號(hào)和網(wǎng)絡(luò)梗的短文本否定詞還特別多。直接拿通用情感模型跑準(zhǔn)確率經(jīng)常不到七成和拋硬幣差不了多少。這個(gè)標(biāo)題要做的事其實(shí)是一條完整的鏈路用爬蟲采集某個(gè)城市/多個(gè)城市的商家評(píng)論落到數(shù)據(jù)庫里做清洗去重再分城市、分店鋪?zhàn)銮楦袃A向分析最后得到“這個(gè)城市商戶口碑整體是偏正面還是偏負(fù)面”的量化結(jié)果。適合想給城市門店選品、給區(qū)域運(yùn)營(yíng)做口碑監(jiān)控或者單純想練手完整數(shù)據(jù)流水線的從業(yè)者。這篇我就按我做過的方案把每一步拆開講代碼都是能直接跑的。2. 用 requests 和 sqlalchemy 搭采集鏈路從頁面 URL 到入庫的最小可跑代碼2.1 為什么是 requests 而不是 scrapy5 萬條數(shù)據(jù)考慮的是性價(jià)比一提到爬蟲很多人的第一反應(yīng)是 scrapy甚至是分布式爬蟲。但 5 萬條城市評(píng)論這個(gè)量級(jí)真的不需要上框架。單進(jìn)程 requests 加一點(diǎn)并發(fā)控制一兩個(gè)小時(shí)就能跑完維護(hù)成本也低得多。用 scrapy 的收益要等數(shù)據(jù)量到百萬級(jí)、需要斷點(diǎn)續(xù)爬和調(diào)度管理時(shí)才明顯到那個(gè)階段再去研究爬蟲管理平臺(tái)和分布式細(xì)節(jié)也不遲。我的選型理由很簡(jiǎn)單requests 的代碼是線性的哪里出錯(cuò)肉眼就能看出來。抓 5 萬條評(píng)論單線程跑大約兩三個(gè)小時(shí)把每條請(qǐng)求間隔控制在 1 到 2 秒對(duì)目標(biāo)站點(diǎn)的壓力也小。早期我踩過最重的坑就是高估了速度需求上了 scrapy結(jié)果一周里有三天在處理代理和去重隊(duì)列核心的評(píng)論數(shù)據(jù)反而沒抓多少。城市評(píng)論的數(shù)據(jù)來源常見的是點(diǎn)評(píng)類平臺(tái)的城市商家頁包括餐飲、酒店、景點(diǎn)幾類。這類頁面的評(píng)論列表通常是滾動(dòng)加載所以不建議直接解析初始 HTML要先看瀏覽器 Network 面板里的 XHR 請(qǐng)求找到返回 JSON 的評(píng)論接口。下面這套代碼把“拿 HTML/JSON → 解析評(píng)論字段 → 入庫”完整串起來適用于絕大多數(shù)返回 JSON 的評(píng)論接口。2.2 抓取評(píng)論首頁數(shù)據(jù)請(qǐng)求頭、超時(shí)與重試是三個(gè)保命參數(shù)import requests import time import random HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36, Accept: application/json, text/plain, */*, Referer: https://example-city-review-site.com/, Accept-Language: zh-CN,zh;q0.9, } def fetch_reviews_json(url, params, retries3): for attempt in range(retries): try: resp requests.get(url, headersHEADERS, paramsparams, timeout10) if resp.status_code 200 and resp.json(): return resp.json() if resp.status_code in (403, 429): # 被限流時(shí)先退避通常等幾秒就好 time.sleep(random.uniform(3, 5) * (attempt 1)) elif resp.status_code 500: time.sleep(random.uniform(2, 4)) except requests.RequestException as e: # 超時(shí)和連接斷開都算瞬時(shí)錯(cuò)誤 print(f請(qǐng)求失敗{e}第 {attempt 1} 次重試) time.sleep(random.uniform(1, 2) * (attempt 1)) return None這段代碼的核心是“有限重試 退避等待”。timeout10 表示連接和讀取都最多等 10 秒避免某個(gè)慢接口把整個(gè)采集卡死。處理 403 和 429 的時(shí)候等待時(shí)間按指數(shù)增長(zhǎng)第一次停 3 到 5 秒第二次停 6 到 10 秒。你實(shí)際調(diào)參的時(shí)候看日志就行如果某個(gè)頁面連續(xù)重試三次都失敗就別再硬抓直接記下來跳過去最后統(tǒng)一補(bǔ)采。解析 JSON 評(píng)論的代碼比解析 HTML 穩(wěn)定得多def parse_comment_items(data): items [] # 不同站點(diǎn)的 JSON 結(jié)構(gòu)不一樣這里只給出最常見的兩層結(jié)構(gòu) for card in data.get(reviews, []): city card.get(city, ) shop_name card.get(shop_name, ) rating card.get(rating, 0) # 評(píng)論內(nèi)容偶爾是 null要兜底 content (card.get(content) or ).strip() if not content: continue items.append({ city: city, shop_name: shop_name, rating: rating, content: content, comment_time: card.get(comment_time, ), }) return items注意這里有個(gè)參數(shù)細(xì)節(jié)rating 保留原始數(shù)值別在采集階段做轉(zhuǎn)換否則后面分析“評(píng)分和情感分?jǐn)?shù)是否一致”時(shí)會(huì)丟失精度。content 為空字符串的評(píng)論直接跳過這類數(shù)據(jù)沒有分析價(jià)值還會(huì)把情感模型的輸出帶偏。2.3 sqlalchemy 儲(chǔ)存評(píng)論數(shù)據(jù)表結(jié)構(gòu)設(shè)計(jì)與批量寫入?yún)?shù)5 萬條評(píng)論拿內(nèi)存 list 存肯定不行爬蟲中斷一次就全丟了。我習(xí)慣直接用 SQLAlchemy 連 MySQL定義一張?jiān)u論表字段不多但要把去重鍵和索引一次建好。from sqlalchemy import create_engine, Column, Integer, String, Float, Text, DateTime from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.declarative import declarative_base from datetime import datetime Base declarative_base() class CityReview(Base): __tablename__ city_reviews id Column(Integer, primary_keyTrue, autoincrementTrue) city Column(String(32), indexTrue) # 城市名聚合分析時(shí)用 shop_name Column(String(128), indexTrue) # 店鋪名 rating Column(Float, default0.0) # 原始評(píng)分可能缺失 content Column(Text) # 清洗前的原文 content_md5 Column(String(32), uniqueTrue, indexTrue) # 去重指紋 comment_time Column(String(32), default) created_at Column(DateTime, defaultdatetime.now) engine create_engine( mysqlpymysql://user:password127.0.0.1:3306/review_db?charsetutf8mb4, pool_size10, pool_recycle3600 ) Base.metadata.create_all(engine) Session sessionmaker(bindengine)content_md5 字段是最關(guān)鍵的它加了 unique 約束重復(fù)評(píng)論第二次入庫時(shí)數(shù)據(jù)庫直接報(bào)錯(cuò)天然擋住了臉。charset 必須寫 utf8mb4因?yàn)樵u(píng)論里全是 emoji 和特殊符號(hào)utf8 存不住。pool_size10 是并發(fā)寫入時(shí)的連接池大小如果后面決定用多線程采集可以調(diào)到 20。先用單線程的話默認(rèn)值就夠。批量入庫的寫法def save_reviews(session, review_items, batch_size500): for i in range(0, len(review_items), batch_size): batch review_items[i:i batch_size] session.add_all([ CityReview(**item, content_md5md5_fingerprint(item[content])) for item in batch ]) try: session.commit() except Exception: # 重復(fù)數(shù)據(jù)會(huì)導(dǎo)致 unique 沖突回滾后逐條試能保留更多數(shù)據(jù) session.rollback() for item in batch: try: session.add(CityReview(**item, content_md5md5_fingerprint(item[content]))) session.commit() except Exception: session.rollback() continuebatch_size500 是我在 MySQL 上試過比較穩(wěn)的閾值。小于 100 時(shí) commit 次數(shù)太多耗時(shí)翻倍大于 1000 時(shí)一次語句過大出錯(cuò)回滾的成本也高。注意 save_reviews 里的 md5_fingerprint 函數(shù)在下一章會(huì)定義這里先用著。3. 清洗和去重環(huán)節(jié)把 5 萬條評(píng)論變成 4 萬條有效語料的完整處理3.1 清洗流程的四個(gè)環(huán)節(jié)去標(biāo)簽、去符號(hào)、壓縮重復(fù)、兜底城市評(píng)論的文本質(zhì)量比商品評(píng)論更混亂。店鋪名、地址、電話會(huì)被系統(tǒng)自動(dòng)拼接進(jìn)文本表情符號(hào)和“哈哈哈哈”無處不在還有人會(huì)用“不錯(cuò)不錯(cuò)不錯(cuò)不錯(cuò)”這種重復(fù)短語湊字?jǐn)?shù)。清洗階段我做四件事第一步去 HTML 標(biāo)簽雖然 JSON 接口一般不返回 HTML但某些平臺(tái)的評(píng)論內(nèi)容里會(huì)混入br換行標(biāo)簽第二步把非中英文和常見標(biāo)點(diǎn)之外的字符移除只保留中文、英文、數(shù)字以及逗號(hào)句號(hào)問號(hào)感嘆號(hào)第三步壓縮超過 3 次的連續(xù)重復(fù)字符第四步去除純空白和長(zhǎng)度過短的評(píng)論。import re def normalize_text(raw): # 1. 去 HTML 標(biāo)簽 text re.sub(r[^], , raw) # 2. 保留中文、英文、數(shù)字和常用標(biāo)點(diǎn) text re.sub(r[^\u4e00-\u9fa5a-zA-Z0-9。、…\s], , text) # 3. 壓縮連續(xù)重復(fù)字符好吃好吃好吃好吃 - 好吃好吃好吃 text re.sub(r(.)\1{4,}, r\1\1\1, text) # 4. 合并多余空格 text re.sub(r\s, , text).strip() return text第 2 步的正則是最容易出錯(cuò)的地方。方括號(hào)里的脫字符表示“取反”意思是保留中文、大小寫英文、數(shù)字、頓號(hào)句號(hào)嘆號(hào)問號(hào)省略號(hào)以及空白其余全部刪除。這里故意沒保留分號(hào)和引號(hào)因?yàn)樵谠u(píng)論語料里它們的信息量很低。正則(.)\1{4,}的作用是把任意連續(xù)出現(xiàn) 5 次及以上的字符壓縮成 3 次“哈哈哈哈哈哈哈哈”就變成“哈哈哈”既保留語氣又限制了重復(fù)帶來的噪聲。注意這個(gè)壓縮只對(duì)單字生效對(duì)“不錯(cuò)不錯(cuò)不錯(cuò)”這類整詞重復(fù)不起作用需要在去重階段單獨(dú)處理。3.2 用 MD5 指紋做精確去重比全表比對(duì)快一個(gè)量級(jí)評(píng)論去重不能直接SELECT content FROM table WHERE content ...幾萬條數(shù)據(jù)逐條比對(duì)要把數(shù)據(jù)庫跑死。正確做法是對(duì)清洗后的文本計(jì)算 MD5用哈希值判重。相同文本必然生成相同 MD5雖然理論上存在碰撞但對(duì)這個(gè)量級(jí)的短文本實(shí)際概率可以忽略。import hashlib def md5_fingerprint(text): return hashlib.md5(text.encode(utf-8)).hexdigest() def deduplicate(items): seen set() unique_items [] for item in items: clean_content normalize_text(item[content]) if len(clean_content) 10: continue fp md5_fingerprint(clean_content) if fp in seen: continue seen.add(fp) item[content] clean_content item[content_md5] fp unique_items.append(item) return unique_items這里有兩個(gè)參數(shù)值得細(xì)說。最短長(zhǎng)度設(shè)為 10是因?yàn)槌鞘性u(píng)論里常見的“很棒”“一般”只有幾個(gè)字信息量太低情感分析里很容易被誤判但也別把閾值設(shè)得太高20 字會(huì)丟掉大量有效的中短評(píng)。seen 集合用內(nèi)存存指紋5 萬條評(píng)論的 MD5 也就幾百 KB完全沒有壓力。我在實(shí)際項(xiàng)目里還發(fā)現(xiàn)一個(gè)現(xiàn)象同一用戶在不同平臺(tái)轉(zhuǎn)發(fā)同一條評(píng)論原文幾乎一致但標(biāo)點(diǎn)有差異。所以去重前要先 normalize_text把全角符號(hào)和多余空格清掉否則去重形同虛設(shè)。3.3 停用詞表構(gòu)建不要直接抄網(wǎng)上的通用版本情感分析不是分詞任務(wù)停用詞表的作用不是刪光虛詞而是刪掉會(huì)干擾情感分?jǐn)?shù)的高頻噪聲詞。城市評(píng)論里出現(xiàn)頻率極高但毫無情感傾向的詞語包括“感覺”“覺得”“這家”“東西”“然后”“反正”“一個(gè)”等等。網(wǎng)上下載的 1500 詞停用表是針對(duì)新聞?wù)Z料的用在評(píng)論上反而會(huì)把“好”“壞”之外的情感詞誤傷。我的做法是先用 jieba 對(duì)所有清洗后的評(píng)論分詞統(tǒng)計(jì)詞頻 Top 500然后人工篩一遍把詞頻高但情感中性的詞挑出來加入停用詞表。這個(gè)篩選過程大約需要半小時(shí)但對(duì)后續(xù)所有分析都是正收益。詞表本身是純文本文件每行一個(gè)詞加載方式和停用詞常規(guī)做法一致。import jieba STOPWORDS set() with open(city_review_stopwords.txt, encodingutf-8) as f: for line in f: word line.strip() if word: STOPWORDS.add(word) def tokenize_for_analysis(text): words jieba.lcut(text) return [w for w in words if w not in STOPWORDS and w.strip()]分詞之后不要急著丟原始文本。我一般會(huì)把清洗后評(píng)論、分詞結(jié)果、情感分?jǐn)?shù)分開存因?yàn)楹竺孀鲈~云和負(fù)面主題抽取還要用分詞結(jié)果。城市評(píng)論里“市中心”“地鐵站”“停車場(chǎng)”這種地點(diǎn)詞會(huì)頻繁出現(xiàn)它們不是噪聲而是城市體驗(yàn)的維度詞應(yīng)該保留這對(duì)判斷“這個(gè)城市交通便利度口碑如何”很有價(jià)值。4. 情感分析從基線到落地snownlp 跑分之后再疊一層詞典修正4.1 城市評(píng)論不是電商評(píng)論為什么直接跑通用模型會(huì)翻車情感分析這塊最省事的做法是用 snownlp 對(duì)每條評(píng)論算一個(gè) sentiment 分?jǐn)?shù)大于 0.5 判正面小于 0.5 判負(fù)面。snownlp 底層是樸素貝葉斯模型理論上一個(gè)函數(shù)就能用。但城市評(píng)論有個(gè)要命的特點(diǎn)口語化程度極高否定詞常和程度副詞疊在一起“沒有想象中那么好吃”“也不是很難吃”這種句子比比皆是。通用模型在電商評(píng)論上訓(xùn)練得多遇到這種句式經(jīng)常出錯(cuò)。所以我的落地路徑分兩層。第一層用 snownlp 跑基線快速得到一個(gè)大體分布第二層疊一個(gè)情感詞典修正模塊專門處理否定詞和程度副詞。不要一上來就微調(diào) BERT 或者做多模態(tài)情感分析——那需要標(biāo)注數(shù)據(jù)、GPU 和額外的文本之外的模態(tài)這個(gè)項(xiàng)目的數(shù)據(jù)規(guī)模撐不起而且完全沒必要。4.2 修正模塊的完整代碼否定詞窗口和程度副詞系數(shù)from snownlp import SnowNLP NEGATION_WORDS {不, 沒, 無, 非, 莫, 別, 未} INTENSIFIERS { 非常: 1.6, 特別: 1.6, 極其: 1.8, 太: 1.3, 很: 1.2, 有點(diǎn): 0.7, 稍微: 0.7, 比較: 0.9, } def adjusted_sentiment(text, window3): base_score SnowNLP(text).sentiments words jieba.lcut(text) score base_score # 先處理程度副詞把 0.4 的“有點(diǎn)差”拉回 0.28 for idx, w in enumerate(words): if w in INTENSIFIERS: factor INTENSIFIERS[w] score 0.5 (score - 0.5) * factor # 再處理否定詞找到否定詞后看它后面 window 個(gè)字里有沒有情感詞 # 如果是否定 負(fù)面詞如“不難吃”整體應(yīng)偏正面 for idx, w in enumerate(words): if w in NEGATION_WORDS: after words[idx 1:idx 1 window] if any(w in POSITIVE_WORDS or SnowNLP(w).sentiments 0.6 for w in after): score 1 - score elif any(w in NEGATIVE_WORDS or SnowNLP(w).sentiments 0.4 for w in after): score 1 - score return score這套邏輯里最關(guān)鍵的是 window3 這個(gè)參數(shù)。它表示否定詞只影響后面 3 個(gè)字范圍避免把“不是這家店但服務(wù)員態(tài)度還行”這種跨分句文本整體翻轉(zhuǎn)。window 太小找不到情感詞太大又會(huì)誤傷。我用下來 3 是最優(yōu)解句子短的時(shí)候改成 2 也行。POSITIVE_WORDS 和 NEGATIVE_WORDS 是兩個(gè)自定義詞表需要維護(hù)我通常各放 50 到 100 個(gè)高頻詞把“難吃”“臟亂”“劃算”“驚艷”這類評(píng)論常用詞手工放進(jìn)去比全部用模型判斷可靠得多。這里提一個(gè)參數(shù)細(xì)節(jié)程度副詞用0.5 (score - 0.5) * factor而不是直接乘是為了保持分?jǐn)?shù)始終在 0 到 1 區(qū)間。score0.6 遇到系數(shù) 1.6會(huì)變成 0.66而不是 0.96這樣不會(huì)把本來微正的句子變成極端正面。4.3 城市級(jí)聚合分析均值和中位數(shù)之外還要看標(biāo)準(zhǔn)差每一家店鋪的情感分?jǐn)?shù)算出來后真正的項(xiàng)目交付物是從城市維度匯總的畫像。把評(píng)分、情感分?jǐn)?shù)、評(píng)論數(shù)按城市分組最直接的分析是看平均情感分但這里有個(gè)隱藏問題平均數(shù)會(huì)被異常值拉偏。import pandas as pd df pd.DataFrame(all_reviews) city_stats ( df.groupby(city) .agg( review_count(sentiment, count), mean_sentiment(sentiment, mean), median_sentiment(sentiment, median), std_sentiment(sentiment, std), ) .sort_values(mean_sentiment, ascendingFalse) )std_sentiment 這個(gè)字段最容易忽略。它反映的是城市內(nèi)部評(píng)論分歧度標(biāo)準(zhǔn)差高說明這個(gè)城市的好評(píng)和差評(píng)極端分化典型的“愛之深責(zé)之切”或者某些商圈宣傳過度標(biāo)準(zhǔn)差低說明整體口碑一致。我在交付報(bào)告里會(huì)專門畫一張散點(diǎn)圖橫軸是評(píng)論數(shù)縱軸是情感分點(diǎn)的大小是標(biāo)準(zhǔn)差一眼就能看出哪些城市屬于“小樣本高口碑”的虛火狀態(tài)——評(píng)論數(shù)低于 500 但情感分接近 0.8 的城市數(shù)據(jù)基本不值得采信。提示情感分?jǐn)?shù)閾值不要死磕 0.5。0.45 到 0.55 之間的評(píng)論大半是中性或混合情感比如“環(huán)境不錯(cuò)但味道一般”。這類樣本應(yīng)該歸入中立區(qū)間單獨(dú)統(tǒng)計(jì)強(qiáng)行二分只會(huì)制造大量錯(cuò)標(biāo)。5. 5 萬條評(píng)論項(xiàng)目避坑記錄從字謎亂碼到重復(fù)入庫的 5 個(gè)真實(shí)排查過程5.1 文本全是字謎和方塊字解析結(jié)果完全不能用現(xiàn)象爬回來的評(píng)論內(nèi)容是一堆不認(rèn)識(shí)的偏旁部首組合單個(gè)字符能對(duì)上組成詞語就看不懂而且數(shù)字全變成了小方塊。原因部分點(diǎn)評(píng)平臺(tái)對(duì)評(píng)論內(nèi)容做了字體反爬。頁面會(huì)把標(biāo)準(zhǔn)字體映射成自定義字體瀏覽器加載時(shí)用加密字庫渲染出正常文字但 requests 抓到的 HTML 里的字符編碼是錯(cuò)位映射的。直接按字符取就是字謎。解決這種情況不要在 HTML 層面硬解。優(yōu)先找該站點(diǎn)是否提供 JSON 接口JSON 通常不加密實(shí)在沒有再看字體文件把自定義字體下載后按字形輪廓映射回標(biāo)準(zhǔn)字。這里要注意一個(gè)關(guān)鍵點(diǎn)每次加載字體映射關(guān)系可能都變所以不能緩存一張映射表用到底要在每次抓取時(shí)同步獲取。最笨也最穩(wěn)的辦法是對(duì)字謎文本截圖做 OCR但那速度太慢5 萬條根本扛不住。5.2 入庫后統(tǒng)計(jì)發(fā)現(xiàn)重復(fù)率超過 40%現(xiàn)象評(píng)論總數(shù)很快到 5 萬條但店鋪維度去重后實(shí)際不足 3 萬條重復(fù)集中在評(píng)分高、評(píng)論多的頭部店鋪。原因評(píng)論接口是按更新時(shí)間排序的頁碼越深新評(píng)論插入越頻繁同一評(píng)論可能同時(shí)出現(xiàn)在第 1 頁和第 3 頁另外評(píng)論列表是滾動(dòng)加載前端滾動(dòng)一次就請(qǐng)求一次接口如果爬蟲翻頁時(shí)沒有記錄已抓取評(píng)論的指紋就會(huì)反復(fù)抓同一批。解決不能只按評(píng)論 ID 去重因?yàn)轫撁嬲故镜?ID 字段可能被截?cái)嗷蛎撁?。按清洗后的文?MD5 在庫里建唯一索引入庫時(shí) catch 重復(fù)異常跳過。我在 2.3 節(jié)的 save_reviews 里已經(jīng)寫了這層邏輯實(shí)際跑下來重復(fù)率從 40% 降到 3% 以內(nèi)。還有 3% 是跨平臺(tái)轉(zhuǎn)載的近似重復(fù)需要靠編輯距離或 SimHash 做一遍模糊去重如果對(duì)精確率要求不高可以不做。5.3 snownlp 把“太難吃了”判成正面情緒現(xiàn)象把“這家店的菜太難吃了不會(huì)再光顧”喂給 snownlpsentiments 返回 0.14方向是對(duì)的但改成“不難吃”之后返回 0.82也算對(duì)真正翻車的是“沒有想象中那么難吃湊合吧”竟然返回 0.11 的負(fù)面分。原因snownlp 的訓(xùn)練語料主要來自帶有明確情感標(biāo)簽的商品評(píng)論對(duì)“沒有想象中那么難吃”這種雙重否定句式?jīng)]有能力建模。它把“難吃”這個(gè)詞的權(quán)重拉得極高根本沒有捕捉到前半句的否定和后半句的“湊合”。解決4.2 節(jié)里的否定詞窗口就是針對(duì)這個(gè)問題的。把“不難吃”識(shí)別為正面把“沒有想象中那么難吃”識(shí)別為中性偏負(fù)面。但注意一個(gè)邊界否定詞窗口對(duì)短句效果好對(duì)長(zhǎng)句會(huì)失效。比如“服務(wù)員說這道菜不難吃但端上來真的一般”——否定詞作用域只在“不難吃”里后面轉(zhuǎn)折句是負(fù)面這種句子我一般直接歸入中立區(qū)不參與城市正面率統(tǒng)計(jì)。5.4 翻頁到第 30 頁開始返回空數(shù)據(jù)但網(wǎng)頁上還有評(píng)論現(xiàn)象爬蟲從第 1 頁翻到第 40 頁前 30 頁正常之后接口返回空列表日志里也沒有報(bào)錯(cuò)以為是數(shù)據(jù)到底了。原因評(píng)論接口翻頁有兩個(gè)限制一是頁碼深度限制很多接口最多返回前 50 頁二是時(shí)間窗口限制按時(shí)間倒序排列時(shí)超過某個(gè)時(shí)間點(diǎn)的評(píng)論不會(huì)被返回。第 30 頁恰好落在窗口邊界再往后就是空。解決改用“時(shí)間游標(biāo)”方式翻頁每次按 last_comment_time 傳參而不是頁號(hào)。第一次請(qǐng)求拿最新一批取這批里最早的評(píng)論時(shí)間作為下一次請(qǐng)求的游標(biāo)服務(wù)器會(huì)返回從這個(gè)時(shí)間往后的更早評(píng)論。這樣翻到 5 萬條都沒問題且天然避免同一時(shí)間點(diǎn)評(píng)論被重復(fù)抓取。5.5 MySQL 連接被反復(fù)打斷寫入速度越來越慢現(xiàn)象數(shù)據(jù)入庫到一半日志突然報(bào) “MySQL server has gone away”重試幾次還是失敗進(jìn)程卡死。原因默認(rèn)連接有一個(gè)超時(shí)時(shí)間爬蟲和清洗預(yù)處理耗時(shí)較長(zhǎng)單條連接隔幾分鐘不操作就會(huì)被服務(wù)端斷開。SQLAlchemy 的連接池不知道連接已失效繼續(xù)復(fù)用就報(bào)錯(cuò)。解決在create_engine里加上pool_recycle3600讓連接池每 1 小時(shí)強(qiáng)制回收重建一次。同時(shí)把采集、清洗、入庫拆成三個(gè)階段不要讓一條 MySQL 連接跨過整個(gè)抓取時(shí)間抓取階段只寫原始 JSON 到本地文件清洗階段再批量入庫這個(gè)習(xí)慣幫我避開了很多鎖庫和斷連的問題。6. 從“跑通”到“可信”構(gòu)造 300 條手工標(biāo)注集摸清情感分析的真實(shí)上限做了清洗去重和詞典修正情感分析只是“能跑”離“可信”還差一個(gè)環(huán)節(jié)你不知道整個(gè) pipeline 的準(zhǔn)確率到底是多少。這時(shí)候靠自己拍腦袋判斷完全不靠譜玄學(xué)問題就得用定量方法解決。我的習(xí)慣是每批數(shù)據(jù)都隨機(jī)抽取 300 條自己人工打標(biāo)然后和算法的結(jié)果做對(duì)比。具體做法把清洗后的評(píng)論按城市分層抽樣每層抽 30 到 50 條湊成 300 條。人工標(biāo)記時(shí)只用兩個(gè)類別正面和負(fù)面把明顯中立的句子也歸到負(fù)面或正面里都不合適所以再加上第三類中立。判定標(biāo)準(zhǔn)要提前定死說過 5 個(gè)字以上正面評(píng)價(jià)且沒有明顯負(fù)面詞的算正面諷刺、比較、雙重否定這種全部算中立不進(jìn)準(zhǔn)確率計(jì)算。這樣算出來的準(zhǔn)確率才是真實(shí)的。from sklearn.metrics import classification_report y_true [1, 0, 1, -1, 1, 0, 0, 1, -1, 1] y_pred [1, 0, 0, -1, 1, 1, 0, 1, 0, 1] # 只看正面和負(fù)面兩類中立樣本單獨(dú)觀察 filtered_true [t for t, p in zip(y_true, y_pred) if t ! -1] filtered_pred [p for t, p in zip(y_true, y_pred) if t ! -1] print(classification_report(filtered_true, filtered_pred, target_names[負(fù)面, 正面]))我一般把 0.45 到 0.55 之間的輸出判為中立。這樣會(huì)犧牲一部分樣本但剩下的樣本準(zhǔn)確率能穩(wěn)定在 0.82 以上。只算正面率時(shí)還有個(gè)技巧用情感分?jǐn)?shù)的標(biāo)準(zhǔn)差代替均值來判斷城市口碑是否健康標(biāo)準(zhǔn)差超過 0.25 的城市光看均值容易被兩極分化的評(píng)論誤導(dǎo)。這個(gè)指標(biāo)比任何花哨的模型都更能說明真實(shí)用戶的分歧程度。最后再提一個(gè)多模態(tài)情感分析的延伸思路城市評(píng)論里混著大量圖片表情如果你的數(shù)據(jù)源能一并抓到評(píng)論配圖可以把文本情感分?jǐn)?shù)和圖片色調(diào)做個(gè)交叉驗(yàn)證——通常配圖明亮的評(píng)論文本正面概率更高。這個(gè)方向值得做但前提是先把文本這條路的口徑校準(zhǔn)好。現(xiàn)在我拿到任何一個(gè)新平臺(tái)的數(shù)據(jù)都會(huì)先花半天人工標(biāo)注 300 條再寫代碼調(diào)參而不是急著全量跑。這個(gè)習(xí)慣幫我省下的返工時(shí)間遠(yuǎn)多于標(biāo)注花費(fèi)的時(shí)間希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取