㊙️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!
㊗️爬虫难度指数:⭐⭐
🚫声明:本数据&代码仅供学习交流,严禁用于商业用途、倒卖数据或违反目标站点的服务条款等,一切后果皆由使用者本人承担。公开榜单数据一般允许访问,但请务必遵守"君子协议",技术无罪,责任在人。

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [1️⃣ 摘要(Abstract)](#1️⃣ 摘要(Abstract))
- [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
- [3️⃣ 合规与注意事项(必读)](#3️⃣ 合规与注意事项(必读))
- [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
- [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
- [6️⃣ 核心实现:请求层(Fetcher)](#6️⃣ 核心实现:请求层(Fetcher))
- [7️⃣ 核心实现:解析层(Parser)](#7️⃣ 核心实现:解析层(Parser))
- [8️⃣ 数据存储与导出(Storage)](#8️⃣ 数据存储与导出(Storage))
- [9️⃣ 运行方式与结果展示(必写)](#9️⃣ 运行方式与结果展示(必写))
- [🔟 常见问题与排错(强烈建议收藏)](#🔟 常见问题与排错(强烈建议收藏))
-
- [问题1:抓到403 Forbidden](#问题1:抓到403 Forbidden)
- [问题2:抓到429 Too Many Requests](#问题2:抓到429 Too Many Requests)
- 问题3:HTML抓到了但解析为空
- 问题4:编码乱码问题
- 问题5:XPath提取结果为空列表
- 问题6:数据库插入失败
- [1️⃣1️⃣ 进阶优化(锦上添花)](#1️⃣1️⃣ 进阶优化(锦上添花))
- [1️⃣2️⃣ 总结与延伸阅读](#1️⃣2️⃣ 总结与延伸阅读)
- [🌟 文末](#🌟 文末)
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏👉《Python爬虫实战》👈
💕订阅后更新会优先推送,按目录学习更高效💯~
1️⃣ 摘要(Abstract)
本文将带你从零构建一个招聘会参会企业信息采集系统 ,使用 requests + lxml + SQLite 技术栈,实现对招聘会官网企业列表的自动化抓取,最终产出包含企业名称、所属行业、招聘岗位数的结构化数据库。
读完本文你将获得:
- 掌握分页列表的自动化遍历与数据提取技巧
- 学会基于数据库的去重与增量更新策略
- 理解爬虫项目从环境搭建到生产部署的完整流程
- 积累一套可复用的爬虫代码框架和异常处理经验
2️⃣ 背景与需求(Why)
为什么要做这个项目?
在求职季或者人力资源分析场景中,我们经常需要:
- 数据分析需求:统计某地区招聘会的行业分布、热门行业、岗位供给趋势
- 信息聚合需求:将分散在多个招聘会的企业信息整合到一处,方便检索
- 自动化监控需求:定期抓取最新招聘会信息,发现新入驻企业或岗位变化
手动复制粘贴效率低且容易出错,而招聘会官网通常会分页展示几十甚至上百家企业,人工处理几乎不可行。因此,我们需要一个自动化的采集工具。
目标站点与字段清单
本文以某地级市人才市场官网的招聘会企业列表为例(该站点结构清晰、无强反爬,适合学习)。
目标字段清单:
| 字段名 | 说明 | 数据类型 | 示例值 |
|---|---|---|---|
| company_name | 企业名称 | VARCHAR(200) | "XX科技有限公司" |
| industry | 所属行业 | VARCHAR(100) | "互联网/电子商务" |
| job_count | 招聘岗位数 | INT | 15 |
| job_fair_id | 招聘会ID | VARCHAR(50) | "2025_spring_01" |
| url | 企业详情页URL | VARCHAR(500) | "https://..." |
| crawl_time | 抓取时间 | DATETIME | "2025-01-29 14:30:00" |
3️⃣ 合规与注意事项(必读)
robots.txt 协议
在开始爬取前,务必检查目标站点的 robots.txt 文件(通常位于 https://example.com/robots.txt),确认是否允许爬虫访问目标页面。大部分人才市场官网的公开招聘会信息页面是允许抓取的,但仍需尊重网站规则。
频率控制与道德约束
- 请求间隔:每次请求后至少等待 1-3 秒,避免对服务器造成压力
- 并发限制:本文采用单线程顺序抓取,如需并发请控制在 3-5 个线程以内
- User-Agent:使用真实浏览器 UA,不伪造身份但也不暴露爬虫特征
数据使用边界
- ✅ 允许:用于个人学习、数据分析、非商业研究
- ❌ 禁止:采集用户隐私信息、绕过付费内容、商业转售数据
- ⚠️ 谨慎:如需商业用途,请联系网站管理员获取授权
4️⃣ 技术选型与整体流程(What/How)
技术选型分析
目标站点特征判断:
经过初步分析,招聘会企业列表页面属于服务端渲染的静态HTML,数据直接嵌入在页面源码中,无需执行JavaScript即可获取。
三种方案对比:
| 方案 | 适用场景 | 本项目是否适用 |
|---|---|---|
| requests + lxml | 静态HTML,数据在源码中 | ✅ 最优选择 |
| Selenium/Playwright | 动态渲染,需要执行JS | ❌ 杀鸡用牛刀 |
| API逆向 | 数据通过接口异步加载 | ❌ 本案例无此情况 |
最终选择:requests + lxml + SQLite
requests:轻量级HTTP库,发送请求、处理响应lxml:高性能HTML解析库,XPath选择器精准定位SQLite:轻量级关系数据库,天然支持去重与增量查询
整体流程设计
json
┌─────────────┐
│ 启动脚本 │
└──────┬──────┘
│
▼
┌─────────────────┐
│ 1. 获取总页数 │ ← 解析首页分页信息
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 2. 遍历每一页 │ ← for page in range(1, total_pages+1)
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 3. 发送HTTP请求 │ ← requests.get() + headers + timeout
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 4. 解析HTML │ ← lxml.etree + XPath提取字段
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 5. 数据清洗 │ ← 去空格、类型转换、缺失值填充
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 6. 去重判断 │ ← 查询数据库是否已存在
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 7. 写入数据库 │ ← INSERT OR IGNORE / UPDATE
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 8. 导出CSV │ ← sqlite3 → pandas → to_csv()
└─────────────────┘
5️⃣ 环境准备与依赖安装(可复现)
Python 版本要求
- 推荐版本:Python 3.8 - 3.11
- 最低版本:Python 3.7(lxml 部分特性需要)
依赖包安装
创建虚拟环境(可选但推荐):
json
python -m venv venv
source venv/bin/activate # Windows用户: venv\Scripts\activate
安装核心依赖:
json
pip install requests==2.31.0
pip install lxml==4.9.3
pip install pandas==2.0.3 # 用于导出CSV,可选
项目目录结构
json
job_fair_crawler/
│
├── crawler.py # 主爬虫逻辑
├── config.py # 配置文件(URL、headers等)
├── database.py # 数据库操作封装
├── utils.py # 工具函数(重试、日志等)
│
├── data/
│ ├── job_fair.db # SQLite数据库文件
│ └── export.csv # 导出的CSV文件
│
├── logs/
│ └── crawler.log # 运行日志
│
└── requirements.txt # 依赖清单
6️⃣ 核心实现:请求层(Fetcher)
请求头配置
请求头是绕过基础反爬的第一道防线。我们需要模拟真实浏览器行为:
python
# config.py
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': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Referer': 'https://www.example-talent-market.com/', # 根据实际情况修改
'Upgrade-Insecure-Requests': '1'
}
BASE_URL = 'https://www.example-talent-market.com/job-fair/companies'
TIMEOUT = 10 # 请求超时时间(秒)
RETRY_TIMES = 3 # 失败重试次数
RETRY_DELAY = 2 # 重试间隔(秒)
请求函数实现
python
# utils.py
import requests
import time
import logging
from config import HEADERS, TIMEOUT, RETRY_TIMES, RETRY_DELAY
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[
logging.FileHandler('logs/crawler.log', encoding='utf-8'),
logging.StreamHandler()
]
)
def fetch_page(url, params=None):
"""
发送HTTP GET请求,支持重试机制
Args:
url: 目标URL
params: URL参数字典
Returns:
response对象 或 None(失败时)
"""
for attempt in range(1, RETRY_TIMES + 1):
try:
response = requests.get(
url,
params=params,
headers=HEADERS,
timeout=TIMEOUT
)
# 检查HTTP状态码
if response.status_code == 200:
response.encoding = response.apparent_encoding # 自动检测编码
logging.info(f"✓ 成功获取: {url} (尝试 {attempt}/{RETRY_TIMES})")
return response
elif response.status_code == 429:
logging.warning(f"⚠ 触发频率限制(429),等待{RETRY_DELAY * 2}秒后重试...")
time.sleep(RETRY_DELAY * 2)
else:
logging.error(f"✗ HTTP {response.status_code}: {url}")
except requests.exceptions.Timeout:
logging.warning(f"⏱ 请求超时,第 {attempt} 次重试...")
except requests.exceptions.ConnectionError:
logging.warning(f"🔌 连接错误,第 {attempt} 次重试...")
except Exception as e:
logging.error(f"❌ 未知错误: {str(e)}")
if attempt < RETRY_TIMES:
time.sleep(RETRY_DELAY)
logging.error(f"✗ 达到最大重试次数,放弃: {url}")
return None
Session 复用(可选优化)
如果需要保持cookie或提升性能,可以使用 requests.Session():
python
class SessionFetcher:
def __init__(self):
self.session = requests.Session()
self.session.headers.update(HEADERS)
def get(self, url, params=None):
# 与上面的fetch_page逻辑相同,但用self.session.get()
pass
7️⃣ 核心实现:解析层(Parser)
解析策略选择
我们使用 XPath 作为主要解析方式,原因如下:
- 比BeautifulSoup性能高3-5倍
- 语法简洁,适合结构化数据提取
- 对动态变化的HTML结构容错性较好
列表页解析逻辑
假设招聘会企业列表HTML结构如下(简化示例):
html
<div class="company-list">
<div class="company-item">
<h3 class="company-name">杭州XX科技有限公司</h3>
<span class="industry">互联网/电子商务</span>
<span class="job-count">招聘<em>15</em>个岗位</span>
<a href="/company/12345">详情</a>
</div>
<!-- 更多企业... -->
</div>
<div class="pagination">
<a href="?page=1">1</a>
<a href="?page=2" class="current">2</a>
<a href="?page=3">3</a>
<!-- 共10页 -->
</div>
解析代码实现:
python
# crawler.py
from lxml import etree
import re
def parse_company_list(html_content):
"""
解析企业列表页,提取所有企业信息
Args:
html_content: HTML源码字符串
Returns:
企业信息列表 [{company_name, industry, job_count, url}, ...]
"""
tree = etree.HTML(html_content)
companies = []
# XPath定位所有企业卡片
company_items = tree.xpath('//div[@class="company-item"]')
for item in company_items:
try:
# 提取企业名称
name_list = item.xpath('.//h3[@class="company-name"]/text()')
company_name = name_list[0].strip() if name_list else None
# 提取行业
industry_list = item.xpath('.//span[@class="industry"]/text()')
industry = industry_list[0].strip() if industry_list else "未知行业"
# 提取岗位数(从"招聘15个岗位"中提取数字)
job_text_list = item.xpath('.//span[@class="job-count"]//text()')
job_text = ''.join(job_text_list)
job_count_match = re.search(r'\d+', job_text)
job_count = int(job_count_match.group()) if job_count_match else 0
# 提取详情链接
url_list = item.xpath('.//a/@href')
relative_url = url_list[0] if url_list else None
full_url = f"https://www.example-talent-market.com{relative_url}" if relative_url else None
# 必须有企业名称才算有效数据
if company_name:
companies.append({
'company_name': company_name,
'industry': industry,
'job_count': job_count,
'url': full_url
})
else:
logging.warning("⚠ 发现无效企业卡片,已跳过")
except Exception as e:
logging.error(f"❌ 解析单个企业时出错: {str(e)}")
continue
return companies
def get_total_pages(html_content):
"""
从首页提取总页数
Args:
html_content: HTML源码
Returns:
总页数(整数)
"""
tree = etree.HTML(html_content)
# 方法1:从分页链接中提取最大页码
page_links = tree.xpath('//div[@class="pagination"]//a/@href')
max_page = 1
for link in page_links:
match = re.search(r'page=(\d+)', link)
if match:
page_num = int(match.group(1))
max_page = max(max_page, page_num)
# 方法2:从"共X页"文本中提取
total_text = tree.xpath('//div[@class="pagination"]//text()')
for text in total_text:
match = re.search(r'共\s*(\d+)\s*页', text)
if match:
return int(match.group(1))
return max_page
字段容错处理
在实际爬取中,网站HTML结构可能不稳定,我们需要多层防御:
python
def safe_extract(xpath_result, default="", processor=None):
"""
安全提取XPath结果,避免IndexError
Args:
xpath_result: XPath查询结果列表
default: 默认值
processor: 后处理函数(如strip、int等)
"""
try:
value = xpath_result[0] if xpath_result else default
if processor:
return processor(value)
return value
except Exception:
return default
# 使用示例
company_name = safe_extract(
item.xpath('.//h3[@class="company-name"]/text()'),
default="未知企业",
processor=lambda x: x.strip()
)
8️⃣ 数据存储与导出(Storage)
数据库设计
使用SQLite创建企业信息表:
python
# database.py
import sqlite3
from datetime import datetime
import logging
class JobFairDB:
def __init__(self, db_path='data/job_fair.db'):
self.db_path = db_path
self.conn = None
self.cursor = None
self._create_table()
def connect(self):
"""建立数据库连接"""
self.conn = sqlite3.connect(self.db_path)
self.cursor = self.conn.cursor()
def close(self):
"""关闭数据库连接"""
if self.conn:
self.conn.commit()
self.conn.close()
def _create_table(self):
"""创建企业信息表"""
self.connect()
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS companies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
company_name VARCHAR(200) NOT NULL,
industry VARCHAR(100),
job_count INTEGER DEFAULT 0,
job_fair_id VARCHAR(50),
url VARCHAR(500) UNIQUE,
crawl_time DATETIME,
update_time DATETIME,
UNIQUE(company_name, job_fair_id)
)
''')
# 创建索引加速查询
self.cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_company_name
ON companies(company_name)
''')
self.cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_crawl_time
ON companies(crawl_time)
''')
self.conn.commit()
self.close()
logging.info("✓ 数据库表初始化完成")
def insert_or_update(self, company_data, job_fair_id='2025_spring'):
"""
插入或更新企业信息(去重逻辑)
Args:
company_data: 企业信息字聘会标识
Returns:
(is_new, company_id) 元组
"""
self.connect()
company_name = company_data['company_name']
industry = company_data.get('industry', '未知行业')
job_count = company_data.get('job_count', 0)
url = company_data.get('url')
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 检查是否已存在
self.cursor.execute('''
SELECT id, job_count FROM companies
WHERE company_name = ? AND job_fair_id = ?
''', (company_name, job_fair_id))
existing = self.cursor.fetchone()
if existing:
company_id, old_job_count = existing
# 如果岗位数有变化,更新记录
if job_count != old_job_count:
self.cursor.execute('''
UPDATE companies
SET job_count = ?, update_time = ?
WHERE id = ?
''', (job_count, now, company_id))
self.conn.commit()
logging.info(f"🔄 更新企业: {company_name} (岗位数 {old_job_count} → {job_count})")
self.close()
return (False, company_id) # 不是新记录
else:
# 插入新记录
self.cursor.execute('''
INSERT INTO companies
(company_name, industry, job_count, job_fair_id, url, crawl_time, update_time)
VALUES (?, ?, ?, ?, ?, ?, ?)
''', (company_name, industry, job_count, job_fair_id, url, now, now))
company_id = self.cursor.lastrowid
self.conn.commit()
self.close()
logging.info(f"✨ 新增企业: {company_name} (岗位数: {job_count})")
return (True, company_id) # 是新记录
def get_all_companies(self, job_fair_id=None):
"""
查询所有企业信息
Args:
job_fair_id: 可选,筛选特定招聘会
Returns:
企业列表
"""
self.connect()
if job_fair_id:
self.cursor.execute('''
SELECT * FROM companies WHERE job_fair_id = ?
ORDER BY crawl_time DESC
''', (job_fair_id,))
else:
self.cursor.execute('SELECT * FROM companies ORDER BY crawl_time DESC')
results = self.cursor.fetchall()
self.close()
return results
def export_to_csv(self, output_path='data/export.csv'):
"""导出数据到CSV文件"""
try:
import pandas as pd
self.connect()
df = pd.read_sql_query('SELECT * FROM companies', self.conn)
self.close()
df.to_csv(output_path, index=False, encoding='utf-8-sig')
logging.info(f"📊 数据已导出到: {output_path}")
return True
except ImportError:
logging.warning("⚠ pandas未安装,无法导出CSV")
return False
去重策略说明
本项目采用双重去重机制:
- 数据库层面 :通过
UNIQUE(company_name, job_fair_id)约束确保同一招聘会中企业名称唯一 - 应用层面 :在
insert_or_update()中先查询后决定插入或更新
增量更新逻辑:
- 如果企业已存在但岗位数变化 → 更新
job_count和update_time - 如果企业不存在 → 插入新记录
- 如果企业和岗位数都未变 → 跳过(避免重复写入)
9️⃣ 运行方式与结果展示(必写)
主程序入口
python
# crawler.py (完整版)
import time
from utils import fetch_page
from database import JobFairDB
import logging
def main():
"""主爬虫函数"""
logging.info("=" * 50)
logging.info("🚀 招聘会企业爬虫启动")
logging.info("=" * 50)
# 初始化数据库
db = JobFairDB()
# 设置招聘会ID(可从命令行参数获取)
job_fair_id = '2025_spring'
# 1. 获取首页,确定总页数
base_url = 'https://www.example-talent-market.com/job-fair/companies'
first_page = fetch_page(base_url, params={'page': 1})
if not first_page:
logging.error("❌ 无法获取首页,爬虫终止")
return
total_pages = get_total_pages(first_page.text)
logging.info(f"📄 共检测到 {total_pages} 页数据")
# 统计变量
new_count = 0
update_count = 0
# 2. 遍历所有页面
for page_num in range(1, total_pages + 1):
logging.info(f"\n▶ 正在抓取第 {page_num}/{total_pages} 页...")
# 发送请求
response = fetch_page(base_url, params={'page': page_num})
if not response:
logging.warning(f"⚠ 第{page_num}页抓取失败,跳过")
continue
# 解析企业列表
companies = parse_company_list(response.text)
logging.info(f" 解析出 {len(companies)} 家企业")
# 3. 存储到数据库
for company in companies:
is_new, company_id = db.insert_or_update(company, job_fair_id)
if is_new:
new_count += 1
else:
update_count += 1
# 4. 礼貌等待(避免频繁请求)
if page_num < total_pages:
time.sleep(2) # 每页间隔2秒
# 5. 导出结果
db.export_to_csv()
# 6. 输出统计
logging.info("\n" + "=" * 50)
logging.info("✅ 爬取任务完成!")
logging.info(f"📊 新增企业: {new_count} 家")
logging.info(f"🔄 更新企业: {update_count} 家")
logging.info(f"💾 数据库路径: data/job_fair.db")
logging.info(f"📄 CSV文件: data/export.csv")
logging.info("=" * 50)
if __name__ == '__main__':
main()
启动命令
json
# 直接运行
python crawler.py
# 或者指定日志级别
python crawler.py --log-level DEBUG
运行结果展示
控制台输出示例:
json
==================================================
🚀 招聘会企业爬虫启动
==================================================
2025-01-29 14:30:01 [INFO] ✓ 数据库表初始化完成
2025-01-29 14:30:02 [INFO] ✓ 成功获取: https://...?page=1 (尝试 1/3)
2025-01-29 14:30:02 [INFO] 📄 共检测到 8 页数据
▶ 正在抓取第 1/8 页...
2025-01-29 14:30:02 [INFO] ✓ 成功获取: https://...?page=1 (尝试 1/3)
2025-01-29 14:30:02 [INFO] 解析出 15 家企业
2025-01-29 14:30:03 [INFO] ✨ 新增企业: 杭州XX科技有限公司 (岗位数: 15)
2025-01-29 14:30:03 [INFO] ✨ 新增企业: 宁波YY智能制造 (岗位数: 8)
...
▶ 正在抓取第 2/8 页...
2025-01-29 14:30:05 [INFO] ✓ 成功获取: https://...?page=2 (尝试 1/3)
2025-01-29 14:30:05 [INFO] 解析出 15 家企业
2025-01-29 14:30:06 [INFO] 🔄 更新企业: 温州ZZ电子商(岗位数 5 → 12)
...
==================================================
✅ 爬取任务完成!
📊 新增企业: 103 家
🔄 更新企业: 12 家
💾 数据库路径: data/job_fair.db
📄 CSV文件: data/export.csv
==================================================
CSV文件示例(前5行):
| id | company_name | industry | job_count | job_fair_id | url | crawl_time | update_time |
|---|---|---|---|---|---|---|---|
| 1 | 杭州XX科技有限公司 | 互联网/电子商务 | 15 | 2025_spring | https://... | 2025-01-29 14:30:03 | 2025-01-29 14:30:03 |
| 2 | 宁波YY智能制造 | 先进制造业 | 8 | 2025_spring | https://... | 2025-01-29 14:30:03 | 2025-01-29 14:30:03 |
| 3 | 温州ZZ电子商务 | 电子商务 | 12 | 2025_spring | https://... | 2025-01-29 14:30:06 | 2025-01-29 14:35:21 |
| 4 | 绍兴AA纺织科技 | 传统制造业 | 6 | 2025_spring | https://... | 2025-01-29 14:30:07 | 2025-01-29 14:30:07 |
| 5 | 嘉兴BB新能源 | 新能源/环保 | 20 | 2025_spring | https://... | 2025-01-29 14:30:08 | 2025-01-29 14:30:08 |
🔟 常见问题与排错(强烈建议收藏)
问题1:抓到403 Forbidden
症状 :response.status_code == 403
可能原因与解决方案:
| 原因 | 解决方案 |
|---|---|
| User-Agent被识别为爬虫 | 更换为最新版本Chrome的UA |
| 缺少Referer头 | 在headers中添加正确的Referer |
| IP被临时封禁 | 降低抓取频率,增加sleep时间到5-10秒 |
| 需要cookie验证 | 使用Session对象,或手动添加cookie |
python
# 应急方案:手动添加cookie
HEADERS['Cookie'] = 'session_id=xxx; user_token=yyy'
问题2:抓到429 Too Many Requests
症状:频繁出现429状态码
解决方案:
python
# 动态调整请求间隔
import random
def smart_sleep():
"""智能等待,避免规律性被识别"""
delay = random.uniform(2, 5) # 2-5秒随机间隔
time.sleep(delay)
# 在每次请求后调用
smart_sleep()
问题3:HTML抓到了但解析为空
症状 :len(companies) == 0,但查看源码有数据
排查步骤:
-
确认是否动态渲染
python# 打印HTML源码前500字符 print(response.text[:500]) # 如果看到<div id="app"></div>这类空壳,说明是JS渲染 -
检查XPath选择器
python# 在浏览器F12中测试XPath $x('//div[@class="company-item"]') # Chrome控制台 # 注意class可能是动态生成的,如"company-item-a3b2c" # 改用包含匹配://div[contains(@class, "company-item")] -
查看是否有iframe嵌套
python# 检查是否有iframe标签 tree.xpath('//iframe/@src')
问题4:编码乱码问题
症状 :中文显示为\xe4\xb8\xad\xe6\x96\x87
解决方案:
python
# 方法1:让requests自动检测编码
response.encoding = response.apparent_encoding
# 方法2:手动指定编码
response.encoding = 'utf-8'
# 方法3:使用chardet库检测
import chardet
encoding = chardet.detect(response.content)['encoding']
html = response.content.decode(encoding)
问题5:XPath提取结果为空列表
症状 :company_name_list = []
调试技巧:
python
# 逐层定位问题
tree = etree.HTML(html_content)
# 1. 先检查大容器能否找到
containers = tree.xpath('//div[@class="company-list"]')
print(f"找到{len(containers)}个容器")
# 2. 再检查子元素
if containers:
items = containers[0].xpath('.//div[@class="company-item"]')
print(f"找到{len(items)}个企业卡片")
# 3. 最后检查目标字段
if items:
names = items[0].xpath('.//h3[@class="company-name"]/text()')
print(f"第一个企业名称: {names}")
问题6:数据库插入失败
症状 :sqlite3.IntegrityError: UNIQUE constraint failed
原因:违反了唯一性约束
解决方案:
python
# 使用 INSERT OR IGNORE 忽略重复记录
self.cursor.execute('''
INSERT OR IGNORE INTO companies (...) VALUES (...)
''')
# 或者使用 INSERT OR REPLACE 覆盖旧记录
self.cursor.execute('''
INSERT OR REPLACE INTO companies (...) VALUES (...)
''')
1️⃣1️⃣ 进阶优化(锦上添花)
优化1:多线程并发抓取
使用 concurrent.futures 实现线程池:
python
from concurrent.futures import ThreadPoolExecutor, as_completed
def crawl_page_wrapper(page_num):
"""线程任务包装函数"""
try:
response = fetch_page(base_url, params={'page': page_num})
if response:
companies = parse_company_list(response.text)
return (page_num, companies)
except Exception as e:
logging.error(f"第{page_num}页抓取异常: {str(e)}")
return (page_num, [])
def main_concurrent():
"""并发版本主函数"""
db = JobFairDB()
total_pages = 8 # 假设已知总页数
# 使用3个线程并发抓取
with ThreadPoolExecutor(max_workers=3) as executor:
futures = {
executor.submit(crawl_page_wrapper, page): page
for page in range(1, total_pages + 1)
}
for future in as_completed(futures):
page_num, companies = future.result()
logging.info(f"第{page_num}页完成,解析{len(companies)}家企业")
for company in companies:
db.insert_or_update(company, '2025_spring')
db.export_to_csv()
注意事项:
- 线程数不要超过5,避免给服务器造成压力
- SQLite不支持真正的并发写入,需要加锁或改用MySQL
优化2:断点续跑机制
记录已抓取的页面,意外中断后可继续:
python
import json
import os
PROGRESS_FILE = 'data/progress.json'
def save_progress(current_page):
"""保存当前进度"""
with open(PROGRESS_FILE, 'w') as f:
json.dump({'last_page': current_page}, f)
def load_progress():
"""加载上次进度"""
if os.path.exists(PROGRESS_FILE):
with open(PROGRESS_FILE, 'r') as f:
data = json.load(f)
return data.get('last_page', 0)
return 0
def main_with_resume():
"""支持断点续跑的主函数"""
start_page = load_progress() + 1
logging.info(f"▶ 从第{start_page}页继续抓取")
for page_num in range(start_page, total_pages + 1):
# ... 抓取逻辑 ...
save_progress(page_num) # 每页完成后保存进度
优化3:日志与监控
更详细的日志记录和统计:
python
import time
class CrawlerStats:
"""爬虫统计类"""
def __init__(self):
self.start_time = time.time()
self.total_requests = 0
self.success_requests = 0
self.failed_requests = 0
self.total_companies = 0
def record_request(self, success=True):
self.total_requests += 1
if success:
self.success_requests += 1
else:
self.failed_requests += 1
def add_companies(self, count):
self.total_companies += count
def report(self):
elapsed = time.time() - self.start_time
success_rate = (self.success_requests / self.total_requests * 100
if self.total_requests > 0 else 0)
report = f"""
╔══════════════════════════════════════╗
║ 爬虫任务统计报告 ║
╠══════════════════════════════════════╣
║ 总耗时: {elapsed:.2f} 秒
║ 总请求数: {self.total_requests}
║ 成功请求: {self.success_requests}
║ 失败请求: {self.failed_requests}
║ 成功率: {success_rate:.2f}%
║ 采集企业数: {self.total_companies}
║ 平均速度: {self.total_companies/elapsed:.2f} 企业/秒
╚══════════════════════════════════════╝
"""
logging.info(report)
优化4:定时任务部署
使用 APScheduler 实现定时抓取:
python
from apscheduler.schedulers.blocking import BlockingScheduler
def scheduled_crawl():
"""定时任务包装函数"""
logging.info("⏰ 定时任务触发,开始抓取...")
main()
if __name__ == '__main__':
scheduler = BlockingScheduler()
# 每天早上9点执行
scheduler.add_job(scheduled_crawl, 'cron', hour=9, minute=0)
# 或者每隔6小时执行一次
# scheduler.add_job(scheduled_crawl, 'interval', hours=6)
logging.info("⏱ 定时任务已启动,等待触发...")
scheduler.start()
Linux cron 方式:
bash
# 编辑crontab
crontab -e
# 每天9:00执行
0 9 * * * cd /path/to/project && /usr/bin/python3 crawler.py >> logs/cron.log 2>&1
1️⃣2️⃣ 总结与延伸阅读
项目复盘
通过本项目,我们完整实现了一个生产级的招聘会企业信息采集系统,包括:
✅ 请求层 :支持重试、超时控制、频率限制的HTTP请求封装
✅ 解析层 :基于XPath的高效HTML解析,容错性强
✅ 存储层 :SQLite数据库去重、增量更新、导出功能
✅ 工程化:日志记录、异常处理、进度保存、统计报告
核心收获:
- 分页处理:先获取总页数,再循环抓取所有页面
- 去重策略:数据库唯一约束 + 应用层判断双重保障
- 增量更新:通过时间戳和版本号实现智能更新
- 容错设计:每一层都有异常捕获和降级处理
下一步可以做什么?
1. 升级到 Scrapy 框架
Scrapy 是工业级爬虫框架,提供:
- 自动去重(通过URL fingerprint)
- 中间件机制(请求/响应拦截)
- 分布式支持(Scrapy-Redis)
- 内置的 Item Pipeline
学习路径:
- 官方文档:https://docs.scrapy.org/
- 将本项目重构为 Scrapy 项目
- 学习 Scrapy-Redis 实现分布式爬虫
2. 处理动态渲染网站
如果目标站点使用 React/Vue 等框架,需要:
- Playwright:新一代浏览器自动化工具,比 Selenium 更快更稳定
- Puppeteer:Chrome DevTools Protocol 的 Python 绑定
- 接口逆向:通过浏览器开发者工具抓包分析 API 接口
推荐阅读:
- Playwright官方文档:https://playwright.dev/python/
- 《Web Scraping with Python》第二版
3. 引入代理池与验证码识别
对于反爬更严格的网站:
-
代理池:轮换IP地址,避免被封
- 免费代理:https://github.com/jhao104/proxy_pool
- 付费代理:阿里云/腾讯云代理服务
-
验证码识别:
- 图形验证码:ddddocr 库
- 滑块验证码:人工打码平台
- reCAPTCHA:2captcha 等第三方服务
4. 数据清洗与分析
采集到的原始数据需要进一步处理:
python
import pandas as pd
import jieba
from collections import Counter
# 行业分布分析
df = pd.read_csv('data/export.csv')
industry_counts = df['industry'].value_counts()
# 岗位数量分析
print(f"平均岗位数: {df['job_count'].mean():.2f}")
print(f"最大岗位数: {df['job_count'].max()}")
# 企业名称词云分析
all_names = ' '.join(df['company_name'])
words = jieba.cut(all_names)
word_freq = Counter(words)
5. 搭建可视化看板
使用 Streamlit 或 Dash 快速搭建数据看板:
python
# dashboard.py
import streamlit as st
import pandas as pd
import plotly.express as px
st.title("📊 招聘会企业数据看板")
df = pd.read_csv('data/export.csv')
# 行业分布饼图
fig = px.pie(df, names='industry', title='行业分布')
st.plotly_chart(fig)
# 岗位数量柱状图
fig2 = px.bar(df.nlargest(20, 'job_count'),
x='company_name', y='job_count',
title='Top 20 招聘岗位最多的企业')
st.plotly_chart(fig2)
运行:streamlit run dashboard.py
参考资源
书籍推荐:
- 《Python网络爬虫从入门到实践》(第2版)
- 《Web Scraping with Python》 by Ryan Mitchell
- 《精通Python爬虫框架Scrapy》
开源项目学习:
- awesome-spider:https://github.com/facert/awesome-spider
- SpiderCollection:https://github.com/shengqiangzhang/examples-of-web-crawlers
工具与库:
- lxml 文档:https://lxml.de/
- requests 文档:https://requests.readthedocs.io/
- SQLite 教程:https://www.sqlitetutorial.net/
最后的话
爬虫技术本质上是数据获取的自动化,但请务必记住:
技术无罪,应用有边界。
在享受自动化带来的便利时,请始终遵守法律法规、尊重网站权益、保护用户隐私。合理使用爬虫技术,它可以成为数据分析、学术研究、商业智能的有力工具;滥用则可能触犯法律,得不偿失。
祝你在数据采集的道路上走得更远!如果本文对你有帮助,欢迎点赞收藏,也欢迎在评论区分享你的爬虫实战经验~
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)

免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。技术无罪,责任在人!!!