☰
Python爬虫实战:从请求解析到动态页面与并发采集
2026/10/29 3:57:19 网站建设 项目流程

简介:这份文档面向具备一定编程基础、希望系统掌握网络数据采集的学习者与开发者,围绕Python爬虫从概念到实战展开,帮助读者理解自动化抓取工具的工作原理并落地到实际项目。内容覆盖爬虫三大核心环节——发送请求、解析网页与数据存储,重点讲解requests、BeautifulSoup、lxml与Scrapy等常用库的特点及适用场景,并延伸至动态加载处理、反爬应对与效率优化等进阶技巧,同时强调遵守法规与网站协议、保护用户隐私。资源包共1个docx文件,约16KB,以文字讲解与代码示例为主,便于按章节顺序阅读或按需查阅。目前已有629人学习,适合需要批量获取公开数据的研究者、构建数据挖掘或信息监测系统的开发者,以及希望借小项目巩固爬虫技能的初学者参考。

1. 从一份爬虫基础包说起:它到底能帮你抓到什么

很多人第一次接触网络爬虫,是被一个很具体的需求逼出来的:手头有一批公开页面要整理成表格,手动复制粘贴到第三十页就崩溃了。这份 Python 爬虫技术资料包,解决的正是这个从零到能跑通的阶段——它把 HTTP 请求、HTML 解析、数据落盘这三件事拆开讲清楚,再串成一条完整链路。适合有一定 Python 基础、但没系统写过采集脚本的开发者,也适合需要批量获取公开数据做分析的研究场景。它不承诺你学完就能横扫所有站点,但能让你在面对一个普通静态页面时,知道第一步发什么请求、第二步从哪里取字段、第三步数据往哪存,以及哪些线不能踩。

2. 请求与解析:把网页拆成可用的数据结构

2.1 requests 发请求:先搞清楚你拿到的是什么

爬虫的第一步永远是发 HTTP 请求。资料里用的是requests,这个选择没什么争议——它的 API 比标准库urllib干净太多,异常处理也更直观。但新手最容易犯的错,是拿到response.text就以为万事大吉,完全不看状态码和编码。

我一般会先把响应对象的关键属性打出来,确认三件事:状态码是不是 200、内容类型是不是 HTML、编码有没有猜错。中文站点最常见的翻车就是编码,response.text默认按响应头里的 charset 解码,响应头没写或者写错了,出来的就是乱码。

import requests url = "https://www.example.com/list" # 带上 UA,很多站点对空 UA 直接返回 403 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", "Accept-Language": "zh-CN,zh;q=0.9", } # timeout 必须设,否则遇到慢响应会一直挂着 resp = requests.get(url, headers=headers, timeout=10) print(resp.status_code) # 先看状态码 print(resp.headers.get("Content-Type")) # 确认是不是 text/html resp.encoding = resp.apparent_encoding # 用内容推测编码,兜底乱码 html = resp.text

这段代码里几个参数值得单独说。timeout=10是硬性要求,不设的话单个请求卡死会拖垮整个脚本;headers里 UA 和Accept-Language是最低限度的伪装,不是让你去对抗什么,而是很多站点的默认策略就是把无头请求挡在门外;resp.apparent_encoding是基于内容统计推测编码,比响应头可靠,代价是多一次计算,数据量不大时完全值得。

还有一个容易被忽略的点:requests.get默认跟随重定向,最终resp.url可能和你请求的 URL 不一样。如果你后续要拼相对链接,得用resp.url做基准,而不是原始 URL。

2.2 BeautifulSoup 与 lxml:两种解析路线的取舍

拿到 HTML 之后就是解析。资料里同时给了BeautifulSoup和lxml,这两者不是替代关系,而是两种使用习惯。

BeautifulSoup的优势是容错性强,遇到标签没闭合、嵌套混乱的页面也能解析出一棵树,API 也友好,find、find_all、select基本覆盖日常需求。缺点是纯 Python 实现,速度慢,处理几万页时差距明显。

lxml走的是 C 语言底层,解析快,支持 XPath,定位精确。缺点是遇到畸形 HTML 时容错不如 BeautifulSoup,偶尔会丢节点。

我的习惯是:页面结构规整、量大,直接用lxml配 XPath;页面脏、结构不稳定,先用 BeautifulSoup 把能拿的拿到,别跟解析器较劲。

