Python爬虫开发环境搭建与HTTP请求基础(Requests/httpx)

搞爬虫第一步,肯定是把环境整利索。现在都2024年了,别再去折腾Python 3.7或者3.8那些老古董了。直接上最新的 Python 3.13.0(2024年10月7日刚发布的),性能优化和新特性支持都更好,写起来也舒心。装环境这块,如果你是Windows用户,记得去官网下安装包的时候,那个"Add Python to PATH"的勾一定要打上,不然装完了还得手动配环境变量,纯属给自己找麻烦。

装好了Python,别急着写代码,先整个虚拟环境。打个比方,这玩意儿就像给每个项目弄个独立的沙盒,A项目用requests 2.31.0,B项目用httpx 0.27.0,互不干扰,省得以后依赖冲突了哭都来不及。命令行敲个 python -m venv venv,然后激活它。

接下来就是装库了。爬虫发请求,以前大家无脑用 requests,但现在趋势变了,咱们得与时俱进。我建议两个都装,同步请求用 requests,异步高并发用 httpx。直接 pip install requests httpx 一把梭。

说到发请求,很多新手一上来就直接 requests.get(url),这其实是大忌。现在的网站反爬都很精明,你连个像样的 User-Agent 都不带,服务器一看就是机器人,直接给你封了。所以,伪装请求头是必修课。

咱们看个最基础的例子,用 requests 去抓个网页,顺便把伪装工作做好:

import requests # 这里的Headers就是你的身份证,别裸奔 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,en;q=0.8' } url = 'https://httpbin.org/get' # 这是一个专门用来测试请求的网站 try: # 加个timeout,别让请求卡死在那儿 response = requests.get(url, headers=headers, timeout=10) # 200说明请求成功 if response.status_code == 200: print("请求成功!") # 注意编码,有时候网页是gbk,需要手动指定 response.encoding = response.apparent_encoding print(response.text) else: print(f"请求失败,状态码:{response.status_code}") except Exception as e: print(f"出错了:{e}")

如果你要搞大规模抓取,上面这种同步请求就太慢了,一个请求没回来,后面的都得排队等着。这时候就得上 httpx 的异步模式了。结合 asyncio,那速度提升是肉眼可见的。

import httpx import asyncio async def fetch_url(client, url): try: response = await client.get(url, timeout=10.0) print(f"抓到 {url} 的状态码是 {response.status_code}") return response.text except Exception as e: print(f"抓 {url} 失败了: {e}") return None async def main(): # 用AsyncClient,记得要关掉它 async with httpx.AsyncClient(headers={'User-Agent': 'Mozilla/5.0'}) as client: tasks = [ fetch_url(client, 'https://httpbin.org/delay/1'), fetch_url(client, 'https://httpbin.org/delay/1'), fetch_url(client, 'https://httpbin.org/delay/1') ] # 同时发三个请求,不用等 await asyncio.gather(*tasks) if __name__ == '__main__': asyncio.run(main())

🔧 实战技巧:在开发环境,建议把 requestshttpx 混着用。调试阶段用 requests 简单直接,等要上线跑批量数据了,再换成 httpx 的异步模式。还有,千万别在代码里硬编码代理或者API Key,用环境变量或者配置文件管理,这是基本的职业素养,也是为了安全。

---

网页数据解析实战:XPath、CSS选择器与正则表达式

拿到网页的HTML源码只是第一步,接下来才是硬骨头——怎么把我们要的数据从这一大坨乱七八糟的标签里抠出来。这就涉及到解析技术了。现在主流的玩法有三种:XPathCSS Selector正则表达式。可以这么理解,前两个是专门干这活的,正则则是个万金油,但在解析HTML结构时,我一般不太推荐用它,除非是处理那种没有标签包裹的纯文本。

现在的网页结构越来越复杂,但解析库倒是挺好用的。我一般用 lxml 或者 ParselParsel 其实是Scrapy框架内置的解析库,但它独立出来用也超级香,因为它同时支持XPath和CSS选择器,不用纠结选哪个库。

先说 XPath。这玩意儿就像是文档的导航地图,用路径表达式来定位节点。比如 //div[@class='content'] 就是找所有class为content的div标签。它的语法很严谨,写好了基本不会出错,就是写起来稍微有点长。

