第一次写爬虫的时候,我以为这事儿特简单。不就是发个请求,拿回HTML,然后一解析嘛。结果呢?第一个小项目,直接给我来了个三连击:网站返回空页面、IP被封、数据乱码。折腾了一整天,差点想砸电脑。
最近翻到以前的代码,发现踩过的坑还挺有意思。分享出来给大家乐呵乐呵。不是那种教科书式的教程,就是我踩过的,能帮你省下几天调试时间的那种。
先说环境
Python你肯定得装一个。我用的是3.11,目前没发现什么兼容性问题。装完Python,接着装这几个库
:
“bash`
pip install requests beautifulsoup4 lxml
当时图省事,没装lxml。用的BeautifulSoup默认的那个html.parser,结果解析一个电商详情页,直接给我报了一堆错误,跟天书一样。后来网上搜了一圈,说换lxml就好了。果然稳了。真的好用,但当时为啥报错,我到现在也没完全搞明白,反正能用就行。
lxml确实快,但它的XPath语法我到现在还记不全,每次都得查文档。不推荐那种硬背的方式,太累。
第一个爬虫,拿豆瓣练手
先拿豆瓣练练手吧,这网站比较友好。抓电影Top250的标题,适合新手。
`python
import requests
from bs4 import Beautifu
lSoup
url = "https://movie.douban.com/top250"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
}
response = requests.get(url, headers=headers)
response.encoding = "utf-8"
soup = BeautifulSoup(response.text, "lxml")
for item in soup.select(".hd"):
title = item.select_one(".title").text.strip()
print(f"电影:{title}")
`
代码跑起来,你可能会发现:打印出来的全是乱码。这就是我踩的第一个坑。页面明明声明是UTF-8,但requests有时候就是会自作聪明,给你用别的编码。一开始搞错了,后来发现,强制设置response.encoding = “utf-8”就解决了。或者用response.apparent_encoding让它自己猜,但偶尔也会猜错,挺烦人的。
当时抓豆瓣,有个电影标题里带了个特殊符号,直接报错了。我排查了半天才发现是编码问题。那种感觉,真是想很沮丧。
动态加载的页面,用Selenium
现在好多网站都是动态加载的,比如知乎的评论区。你用requests去抓HTML,拿到手的是个空壳子,啥数据也没有。这时候就只能上Selenium了。
这玩意儿确实好用,能模拟真人浏览器,执行JavaScript。但代价就是慢,而且容易被网站检测出来。
`python
from selenium import webdriver
from selenium.webdriver.common.by import By
import time
driver = webdriver.Chrome()
driver.get("https://www.zhihu.com/question/123456789")
time.sleep(3)
answers = driver.find_elements(By.CLASS_NAME, "RichText")
for answer in answers:
print(answer.text[:100])
`
这个设计真的挺反人类的。每次运行都要打开一个浏览器窗口,速度慢得像乌龟爬。有个取巧的办法是用无头模式,就是不显示界面。
`python`
options = webdriver.ChromeOptions()
options.add_argument("--headless")
driver = webdriver.Chrome(options=options)
无头模式快很多,但有些网站能检测到你用的是Selenium,直接给你返回一个验证码。这时候还得再加点参数伪装一下。
`python`
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
官方文档关于这段文档不够清晰,我查了好几个论坛才搞明白,真是头大。不推荐直接照搬,建议自己试几轮。
异常处理,保命用的
爬虫最怕什么?不是网站封你,而是突然网络断了,或者网站结构变了。如果不加异常处理,脚本直接崩溃,前面抓的数据全白费了。不加异常处理的爬虫,就像没带伞出门,随时可能被淋成落汤鸡。
`python
import time
from random import uniform
def safe_request(url, retries=3):
for i in range(retries):
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
return response
except requests.exceptions.Timeout:
print(f"请求超时,第{i+1}次重试")
time.sleep(uniform(1, 3))
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
print("被限速了,等10秒再试")
time.sleep(10)
else:
print(f"HTTP错误:{e}")
return None
return None
`
这里有个小细节:uniform(1, 3)是产生1到3秒的随机数。为啥要随机?因为如果你每次等待时间都一样,网站很容易就认出你是机器。我试过固定3秒,结果IP分分钟被封。加了随机延迟,就像人类操作了。
解析速度,从4.8秒到0.3秒
解析数据时,正则表达式最快,但写起来太费劲。BeautifulSoup很友好,但慢。如果你要抓几千页,那性能差距就出来了。
我专门测试过:用BeautifulSoup解析1000条数据需要4.8秒,换成lxml的etree只需要0.3秒。真心推荐这个组合,省时间。
`python
from lxml import etree
html = etree.HTML(response.text)
titles = html.xpath('//div[@class="hd"]/a/span[@class="title"]/text()')
for title in titles:
print(title)
`
XPath的写法看起来有点吓人,但常用的就那么几个://表示任意层级,[@class=”xxx”]是属性筛选,text()是取文本。大部分时候,你从浏览器开发者工具里直接复制XPath就行,省不少力气。
数据存储,别用CSV了
刚开始我习惯把数据存成CSV,但一遇到中文、特殊字符就各种乱码。后来改用SQLite数据库,既稳定又方便查询。CSV这玩意儿,我劝你趁早放弃,尤其是抓中文数据的时候,各种乱码能让你怀疑人生。
`python
import sqlite3
conn = sqlite3.connect("movies.db")
c = conn.cursor()
c.execute("""
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
rating REAL,
year INTEGER
)
""")
c.execute("INSERT INTO movies (title, rating) VALUES (?, ?)",
("肖申克的救赎", 9.7))
conn.commit()
conn.close()
`
为什么要用?占位符而不是直接拼接字符串?因为直接拼接容易引发SQL注入。虽然爬虫一般不用担心这个,但养成好习惯总没错。我一开始没注意,后来被人吐槽了。
说几个能立刻用的点
做随机延迟,效果不错。不推荐固定延迟,太容易被识别。能提速10倍以上。如果遇到复杂HTML,考虑用XPath。包裹所有网络请求,加上重试机制。我见过有人跑了一天的爬虫,因为一次网络波动全废了,数据写了一半,那感觉糟透了。对了,最后提醒一句:爬虫有底线。有次我抓一个论坛,并发开太大,直接把人家服务器搞挂了。后来被管理员发邮件警告,那之后我就老实了。别抓用户隐私,别给服务器造成压力,遵守robots.txt`规则。这不光是法律问题,也是技术人的基本素养。