2026/11/5 6:56:37

网页标题抓取实战:应对HTML多变、编码错乱与反爬拦截

网页标题抓取实战:应对HTML多变、编码错乱与反爬拦截 简介本资源是一份面向Python初学者与入门级开发者的爬虫实践教程聚焦网页标题抓取这一典型数据采集任务帮助读者快速掌握requests与BeautifulSoup协同工作的核心流程。资源以1个17KB的Word文档.docx形式提供内容涵盖环境配置、完整可运行代码、逐段函数解析、运行示例及反爬注意事项结构清晰、即学即用。文档中不仅给出标准标题提取方案还明确提示如何根据目标网站HTML结构调整选择器、添加异常处理与请求延时等进阶要点附带本地文件保存逻辑与调试建议具备扎实的实操指导价值。目前已有177人学习下载适合希望夯实Python网络请求与HTML解析基础、完成小规模信息聚合或课程作业的开发者。1. 这不是“写个 requests 就完事”的爬虫它专治标题抓取场景下 HTML 结构多变、编码错乱、反爬静默拦截这三类高频翻车现场你试过用requests.get(url).text直接.find_all(h2)却返回空列表吗不是代码写错了是目标页用了h3 classarticle-title或是标题藏在div>def fetch_titles(url, selector_configNone): 抓取文章标题支持三种 selector 策略 1. CSS 选择器推荐精准匹配 class/id/属性如 h1.entry-title, h2.article-heading 2. 标签名兼容fallback 到 tag如 [h1, h2, h3] 3. 正则提取兜底当 HTML 结构混乱时用正则从 title 或 script 中捞 if selector_config is None: # 默认配置先试 CSS 选择器再 fallback 到 tag 列表 selector_config { css: h1, h2, h3, [class*title], [class*heading], [itempropheadline], tags: [h1, h2, h3], regex_patterns: [rtitle[^]*(.*?)/title, rtitle\s*:\s*(.*?)] } response requests.get(url, timeout10) response.raise_for_status() # 关键自动探测编码而非硬设 utf-8 detected_encoding response.apparent_encoding.lower() if utf not in detected_encoding and gb not in detected_encoding: detected_encoding utf-8 # 保险 fallback soup BeautifulSoup(response.content, html.parser, from_encodingdetected_encoding) titles [] # 第一级CSS 选择器最准 if selector_config.get(css): css_titles soup.select(selector_config[css]) titles.extend([t.get_text(stripTrue) for t in css_titles if t.get_text(stripTrue)]) # 第二级标签名遍历兼容老站 if not titles and selector_config.get(tags): for tag in selector_config[tags]: tag_titles soup.find_all(tag) titles.extend([t.get_text(stripTrue) for t in tag_titles if t.get_text(stripTrue)]) # 第三级正则兜底防极端情况 if not titles and selector_config.get(regex_patterns): raw_html response.text for pattern in selector_config[regex_patterns]: matches re.findall(pattern, raw_html, re.IGNORECASE | re.DOTALL) titles.extend([m.strip() for m in matches if m.strip()]) return list(dict.fromkeys(titles)) # 去重保持顺序提示response.content传给 BeautifulSoup 比response.text更安全——前者是原始字节流后者已按response.encoding解码而response.encoding经常不准。from_encoding参数强制指定解析编码避免乱码。这段代码的核心逻辑是不依赖单一 selector而是构建一个可配置、可降级、可扩展的定位链。selector_config可以按站点单独配置比如抓取某新闻站时传入{css: div.news-item h1, div.news-item h2}抓取某论坛时传入{tags: [h3]}。list(dict.fromkeys(...))去重既保留首次出现顺序又过滤重复标题常见于侧边栏重复显示。2.3 如何快速确定目标站的最优 selector——三步现场诊断法别猜用浏览器 DevTools 实时验证打开目标页 → 右键「查看页面源代码」→ CtrlF 搜关键词找到一个真实标题看它包裹在哪一层标签里记下 class、id、父元素结构。例如搜到h1 classpost-titlePython 爬虫避坑指南/h1那h1.post-title就是首选。在 Console 中用document.querySelectorAll()快速验证输入document.querySelectorAll(h1.post-title, h2.article-title)看返回 NodeList 长度是否等于页面可见标题数。如果为 0说明 class 名动态生成如 Vue 的>script typeapplication/ldjson {context:https://schema.org,type:NewsArticle,headline:标题在这里} /script这时正则rheadline\s*:\s*(.*?)就比 DOM 解析更稳。这三步做完selector_config就有了实测依据而不是凭空写h2。3. 编码与字符处理response.text是最大陷阱apparent_encoding也不总靠谱3.1 为什么response.text会制造乱码——requests 的编码决策链真相requests 的response.text并非直接解码response.content而是按以下优先级决定编码HTTP 响应头Content-Type中的charset如charsetgbkHTMLmeta charsetutf-8标签需解析 HTML 才能读到response.apparent_encodingchardet 库推测准确率约 78%最终 fallback 到ISO-8859-1问题在于第 1 步的charset经常缺失或错误第 2 步需先解析 HTML但解析前又得解码第 3 步 chardet 对短文本如只有标题推测极不准。结果就是response.text一解码就乱get_text()输出一堆 。3.2 真实有效的编码处理流程先content后apparent_encoding再人工校验我们改用以下流程覆盖 99% 的编码场景def get_clean_content(response): 安全获取可解析的 HTML 字符串 步骤1. 用 apparent_encoding 初步解码 → 2. 检查是否含乱码字符 → 3. 若乱码尝试常见编码重试 # Step 1: 用 apparent_encoding 初步解码 try: text response.content.decode(response.apparent_encoding) except (UnicodeDecodeError, LookupError): # apparent_encoding 失败fallback 到 utf-8 text response.content.decode(utf-8, errorsignore) # Step 2: 检查乱码中文常见乱码字符 if any(c in text for c in [, \ufffd, \u00a0]): # Step 3: 尝试常见编码按概率排序 encodings_to_try [gbk, gb2312, utf-8-sig, latin-1] for enc in encodings_to_try: try: text response.content.decode(enc) # 验证解码后是否含合理中文至少 2 个连续中文字符 if re.search(r[\u4e00-\u9fff]{2,}, text): break except (UnicodeDecodeError, LookupError): continue return text # 在 fetch_titles 中替换原 soup 构建逻辑 raw_html get_clean_content(response) soup BeautifulSoup(raw_html, html.parser)这段逻辑的关键是不信任任何单一编码源而是用“解码 → 检查乱码 → 换编码重试”的闭环。errorsignore避免解码中断re.search(r[\u4e00-\u9fff]{2,}, text)是轻量级中文验证——比len(text) 100更准因为很多乱码页 HTML 结构完整但正文是 。3.3 标题清洗不只是strip()还要处理 HTML 实体、多余空白、广告干扰抓到的标题常含nbsp;、quot;、\xa0、连续空格、甚至br标签。直接get_text(stripTrue)不够import html import re def clean_title(title): 标题深度清洗 - 解码 HTML 实体amp; → , nbsp; → 空格 - 替换各种空白符为单空格 - 移除首尾不可见字符\ufeff, \u200b - 过滤纯符号/过短标题5 字且无中文 if not isinstance(title, str): return # 1. HTML 实体解码 title html.unescape(title) # 2. 统一空白符\s 包含 \n\t\r\u00a0\u200b 等 title re.sub(r\s, , title) # 3. 移除 BOM 和零宽字符 title re.sub(r[\ufeff\u200b\u200c\u200d], , title) # 4. strip title title.strip() # 5. 过滤无效标题长度5 且不含中文/英文字母/数字 if len(title) 5 and not re.search(r[\u4e00-\u9fff\w\d], title): return return title # 在提取标题时应用 title_list [clean_title(title.get_text()) for title in titles if clean_title(title.get_text())]注意html.unescape()必须放在get_text()之后因为get_text()已把 HTML 标签剥离只剩文本内容中的实体。若在get_text()前调用对h2quot;标题quot;/h2无效。4. 避坑标题抓取中 5 个让你重启三次 IDE 的真实问题4.1 现象find_all(h2)返回空列表但浏览器明明能看到标题原因目标页是 JavaScript 渲染如 React/Vue SSRrequests获取的是初始 HTML标题在script标签里或由 AJAX 加载。解决启用selector_config[regex_patterns]或改用 Selenium本资源包附带轻量版selenium_fetch_titles.py仅启动 Chrome Headless不装完整 WebDriver。4.2 现象标题里出现大量 或????原因response.apparent_encoding错误推测为ISO-8859-1而实际是gbk。解决禁用response.text全程用response.contentget_clean_content()流程见 3.2 节。4.3 现象同一个 URL第一次运行有标题第二次就 403原因网站服务端记录了请求指纹User-Agent、IP、Headers第二次识别为爬虫。解决在requests.get()中添加随机 UA 和 Refererheaders { User-Agent: random.choice([ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ]), Referer: https://www.google.com/ } response requests.get(url, headersheaders, timeout10)4.4 现象抓到的标题包含导航栏、侧边栏、页脚文字原因find_all(h2)无范围限制把全页所有h2都抓了。解决用 CSS 选择器限定区域如main h2, article h2, .content h2或用soup.find(main).find_all(h2)先定位主体区域。4.5 现象save_titles()保存后文件打开是乱码Notepad 显示方块原因Windows 记事本默认用ANSI即本地编码打开.txt而 Python 用utf-8写入。解决保存时显式声明 BOM让 Notepad 识别为 UTF-8with open(filename, w, encodingutf-8-sig) as f: # 注意是 utf-8-sig for title in titles: f.write(title \n)5. 反爬与请求管理不用 Selenium 也能扛住 80% 的基础反爬检测5.1 为什么time.sleep()不是银弹但Session复用是刚需很多教程说“加time.sleep(1)就行”但实际中sleep(1)对单次请求有效但批量抓 100 个 URL 时总耗时飙升 100 秒体验极差更关键的是不复用Session会导致每次请求都新建 TCP 连接服务器极易识别为独立爬虫会话。正确做法是Session复用 随机间隔 请求头轮换import time import random from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry # 创建带重试和连接池的 Session session requests.Session() retry_strategy Retry( total3, backoff_factor1, status_forcelist[429, 500, 502, 503, 504], ) adapter HTTPAdapter(max_retriesretry_strategy, pool_connections10, pool_maxsize10) session.mount(http://, adapter) session.mount(https://, adapter) # 随机 UA 池精简版实际可用 50 条 USER_AGENTS [ Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Safari/605.1.15, ] def fetch_with_session(url, delay_range(0.5, 2.5)): 带 Session 复用、随机延迟、UA 轮换的请求 time.sleep(random.uniform(*delay_range)) # 随机延迟非固定值 headers { User-Agent: random.choice(USER_AGENTS), Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, Accept-Encoding: gzip, deflate, Connection: keep-alive, } try: response session.get(url, headersheaders, timeout10) response.raise_for_status() return response except Exception as e: print(fRequest failed for {url}: {e}) return None # 在主程序中调用 if __name__ __main__: urls [https://example1.com, https://example2.com] for url in urls: print(fFetching {url}...) response fetch_with_session(url) if response: titles fetch_titles_from_response(response) # 复用前面的 fetch_titles 逻辑 save_titles(titles, ftitles_{hash(url)}.txt)Session复用让 TCP 连接复用pool_connections10支持并发Retry自动处理 429/503random.uniform(0.5, 2.5)比固定sleep(1)更难被频率规则识别。5.2robots.txt不是法律但它是你的第一道风险探针别跳过robots.txt它虽无强制力但能快速暴露站点态度def check_robots_txt(url): 检查 robots.txt返回是否允许抓取 / 文章路径 parsed urllib.parse.urlparse(url) robots_url f{parsed.scheme}://{parsed.netloc}/robots.txt try: response requests.get(robots_url, timeout5) if response.status_code 200: content response.text # 检查是否禁止 /article/ /post/ 等常见路径 disallowed re.findall(rDisallow:\s*(/[^\\n]*), content, re.I) for path in disallowed: if re.search(rarticle|post|news|blog, path, re.I): print(fWarning: robots.txt disallows {path}, proceed with caution.) return False return True except: return True # 无法访问 robots.txt按允许处理 # 主程序开头调用 if not check_robots_txt(url): print(robots.txt suggests restriction. Exiting.) exit(1)这不是道德绑架而是工程实践提前知道风险比被抓包后 debug 强十倍。6. 进阶技巧用pandas批量验证标题质量自动生成抓取报告6.1 为什么手动检查 100 个标题是自杀行为——用数据思维做 QA当你批量抓取 50 个网站每个返回 10~50 个标题人工核对效率极低。我们用pandas构建一个轻量 QA 流程自动标记可疑标题import pandas as pd import re def analyze_titles(titles, urlunknown): 分析标题列表返回带质量标记的 DataFrame df pd.DataFrame(titles, columns[title]) df[url] url df[length] df[title].str.len() df[has_chinese] df[title].str.contains(r[\u4e00-\u9fff], naFalse) df[has_alpha] df[title].str.contains(r[a-zA-Z], naFalse) df[word_count] df[title].str.split().str.len().fillna(0) # 质量标记0好1警告2严重问题 df[quality_flag] 0 df.loc[df[length] 5, quality_flag] 1 df.loc[~df[has_chinese] ~df[has_alpha], quality_flag] 2 df.loc[df[word_count] 20, quality_flag] 1 # 过长可能含广告 return df # 批量抓取并分析 all_dfs [] urls [https://site-a.com, https://site-b.com] for url in urls: titles fetch_titles(url) df analyze_titles(titles, url) all_dfs.append(df) full_report pd.concat(all_dfs, ignore_indexTrue) full_report.to_csv(title_qa_report.csv, indexFalse, encodingutf-8-sig) # 输出摘要 print(\n 抓取质量摘要 ) print(f总标题数: {len(full_report)}) print(f高质量标题: {len(full_report[full_report[quality_flag]0])}) print(f需人工复核: {len(full_report[full_report[quality_flag]0])}) print(f严重问题标题: {len(full_report[full_report[quality_flag]2])})生成的title_qa_report.csv可直接用 Excel 打开按quality_flag排序一眼定位问题标题。utf-8-sig确保 Excel 正确显示中文。6.2 抓取失败归因分析不是报错就重试要分清是网络问题还是目标问题我们把失败原因分类记录避免盲目重试def robust_fetch(url, max_retries3): 带归因的健壮抓取 for i in range(max_retries): try: response fetch_with_session(url) if response and response.status_code 200: return {status: success, response: response} elif response: return {status: http_error, code: response.status_code} except requests.exceptions.Timeout: return {status: timeout, retry: i1} except requests.exceptions.ConnectionError: return {status: connection_error} except Exception as e: return {status: unknown_error, error: str(e)} return {status: failed_after_retries} # 使用 result robust_fetch(https://example.com) if result[status] success: titles fetch_titles_from_response(result[response]) elif result[status] timeout: print(网络超时建议检查代理或重试) elif result[status] http_error: print(fHTTP 错误 {result[code]}目标站可能拒绝访问)6.3 从那以后我每次批量抓取前都强制走一遍check_robots_txtanalyze_titles的 QA 流程哪怕只抓 3 个站。因为标题质量不达标后面所有 NLP、摘要、向量化都是在垃圾上建楼——看着很炫实际毫无价值。希望帮到你。本文还有配套的精品资源点击获取