Python爬虫实战:构建招聘会数据采集系统 - requests+lxml 实战企业名单爬取与智能分析!

㊗️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~

㊙️本期爬虫难度指数:⭐⭐⭐

🉐福利: 一次订阅后,专栏内的所有文章可永久免费看,持续更新中,保底1000+(篇)硬核实战内容。

全文目录:

    • [🌟 开篇语](#🌟 开篇语)
    • [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))
    • [🔟 运行方式与结果展示(必写)](#🔟 运行方式与结果展示(必写))
    • [1️⃣1️⃣ 常见问题与排错(FAQ)](#1️⃣1️⃣ 常见问题与排错(FAQ))
      • [Q1: 表格有合并单元格,数据错位怎么办?](#Q1: 表格有合并单元格,数据错位怎么办?)
      • [Q2: 翻页是 JavaScript 链接,没有 href 怎么办?](#Q2: 翻页是 JavaScript 链接,没有 href 怎么办?)
      • [Q3: 中文显示为乱码()](#Q3: 中文显示为乱码())
      • [Q4: 页面能看到数据,但爬下来是空的](#Q4: 页面能看到数据,但爬下来是空的)
    • [1️⃣2️⃣ 进阶优化(Advanced Optimization)](#1️⃣2️⃣ 进阶优化(Advanced Optimization))
      • [1. 增量更新(避免重复爬取)](#1. 增量更新(避免重复爬取))
      • [2. 行业分类智能化](#2. 行业分类智能化)
      • [3. 数据可视化分析](#3. 数据可视化分析)
    • [1️⃣3️⃣ 总结与延伸阅读](#1️⃣3️⃣ 总结与延伸阅读)
      • [📚 项目复盘](#📚 项目复盘)
      • [🚀 职业发展建议](#🚀 职业发展建议)
      • [🔗 延伸阅读](#🔗 延伸阅读)
      • 附录:完整项目文件结构
    • [🌟 文末](#🌟 文末)
      • [✅ 专栏持续更新中|建议收藏 + 订阅](#✅ 专栏持续更新中|建议收藏 + 订阅)
      • [✅ 互动征集](#✅ 互动征集)
      • [✅ 免责声明](#✅ 免责声明)

🌟 开篇语

哈喽,各位小伙伴们你们好呀~我是【喵手】。

运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO

欢迎大家常来逛逛,一起学习,一起进步~🌟

我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略反爬对抗 ,从数据清洗分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上

📌 专栏食用指南(建议收藏)

  • ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
  • ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
  • ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
  • ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用

📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅专栏👉《Python爬虫实战》👈,一次订阅后,专栏内的所有文章可永久免费阅读,持续更新中。

💕订阅后更新会优先推送,按目录学习更高效💯~

1️⃣ 摘要(Abstract)

一句话概括:使用 Python requests + lxml 爬取公开招聘会网站的参展企业信息(企业名称、所属行业、展位号、招聘岗位、联系方式等),最终输出为结构化的 SQLite 数据库 + Excel/CSV 文件,支持行业分析、岗位匹配、企业筛选等应用场景。

读完本文你将获得

  • 掌握公共服务类网站(政府/高校/展会)的数据采集技巧
  • 学会处理表格数据、分页列表、详情页嵌套的复杂结构
  • 获得一套生产级的招聘信息采集系统(支持多场次、多地区、历史对比)
  • 理解公共数据采集的合规要求和最佳实践

2️⃣ 背景与需求(Why)

为什么要爬取招聘会数据?

每年全国各地举办数千场线上线下招聘会,但信息分散在各个平台,给求职者和研究者带来诸多不便。通过爬虫采集数据后,我们可以:

  • 求职者视角

    • 快速筛选目标行业和岗位
    • 对比不同招聘会的企业质量
    • 提前了解展位分布,规划参会路线
    • 追踪心仪企业的招聘动态
  • HR/企业视角

    • 分析竞品企业的招聘策略
    • 了解行业人才需求趋势
    • 评估参展招聘会的效果
  • 数据分析师视角

    • 研究地区产业结构(哪些行业活跃)
    • 分析就业市场供需关系
    • 预测行业发展趋势
    • 制作可视化报告
  • 学校/政府视角

    • 监测就业市场动态
    • 优化招聘会组织
    • 为学生提供就业指导

典型招聘会网站结构

网站类型

  1. 政府人社部门网站(如各地人力资源市场)
  2. 高校就业信息网(如各大学就业指导中心)
  3. 专业招聘会平台(如智联、前程无忧的线下招聘会)
  4. 行业协会网站(如互联网协会、制造业协会)

典型页面结构

json 复制代码
首页(招聘会列表)
├── 招聘会A - 2025春季综合招聘会
│     ├── 基本信息(时── 参展企业列表
│           ├── 企业1(名称、行业、展位、岗位)
│           ├── 企业2
│           └── 企业3
│
├── 招聘会B - IT互联网专场
└── 招聘会C - 制造业专场

目标字段清单

字段名 说明 示例值 获取难度
fair_name 招聘会名称 "2025春季综合招聘会" ⭐ 简单
fair_date 举办日期 "2025-03-15" ⭐ 简单
fair_location 举办地点 "市体育馆一楼" ⭐ 简单
company_name 企业名称 "XX科技有限公司" ⭐ 简单
industry 所属行业 "互联网/电子商务" ⭐⭐ 中等
booth_number 展位号 "A101" ⭐ 简单
positions 招聘岗位 ["Java工程师", "产品经理"] ⭐⭐ 中等
headcount 招聘人数 "10-20人" ⭐⭐ 中等
requirements 岗位要求 "本科及以上,3年经验" ⭐⭐⭐ 困难
contact 联系方式 "hr@example.com" ⭐⭐ 中等
company_scale 企业规模 "500-1000人" ⭐⭐ 中等

扩展字段(可选)

  • 企业性质(国企/民企/外企)
  • 薪资范围
  • 工作地点
  • 企业官网
  • 企业简介

3️⃣ 合规与注意事项(必写)

公共数据采集的法律边界

招聘会信息属于公开信息,但仍需注意:

允许采集的情况

  • 政府/高校官网公开发布的招聘会信息
  • 明确标注"公开"的企业参展名单
  • 用于个人求职、学术研究、非商业分析

禁止采集的情况

  • 需要登录才能查看的内部信息
  • 明确标注"仅供内部使用"的数据
  • 个人隐私信息(HR个人电话、家庭住址等)
  • 用于商业转售、垃圾营销

robots.txt 检查

大多数政府和高校网站的 robots.txt 比较宽松:

json 复制代码
# 典型的政府/高校网站 robots.txt

User-agent: *
Disallow: /admin/
Disallow: /api/
Allow: /jobfair/
Allow: /news/

解读

  • ✅ 允许爬取招聘会信息页面(/jobfair/
  • ❌ 禁止爬取管理后台(/admin/

数据使用规范

个人信息保护

  • ❌ 不采集HR的个人手机号(只采集企业公开邮箱)
  • ❌ 不采集企业内部通讯录
  • ✅ 可以采集企业官方联系方式

数据使用边界

  • ✅ 个人求职:筛选目标企业、规划参会路线
  • ✅ 学术研究:就业市场分析、产业结构研究
  • ✅ 非商业分享:与同学分享招聘会信息
  • ❌ 商业转售:打包数据卖给猎头公司
  • ❌ 垃圾营销:批量发送推广邮件

频率控制建议

公共服务网站通常带宽有限,建议:

  • 请求间隔:3-5秒(比商业网站更保守)
  • 并发限制:单线程或最多2个并发
  • 时间选择:避开工作时间高峰(9:00-11:00, 14:00-16:00)
  • 总量控制:单次不超过1000个请求

道德准则

  • 🤝 善意原则:不对服务器造成压力
  • 📚 归属原则:使用数据时注明来源
  • 🔒 隐私原则:不采集和传播个人隐私
  • ⚖️ 合法原则:遵守《网络安全法》《数据安全法》

4️⃣ 技术选型与整体流程(What/How)

静态 vs 动态分析

招聘会网站大多是传统的服务端渲染,非常适合静态抓取:

特征 静态网站(多数招聘会) 动态网站(少数新式平台)
HTML来源 服务器直接渲染 JavaScript动态生成
查看源代码 能看到完整数据 只能看到空壳
爬取难度 ⭐ 简单 ⭐⭐⭐ 困难
工具选择 requests + lxml Selenium/Playwright

判断方法

python 复制代码
# 访问页面后,右键 → 查看源代码
# 如果能在HTML中搜索到企业名称,就是静态的

# 示例:静态渲染(可以看到数据)
<tr>
    <td>阿里巴巴集团</td>
    <td>互联网</td>
    <td>A101</td>
</tr>

# 示例:动态渲染(只有空标签)
<div id="company-list"></div>
<script src="app.js"></script>

技术栈选择

组件 技术选择 理由
HTTP请求 requests + Session 轻量、稳定、支持Cookie
HTML解析 lxml + XPath 速度快、内存占用低
数据清洗 re (正则) + pandas 处理各种格式异常
数据存储 SQLite + Excel 方便查询 + 易于分享
数据分析 pandas + matplotlib 统计分析 + 可视化

整体流程设计

json 复制代码
[招聘会列表页] → 获取所有招聘会的链接
      ↓
[单个招聘会详情页] → 提取基本信息(时间、地点)
      ↓
[企业列表表格] → 解析所有参展企业
      ↓
   提取企业信息(名称、行业、展位、岗位)
      ↓
[企业详情页](可选)→ 补充详细信息
      ↓
   数据清洗(行业标准化、展位号规范化)
      ↓
   存储到 SQLite + 导出 Excel/CSV
      ↓
   数据分析(行业分布、热门岗位)

分步说明

  1. 步骤1:获取招聘会列表

    • URL示例:https://job.example.edu.cn/jobfair/list
    • 提取:招聘会名称、日期、详情链接
  2. 步骤2:进入招聘会详情

    • URL示例:https://job.example.edu.cn/jobfair/detail?id=12345
    • 提取:举办时间、地点、主题、参展企业数
  3. 步骤3:解析企业表格

    • 通常是 <table> 标签或 <ul> 列表
    • 可能分页(需要处理翻页逻辑)
  4. 步骤4:数据清洗

    • 行业标准化:"IT/互联网" → "信息技术"
    • 展位号规范化:"A-101" → "A101"
    • 去除重复企业
  5. 步骤5:存储与导出

    • SQLite:支持复杂查询
    • Excel:方便HR/求职者查看

5️⃣ 环境准备与依赖安装(可复现)

Python 版本

建议使用 Python 3.10+(本文基于 Python 3.10.8 测试通过)

依赖安装

bash 复制代码
pip install requests lxml pandas openpyxl python-dateutil --break-system-packages

# 可选:数据可视化
pip install matplotlib seaborn wordcloud --break-system-packages

依赖说明

版本要求 用途 是否必需
requests >=2.28.0 HTTP请求 ✅ 必需
lxml >=4.9.0 HTML解析 ✅ 必需
pandas >=1.5.0 数据处理 ✅ 必需
openpyxl >=3.0.0 Excel导出 ✅ 必需
python-dateutil >=2.8.0 日期解析 ✅ 必需
matplotlib >=3.5.0 图表生成 ⭐ 可选
seaborn >=0.12.0 高级可视化 ⭐ 可选

项目结构

json 复制代码
jobfair_crawler/
│
├── main.py                  # 主入口
├── config.py                # 配置文件
├── fetcher.py               # 请求层
├── parser.py                # 解析层
├── cleaner.py               # 数据清洗层
├── storage.py               # 存储层
├── analyzer.py              # 数据分析层(可选)
├── requirements.txt         # 依赖清单
│
├── data/
│   ├── jobfairs.db          # SQLite数据库
│   ├── 2025春季招聘会.xlsx   # Excel导出
│   └── analysis/            # 分析结果
│       ├── industry_chart.png
│       └── position_wordcloud.png
│
├── logs/
│   ├── crawler.log          # 运行日志
│   └── error.log            # 错误日志
│
└── tests/
    ├── test_parser.py       # 单元测试
    └── test_cleaner.py

6️⃣ 核心实现:请求层(Fetcher)

代码实现(fetcher.py

python 复制代码
"""
招聘会数据采集 - 请求层

功. 发送HTTP请求获取HTML
2. 处理Session和Cookie
3. 自动重试和错误处理
4. 频率控制(保护公共服务器)

作者:YourName
日期:2025-01-27
"""

import requests
import time
import logging
from typing import Optional
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)


class JobFairFetcher:
    """招聘会网站请求器"""
    
    def __init__(self):
        """初始化请求器"""
        # 创建Session(复用连接)
        # 解释:Session会保持Cookie和连接池,提升性能
        self.session = self._create_session()
        
        # 请求头(模拟真实浏览器)
        # 解释:政府/高校网站通常不检查User-Agent,但保险起见还是要设置
        self.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',
            'Upgrade-Insecure-Requests': '1',
        }
        
        # 请求配置
        self.timeout = 20  # 超时时间(秒)- 政府网站可能比较慢
        self.delay = 3.0  # 请求间隔(秒)- 保守策略
        self.last_request_time = 0
        
        # 统计信息
        self.stats = {
            'total_requests': 0,
            'success': 0,
            'failed': 0,
            'retries': 0
        }
    
    def _create_session(self) -> requests.Session:
        """
        创建带重试机制的Session
        
        Returns:
            配置好的Session对象
            
        解释:
            - 使用Retry策略自动处理临时性网络错误
            - 指数退避(backoff_factor):第1次重试等1秒,第2次等2秒,第3次等4秒
            - status_forcelist:遇到这些HTTP状态码时触发重试
        """
        session = requests.Session()
        
        # 重试策略配置
        retry_strategy = Retry(
            total=3,  # 最多重试3次
            backoff_factor=1,  # 重试间隔递增因子
            status_forcelist=[429, 500, 502, 503, 504],  # 触发重试的状态码
            allowed_methods=["HEAD", "GET", "OPTIONS"]  # 只对这些方法重试
        )
        
        # 挂载到Session
        adapter = HTTPAdapter(max_retries=retry_strategy)
        session.mount("http://", adapter)
        session.mount("https://", adapter)
        
        return session
    
    def _rate_limit(self):
        """
        请求频率控制
        
        解释:
            - 计算距离上次请求的时间
            - 如果间隔不够,则等待
            - 对公共服务器友好,避免造成压力
        """
        elapsed = time.time() - self.last_request_time
        
        if elapsed < self.delay:
            sleep_time = self.delay - elapsed
            
            if sleep_time > 0:
                logger.debug(f"⏱️ 等待 {sleep_time:.1f}秒以控制频率")
                time.sleep(sleep_time)
        
        self.last_request_time = time.time()
    
    def fetch(self, url: str, params: dict = None, encoding: str = 'utf-8') -> Optional[str]:
        """
        获取页面HTML
        
        Args:
            url: 目标URL
            params: URL参数(如分页参数)
            encoding: 页面编码(默认UTF-8,某些老网站可能是GBK)
            
        Returns:
            HTML字符串,失败返回None
            
        示例:
            # 基本请求
            html = fetcher.fetch('https://job.example.edu.cn/jobfair/list')
            
            # 带参数(分页)
            html = fetcher.fetch(
                'https://job.example.edu.cn/jobfair/list',
                params={'page': 2, 'pageSize': 20}
            )
            
            # 指定编码(老网站可能用GBK)
            html = fetcher.fetch('https://old-site.gov.cn/list', encoding='gbk')
        """
        # 频率控制
        self._rate_limit()
        
        # 统计
        self.stats['total_requests'] += 1
        
        try:
            # 发送GET请求
            # 解释:
            # - headers: 伪装成浏览器
            # - params: URL查询参数(自动编码)
            # - timeout: 防止长时间卡死
            # - allow_redirects: 允许自动跟随重定向
            response = self.session.get(
                url,
                headers=self.headers,
                params=params,
                timeout=self.timeout,
                allow_redirects=True
            )
            
            # 检查HTTP状态码
            # 解释:raise_for_status() 会在状态码>=400时抛出异常
            response.raise_for_status()
            
            # 处理编码
            # 解释:有些网站的编码声明不准确,需要手动指定
            if encoding:
                response.encoding = encoding
            
            # 统计
            self.stats['success'] += 1
            
            logger.info(f"✅ 成功: {url} ({len(response.text)} 字符)")
            return response.text
            
        except requests.exceptions.HTTPError as e:
            status_code = e.response.status_code
            
            if status_code == 403:
                logger.error(f"🚫 403 Forbidden: {url}")
                logger.info("💡 可能原因:需要登录、IP被限制、或访问了禁止页面")
            elif status_code == 404:
                logger.warning(f"📭 404 Not Found: {url}")
                logger.info("💡 可能原因:链接失效、招聘会已结束")
            elif status_code == 500:
                logger.error(f"💥 500 Server Error: {url}")
                logger.info("💡 可能原因:服务器内部错误,稍后重试")
            else:
                logger.error(f"❌ HTTP {status_code}: {url}")
            
            self.stats['failed'] += 1
            return None
            
        except requests.exceptions.Timeout:
            logger.error(f"⏱️ 请求超时: {url}")
            logger.info("💡 建议:增加timeout时间或稍后重试")
            self.stats['failed'] += 1
            return None
            
        except requests.exceptions.ConnectionError:
            logger.error(f"🔌 连接失败: {url}")
            logger.info("💡 可能原因:网络问题、DNS解析失败、服务器维护")
            self.stats['failed'] += 1
            return None
            
        except Exception as e:
            logger.error(f"❌ 未知错误: {url} - {str(e)}")
            self.stats['failed'] += 1
            return None
    
    def fetch_with_post(self, url: str, data: dict = None) -> Optional[str]:
        """
        使用POST方法请求(某些搜索功能需要)
        
        Args:
            url: 目标URL
            data: POST数据(表单数据)
            
        Returns:
            HTML字符串
            
        示例:
            # 搜索功能(可能需要POST)
            html = fetcher.fetch_with_post(
                'https://job.example.edu.cn/search',
                data={'keyword': '计算机', 'industry': 'IT'}
            )
        """
        self._rate_limit()
        self.stats['total_requests'] += 1
        
        try:
            response = self.session.post(
                url,
                headers=self.headers,
                data=data,
                timeout=self.timeout
            )
            response.raise_for_status()
            
            self.stats['success'] += 1
            logger.info(f"✅ POST成功: {url}")
            return response.text
            
        except Exception as e:
            logger.error(f"❌ POST失败: {url} - {str(e)}")
            self.stats['failed'] += 1
            return None
    
    def get_stats(self) -> dict:
        """
        获取请求统计信息
        
        Returns:
            统计字典
        """
        if self.stats['total_requests'] > 0:
            success_rate = self.stats['success'] / self.stats['total_requests'] * 100
        else:
            success_rate = 0
        
        return {
            **self.stats,
            'success_rate': f"{success_rate:.2f}%"
        }
    
    def close(self):
        """关闭Session"""
        self.session.close()
        logger.info("🔒 Session已关闭")


# ========== 使用示例 ==========
if __name__ == "__main__":
    # 初始化
    fetcher = JobFairFetcher()
    
    # 测试:获取招聘会列表页
    html = fetcher.fetch('https://job.example.edu.cn/jobfair/list')
    
    if html:
        print(f"成功获取HTML,长度:{len(html)}")
        print(f"前500个字符:\n{html[:500]}")
    
    # 查看统计
    print(f"\n统计信息:{fetcher.get_stats()}")
    
    # 关闭
    fetcher.close()

关键技术点详解

1. 编码处理的重要性

很多政府和高校网站使用老旧的编码:

python 复制代码
# 问题:中文显示为乱码
html = requests.get(url).text
# 输出:    ˾ (乱码)

# 解决方案1:指定编码
response = requests.get(url)
response.encoding = 'gbk'  # 或 'gb自动检测
import chardet

response = requests.get(url)
detected = chardet.detect(response.content)
response.encoding = detected['encoding']
html = response.text

常见编码

  • utf-8:新式网站(推荐)
  • gbk / gb2312:老式中文网站
  • gb18030:兼容性最好的中文编码
2. Session的优势
python 复制代码
# ❌ 不使用Session(每次都建立新连接)
for url in urls:
    html = requests.get(url).text  # 慢!

# ✅ 使用Session(复用连接)
session = requests.Session()
for url in urls:
    html = session.get(url).text  # 快!

性能对比

  • 无Session:每次请求都要DNS解析 + TCP握手 + TLS握手
  • 有Session:只有第一次请求需要握手,后续复用连接
  • 速度提升:30-50%
3. 超时时间的设置
python 复制代码
# 政府/高校网站可能比较慢,建议设置较长的超时
timeout = 20  # 秒

# 也可以分别设置连接超时和读取超时
timeout = (5, 20)  # (连接超时, 读取超时)

7️⃣ 核心实现:解析层(Parser)

代码实现(parser.py

python 复制代码
"""
招聘会数据采集 - 解析层

功能:
1. 解析招聘会列表页
2. 解析企业信息表格
3. 处理分页逻辑
4. 提取详情页数据

作者:YourName
日期:2025-01-27
"""

from lxml import etree
from typing import List, Dict, Optional
import re
import logging

logger = logging.getLogger(__name__)


class JobFairParser:
    """招聘会页面解析器"""
    
    @staticmethod
    def parse_fair_list(html: str) -> List[Dict]:
        """
        解析招聘会列表页
        
        Args:
            html: 列表页HTML
            
        Returns:
            招聘会列表 [{'name': '...', 'date': '...', 'url': '...'}, ...]
            
        解释:
            招聘会列表页通常有以下几种结构:
            1. <ul> + <li> 列表
            2. <table> 表格
            3. <div> 卡片布局
        """
        tree = etree.HTML(html)
        fairs = []
        
        # 方式1:从<ul>列表提取
        # 典型结构:
        # <ul class="jobfair-list">
        #     <li>
        #         <a href="/detail?id=123">2025春季招聘会</a>
        #         <span class="date">2025-03-15</span>
        #     </li>
        # </ul>
        fair_items = tree.xpath('//ul[contains(@class, "jobfair")]//li')
        
        if not fair_items:
            # 方式2:从<table>提取
            fair_items = tree.xpath('//table//tr[position()>1]')  # 跳过表头
        
        for item in fair_items:
            try:
                fair_data = JobFairParser._parse_fair_item(item)
                
                if fair_data and fair_data.get('name'):
                    fairs.append(fair_data)
            except Exception as e:
                logger.warning(f"⚠️ 解析招聘会项失败: {str(e)}")
                continue
        
        logger.info(f"📋 解析到 {len(fairs)} 个招聘会")
        return fairs
    
    @staticmethod
    def _parse_fair_item(item) -> Dict:
        """
        解析单个招聘会项
        
        Args:
            item: lxml Element对象
            
        Returns:
            招聘会信息字典
        """
        # 提取名称和链接
        # XPath解释:
        # .//a → 在当前节点内查找a标签
        # [1] → 取第一个匹配
        link = item.xpath('.//a/@href')
        name = item.xpath('.//a/text()')
        
        # 提取日期
        # 可能在span、td或直接在文本中
        date_text = JobFairParser._extract_text(item, './/span[contains(@class, "date")]//text()')
        
        if not date_text:
            # 备用:从整个文本中提取日期
            full_text = ''.join(item.xpath('.//text()'))
            date_match = re.search(r'(\d{4}[-/年]\d{1,2}[-/月]\d{1,2}[日]?)', full_text)
            if date_match:
                date_text = date_match.group(1)
        
        # 提取地点
        location = JobFairParser._extract_text(item, './/span[contains(@class, "location")]//text()')
        
        # 组装数据
        fair_data = {
            'name': name[0].strip() if name else "",
            'date': date_text.strip() if date_text else "",
            'location': location.strip() if location else "",
            'url': link[0] if link else ""
        }
        
        # 处理相对路径
        if fair_data['url'] and not fair_data['url'].startswith('http'):
            # 假设baseurl从配置读取
            fair_data['url'] = f"https://job.example.edu.cn{fair_data['url']}"
        
        return fair_data
    
    @staticmethod
    def parse_company_table(html: str) -> List[Dict]:
        """
        解析企业信息表格
        
        Args:
            html: 招聘会详情页HTML
            
        Returns:
            企业列表
            
        解释:
            企业信息通常以表格形式展示:
            <table>
                <tr>
                    <th>序号</th><th>企业名称</th><th>所属行业</th><th>展位号</th><th>招聘岗位</th>
                </tr>
                <tr>
                    <td>1</td><td>阿里巴巴</td><td>互联网</td><td>A101</td><td>Java工程师</td>
                </tr>
            </table>
        """
        tree = etree.HTML(html)
        companies = []
        
        # 查找表格
        # 解释:通常表格会有特定的class或id
        table = tree.xpath('//table[contains(@class, "company")]')
        
        if not table:
            # 备用:查找所有table,取第一个
            table = tree.xpath('//table')
        
        if not table:
            logger.warning("⚠️ 未找到企业信息表格")
            return []
        
        table = table[0]
        
        # 获取表头(用于定位列)
        # 解释:通过表头判断每列的含义
        headers = table.xpath('.//tr[1]//th/text()')
        headers = [h.strip() for h in headers]
        
        logger.debug(f"表头: {headers}")
        
        # 定位关键列的索引
        # 解释:不同网站的列顺序可能不同,需要动态查找
        col_indices = JobFairParser._find_column_indices(headers)
        
        # 解析数据行(跳过表头)
        rows = table.xpath('.//tr[position()>1]')
        
        for row in rows:
            try:
                company_data = JobFairParser._parse_company_row(row, col_indices)
                
                if company_data and company_data.get('company_name'):
                    companies.append(company_data)
            except Exception as e:
                logger.warning(f"⚠️ 解析企业行失败: {str(e)}")
                continue
        
        logger.info(f"🏢 解析到 {len(companies)} 家企业")
        return companies
    
    @staticmethod
    def _find_column_indices(headers: List[str]) -> Dict[str, int]:
        """
        查找关键列的索引位置
        
        Args:
            headers: 表头列表
            
        Returns:
            列索引字典 {'company_name': 1, 'industry': 2, ...}
            
        解释:
            因为不同网站的表头可能不同,需要模糊匹配
            例如:"企业名称"/"公司名称"/"单位名称" 都应该被识别为公司名
        """
        indices = {}
        
        # 定义匹配规则(支持多种表达)
        # 解释:使用正则表达式模糊匹配
        column_patterns = {
            'company_name': r'(企业|公司|单位)名称',
            'industry': r'(所属)?行业|类别',
            'booth': r'展位(号)?|摊位',
            'positions': r'(招聘)?岗位|职位',
            'headcount': r'(招聘)?人数|名额',
            'contact': r'联系(方式|电话|邮箱)',
            'requirements': r'(岗位)?要求|学历'
        }
        
        for key, pattern in column_patterns.items():
            for idx, header in enumerate(headers):
                if re.search(pattern, header, re.IGNORECASE):
                    indices[key] = idx
                    break
        
        logger.debug(f"列索引映射: {indices}")
        return indices
    
    @staticmethod
    def _parse_company_row(row, col_indices: Dict[str, int]) -> Dict:
        """
        解析企业信息行
        
        Args:
            row: 表格行元素
            col_indices: 列索引映射
            
        Returns:
            企业数据字典
        """
        # 获取所有列的文本
        # 解释:.//td → 查找所有td标签
        cells = row.xpath('.//td')
        
        if not cells:
            return {}
        
        # 提取文本内容
        # 解释:itertext() 会递归获取所有子元素的文本
        cell_texts = [''.join(cell.itertext()).strip() for cell in cells]
        
        # 根据列索引提取数据
        company_data = {}
        
        for key, idx in col_indices.items():
            if idx < len(cell_texts):
                company_data[key] = cell_texts[idx]
            else:
                company_data[key] = ""
        
        # 如果没有找到列索引,尝试按顺序猜测
        # 解释:兜底策略,假设前几列通常是:序号、名称、行业、展位
        if not col_indices:
            if len(cell_texts) >= 4:
                company_data = {
                    'company_name': cell_texts[1] if len(cell_texts) > 1 else "",
                    'industry': cell_texts[2] if len(cell_texts) > 2 else "",
                    'booth': cell_texts[3] if len(cell_texts) > 3 else "",
                    'positions': cell_texts[4] if len(cell_texts) > 4 else ""
                }
        
        return company_data
    
    @staticmethod
    def parse_pagination(html: str) -> Dict:
        """
        解析分页信息
        
        Args:
            html: 页面HTML
            
        Returns:
            分页信息 {'current_page': 1, 'total_pages': 10, 'has_next': True}
        """
        tree = etree.HTML(html)
        
        pagination_info = {
            'current_page': 1,
            'total_pages': 1,
            'has_next': False
        }
        
        # 方式1:从分页控件提取
        # 示例结构:
        # <div class="pagination">
        #     <span class="current">1</span>
        #     <a href="?page=2">2</a>
        #     <a href="?page=3">3</a>
        #     <span class="total">共10页</span>
        # </div>
        
        # 当前页
        current = tree.xpath('//span[@class="current"]/text()')
        if current:
            try:
                pagination_info['current_page'] = int(current[0])
            except:
                pass
        
        # 总页数
        total_text = tree.xpath('//span[contains(@class, "total")]//text()')
        if total_text:
            total_text = ''.join(total_text)
            # 提取数字:如"共10页" → 10
            match = re.search(r'(\d+)', total_text)
            if match:
                pagination_info['total_pages'] = int(match.group(1))
        
        # 是否有下一页
        next_link = tree.xpath('//a[contains(text(), "下一页") or contains(text(), "next")]')
        pagination_info['has_next'] = len(next_link) > 0
        
        return pagination_info
    
    @staticmethod
    def _extract_text(element, xpath: str) -> str:
        """
        提取文本内容(辅助函数)
        
        Args:
            element: lxml Element或tree
            xpath: XPath表达式
            
        Returns:
            提取的文本
        """
        try:
            result = element.xpath(xpath)
            if result:
                # 如果是列表,合并所有文本
                if isinstance(result, list):
                    text = ''.join(str(r) for r in result)
                else:
                    text = str(result)
                return text.strip()
        except:
            pass
        return ""


# ========== 使用示例 ==========
if __name__ == "__main__":
    # 假设已经获取了HTML
    with open('test_data/fair_list.html', 'r', encoding='utf-8') as f:
        html = f.read()
    
    parser = JobFairParser()
    
    # 解析招聘会列表
    fairs = parser.parse_fair_list(html)
    print(f"解析到 {len(fairs)} 个招聘会")
    
    if fairs:
        print(f"\n第一个招聘会:")
        for key, value in fairs[0].items():
            print(f"  {key}: {value}")

🔟 运行方式与结果展示(必写)

主程序(main.py

这是整个系统的调度中心,负责将之前的各个模块(请求、解析、清洗、存储)串联起来。

python 复制代码
"""
招聘会数据采集 - 主程序

功能:
1. 遍历招聘会列表页
2. 进入每个招聘会详情页
3. 提取企业名单并清洗
4. 保存到数据库并导出Excel
5. 生成可视化分析报告(可选)

作者:YourName
日期:2025-01-27
"""

import logging
from pathlib import Path
from fetcher import JobFairFetcher
from parser import JobFairParser
from cleaner import CompanyCleaner
from storage import JobFairStorage
import time
from datetime import datetime

# 配置日志
Path("logs").mkdir(exist_ok=True)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s [%(levelname)s] %(name)s - %(message)s',
    handlers=[
        logging.FileHandler('logs/crawler.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)


class JobFairCrawler:
    """招聘会爬虫主控制器"""
    
    def __init__(self):
        """初始化各个组件"""
        self.fetcher = JobFairFetcher()
        self.parser = JobFairParser()
        self.cleaner = CompanyCleaner()
        self.storage = JobFairStorage()
        
        # 统计信息
        self.stats = {
            'start_time': datetime.now(),
            'total_fairs': 0,
            'total_companies': 0,
            'success': 0,
            'failed': 0
        }
    
    def crawl_site(self, base_url: str, list_url: str, max_pages: int = 5):
        """
        爬取整个站点的招聘会数据
        
        Args:
            base_url: 网站根域名(用于拼接相对路径)
            list_url: 招聘会列表页URL
            max_pages: 最多爬取多少页列表
        """
        logger.info(f"🚀 开始采集招聘会数据: {list_url}")
        
        for page in range(1, max_pages + 1):
            logger.info(f"\n📄 正在处理列表页: 第 {page} 页...")
            
            # 构造分页URL(根据实际网站规则调整)
            # 示例:?page=1, ?page=2
            current_list_url = f"{list_url}?page={page}"
            html = self.fetcher.fetch(current_list_url)
            
            if not html:
                logger.error("❌ 无法获取列表页,跳过")
                continue
            
            # 解析招聘会列表
            fairs = self.parser.parse_fair_list(html)
            
            if not fairs:
                logger.info("📭 当前页无招聘会数据,停止采集")
                break
            
            logger.info(f"📋 发现 {len(fairs)} 场招聘会,开始逐个采集...")
            
            # 遍历每个招聘会
            for fair in fairs:
                self.crawl_fair_detail(base_url, fair)
                
                # 招聘会之间增加延迟
                time.sleep(2)
            
            # 检查是否有下一页
            pagination = self.parser.parse_pagination(html)
            if not pagination.get('has_next'):
                logger.info("📌 已到达列表最后一页")
                break
        
        self._print_final_report()
    
    def crawl_fair_detail(self, base_url: str, fair_info: dict):
        """
        爬取单个招聘会的详情(企业名单)
        
        Args:
            base_url: 网站根域名
            fair_info: 招聘会基本信息字典
        """
        fair_name = fair_info.get('name', '未知招聘会')
        fair_url = fair_info.get('url', '')
        
        # 处理相对路径
        if fair_url and not fair_url.startswith('http'):
            fair_url = f"{base_url.rstrip('/')}/{fair_url.lstrip('/')}"
        
        logger.info(f"  👉 进入招聘会: {fair_name}")
        
        # 获取详情页HTML
        html = self.fetcher.fetch(fair_url)
        if not html:
            logger.warning(f"  ⚠️ 无法获取详情页: {fair_url}")
            return
        
        # 解析企业列表
        companies = self.parser.parse_company_table(html)
        
        if not companies:
            logger.warning("  📭 未找到参展企业名单")
            return
        
        logger.info(f"  🏢 解析到 {len(companies)} 家企业,开始清洗入库...")
        
        # 批量清洗并保存
        saved_count = 0
        cleaned_companies = []
        
        for company in companies:
            # 数据清洗
            cleaned = self.cleaner.clean_company(company)
            
            if not cleaned:
                continue
            
            # 补充招聘会信息(外键关联)
            cleaned['fair_name'] = fair_name
            cleaned['fair_date'] = fair_info.get('date', '')
            cleaned['source_url'] = fair_url
            
            cleaned_companies.append(cleaned)
        
        # 保存到数据库
        if cleaned_companies:
            count = self.storage.save_companies(cleaned_companies)
            saved_count = count
            self.stats['total_companies'] += count
            self.stats['success'] += 1
        else:
            self.stats['failed'] += 1
            
        self.stats['total_fairs'] += 1
        logger.info(f"  ✅ 成功入库 {saved_count} 家企业")
    
    def export_data(self):
        """导出数据"""
        logger.info("\n📤 开始导出数据...")
        
        try:
            # 导出Excel
            self.storage.export_to_excel()
            
            # 导出CSV
            self.storage.export_to_csv()
            
            logger.info("✅ 数据导出完成!")
        except Exception as e:
            logger.error(f"❌ 数据导出失败: {str(e)}")
    
    def _print_final_report(self):
        """打印最终报告"""
        elapsed = (datetime.now() - self.stats['start_time']).total_seconds()
        
        logger.info(f"""
        {'='*60}
        ========== 🎓 采集任务完成 ==========
        
        📊 统计数据:
           - 采集招聘会: {self.stats['total_fairs']} 场
           - 采集企业数: {self.stats['total_companies']} 家
           - 成功保存场次: {self.stats['success']}
           - 失败/空场次: {self.stats['failed']}
        
        ⏱️  耗时: {elapsed/60:.1f} 分钟
        
        📂 结果文件:
           - 数据库: data/jobfairs.db
           - Excel: data/jobfairs_export.xlsx
        {'='*60}
        """)
    
    def close(self):
        """关闭连接"""
        self.fetcher.close()
        self.storage.close()


if __name__ == "__main__":
    # 配置目标网站(根据实际情况修改)
    BASE_URL = "https://job.example.edu.cn"
    LIST_URL = "https://job.example.edu.cn/jobfair/list"
    
    crawler = JobFairCrawler()
    
    try:
        # 启动爬虫
        crawler.crawl_site(
            base_url=BASE_URL,
            list_url=LIST_URL,
            max_pages=3  # 测试时只爬3页
        )
        
        # 导出数据
        crawler.export_data()
        
    except KeyboardInterrupt:
        logger.info("⚠️ 用户中断程序")
    except Exception as e:
        logger.error(f"❌ 程序异常退出: {str(e)}")
    finally:
        crawler.close()

启动方式

  1. 准备环境

    bash 复制代码
    # 创建目录
    mkdir -p jobfair_crawler/data jobfair_crawler/logs
    
    # 安装依赖
    pip install -r requirements.txt
  2. 修改配置

    打开 main.py,修改 BASE_URLLIST_URL 为你实际想爬的目标网站(例如某高校就业网)。

  3. 运行程序

    bash 复制代码
    python main.py

输出示例

终端日志

text 复制代码
2025-01-27 10:00:01 [INFO] 🚀 开始采集招聘会数据: https://job.example.edu.cn/jobfair/list

2025-01-27 10:00:01 [INFO] 📄 正在处理列表页: 第 1 页...
2025-01-27 10:00:02 [INFO] ✅ 成功: https://job.example.edu.cn/jobfair/list?page=1 (45KB)
2025-01-27 10:00:02 [INFO] 📋 发现 10 场招聘会,开始逐个采集...

2025-01-27 10:00:02 [INFO]   👉 进入招聘会: 2025届毕业生春季大型双选会
2025-01-27 10:00:03 [INFO]   ✅ 成功: https://job.example.edu.cn/detail?id=1001 (120KB)
2025-01-27 10:00:03 [INFO]   🏢 解析到 250 家企业,开始清洗入库...
2025-01-27 10:00:03 [INFO]   ✅ 成功入库 248 家企业

2025-01-27 10:00:05 [INFO]   👉 进入招聘会: 信息技术专场招聘会
...
2025-01-27 10:05:30 [INFO] 📤 开始导出数据...
2025-01-27 10:05:32 [INFO] 📊 Excel导出完成: data/jobfairs_export.xlsx (1250行)
2025-01-27 10:05:32 [INFO] ✅ 数据导出完成!

Excel 文件结构

招聘会名称 举办日期 企业名称 行业分类 展位号 招聘岗位 招聘人数 联系方式
2025春季双选会 2025-03-15 腾讯科技 互联网 A01 后端开发, 产品经理 50 hr@tencent.com
2025春季双选会 2025-03-15 中建三局 建筑/房地产 B05 土木工程师 20 zhaopin@cscec.com

1️⃣1️⃣ 常见问题与排错(FAQ)

在爬取招聘会网站时,你可能会遇到以下典型问题:

Q1: 表格有合并单元格,数据错位怎么办?

现象

html 复制代码
<tr>
    <td rowspan="2">互联网行业</td> <!-- 合并单元格 -->
    <td>腾讯</td>
    <td>A01</td>
</tr>
<tr>
    <!-- 这里少了第一列,导致数据错位 -->
    <td>阿里</td>
    <td>A02</td>
</tr>

解决方案

在解析时记录上一行的值。

python 复制代码
def parse_merged_table(rows):
    last_industry = ""
    for row in rows:
        cells = row.xpath('.//td')
        if len(cells) == 3:  # 完整行
            industry = cells[0].text
            company = cells[1].text
            last_industry = industry
        elif len(cells) == 2:  # 被合并行
            industry = last_industry  # 使用上一行的值
            company = cells[0].text

Q2: 翻页是 JavaScript 链接,没有 href 怎么办?

现象
<a href="javascript:__doPostBack('Page$2','')">下一页</a>

解决方案

这种通常是 ASP.NET 网站。不要尝试执行 JS,而是模拟 POST 请求

  1. 打开浏览器开发者工具(F12)-> Network。
  2. 点击翻页,查看 Network 中的请求。
  3. 找到 Form Data(表单数据),通常包含 __EVENTTARGET, __EVENTARGUMENT, __VIEWSTATE 等隐藏字段。
  4. 使用 fetcher.fetch_with_post 发送包含这些数据的请求。

Q3: 中文显示为乱码()

原因

政府/高校老网站常用 GBK 或 GB2312 编码,而 requests 默认推测为 ISO-8859-1。

解决方案

python 复制代码
# 手动指定编码
response = requests.get(url)
response.encoding = 'gbk'  # 或 'gb18030' (兼容性更好)
html = response.text

Q4: 页面能看到数据,但爬下来是空的

原因

数据是通过 Ajax 动态加载的,HTML 源码里只有一个空的 <div id="list"></div>

解决方案

  1. 打开 Network -> XHR/Fetch。
  2. 刷新页面,寻找返回 JSON 数据的接口(通常叫 getList, query, search)。
  3. 直接请求这个 API 接口,解析 JSON 数据(比解析 HTML 更简单!)。

1️⃣2️⃣ 进阶优化(Advanced Optimization)

1. 增量更新(避免重复爬取)

招聘会列表是不断更新的,我们需要一种机制只爬新的。

实现思路

在数据库中记录已爬取的 URL 的哈希值。

python 复制代码
def crawl_incremental(self):
    # 1. 获取库中已有的URL
    existing_urls = self.storage.get_all_urls()
    
    # 2. 爬取列表
    for fair in fair_list:
        if fair['url'] in existing_urls:
            logger.info(f"⏭️ 跳过已爬取: {fair['name']}")
            continue  # 跳过
            
        # 3. 爬取新详情
        self.crawl_fair_detail(fair)

2. 行业分类智能化

原始数据中的行业五花八门(如"IT"、"软体开发"、"互联网+")。为了方便分析,可以建立映射表。

实现思路

python 复制代码
INDUSTRY_MAP = {
    '计算机': '信息技术',
    '软件': '信息技术',
    '互联网': '信息技术',
    '房地产': '建筑地产',
    '土木': '建筑地产',
    '银行': '金融',
    '证券': '金融'
}

def normalize_industry(raw_industry):
    for key, value in INDUSTRY_MAP.items():
        if key in raw_industry:
            return value
    return "其他"

3. 数据可视化分析

爬取数据后,直接生成图表给 HR 或学生看。

python 复制代码
import matplotlib.pyplot as plt

def plot_industry_distribution(storage):
    # 从数据库查询行业统计
    data = storage.query("SELECT industry, COUNT(*) FROM companies GROUP BY industry")
    
    # 画饼图
    plt.pie([x[1] for x in data], labels=[x[0] for x in data], autopct='%1.1f%%')
    plt.title('参展企业行业分布')
    plt.savefig('industry_chart.png')

1️⃣3️⃣ 总结与延伸阅读

📚 项目复盘

通过本项目,我们完成了一个从采集到分析的全流程系统

  1. Fetcher:解决了 Session 管理、编码识别、频率控制等底层问题。
  2. Parser:利用 XPath 灵活处理了列表和表格的解析,兼容多种页面结构。
  3. Cleaner:将杂乱的展位号、行业名称标准化,提升了数据价值。
  4. Storage:实现了 SQLite 持久化和 Excel 导出,满足了不同场景需求。

这不仅仅是一个爬虫,更是一个就业市场情报系统的原型。

🚀 职业发展建议

如果你想将爬虫作为职业技能:

  1. 深入学习 JS 逆向:现在的网站越来越动态化,掌握 Chrome 调试、Hook 技术、AST 混淆还原是高薪必备。
  2. 掌握分布式爬虫:学习 Scrapy-Redis,应对千万级数据量的采集。
  3. 关注数据合规:了解 GDPR、《数据安全法》,知道什么能爬、什么不能爬,保护自己。

🔗 延伸阅读

  • 书籍:《Python3 网络爬虫开发实战》(崔庆才 著)------ 爬虫界的"红宝书"。
  • 工具Playwright ------ 比 Selenium 更快更现代的自动化工具,适合处理动态网站。
  • 数据分析Pandas 官方文档 ------ 学会如何清洗和分析爬下来的数据。

附录:完整项目文件结构

为了方便你复制代码,再次梳理文件清单:

  1. config.py: 配置 URL、Headers、数据库路径。
  2. fetcher.py: 负责网络请求。
  3. parser.py: 负责 HTML 解析。
  4. cleaner.py: 负责数据清洗。
  5. storage.py: 负责数据库和 Excel 操作。
  6. main.py: 程序入口。
  7. requirements.txt: 依赖库。

最后的话

技术本身是中性的,爬虫技术既可以用来恶意抢票,也可以用来聚合信息帮助大学生就业。希望你能善用这项技术,创造真正的社会价值

🌟 文末

好啦~以上就是本期的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

✅ 专栏持续更新中|建议收藏 + 订阅

墙裂推荐订阅专栏 👉 《Python爬虫实战》,本专栏秉承着以"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一期内容都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴 :强烈建议先订阅专栏 《Python爬虫实战》,再按目录大纲顺序学习,效率十倍上升~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】等写成某期实战?

评论区留言告诉我你的需求,我会优先安排实现(更新)哒~


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


✅ 免责声明

本文爬虫思路、相关技术和代码仅用于学习参考,对阅读本文后的进行爬虫行为的用户本作者不承担任何法律责任。

使用或者参考本项目即表示您已阅读并同意以下条款:

  • 合法使用: 不得将本项目用于任何违法、违规或侵犯他人权益的行为,包括但不限于网络攻击、诈骗、绕过身份验证、未经授权的数据抓取等。
  • 风险自负: 任何因使用本项目而产生的法律责任、技术风险或经济损失,由使用者自行承担,项目作者不承担任何形式的责任。
  • 禁止滥用: 不得将本项目用于违法牟利、黑产活动或其他不当商业用途。
  • 使用或者参考本项目即视为同意上述条款,即 "谁使用,谁负责" 。如不同意,请立即停止使用并删除本项目。!!!
相关推荐
专注VB编程开发20年2 小时前
python图片验证码识别selenium爬虫--超级鹰实现自动登录,滑块,点击
数据库·python·mysql
iFeng的小屋2 小时前
【2026最新当当网爬虫分享】用Python爬取千本日本相关图书,自动分析价格分布!
开发语言·爬虫·python
民乐团扒谱机2 小时前
【微科普】3D 演奏蠕虫分析图:解码音乐表演情感的 “可视化语言”
python·可视化·音乐·3d图·3d蠕虫
芝士爱知识a2 小时前
AlphaGBM 深度解析:下一代基于 AI 与蒙特卡洛的智能期权分析平台
数据结构·人工智能·python·股票·alphagbm·ai 驱动的智能期权分析·期权
数研小生3 小时前
关键词搜索京东列表API技术对接指南
大数据·数据库·爬虫
52Hz1183 小时前
力扣230.二叉搜索树中第k小的元素、199.二叉树的右视图、114.二叉树展开为链表
python·算法·leetcode
喵手3 小时前
Python爬虫实战:网页截图归档完全指南 - 构建生产级页面存证与历史回溯系统!
爬虫·python·爬虫实战·零基础python爬虫教学·网页截图归档·历史回溯·生产级方案
张3蜂3 小时前
Python 四大 Web 框架对比解析:FastAPI、Django、Flask 与 Tornado
前端·python·fastapi