from bs4 import BeautifulSoup from lxml import etree # 路线一:BeautifulSoup,按标签和属性定位 soup = BeautifulSoup(html, "html.parser") items = soup.find_all("div", class_="item") for item in items: title_tag = item.find("h2") title = title_tag.get_text(strip=True) if title_tag else "" link = item.find("a") href = link.get("href") if link else "" print(title, href) # 路线二:lxml + XPath,适合结构稳定的列表页 tree = etree.HTML(html) titles = tree.xpath('//div[@class="item"]/h2/text()') links = tree.xpath('//div[@class="item"]/a/@href')

两段代码做的是同一件事,差别在于定位方式。BeautifulSoup 的find是逐层找,写起来直观;XPath 是一步到位,//div[@class="item"]/h2/text()直接取文本,@href直接取属性。XPath 的坑在于text()只取直接文本节点,如果h2里还嵌了span,文本会漏,这时候要用string(.)或者//text()再拼接。

提示:解析前先确认页面是不是服务端渲染。如果resp.text里根本搜不到你要的字段,八成是 JS 动态加载,换解析库没用,得换思路。

2.3 数据落盘:从列表到 Excel 的完整链路

数据取出来之后要存。资料里用pandas写 Excel,这对做分析的人最顺手,但要注意字段对齐——解析阶段如果某个字段缺失返回了None,直接塞进 DataFrame 会变成空值,写出来是空白单元格,后续处理容易踩坑。

import pandas as pd rows = [] for item in items: title_tag = item.find("h2") link = item.find("a") rows.append({ "title": title_tag.get_text(strip=True) if title_tag else "", "url": link.get("href") if link else "", }) df = pd.DataFrame(rows, columns=["title", "url"]) # 去重,列表页翻页时经常出现重复条目 df.drop_duplicates(subset=["url"], inplace=True) df.to_excel("output.xlsx", index=False)

这里columns显式指定了列顺序,避免某条记录字段缺失导致列错位;drop_duplicates按 URL 去重,是因为翻页采集时边界页经常重复。如果数据量大到几十万行,Excel 就不合适了,换 CSV 或者直接写 SQLite,pandas的to_csv和to_sql都是一行的事。

3. 动态页面与并发:当静态解析不够用的时候

3.1 Selenium 接管动态渲染:什么时候必须上浏览器

静态请求拿不到数据,是采集过程中最典型的卡点。页面源码里只有一个空壳div,数据靠 AJAX 回来再渲染,这时候requests拿到的 HTML 里根本没有目标字段。资料里给的方案是Selenium,思路是驱动真实浏览器执行 JS,等渲染完再取page_source。

from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC options = webdriver.ChromeOptions() options.add_argument("--headless") # 无头模式,不弹窗口 options.add_argument("--disable-gpu") driver = webdriver.Chrome(options=options) try: driver.get("https://www.example.com/dynamic") # 显式等待目标元素出现,别用 sleep 硬等 WebDriverWait(driver, 15).until( EC.presence_of_element_located((By.CSS_SELECTOR, ".item")) ) html = driver.page_source finally: driver.quit() # 一定要 quit,否则进程残留

关键点在等待策略。很多人图省事写time.sleep(5),网络快的时候白等,网络慢的时候还是拿不到。WebDriverWait配presence_of_element_located是等元素真正出现,超时抛异常,逻辑更可控。driver.quit()放在finally里,是因为异常退出时浏览器进程不关,跑几次就攒一堆僵尸进程,机器直接卡死——这个坑我踩过不止一次。

Selenium 的代价是慢,一个页面几秒起步,量大了不现实。所以我的判断标准是:能用接口直接拿 JSON 的,优先找接口;接口拿不到的,再上浏览器;浏览器方案只用于关键页面,不做全量。

3.2 多线程采集:并发不是越多越好

单线程串行发请求,瓶颈在等待网络往返。资料里用threading做并发,思路是对的,但线程数不能拍脑袋定。

import threading import requests from queue import Queue urls = [f"https://www.example.com/page/{i}" for i in range(1, 51)] q = Queue() for u in urls: q.put(u) results = [] lock = threading.Lock() def worker(): while not q.empty(): url = q.get() try: resp = requests.get(url, timeout=10) with lock: results.append((url, resp.status_code)) except requests.RequestException as e: with lock: results.append((url, str(e))) finally: q.task_done() threads = [threading.Thread(target=worker) for _ in range(5)] for t in threads: t.start() for t in threads: t.join()

这里用Queue做任务分发,天然线程安全,比手动切分列表省心。lock保护共享的results列表,因为list.append在多线程下虽然多数时候没事,但不是原子操作,量大了会丢数据。线程数设 5 是个保守值,实际取决于目标站点的承受能力和你的网络出口,盲目开到 50 只会更快触发限流。

