1. 项目概述:Python全站名言数据采集实战
这个项目源于我最近为一家文化类创业公司做的数据采集需求。他们需要建立一个包含古今中外名人名言的数据仓库,用于内容推荐系统的训练。市面上虽然有现成的名言数据集,但要么分类体系不符合需求,要么数据质量参差不齐。于是我们决定自己动手,用Python构建一个高效、稳定的全站名言采集系统。
全站采集与普通单页采集最大的区别在于需要处理复杂的网站结构和反爬机制。以名言网站为例,典型的结构包含:按作者索引页、按分类索引页、名言详情页,有些还有用户评论和评分系统。我们的爬虫需要像一位耐心的图书管理员,系统地遍历每个书架(索引页),记录每本书的位置(详情页URL),最后摘抄其中的精华内容(名言文本)。
提示:在开始前请确保已了解目标网站的robots.txt协议,商业用途的数据采集务必获得官方授权。本文以技术学习为目的,采集频率控制在合理范围。
2. 技术选型与工具准备
2.1 核心工具链选择
经过对比测试,我们最终确定的工具组合是:
-
请求库
:Requests + Retrying(而非Scrapy框架),适合中等规模的定向采集
-
解析库
:PyQuery + lxml,比BeautifulSoup更快的XML解析性能
-
并发控制
:ThreadPoolExecutor配合Semaphore,精确控制并发数
-
存储方案
:MySQL关系型存储(适合结构化名言数据)+ 本地JSON备份
# 基础依赖安装
pip install requests retrying pyquery lxml mysql-connector-python
2.2 反爬应对策略
名言类网站常见的反爬手段和应对方案:
| UA检测 | 返回403状态码 | 轮换User-Agent池 |
| IP限制 | 突然大量503错误 | 1. 使用代理IP 2. 降低请求频率 |
| 行为验证 | 弹出验证码 | 1. 控制点击间隔 2. 使用selenium备用方案 |
| 参数加密 | 动态生成token | 分析前端JS生成逻辑 |
我们在项目中创建了智能请求间隔控制器,会根据响应时间动态调整采集速度:
class SmartDelay:
def __init__(self, base_delay=1.0):
self.last_response_time = None
self.base_delay = base_delay
def wait(self, response_time):
if self.last_response_time:
delta = response_time – self.last_response_time
self.base_delay += delta * 0.5 # 响应变慢就增加延迟
self.last_response_time = response_time
time.sleep(max(0.5, self.base_delay)) # 不低于0.5秒
3. 核心采集逻辑实现
3.1 网站结构分析
以某知名名言网站为例,其数据层级为:
我们采用广度优先(BFS)的采集策略:
def crawl_site(start_url):
visited = set()
queue = deque([start_url])
while queue:
current_url = queue.popleft()
if current_url in visited:
continue
html = download_page(current_url)
page_type = classify_page(html)
if page_type == "category":
new_urls = extract_category_links(html)
queue.extend(new_urls)
elif page_type == "author":
new_urls = extract_author_links(html)
queue.extend(new_urls)
elif page_type == "quote":
save_quote(extract_quote_data(html))
visited.add(current_url)
3.2 数据提取技巧
名言页面通常包含以下字段:
- 名言正文
- 作者信息
- 分类标签
- 发表日期(如有)
- 点赞/收藏数(如有)
使用PyQuery提取的示例:
def extract_quote_data(html):
doc = pq(html)
return {
"content": doc('.quote-text').text().strip(),
"author": {
"name": doc('.author-name').text(),
"birth": doc('.author-birth').attr('data-year'),
},
"tags": [pq(tag).text() for tag in doc('.quote-tags a')],
"meta": {
"likes": int(doc('.like-count').text()),
"date": parse_date(doc('.publish-date').text())
}
}
注意:实际项目中应该添加字段有效性验证,比如检查content是否为空,date格式是否合法等
4. 数据存储与清洗
4.1 数据库设计
我们使用MySQL存储结构化数据,主要表结构如下:
CREATE TABLE authors (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
birth_year SMALLINT,
death_year SMALLINT,
country VARCHAR(50),
INDEX (name)
);
CREATE TABLE quotes (
id INT AUTO_INCREMENT PRIMARY KEY,
content TEXT NOT NULL,
author_id INT NOT NULL,
origin_url VARCHAR(255),
publish_date DATE,
FOREIGN KEY (author_id) REFERENCES authors(id),
FULLTEXT INDEX (content)
);
CREATE TABLE tags (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE quote_tags (
quote_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (quote_id, tag_id),
FOREIGN KEY (quote_id) REFERENCES quotes(id),
FOREIGN KEY (tag_id) REFERENCES tags(id)
);
4.2 数据清洗策略
原始采集数据常见问题及处理方法:
编码问题
:使用
ftfy
库修复mojibake字符
import ftfy
fixed_text = ftfy.fix_text(raw_text)
重复名言检测
:计算文本指纹进行去重
def generate_fingerprint(text):
text = re.sub(r'\\s+', '', text.lower())
return hashlib.md5(text.encode()).hexdigest()
作者信息补全
:对于只有名字没有详细信息的作者,调用维基百科API补全
5. 高级技巧与优化方案
5.1 增量采集实现
为避免重复采集整个网站,我们设计了基于时间戳的增量采集方案:
Last-Modified
HTTP头
def need_update(url, last_crawl_time):
headers = {'If-Modified-Since': last_crawl_time}
resp = requests.head(url, headers=headers)
if resp.status_code == 304:
return False
# 对动态内容计算关键区域hash
current_hash = calc_content_hash(url)
return current_hash != get_stored_hash(url)
5.2 断点续采机制
对于大规模采集,实现断点恢复功能至关重要:
atexit
注册退出时的保存钩子
import atexit
import pickle
class CrawlState:
def __init__(self):
self.queue = deque()
self.visited = set()
atexit.register(self.save_state)
def save_state(self):
with open('crawl_state.pkl', 'wb') as f:
pickle.dump({'queue': list(self.queue), 'visited': list(self.visited)}, f)
def load_state(self):
try:
with open('crawl_state.pkl', 'rb') as f:
data = pickle.load(f)
self.queue = deque(data['queue'])
self.visited = set(data['visited'])
except FileNotFoundError:
pass
6. 常见问题与解决方案
6.1 反爬突破实战记录
案例:某网站使用动态token验证
解决步骤:
X-Token
头
import execjs
with open('token_generator.js') as f:
js_code = f.read()
ctx = execjs.compile(js_code)
token = ctx.call('generateToken', '/api/quotes')
headers = {'X-Token': token}
6.2 性能优化指标
优化前后的性能对比(采集10万条名言):
| 总耗时 | 6小时 | 2.5小时 |
| 内存占用 | 1.2GB | 350MB |
| 失败率 | 15% | 3% |
| 数据完整度 | 92% | 99.7% |
关键优化点:
7. 项目扩展方向
7.1 数据质量监控
建立自动化质量检查流程:
def quality_check(quote):
checks = {
'length_check': len(quote['content']) >= 10,
'author_check': bool(quote['author']),
'duplicate_check': not is_duplicate(quote['content']),
'sentiment_check': analyze_sentiment(quote['content'])['polarity'] > -0.5
}
return checks
7.2 自动化更新管道
使用Airflow构建数据管道:
from airflow import DAG
from airflow.operators.python import PythonOperator
dag = DAG('quote_pipeline', schedule_interval='@weekly')
def crawl_task():
# 采集逻辑
def clean_task():
# 清洗逻辑
def analyze_task():
# 分析逻辑
crawl = PythonOperator(task_id='crawl', python_callable=crawl_task, dag=dag)
clean = PythonOperator(task_id='clean', python_callable=clean_task, dag=dag)
analyze = PythonOperator(task_id='analyze', python_callable=analyze_task, dag=dag)
crawl >> clean >> analyze
在实际部署中发现,使用Docker容器化爬虫可以更好地管理依赖和环境。我们为项目创建了包含所有依赖的Docker镜像,并通过环境变量控制运行参数:
FROM python:3.9
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "main.py", "–mode", "${RUN_MODE}"]