再看 CSS Selector。如果你懂点前端,这个简直就是亲妈。div.content > p 这种写法,比XPath看着清爽多了。在浏览器里打开开发者工具,选中元素,右键复制Selector,虽然有时候复制出来的很烂,但稍微改改就能用。

咱们直接上代码,用 Parsel 来演示怎么解析一个简单的图书列表页面(这里用一段模拟的HTML字符串):

from parsel import Selector html_content = """ <div class="book-list"> <div class="book-item"> <h2 class="title">Python编程从入门到实践</h2> <p class="price">¥89.00</p> <p class="author">作者:Eric Matthes</p> </div> <div class="book-item"> <h2 class="title">流畅的Python</h2> <p class="price">¥139.00</p> <p class="author">作者:Luciano Ramalho</p> </div> </div> """ # 初始化Selector sel = Selector(text=html_content) # 1. 用XPath提取所有书名 # 注意,xpath返回的是选择器列表,要用get()或getall()提取文本 book_titles_xpath = sel.xpath('//h2[@class="title"]/text()').getall() print("XPath提取的书名:", book_titles_xpath) # 2. 用CSS Selector提取所有价格 book_prices_css = sel.css('p.price::text').getall() print("CSS提取的价格:", book_prices_css) # 3. 混合使用:先定位大标签,再在里面细挖 items = sel.css('div.book-item') for item in items: # 在子选择器里继续用xpath或css title = item.xpath('./h2/text()').get() author = item.css('p.author::text').get() print(f"书籍信息: {title} - {author}")

有时候,数据藏得比较深,或者不是标准的标签格式,这时候 正则表达式(re) 就派上用场了。比如你想从一段文本里把所有的手机号或者邮箱抠出来,正则绝对是首选。但千万别试图用正则去解析整个HTML树,那是自寻死路,因为HTML标签是可以嵌套的,正则处理嵌套结构非常无力。

import re text_data = "我的邮箱是 example@test.com,他的邮箱是 admin@site.org,欢迎联系。" # 简单的邮箱正则匹配 pattern = r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' emails = re.findall(pattern, text_data) if emails: print("找到的邮箱:", emails) else: print("没找到邮箱")

🔧 实战技巧:新手最容易踩的坑就是“写死”选择器。比如 div:nth-child(3),这种一旦网页改版或者多了一个广告位,你的爬虫就挂了。尽量用那种语义化的class或者id来定位,比如 div.price 就比 div:nth-child(2) 稳得多。另外,写解析规则的时候,先在浏览器的Console里用 $x('your_xpath') 或者 $$('your_css') 测试一下,能匹配到再写到代码里,效率翻倍。

---

动态网页爬取:Playwright与Selenium应对JavaScript渲染

现在的网站,尤其是那些电商、社交媒体,早就不是当年那种直接把数据塞在HTML里返回给你的“老实人”了。它们大量使用JavaScript动态渲染,也就是你拿到手的HTML可能就是个空架子,数据都是浏览器跑了一段JS之后才填进去的。这时候,你用刚才说的 requests 去抓,抓回来的就是个寂寞。

这就轮到无头浏览器(Headless Browser)出场了。以前大家都是用 Selenium,但这玩意儿配置起来贼麻烦,还得下驱动,而且速度慢得像蜗牛。根据2024年的技术趋势,Playwright 正在逐步取代 Selenium。它是微软出的,支持 Chromium、Firefox 和 WebKit,最关键的是,它自带浏览器驱动,不用你手动去配,而且反检测能力比Selenium强不少。

换个角度看,Playwright 就是让你用代码去控制一个真实的浏览器,让浏览器去加载页面、执行JS,等数据都出来了,你再去“偷”数据。

咱们看个实战例子,用 Playwright 去抓取一个需要滚动或者点击才能加载数据的页面(这里以访问动态渲染页面为例):

