聊聊我用爬虫搞数据的一些实战经验
说来也巧,写iOS写了快六年,从Swift刚出来那会儿就开始折腾,眼看着Objective-C一步步被"宣判死刑",自己也跟着从OC转Swift,再到后来搞SwiftUI,算是见证了苹果生态这几年的大变迁。坐标杭州,在滨江这边待过两家不大不小的公司,最近网易和阿里都有在聊,想着趁这段空窗期把最近学的一些东西整理整理。
其实吧,做iOS开发久了,你会发现很多技能是可以迁移的。比如最近我在搞AI相关的东西,搞着搞着就发现——数据从哪来?总不可能全靠手写吧?于是乎,爬虫这个技能点就被我点亮了。今天就来聊聊这段时间搞爬虫的一些实战心得,算是给自己做个复盘。
为什么一个iOS开发要去搞爬虫
事情是这样的。上个月我在研究一个AI小项目,想做一个基于真实数据的推荐系统demo。理想很丰满,现实很骨感——公开的数据集要么太老,要么根本不符合我的场景需求。
找数据找得我头都大了,跟大学时候写毕业论文那会儿有得一拼。后来一咬牙一跺脚:算了,自己爬吧。反正Python之前学过一点,又不是完全零基础。
现在想想,这个决定还挺对的。搞爬虫的过程让我对网络请求、数据解析、反爬策略这些底层逻辑有了更深的理解,回过头来看iOS里的网络层设计,反而有种"哦原来如此"的感觉。
第一次实战:从简单页面开始
我的第一个目标是爬某个技术社区的帖子数据。说实话,一开始真的想得太简单了。
import requests
from bs4 import BeautifulSoup
url = "https://example-tech-community.com/posts"
response = requests.get(url)
soup = BeautifulSoup(response.text, 'html.parser')
posts = soup.find_all('div', class_='post-item')
for post in posts:
title = post.find('h2').text
print(title)
跑了一下,嗯,数据是拿到了。但当我想爬第二页、第三页的时候,问题来了——这个网站有反爬机制。
连续请求了几次之后,直接给我返回了403,IP被封了。当时那个心情,真的,就像线上出了P0事故一样,心凉了半截。
反爬对抗:一场没有硝烟的战争
被封IP之后,我开始了漫长的反爬研究。说实话,这段经历让我对"攻防"这件事有了全新的认识。
请求头伪装
最基础的一步,就是伪装请求头。很多网站的反爬其实很初级,就是看看你的User-Agent是不是正常的浏览器。
import random
USER_AGENTS = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
]
headers = {
"User-Agent": random.choice(USER_AGENTS),
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
"Referer": "https://www.google.com/",
}
这招对付一些初级反爬还是管用的。但稍微有点经验的网站,光靠这个远远不够。
IP代理池
IP被封之后,最直接的解决方案就是换IP。我搞了一个代理池,定时从免费的代理网站抓取可用代理,然后请求的时候随机切换。
import requests
import time
class ProxyPool:
def __init__(self):
self.proxies = []
self.current_index = 0
def add_proxy(self, proxy):
self.proxies.append(proxy)
def get_proxy(self):
if not self.proxies:
return None
proxy = self.proxies[self.current_index]
self.current_index = (self.current_index + 1) % len(self.proxies)
return {"http": proxy, "https": proxy}
proxy_pool = ProxyPool()
# 假设这里添加了一些代理
proxy_pool.add_proxy("http://123.45.67.89:8080")
proxy_pool.add_proxy("http://98.76.54.32:3128")
def fetch_with_proxy(url):
proxy = proxy_pool.get_proxy()
try:
response = requests.get(url, headers=headers, proxies=proxy, timeout=10)
return response
except Exception as e:
print(f"请求失败: {e}")
return None
但免费代理的质量你懂的,十个里面有九个是废的。后来我咬咬牙买了个付费代理服务,效果好了很多。这也算是花钱买教训吧。
请求频率控制
这个是我踩坑最深的一点。一开始我为了追求速度,并发开得贼猛,结果就是——被封得更惨了。
后来我学乖了,加了随机延时:
import time
import random
def polite_fetch(url):
# 随机延时1-3秒
time.sleep(random.uniform(1, 3))
response = requests.get(url, headers=headers)
return response
别小看这几秒钟的延时,它能让你的爬虫活得久很多。做爬虫跟做人一样,不能太激进,要懂得"可持续发展"。
动态页面的噩梦
如果说静态页面爬取是新手村,那动态页面就是第一个Boss。
现在很多网站都是前后端分离的,页面内容靠JavaScript动态渲染。你拿到的HTML源码里,可能只有一个空的<div id="app"></div>,数据全在JS里。
一开始我试图用正则去匹配JS里的数据,搞了两天,头发掉了一把,效果奇差。后来朋友推荐我试试Selenium。
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
def fetch_dynamic_page(url):
options = webdriver.ChromeOptions()
options.add_argument('--headless') # 无头模式
options.add_argument('--disable-gpu')
driver = webdriver.Chrome(options=options)
try:
driver.get(url)
# 等待内容加载完成
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CLASS_NAME, "content-loaded"))
)
page_source = driver.page_source
return page_source
finally:
driver.quit()
Selenium确实能解决动态渲染的问题,但代价也很明显——慢。一个页面要等好几秒,如果要爬几千个页面,时间成本根本扛不住。
后来我又研究了一下,发现很多动态页面的数据其实是通过API接口获取的。打开浏览器的开发者工具,看看Network面板,找到那个返回JSON数据的接口,直接请求这个接口,比用Selenium渲染整个页面快得多。
这个思路的转变,让我想起了做iOS时处理网络请求的经验。很多时候,我们不需要关心UI怎么渲染,只需要关心数据从哪来、格式是什么。爬虫也是一样的道理。
数据清洗:脏活累活
爬到数据只是第一步,数据清洗才是真正耗时的地方。
爬下来的数据,格式五花八门:有的带HTML标签,有的有奇怪的换行符,有的字段缺失,有的数据重复……简直就像一个没有经过Code Review的代码库,惨不忍睹。
import re
import pandas as pd
def clean_data(raw_data):
cleaned = []
for item in raw_data:
# 去除HTML标签
text = re.sub(r'<[^>]+>', '', item.get('content', ''))
# 去除多余空白
text = re.sub(r'\s+', ' ', text).strip()
# 过滤太短的内容
if len(text) > 50:
cleaned.append({
'title': item.get('title', '').strip(),
'content': text,
'author': item.get('author', 'unknown'),
'date': item.get('date', '')
})
return cleaned
# 去重
df = pd.DataFrame(cleaned_data)
df.drop_duplicates(subset=['title', 'content'], inplace=True)
说实话,数据清洗这块没什么技术含量,就是个体力活。但千万别小看它,数据质量直接决定了你后续分析或模型训练的效果。垃圾进,垃圾出,这话一点不假。
一些架构上的思考
搞了一段时间爬虫之后,我也在思考怎么把零散的脚本整合成一个相对完整的系统。毕竟,每次都要手动跑脚本、手动清洗数据,效率太低了。
我参考了一些开源项目的架构,设计了一个简单的爬虫框架:
| 模块 | 职责 | 技术选型 |
|---|---|---|
| 调度器 | 管理爬取任务、控制并发 | Celery + Redis |
| 下载器 | 负责HTTP请求、处理代理 | aiohttp |
| 解析器 | 解析HTML/JSON、提取数据 | BeautifulSoup / jsonpath |
| 存储器 | 数据持久化 | MongoDB |
| 监控 | 任务状态、异常告警 | Prometheus + Grafana |
用异步的方式来做下载器,效率提升非常明显。之前用requests同步爬,一分钟最多处理几十个页面;换成aiohttp异步之后,一分钟能处理几百个。
import aiohttp
import asyncio
async def fetch_async(url, semaphore):
async with semaphore:
async with aiohttp.ClientSession() as session:
try:
async with session.get(url, headers=headers, timeout=10) as response:
return await response.text()
except Exception as e:
print(f"异步请求失败: {e}")
return None
async def main(urls):
semaphore = asyncio.Semaphore(20) # 控制并发数
tasks = [fetch_async(url, semaphore) for url in urls]
results = await asyncio.gather(*tasks)
return results
# 运行
urls = ["https://example.com/page/1", "https://example.com/page/2", ...]
results = asyncio.run(main(urls))
这个架构跑起来之后,整体稳定性好了很多。之前脚本跑着跑着就挂了,现在有了任务队列和重试机制,即使某个请求失败也不会影响整体进度。
合规性:别忘了这条红线
最后想聊一个很多人容易忽略的问题——合规性。
爬虫这个东西,用好了是利器,用不好就是违法。之前不是有个案例嘛,某公司爬虫把人家服务器搞崩了,最后负责人进去了。
我给自己定了几条规矩:
- 遵守robots.txt:人家不让爬的,就别爬
- 控制请求频率:别把人家服务器搞崩了
- 不爬敏感数据:用户隐私、商业机密这些碰都别碰
- 数据用途合法:爬来的数据别拿去干违法的事
做技术的人,有时候容易沉浸在"我能做到"的快感里,而忽略了"我该不该做"的问题。这条红线,一定要守住。
写在最后
从iOS开发到搞爬虫,这个跨度看起来有点大,但其实底层的东西是相通的。网络请求、数据解析、并发控制、异常处理……这些概念在任何语言、任何领域都是通用的。
最近面试的时候,有面试官问我为什么一个iOS开发要学爬虫,我说:"技多不压身嘛,而且现在AI这么火,数据是AI的燃料,不懂数据怎么行?"
其实还有一个没说出口的原因——杭州这边的就业环境,纯iOS开发的岗位确实在缩减。不给自己多找几条路,万一哪天被优化了,连个退路都没有。
好了,今天就聊到这里。如果你也在搞爬虫,或者对AI相关的数据处理感兴趣,欢迎来交流。毕竟,一个人踩坑不如大家一起踩坑,对吧?
下次有机会再聊聊我是怎么用爬来的数据训练一个小模型的,那个故事就更精彩了(也更心酸)。


评论 0