注意:并发的前提是目标站点允许。请求间隔、并发数这些参数,本质是在你的效率和对方服务器压力之间找平衡,不是技术炫技。

3.3 反爬应对:把请求做得像正常访问

资料里列了几条常规手段:设请求间隔、补全请求头、控制频率。这些不是玄学,是基本礼貌。robots.txt该看就看,里面写了Disallow的路径就别碰,这是底线。

请求间隔的实现很简单,在每次请求后加随机延时:

import time import random for url in urls: resp = requests.get(url, headers=headers, timeout=10) # 随机间隔 1~3 秒,避免固定节奏被识别 time.sleep(random.uniform(1, 3))

固定间隔和随机间隔的区别在于,固定节奏的请求序列特征太明显,随机化之后更接近人的浏览行为。但这不是让你去突破什么限制,而是让采集行为落在合理范围内。遇到明确要求登录、明确标注禁止采集的内容,正确做法是停手,不是加码。

4. 避坑与排查:那些跑起来才知道的问题

4.1 状态码 200 但数据为空

现象:请求返回 200,resp.text也有内容,但解析出来字段全是空。

原因:页面是 JS 渲染的,源码里只有占位节点;或者你用的选择器和实际 DOM 结构对不上,比如 class 名带了哈希后缀。

解决:先在浏览器里关掉 JS 看页面还剩什么,确认数据是不是服务端直出;再用开发者工具复制实际选择器,别凭记忆写。如果确认是动态渲染,转 Selenium 或直接找 XHR 接口。

4.2 中文乱码反复出现

现象:标题里出现æ–‡ç«这类字符,或者一堆问号。

原因:响应头 charset 缺失或错误,requests按默认 ISO-8859-1 解码。

解决:统一在取text前设resp.encoding = resp.apparent_encoding,或者直接resp.content.decode("utf-8", errors="ignore")。后者适合确认目标站是 UTF-8 的情况,errors="ignore"兜底个别坏字节。

4.3 线程跑完数据少了一截

现象:50 个 URL,结果列表只有 40 多条。

原因:共享列表写入没加锁,或者异常被吞掉没记录。

解决:所有对共享结构的写操作套lock;except里别只写pass,至少把 URL 和异常记下来,事后能对账。我一般会在最后打印len(results)和len(urls)对比,对不上就说明有丢失。

4.4 Selenium 跑几次后机器变卡

现象:脚本执行越来越慢,任务管理器里一堆浏览器进程。

原因:异常路径下driver.quit()没执行,进程残留。

解决:try/finally包住整个浏览器操作,finally里必调quit()。更稳的做法是用上下文管理器封装,退出时自动清理。

4.5 翻页翻到重复数据

现象:导出的表格里同一条记录出现多次。

原因:列表页分页参数边界处理不当,最后一页和空页返回了相同内容;或者页面本身有推荐位混在列表里。

解决:落盘前按唯一键去重,URL 或 ID 都行;翻页循环里加终止条件,比如连续两页无新数据就停,别死磕页码上限。

5. 从能跑到跑稳:一套可复用的采集骨架

把前面几块拼起来,我一般会固定一个骨架:配置区放 URL 模板、请求头、并发数和输出路径;采集函数只负责发请求和返回原始 HTML;解析函数只负责从 HTML 里取字段,不碰网络;主流程负责调度、去重和落盘。这样拆的好处是,换站点时只改解析函数,调度和存储逻辑不动。

验证采集结果是否可信,我习惯做三件事。第一,抽样比对:随机抽 5 条记录,手动打开对应页面核对字段,确认没有错位。第二,数量对账:请求了多少个 URL,成功多少、失败多少、去重后多少,三个数字要能对上。第三,边界检查:翻到第一页和最后一页,确认没有漏采也没有重复采。

检查项做法不合格的表现
字段准确性随机抽 5 条人工核对标题和链接对不上
数量完整性请求数、成功数、去重数三方对账差值超过预期失败率
边界覆盖单独跑首页和末页首页漏采或末页重复
编码正确性检查含中文的字段乱码或问号

还有一个习惯值得养成:每次采集前先跑一个只抓 3 条的小样本,确认解析规则和落盘格式都对,再放开全量。全量跑完发现字段全错、重来一遍的代价,远比先跑小样本高。这套流程不复杂,但能挡掉大部分低级错误。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询