# 先 pip install playwright # 装完库后,记得运行一次:playwright install,它会自动下载浏览器驱动 from playwright.sync_api import sync_playwright def run_playwright(): with sync_playwright() as p: # 启动Chromium浏览器,headless=False表示可以看到浏览器界面,方便调试 # 关键点:生产环境才用headless=True browser = p.chromium.launch(headless=False) page = browser.new_page() print("正在访问页面...") page.goto('https://httpbin.org/forms/post', wait_until='networkidle') # 有时候页面加载慢,可以用wait_for_selector等待某个元素出现 # page.wait_for_selector('.target-class') # 获取渲染后的完整HTML content = page.content() print("页面标题:", page.title()) # 甚至还可以截图看看 page.screenshot(path='screenshot.png') print("截图已保存") browser.close() if __name__ == '__main__': run_playwright()

如果你非要用 Selenium(虽然我不推荐,但有些老项目还在用),那配置起来是这样的,注意看区别:

# 先 pip install selenium # 还得去下载对应浏览器版本的driver,比如chromedriver,放在PATH里或者指定路径 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 import time def run_selenium(): # 配置选项,比如无头模式 options = webdriver.ChromeOptions() options.add_argument('--headless') options.add_argument('user-agent=Mozilla/5.0') driver = webdriver.Chrome(options=options) try: driver.get('https://httpbin.org/') # Selenium 4 以后推荐用 WebDriverWait 显式等待,别用 time.sleep wait = WebDriverWait(driver, 10) # 等待某个元素加载出来 # element = wait.until(EC.presence_of_element_located((By.ID, 'content'))) print("当前URL:", driver.current_url) html = driver.page_source # print(html) finally: driver.quit() if __name__ == '__main__': run_selenium()

对比一下你就知道,Playwright 的 API 设计更现代化,而且它处理那些复杂的SPA(单页应用)时,支持 networkidle 这种等待策略,比 Selenium 的强制等待或者傻傻的轮询要智能得多。

⚡ 效率提示:用无头浏览器最耗资源,别动不动就开几十个浏览器实例,服务器内存会爆的。如果数据不是非得JS渲染才能拿,千万别用这招。还有,现在很多网站会检测你的浏览器指纹,比如检测 navigator.webdriver 属性。Playwright 在这方面做了很多反检测优化,但如果你发现被封了,可以搜一下怎么隐藏 webdriver 特征,或者在启动参数里加一些随机的指纹信息,伪装成真实用户。记住,爬动态网页是下策,能直接找到背后的API接口(通过抓包工具看XHR请求)才是上策,那速度比跑浏览器快N倍。

4. 高性能爬虫架构:异步asyncio与Scrapy框架中间件详解

换个角度看,写爬虫最开始大家都是用 requests 加个 for 循环,跑起来倒是挺顺手,但一旦你要抓几万甚至几十万条数据,那速度简直慢到让人怀疑人生。为啥?因为你的程序在等服务器响应的时候,CPU是闲着的,啥也没干。这时候,咱们就得请出 异步编程(asyncio)Scrapy中间件 这两个大杀器了。

异步 asyncio 与 aiohttp 实战

在 Python 3.13.0(2024年10月刚发布的版本)里,异步编程已经非常成熟了。咱们不用再像以前那样写回调地狱,直接上 async/await 语法糖。配合 aiohttp 库,咱们能让程序在等待网页返回的时候,顺手去请求下一个网页,效率直接起飞。

经验之谈提醒:很多新手以为异步就是多线程,其实不是。异步是单线程在IO等待时切换任务,开销比线程小多了。

下面是一个完整的异步爬虫示例,咱们直接抓取一些测试URL,看看并发的效果:

import asyncio import aiohttp import time # 定义一个异步的获取函数 async def fetch_url(session, url, semaphore): # 使用信号量控制并发数,防止把对方服务器打崩,也防止自己被封 async with semaphore: try: # 伪装一下 User-Agent,这是基本礼貌 headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'} async with session.get(url, headers=headers, timeout=10) as response: # 假设咱们只关心状态码和长度 html = await response.text() print(f"抓到 {url}, 状态码: {response.status}, 长度: {len(html)}") return len(html) except Exception as e: print(f"抓 {url} 出错了: {e}") return 0 async def main(): # 咱们准备一堆URL(这里用httpbin做测试,实际换成你的目标站) urls = [f'http://httpbin.org/delay/1' for _ in range(10)] # 模拟10个延迟1秒的请求 # 限制并发数为5,别太猛 semaphore = asyncio.Semaphore(5) # 创建一个TCPConnector,限制连接数 connector = aiohttp.TCPConnector(limit=10) start_time = time.time() # 使用 aiohttp.ClientSession 作为上下文管理器 async with aiohttp.ClientSession(connector=connector) as session: # 创建任务列表 tasks = [fetch_url(session, url, semaphore) for url in urls] # 等待所有任务完成 await asyncio.gather(*tasks) end_time = time.time() print(f"异步抓取完成,总耗时: {end_time - start_time:.2f} 秒") if __name__ == '__main__': # Python 3.13 运行异步代码的标准姿势 asyncio.run(main())

🔧 实战技巧:跑这段代码你会发现,虽然每个请求都延时1秒,但10个请求并发5个,总耗时大概也就2-3秒,而不是顺序执行的10秒。核心要点:一定要加 semaphore(信号量)限制并发,不然有些网站防火墙直接把你IP送进小黑屋。

Scrapy 中间件:爬虫的“中间件”加工厂

如果你觉得手撸 asyncio 还是太底层,那 Scrapy 绝对是你的好伙伴。Scrapy 自带异步引擎,比咱们自己写的 asyncio 代码要稳得多。在 Scrapy 中,中间件(Middleware)是核心,它就像流水线上的工人,请求发出去之前和响应拿回来之后,都要经过它们处理。

咱们最常改的就是 Downloader Middleware。比如,你想给每个请求加个代理,或者处理特殊的异常。

