从0到1写个爬虫:Python实战踩坑全记录

第一次写爬虫的时候,我以为这事儿特简单。不就是发个请求,拿回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秒,换成lxmletree只需要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注入。虽然爬虫一般不用担心这个,但养成好习惯总没错。我一开始没注意,后来被人吐槽了。

说几个能立刻用的点

  • 加User-Agent和随机延迟:任何爬虫都要伪装成浏览器,请求间隔至少1秒。我用time.sleep(uniform(1, 3))做随机延迟,效果不错。不推荐固定延迟,太容易被识别。
  • 优先用lxml解析:BeautifulSoup默认解析器太慢,换lxml能提速10倍以上。如果遇到复杂HTML,考虑用XPath。
  • 异常处理是保命符:写try-except包裹所有网络请求,加上重试机制。我见过有人跑了一天的爬虫,因为一次网络波动全废了,数据写了一半,那感觉糟透了。
  • 对了,最后提醒一句:爬虫有底线。有次我抓一个论坛,并发开太大,直接把人家服务器搞挂了。后来被管理员发邮件警告,那之后我就老实了。别抓用户隐私,别给服务器造成压力,遵守robots.txt`规则。这不光是法律问题,也是技术人的基本素养。

    滚动至顶部