下面是一个 Scrapy 中间件的代码示例,演示如何动态设置 User-Agent 和代理(假设你的代理是 http://proxy.com:8080):

import random class CustomProxyMiddleware: def __init__(self): # 这里可以放一堆User-Agent,或者从文件读 self.user_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) 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.0 Safari/605.1.15' ] # 假设你有一个代理池,这里简单写死一个,实战中建议从数据库或API获取 self.proxy = "http://127.0.0.1:7890" def process_request(self, request, spider): # 1. 随机设置 User-Agent ua = random.choice(self.user_agents) request.headers['User-Agent'] = ua # 2. 设置代理 # 注意:如果目标网站是 https,这里也要用 https 代理,或者让中间件自动处理 request.meta['proxy'] = self.proxy # 如果代理需要认证,加上这个 # proxy_user_pass = "user:password" # import base64 # encoded_user_pass = base64.b64encode(proxy_user_pass.encode('utf-8')).decode('utf-8') # request.headers['Proxy-Authorization'] = 'Basic ' + encoded_user_pass return None def process_response(self, request, response, spider): # 如果返回的状态码不对,比如403,可以在这里重试或者换代理 if response.status in [403, 429, 500]: spider.logger.warning(f"遇到异常状态码 {response.status},可能需要换代理或重试") # 这里可以返回 request 重新调度,或者 None 丢弃 return response

要在 Scrapy 里用这个中间件,得去 settings.py 里开启它:

DOWNLOADER_MIDDLEWARES = { 'your_project_name.middlewares.CustomProxyMiddleware': 543, # 数字越小越先执行 }

🔧 实战技巧:在 Scrapy 中间件里,千万别写阻塞性的代码(比如 time.sleep 或者同步的 requests.get)。Scrapy 是异步的,你一阻塞,整个引擎就卡住了。要是有耗时操作,要么用 defer,要么扔到线程池里跑。

5. 反爬对抗与合规策略:代理池、验证码识别及Robots协议

现在的网站反爬手段那是越来越精了,光靠改个 User-Agent 就想蒙混过关?基本不可能。咱们做爬虫的,得学会跟反爬工程师“斗智斗勇”,但前提是得守住合规这条底线。

代理池与反爬对抗

换个角度看,网站封你IP是最基础的操作。一旦你请求频率稍微高一点,或者触发了风控,你的IP就进黑名单了。这时候,代理池就是救命稻草。

现在的趋势是,单纯的透明代理已经不好使了,很多网站能检测出来。咱们得用那种高匿代理,甚至还得考虑IP的纯净度。结合 2024-2026年的技术趋势,智能化反反爬正在兴起,咱们可以写个简单的逻辑来维护代理池的可用性。

这里是一个简单的代理池测试与使用的逻辑(配合 httpx 库,这也是现在很火的异步HTTP库):

import httpx import asyncio # 假设这是你的代理列表,实际中应该从Redis或数据库拿 proxy_list = [ "http://proxy1.com:8080", "http://proxy2.com:8080", "http://proxy3.com:8080" ] async def check_proxy(proxy): """检查代理是否还活着""" try: # 用 httpx 的 AsyncClient 测试 # 注意:httpx 支持 HTTP/2,现在很多现代网站都上 HTTP/2 了,这点比 requests 强 async with httpx.AsyncClient(proxies=proxy, timeout=5) as client: # 访问一个检测IP的网站 resp = await client.get("http://httpbin.org/ip") if resp.status_code == 200: print(f"代理 {proxy} 可用,IP: {resp.json().get('origin')}") return proxy except Exception as e: print(f"代理 {proxy} 挂了: {e}") return None async def get_valid_proxy(): """获取一个可用的代理""" tasks = [check_proxy(p) for p in proxy_list] results = await asyncio.gather(*tasks) # 过滤掉 None valid_proxies = [p for p in results if p] if valid_proxies: return random.choice(valid_proxies) else: raise Exception("代理池空了,快去补充!") # 实际抓取时这么用 async def fetch_with_proxy(url): proxy = await get_valid_proxy() try: async with httpx.AsyncClient(proxies=proxy, timeout=10, verify=False) as client: headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'} res = await client.get(url, headers=headers) return res.text except Exception as e: print(f"用代理 {proxy} 抓取出错: {e}") # 这里可以把失效的代理从池子里删掉 return None

📌 要点提醒:别去用那些免费的公开代理,十个有九个不能用,还有一个慢得要死。如果是正经项目,建议买付费的住宅代理(Residential Proxy),虽然贵点,但稳定性高,不容易被封。

验证码识别与动态渲染

遇到验证码就头大?现在的验证码早就不是简单的4位数字了,什么滑块、点选、甚至还有逻辑题。

根据社区热门讨论,现在大家都在聊用 GPT-4 Vision 或者专门的 OCR 模型来识别复杂验证码。但对于新手,咱们先搞定基础的。如果是简单的图文验证码,可以用 ddddocr 这个库,它是基于深度学习的,识别率还行,而且不用自己训练模型。

import ddddocr import requests # 初始化 ocr,第一次运行会下载模型,稍微有点慢 ocr = ddddocr.DdddOcr() # 假设咱们抓到了验证码图片的链接 captcha_url = "http://example.com/captcha.jpg" img_bytes = requests.get(captcha_url).content # 识别 result = ocr.classification(img_bytes) print(f"识别出来的验证码是: {result}")

要是遇到那种 JavaScript 重度渲染 的网页(比如 SPA 单页应用),requests 抓回来可能就是一堆空架子,数据都在 JS 里动态加载。这时候,你就需要 Playwright 了。参考最新的技术趋势,Playwright 正在逐步取代 Selenium,因为它更快,反检测能力更强,而且支持无头模式(Headless)。

from playwright.sync_api import sync_playwright with sync_playwright() as p: # 启动 Chromium 浏览器,headless=False 可以看到浏览器界面,方便调试 browser = p.chromium.launch(headless=True) page = browser.new_page() # 访问页面 page.goto("https://example.com/login") # 等待某个元素加载出来(这比 time.sleep 靠谱多了) page.wait_for_selector("#content") # 获取渲染后的内容 content = page.content() print("拿到渲染后的页面内容了!") browser.close()

合规策略:Robots 协议与法律边界

咱们搞技术的,不能光顾着抓数据,还得懂法。现在数据法规越来越严,爬数据搞不好就变成“不正当竞争”了。

💡 经验总结:如果你不确定某个数据能不能爬,或者这个网站反爬特别凶(比如有法律风险警告),千万别硬刚。换个数据源,或者看看有没有官方 API。咱们写代码是为了解决问题,不是为了进局子喝茶。