Educoder网页抓取实战:用Python爬取国防科技大学招生数据(附完整代码)

Python爬虫:全国大学招生信息(一):爬取数据 (多进程、多线程、代理) 无聊爬爬,仅供学习,无其他用途 这几天在高考派(http://www.gaokaopai.com/)上爬招生信息,其中也绕了不少弯路也学到了许多。 以下为涉及到的模块 import requests from fake_useragent import UserAgent from multiprocessing import Process import urllib.request... 阅读详情

从零到一:构建你的第一个网页数据采集器

最近在辅导几位刚入门编程的朋友时,我发现一个有趣的现象:很多人对“爬虫”这个词既感到好奇又有些畏惧。好奇是因为它听起来很酷,能自动从网上抓取数据;畏惧则是觉得这技术门槛高,涉及复杂的网络协议和反爬机制。其实,爬虫的核心逻辑远比想象中简单——本质上就是模拟浏览器向服务器发送请求,然后解析返回的HTML文档,从中提取我们需要的信息。

今天,我想抛开那些复杂的框架和高级技巧,带大家用最基础的Python标准库,亲手打造一个能实际运行的网页数据采集脚本。我们不会涉及任何需要绕过限制的技术,而是专注于理解HTTP请求、HTML解析和数据清洗这个完整的工作流。无论你是正在完成编程平台作业的学生,还是希望用自动化工具辅助日常工作的职场新人,这篇内容都将为你提供一个清晰、安全的实践路径。

1. 理解网页数据采集的基本原理

在动手写代码之前,我们需要先弄清楚几个关键概念。很多人一上来就急着复制粘贴代码,结果遇到问题却不知道如何调试,根本原因是对底层机制不了解。

网页到底是什么? 从技术角度看,你在浏览器中看到的每一个页面,实际上都是服务器返回的一段文本代码,主要是HTML格式。浏览器的工作就是解析这段代码,将其渲染成可视化的页面。而我们做数据采集,就是跳过渲染环节,直接处理原始的HTML文本。

这里有个重要的区别需要明确:

  • 静态网页:服务器返回的HTML包含了所有显示内容,数据直接嵌入在代码中
  • 动态网页:HTML只是一个骨架,数据通过JavaScript异步加载,需要额外处理

我们今天要处理的是典型的静态网页,这也是初学者最好的起点。

1.1 HTTP请求与响应

当你访问一个网址时,背后发生了什么?简单来说,你的浏览器(客户端)向服务器发送了一个HTTP请求,服务器处理请求后返回一个HTTP响应。我们的爬虫就是要模拟这个过程。

用Python实现这个过程的代码非常简单:

import urllib.request

# 创建一个请求对象
request = urllib.request.Request('http://example.com')

# 发送请求并获取响应
response = urllib.request.urlopen(request)

# 读取响应内容
html_content = response.read()

print(f"状态码: {response.status}")
print(f"响应头: {response.headers}")
print(f"内容长度: {len(html_content)} 字节")

注意:在实际操作中,建议为请求添加适当的请求头,特别是User-Agent,这能让你的请求看起来更像普通浏览器访问,避免被某些网站拒绝。

1.2 HTML结构与数据定位

拿到HTML文档后,下一步就是从中找到我们需要的数据。这就需要对HTML结构有基本的了解。

HTML文档由各种标签组成,形成树状结构。常见的数据通常存储在特定的标签中:

标签常见用途示例
<table>表格数据成绩单、价格列表
<div>内容分区文章正文、产品描述
<span>行内元素价格、评分
<a>超链接导航菜单、相关链接
<p>段落文章内容

对于表格数据,HTML有标准的表示方式:

<table>
  <tr>  <!-- 表格行 -->
    <th>姓名</th>  <!-- 表头单元格 -->
    <th>成绩</th>
  </tr>
  <tr>
    <td>张三</td>  <!-- 数据单元格 -->
    <td>95</td>
  </tr>
</table>

理解这个结构后,我们就可以通过寻找特定的标签模式来提取数据了。

2. 环境准备与工具选择

工欲善其事,必先利其器。虽然我们强调用基础库实现,但合理的工具选择能让开发过程更顺畅。

2.1 Python环境配置

首先确保你安装了Python 3.6或更高版本。可以通过命令行检查:

python --version
# 或
python3 --version

如果你需要同时管理多个Python版本,我推荐使用pyenv(Linux/Mac)或直接安装Anaconda(跨平台)。对于Windows用户,从Python官网下载安装包是最直接的方式。

必要的库安装:

我们主要使用Python标准库,这意味着大部分需要的工具已经内置了。但为了更好的开发体验,可以安装几个辅助工具:

pip install ipython  # 交互式Python环境,方便调试
pip install beautifulsoup4  # 虽然本次不用,但它是更友好的HTML解析器
pip install requests  # 更人性化的HTTP库,可作为学习参考

2.2 开发工具推荐

选择适合自己的代码编辑器很重要。以下是我个人使用过的几种选择对比:

工具优点适合人群
VS Code免费、轻量、插件丰富大多数开发者
PyCharm专业Python IDE、调试功能强大专业Python开发者
Jupyter Notebook交互式、适合数据分析数据科学初学者
Sublime Text启动快、简洁喜欢轻量编辑器的用户

我个人的工作流是:用VS Code写代码,用IPython交互环境测试片段代码。这种组合既保持了项目的完整性,又能快速验证想法。

2.3 浏览器开发者工具的使用

现代浏览器都内置了强大的开发者工具,这是网页数据采集的“瑞士军刀”。以Chrome为例:

  1. 按F12打开开发者工具
  2. 切换到“Elements”标签页,可以看到页面的完整HTML结构
  3. 使用左上角的箭头工具,点击页面上的元素,会自动定位到对应的HTML代码
  4. “Network”标签页可以查看所有的网络请求,包括我们发送的HTTP请求和接收的响应

一个实用技巧:在Elements面板中右键点击某个元素,选择“Copy” -> “Copy selector”或“Copy XPath”,可以快速获取该元素的定位路径,这在编写解析代码时很有参考价值。

3. 实战:构建完整的数据采集流程

现在让我们进入实战环节。我将通过一个完整的案例,展示如何从目标网页采集数据并保存为结构化格式。

3.1 第一步:获取网页内容

获取网页内容是所有数据采集工作的起点。使用urllib.request模块,我们可以轻松实现这个功能。

import urllib.request
import urllib.error

def fetch_webpage(url, timeout=10):
    """
    获取指定URL的网页内容
    
    参数:
        url: 目标网页地址
        timeout: 请求超时时间(秒)
    
    返回:
        解码后的网页文本内容
    """
    # 设置请求头,模拟浏览器访问
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
    }
    
    try:
        # 创建请求对象
        req = urllib.request.Request(url, headers=headers)
        
        # 发送请求
        with urllib.request.urlopen(req, timeout=timeout) as response:
            # 读取二进制内容
            raw_data = response.read()
            
            # 尝试多种编码方式解码
            encodings = ['utf-8', 'gbk', 'gb2312', 'iso-8859-1']
            html_content = None
            
            for encoding in encodings:
                try:
                    html_content = raw_data.decode(encoding)
                    break
                except UnicodeDecodeError:
                    continue
            
            if html_content is None:
                # 如果所有编码都失败,使用替代字符处理
                html_content = raw_data.decode('utf-8', errors='replace')
            
            return html_content
            
    except urllib.error.URLError as e:
        print(f"网络请求失败: {e.reason}")
        return None
    except Exception as e:
        print(f"发生未知错误: {str(e)}")
        return None

# 使用示例
if __name__ == "__main__":
    url = "http://example.com"
    content = fetch_webpage(url)
    
    if content:
        print(f"成功获取网页,内容长度: {len(content)} 字符")
        # 保存到文件以便后续分析
        with open("webpage.html", "w", encoding="utf-8") as f:
            f.write(content)

这段代码有几个关键点需要注意:

  1. 异常处理:网络请求可能因各种原因失败,良好的异常处理能让程序更健壮
  2. 编码检测:不同网站可能使用不同的字符编码,尝试多种编码可以提高成功率
  3. 超时设置:避免因网络问题导致程序长时间挂起
  4. 请求头设置:合理的User-Agent可以减少被拒绝的概率

3.2 第二步:解析HTML提取目标数据

获取到HTML内容后,下一步就是从中提取我们需要的数据。这里我展示两种方法:字符串查找和正则表达式。

方法一:基于字符串查找的提取

对于结构简单、模式固定的网页,使用字符串查找方法就足够了:

def extract_links_by_text(html_content, keyword, base_url=""):
    """
    通过关键词查找并提取链接
    
    参数:
        html_content: HTML文本内容
        keyword: 要查找的关键词
        base_url: 基础URL,用于构建完整链接
    
    返回:
        找到的链接列表
    """
    links = []
    start_index = 0
    
    while True:
        # 查找关键词位置
        keyword_pos = html_content.find(keyword, start_index)
        if keyword_pos == -1:
            break
        
        # 向前查找href属性
        href_start = html_content.rfind('href="', 0, keyword_pos)
        if href_start == -1:
            href_start = html_content.rfind("href='", 0, keyword_pos)
            quote_char = "'"
        else:
            quote_char = '"'
        
        if href_start != -1:
            # 找到href属性开始位置
            url_start = href_start + 6  # href=" 或 href=' 的长度
            
            # 查找URL结束位置
            url_end = html_content.find(quote_char, url_start)
            
            if url_end != -1:
                url = html_content[url_start:url_end]
                
                # 处理相对URL
                if url.startswith("http"):
                    full_url = url
                else:
                    full_url = base_url.rstrip("/") + "/" + url.lstrip("/")
                
                links.append(full_url)
        
        # 更新查找起始位置
        start_index = keyword_pos + len(keyword)
    
    return links

方法二:使用正则表达式提取

对于更复杂的模式匹配,正则表达式是更强大的工具:

import re

def extract_table_data(html_content):
    """
    使用正则表达式提取表格数据
    
    参数:
        html_content: HTML文本内容
    
    返回:
        二维列表形式的表格数据
    """
    # 查找所有表格
    table_pattern = r'<table[^>]*>(.*?)</table>'
    tables = re.findall(table_pattern, html_content, re.DOTALL | re.IGNORECASE)
    
    all_table_data = []
    
    for table_html in tables:
        # 查找所有行
        row_pattern = r'<tr[^>]*>(.*?)</tr>'
        rows = re.findall(row_pattern, table_html, re.DOTALL | re.IGNORECASE)
        
        table_data = []
        
        for row_html in rows:
            # 查找所有单元格
            cell_pattern = r'<t[dh][^>]*>(.*?)</t[dh]>'
            cells = re.findall(cell_pattern, row_html, re.DOTALL | re.IGNORECASE)
            
            # 清洗单元格内容
            cleaned_cells = []
            for cell in cells:
                # 移除HTML标签
                cell = re.sub(r'<[^>]+>', '', cell)
                # 处理HTML实体
                cell = cell.replace('&nbsp;', ' ')
                cell = cell.replace('&amp;', '&')
                cell = cell.replace('&lt;', '<')
                cell = cell.replace('&gt;', '>')
                # 去除空白字符
                cell = ' '.join(cell.split())
                cleaned_cells.append(cell)
            
            if cleaned_cells:  # 忽略空行
                table_data.append(cleaned_cells)
        
        if table_data:  # 忽略空表
            all_table_data.append(table_data)
    
    return all_table_data

提示:正则表达式虽然强大,但编写复杂的模式容易出错。建议先在小段文本上测试,确保模式能正确匹配目标内容。对于复杂的HTML解析,BeautifulSoup是更好的选择,但理解正则表达式的工作原理仍然很重要。

3.3 第三步:数据清洗与格式化

从HTML中提取的数据通常包含各种噪音,需要进行清洗才能使用。常见的数据清洗任务包括:

  • 移除多余的空白字符(空格、制表符、换行符)
  • 处理HTML实体(如&nbsp;、&amp;等)
  • 转换数字格式
  • 处理缺失值

下面是一个综合的数据清洗函数:

def clean_extracted_data(data_list):
    """
    清洗提取的数据
    
    参数:
        data_list: 原始数据列表
    
    返回:
        清洗后的数据
    """
    cleaned_data = []
    
    for item in data_list:
        if isinstance(item, list):
            # 递归处理嵌套列表
            cleaned_data.append(clean_extracted_data(item))
        elif isinstance(item, str):
            # 字符串清洗
            cleaned = item.strip()
            
            # 替换常见HTML实体
            html_entities = {
                '&nbsp;': ' ',
                '&amp;': '&',
                '&lt;': '<',
                '&gt;': '>',
                '&quot;': '"',
                '&#39;': "'"
            }
            
            for entity, replacement in html_entities.items():
                cleaned = cleaned.replace(entity, replacement)
            
            # 合并多个连续空格
            cleaned = ' '.join(cleaned.split())
            
            # 尝试转换为数字
            if cleaned.replace('.', '', 1).replace('-', '', 1).isdigit():
                if '.' in cleaned:
                    try:
                        cleaned = float(cleaned)
                    except ValueError:
                        pass
                else:
                    try:
                        cleaned = int(cleaned)
                    except ValueError:
                        pass
            
            cleaned_data.append(cleaned)
        else:
            cleaned_data.append(item)
    
    return cleaned_data

3.4 第四步:数据存储与导出

清洗后的数据需要保存到文件或数据库中,以便后续使用。这里介绍几种常见的存储方式:

1. 保存为文本文件

最简单的存储方式,适合小规模数据:

def save_as_text(data, filename, delimiter='\t'):
    """
    将数据保存为文本文件
    
    参数:
        data: 要保存的数据(二维列表)
        filename: 输出文件名
        delimiter: 列分隔符
    """
    with open(filename, 'w', encoding='utf-8') as f:
        for row in data:
            if isinstance(row, list):
                line = delimiter.join(str(cell) for cell in row)
            else:
                line = str(row)
            f.write(line + '\n')
    print(f"数据已保存到 {filename}")

2. 保存为CSV文件

CSV(逗号分隔值)是更结构化的文本格式,能被Excel等工具直接打开:

import csv

def save_as_csv(data, filename):
    """
    将数据保存为CSV文件
    
    参数:
        data: 要保存的数据(二维列表)
        filename: 输出文件名
    """
    with open(filename, 'w', encoding='utf-8', newline='') as f:
        writer = csv.writer(f)
        writer.writerows(data)
    print(f"数据已保存到 {filename}")

3. 保存为JSON文件

JSON格式适合保存结构化数据,便于其他程序读取:

import json

def save_as_json(data, filename):
    """
    将数据保存为JSON文件
    
    参数:
        data: 要保存的数据
        filename: 输出文件名
    """
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    print(f"数据已保存到 {filename}")

4. 完整案例:多页面数据采集系统

现在让我们把这些知识点整合起来,构建一个完整的系统,用于采集多个相关页面的数据。

4.1 系统架构设计

我们的系统需要处理以下几个核心任务:

  1. 主页面解析:从目录页提取所有子页面的链接
  2. 子页面采集:遍历所有子页面并获取内容
  3. 数据提取:从每个子页面中提取目标数据
  4. 数据整合:将所有数据合并并保存

下面是完整的实现代码:

import urllib.request
import urllib.error
import re
import time
import json
from typing import List, Dict, Any
import os

class WebDataCollector:
    """网页数据采集器"""
    
    def __init__(self, base_url: str, output_dir: str = "output"):
        """
        初始化采集器
        
        参数:
            base_url: 基础URL
            output_dir: 输出目录
        """
        self.base_url = base_url
        self.output_dir = output_dir
        self.session_headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
            'Connection': 'keep-alive'
        }
        
        # 创建输出目录
        os.makedirs(output_dir, exist_ok=True)
    
    def fetch_page(self, url: str, retry_count: int = 3) -> str:
        """
        获取网页内容,支持重试
        
        参数:
            url: 目标URL
            retry_count: 重试次数
        
        返回:
            网页HTML内容
        """
        for attempt in range(retry_count):
            try:
                req = urllib.request.Request(url, headers=self.session_headers)
                with urllib.request.urlopen(req, timeout=15) as response:
                    raw_data = response.read()
                    
                    # 检测编码
                    encoding = response.headers.get_content_charset()
                    if not encoding:
                        # 尝试从HTML meta标签检测编码
                        encoding_match = re.search(
                            r'<meta[^>]*charset=["\']?([\w-]+)["\']?',
                            raw_data[:1000].decode('ascii', errors='ignore')
                        )
                        encoding = encoding_match.group(1) if encoding_match else 'utf-8'
                    
                    return raw_data.decode(encoding)
                    
            except Exception as e:
                if attempt == retry_count - 1:
                    print(f"获取 {url} 失败: {str(e)}")
                    return ""
                print(f"第{attempt + 1}次尝试失败,{retry_count - attempt - 1}次后重试...")
                time.sleep(2 ** attempt)  # 指数退避
        
        return ""
    
    def extract_subpage_links(self, html_content: str, pattern: str) -> List[str]:
        """
        提取子页面链接
        
        参数:
            html_content: 主页面HTML内容
            pattern: 用于识别链接的模式
        
        返回:
            子页面URL列表
        """
        # 使用正则表达式查找所有匹配的链接
        link_pattern = rf'<a[^>]*href=["\']([^"\']*{pattern}[^"\']*)["\'][^>]*>'
        matches = re.findall(link_pattern, html_content, re.IGNORECASE)
        
        # 去重并构建完整URL
        unique_links = []
        seen = set()
        
        for link in matches:
            if link in seen:
                continue
            seen.add(link)
            
            # 构建完整URL
            if link.startswith('http'):
                full_url = link
            elif link.startswith('/'):
                # 处理绝对路径
                base_domain = re.match(r'(https?://[^/]+)', self.base_url).group(1)
                full_url = base_domain + link
            else:
                # 处理相对路径
                full_url = self.base_url.rstrip('/') + '/' + link.lstrip('/')
            
            unique_links.append(full_url)
        
        return unique_links
    
    def extract_table_data(self, html_content: str, table_index: int = 0) -> List[List[str]]:
        """
        提取指定表格的数据
        
        参数:
            html_content: 网页HTML内容
            table_index: 表格索引(0表示第一个表格)
        
        返回:
            表格数据(二维列表)
        """
        # 查找所有表格
        tables = re.findall(r'<table[^>]*>(.*?)</table>', 
                           html_content, re.DOTALL | re.IGNORECASE)
        
        if not tables or table_index >= len(tables):
            return []
        
        target_table = tables[table_index]
        
        # 提取所有行
        rows = re.findall(r'<tr[^>]*>(.*?)</tr>', 
                         target_table, re.DOTALL | re.IGNORECASE)
        
        table_data = []
        
        for row in rows:
            # 提取所有单元格
            cells = re.findall(r'<t[dh][^>]*>(.*?)</t[dh]>', 
                              row, re.DOTALL | re.IGNORECASE)
            
            if not cells:
                continue
            
            # 清洗单元格内容
            cleaned_cells = []
            for cell in cells:
                # 移除HTML标签
                cell = re.sub(r'<[^>]+>', '', cell)
                # 解码HTML实体
                cell = self._decode_html_entities(cell)
                # 规范化空白字符
                cell = ' '.join(cell.split())
                cleaned_cells.append(cell)
            
            table_data.append(cleaned_cells)
        
        return table_data
    
    def _decode_html_entities(self, text: str) -> str:
        """解码HTML实体"""
        html_entities = {
            '&nbsp;': ' ', '&amp;': '&', '&lt;': '<', '&gt;': '>',
            '&quot;': '"', '&#39;': "'", '&ndash;': '-', '&mdash;': '—',
            '&hellip;': '...', '&copy;': '©', '&reg;': '®'
        }
        
        for entity, char in html_entities.items():
            text = text.replace(entity, char)
        
        # 处理数字实体(如&#1234;)
        def replace_decimal(match):
            code = int(match.group(1))
            return chr(code) if code < 0x110000 else match.group(0)
        
        text = re.sub(r'&#(\d+);', replace_decimal, text)
        
        # 处理十六进制实体(如&#x4e2d;)
        def replace_hex(match):
            code = int(match.group(1), 16)
            return chr(code) if code < 0x110000 else match.group(0)
        
        text = re.sub(r'&#x([0-9a-fA-F]+);', replace_hex, text)
        
        return text
    
    def collect_data(self, main_url: str, link_pattern: str) -> Dict[str, Any]:
        """
        执行完整的数据采集流程
        
        参数:
            main_url: 主页面URL
            link_pattern: 子页面链接匹配模式
        
        返回:
            采集到的所有数据
        """
        print(f"开始采集主页面: {main_url}")
        
        # 1. 获取主页面内容
        main_html = self.fetch_page(main_url)
        if not main_html:
            print("无法获取主页面内容")
            return {}
        
        # 保存主页面内容供调试
        with open(os.path.join(self.output_dir, "main_page.html"), 
                 "w", encoding="utf-8") as f:
            f.write(main_html)
        
        # 2. 提取子页面链接
        subpage_urls = self.extract_subpage_links(main_html, link_pattern)
        print(f"找到 {len(subpage_urls)} 个子页面")
        
        all_data = {}
        
        # 3. 遍历所有子页面
        for i, url in enumerate(subpage_urls, 1):
            print(f"正在处理子页面 {i}/{len(subpage_urls)}: {url}")
            
            # 添加延迟,避免请求过快
            if i > 1:
                time.sleep(1)
            
            # 获取子页面内容
            subpage_html = self.fetch_page(url)
            if not subpage_html:
                print(f"跳过无法访问的页面: {url}")
                continue
            
            # 4. 提取表格数据
            table_data = self.extract_table_data(subpage_html)
            
            if table_data:
                # 使用URL的最后部分作为键名
                key = url.split('/')[-1].split('.')[0] or f"page_{i}"
                all_data[key] = {
                    'url': url,
                    'data': table_data,
                    'timestamp': time.strftime("%Y-%m-%d %H:%M:%S")
                }
                
                # 保存单个页面的数据
                self._save_single_page(key, table_data)
            
            print(f"  提取到 {len(table_data)} 行数据")
        
        # 5. 保存所有数据
        self._save_all_data(all_data)
        
        return all_data
    
    def _save_single_page(self, page_name: str, data: List[List[str]]):
        """保存单个页面的数据"""
        # 保存为CSV
        csv_path = os.path.join(self.output_dir, f"{page_name}.csv")
        with open(csv_path, 'w', encoding='utf-8', newline='') as f:
            import csv
            writer = csv.writer(f)
            writer.writerows(data)
        
        # 保存为JSON
        json_path = os.path.join(self.output_dir, f"{page_name}.json")
        with open(json_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
    
    def _save_all_data(self, all_data: Dict[str, Any]):
        """保存所有数据"""
        # 保存汇总数据
        summary_path = os.path.join(self.output_dir, "summary.json")
        with open(summary_path, 'w', encoding='utf-8') as f:
            json.dump(all_data, f, ensure_ascii=False, indent=2)
        
        # 生成数据统计报告
        stats = {
            'total_pages': len(all_data),
            'total_rows': sum(len(page['data']) for page in all_data.values()),
            'pages': {key: len(data['data']) for key, data in all_data.items()},
            'collection_time': time.strftime("%Y-%m-%d %H:%M:%S")
        }
        
        stats_path = os.path.join(self.output_dir, "statistics.json")
        with open(stats_path, 'w', encoding='utf-8') as f:
            json.dump(stats, f, ensure_ascii=False, indent=2)
        
        print(f"\n数据采集完成!")
        print(f"共处理 {stats['total_pages']} 个页面")
        print(f"共提取 {stats['total_rows']} 行数据")
        print(f"数据已保存到 {self.output_dir} 目录")

# 使用示例
if __name__ == "__main__":
    # 配置采集器
    collector = WebDataCollector(
        base_url="http://example.com/directory",
        output_dir="collected_data"
    )
    
    # 执行数据采集
    data = collector.collect_data(
        main_url="http://example.com/directory/index.html",
        link_pattern="detail"  # 匹配包含"detail"的链接
    )

4.2 错误处理与日志记录

在实际运行中,可能会遇到各种问题。完善的错误处理和日志记录能帮助我们快速定位问题:

import logging
from datetime import datetime

def setup_logging(log_dir="logs"):
    """配置日志系统"""
    os.makedirs(log_dir, exist_ok=True)
    
    log_filename = datetime.now().strftime("crawler_%Y%m%d_%H%M%S.log")
    log_path = os.path.join(log_dir, log_filename)
    
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
        handlers=[
            logging.FileHandler(log_path, encoding='utf-8'),
            logging.StreamHandler()  # 同时输出到控制台
        ]
    )
    
    return logging.getLogger(__name__)

class RobustDataCollector(WebDataCollector):
    """增强版数据采集器,包含错误恢复功能"""
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.logger = setup_logging()
        self.failed_urls = []
    
    def fetch_page_with_retry(self, url, max_retries=3):
        """带重试机制的页面获取"""
        for retry in range(max_retries):
            try:
                self.logger.info(f"尝试获取 {url} (第{retry + 1}次)")
                html = self.fetch_page(url)
                
                if html and len(html) > 100:  # 简单的内容有效性检查
                    return html
                else:
                    self.logger.warning(f"获取的内容过短或为空: {url}")
                    
            except Exception as e:
                self.logger.error(f"获取 {url} 失败: {str(e)}")
                
            if retry < max_retries - 1:
                wait_time = 2 ** retry  # 指数退避
                self.logger.info(f"等待 {wait_time} 秒后重试...")
                time.sleep(wait_time)
        
        self.failed_urls.append(url)
        self.logger.error(f"最终失败: {url}")
        return None
    
    def resume_from_checkpoint(self, checkpoint_file):
        """从检查点恢复"""
        if os.path.exists(checkpoint_file):
            with open(checkpoint_file, 'r', encoding='utf-8') as f:
                checkpoint = json.load(f)
            
            self.logger.info(f"从检查点恢复: {checkpoint_file}")
            return checkpoint.get('processed_urls', []), checkpoint.get('pending_urls', [])
        
        return [], []
    
    def save_checkpoint(self, checkpoint_file, processed_urls, pending_urls):
        """保存检查点"""
        checkpoint = {
            'processed_urls': processed_urls,
            'pending_urls': pending_urls,
            'timestamp': time.time()
        }
        
        with open(checkpoint_file, 'w', encoding='utf-8') as f:
            json.dump(checkpoint, f, ensure_ascii=False, indent=2)

4.3 性能优化建议

当需要采集大量页面时,性能就变得重要了。以下是一些优化建议:

  1. 并发请求:对于IO密集型的网络请求,可以使用多线程或异步IO提高效率
  2. 连接复用:保持HTTP连接持久化,减少TCP握手开销
  3. 缓存机制:对已获取的页面进行缓存,避免重复请求
  4. 增量采集:只采集自上次采集以来有变化的内容

这里提供一个简单的多线程示例:

import concurrent.futures
from threading import Lock

class ConcurrentCollector(RobustDataCollector):
    """支持并发采集的收集器"""
    
    def __init__(self, max_workers=5, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.max_workers = max_workers
        self.lock = Lock()
        self.concurrent_data = {}
    
    def collect_concurrently(self, urls):
        """并发采集多个URL"""
        with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            # 提交所有任务
            future_to_url = {
                executor.submit(self.fetch_page_with_retry, url): url 
                for url in urls
            }
            
            # 处理完成的任务
            for future in concurrent.futures.as_completed(future_to_url):
                url = future_to_url[future]
                try:
                    html = future.result()
                    if html:
                        # 提取数据
                        table_data = self.extract_table_data(html)
                        
                        with self.lock:
                            self.concurrent_data[url] = {
                                'html_length': len(html),
                                'data_rows': len(table_data),
                                'sample': table_data[:2] if table_data else []
                            }
                        
                        self.logger.info(f"完成: {url} ({len(table_data)} 行数据)")
                    else:
                        self.logger.warning(f"无内容: {url}")
                        
                except Exception as e:
                    self.logger.error(f"处理 {url} 时出错: {str(e)}")
        
        return self.concurrent_data

5. 最佳实践与注意事项

在结束之前,我想分享一些在实际项目中积累的经验和注意事项。这些建议能帮助你避免常见的陷阱,写出更健壮、更可维护的代码。

5.1 遵守网站规则与道德准则

数据采集虽然技术上有趣,但必须遵守一定的规则:

  • 查看robots.txt:大多数网站都有robots.txt文件,指明了哪些页面允许爬取
  • 尊重网站负载:在请求间添加适当延迟,避免对目标网站造成过大压力
  • 遵守使用条款:查看网站的服务条款,确保你的采集行为不被禁止
  • 仅采集公开数据:不要尝试获取需要登录或付费才能访问的内容
  • 注明数据来源:如果公开发布采集的数据,应注明原始来源

这里有一个检查robots.txt的简单实现:

import urllib.robotparser

def check_robots_permission(url, user_agent="*"):
    """
    检查robots.txt是否允许爬取指定URL
    
    参数:
        url: 要检查的URL
        user_agent: 用户代理字符串
    
    返回:
        是否允许爬取
    """
    try:
        # 解析robots.txt URL
        from urllib.parse import urlparse
        parsed = urlparse(url)
        robots_url = f"{parsed.scheme}://{parsed.netloc}/robots.txt"
        
        # 创建解析器
        rp = urllib.robotparser.RobotFileParser()
        rp.set_url(robots_url)
        rp.read()
        
        return rp.can_fetch(user_agent, url)
        
    except Exception as e:
        print(f"检查robots.txt时出错: {str(e)}")
        # 如果无法读取robots.txt,默认允许(但应谨慎)
        return True

5.2 代码质量与可维护性

写出能工作的代码是一回事,写出易于维护的代码是另一回事:

1. 模块化设计 将功能分解为独立的函数和类,每个部分只做一件事。这样不仅便于测试,也方便后续修改和扩展。

2. 配置外部化 不要将URL、路径等硬编码在代码中。使用配置文件或命令行参数:

import configparser
import argparse

def load_config(config_file="config.ini"):
    """加载配置文件"""
    config = configparser.ConfigParser()
    config.read(config_file, encoding='utf-8')
    return config

def parse_arguments():
    """解析命令行参数"""
    parser = argparse.ArgumentParser(description='网页数据采集器')
    parser.add_argument('--url', help='目标URL')
    parser.add_argument('--output', default='output', help='输出目录')
    parser.add_argument('--workers', type=int, default=3, help='并发工作数')
    return parser.parse_args()

3. 完善的文档 为重要的函数和类添加文档字符串,说明参数、返回值和功能:

def extract_data(html, pattern, options=None):
    """
    从HTML中提取匹配指定模式的数据
    
    参数:
        html: HTML文本内容
        pattern: 正则表达式模式
        options: 可选配置字典,包含:
            - case_sensitive: 是否区分大小写(默认False)
            - multi_line: 是否多行匹配(默认False)
            - extract_groups: 提取匹配组还是整个匹配(默认True)
    
    返回:
        匹配到的字符串列表
    """
    if options is None:
        options = {}
    
    flags = 0
    if not options.get('case_sensitive', False):
        flags |= re.IGNORECASE
    if options.get('multi_line', False):
        flags |= re.MULTILINE
    
    matches = re.findall(pattern, html, flags)
    
    # 处理提取逻辑...
    return processed_matches

5.3 调试技巧与工具

遇到问题时,有效的调试方法能节省大量时间:

1. 保存中间结果 在关键步骤保存中间结果到文件,便于分析问题:

def debug_save(content, filename, description=""):
    """保存调试信息"""
    debug_dir = "debug_output"
    os.makedirs(debug_dir, exist_ok=True)
    
    path = os.path.join(debug_dir, filename)
    with open(path, 'w', encoding='utf-8') as f:
        if description:
            f.write(f"# {description}\n\n")
        f.write(content)
    
    print(f"调试信息已保存: {path}")

2. 使用交互式环境 在IPython或Jupyter中逐步测试代码片段:

# 在IPython中测试
html = fetch_webpage("http://example.com")
# 查看前500个字符
print(html[:500])
# 测试正则表达式
pattern = r'<title>(.*?)</title>'
match = re.search(pattern, html)
if match:
    print(f"标题: {match.group(1)}")

3. 可视化解析结果 对于复杂的HTML结构,可以将其转换为更易读的形式:

def pretty_print_html(html, max_length=1000):
    """美化HTML输出,便于阅读"""
    from html.parser import HTMLParser
    
    class SimpleParser(HTMLParser):
        def __init__(self):
            super().__init__()
            self.indent = 0
            self.result = []
        
        def handle_starttag(self, tag, attrs):
            indent = "  " * self.indent
            attr_str = " ".join(f'{k}="{v}"' for k, v in attrs)
            self.result.append(f"{indent}<{tag} {attr_str}>")
            self.indent += 1
        
        def handle_endtag(self, tag):
            self.indent -= 1
            indent = "  " * self.indent
            self.result.append(f"{indent}</{tag}>")
        
        def handle_data(self, data):
            if data.strip():
                indent = "  " * self.indent
                self.result.append(f"{indent}{data.strip()}")
    
    parser = SimpleParser()
    parser.feed(html[:max_length])
    return "\n".join(parser.result)

5.4 常见问题与解决方案

在实际操作中,你可能会遇到以下问题:

问题1:编码不一致导致乱码

  • 症状:提取的中文显示为乱码
  • 解决方案:尝试多种编码,或从HTTP头、HTML meta标签检测编码

问题2:网站结构变化导致提取失败

  • 症状:之前能正常运行的代码突然无法提取数据
  • 解决方案:使用更宽松的匹配模式,或添加多种匹配策略

问题3:请求被拒绝

  • 症状:返回403错误或验证码页面
  • 解决方案:添加合理的请求头,控制请求频率,或使用会话保持

问题4:数据格式不一致

  • 症状:同一网站不同页面的数据格式不同
  • 解决方案:编写更健壮的解析逻辑,或添加数据清洗和标准化步骤

这里提供一个处理动态内容的简单示例(注意:仅适用于简单的JavaScript渲染):

def extract_dynamic_content(url):
    """
    尝试提取简单动态加载的内容
    
    注意:这种方法有限,复杂的动态网站需要Selenium等工具
    """
    # 有些网站的数据在JavaScript变量中
    html = fetch_webpage(url)
    
    # 查找JSON数据
    json_pattern = r'var\s+data\s*=\s*(\{.*?\});'
    matches = re.findall(json_pattern, html, re.DOTALL)
    
    if matches:
        try:
            import json
            data = json.loads(matches[0])
            return data
        except json.JSONDecodeError:
            pass
    
    # 查找JavaScript数组
    array_pattern = r'var\s+list\s*=\s*(\[.*?\]);'
    matches = re.findall(array_pattern, html, re.DOTALL)
    
    if matches:
        try:
            import json
            # 清理JavaScript特定语法
            cleaned = matches[0].replace("'", '"')
            data = json.loads(cleaned)
            return data
        except:
            pass
    
    return None

我在实际项目中遇到过最棘手的问题是一个网站频繁更换HTML结构,导致解析规则需要不断调整。最终解决方案是设计了一套基于多重选择器的解析策略:首先尝试CSS选择器,如果失败则尝试XPath,最后才使用正则表达式。同时建立了监控机制,当解析成功率低于阈值时自动发送警报,这样我们就能及时更新解析规则,而不是等到用户报告问题才发现。

另一个有用的经验是始终保存原始HTML和提取结果的对应关系。这样当解析出现问题时,可以回查原始数据,分析是网站结构变化了,还是我们的解析逻辑有缺陷。我们专门设计了一个版本控制系统来管理这些数据,每个采集任务都会保存时间戳、原始HTML、解析结果和解析规则版本,这个实践在后续的问题排查中发挥了巨大作用。

利用Python爬取各大高校信息并可视化分析,让你填志愿选学校不再迷茫 高考结束了,接下来最重要的就是玩玩玩,然后准备报志愿吧。 关于学校的填报 不管是能填几个平行志愿,一定要把所有志愿和专业都填上,不然滑档都没地方给你哭的。但一定要量力而行,切忌好高骛远,选报志愿留有余地,参考各校专业录取的平均分、位次。 那句口决要牢记。冲一冲、稳一稳、保一保、垫一垫。 “冲一冲”把有希望够得着的理想学校作为A志愿。 “稳一稳”把与自己成绩门当户对的学校作为中间段。 “保一保”把比自己成绩稍低点的学校作为后半段。 “垫一垫”把最后一两个志愿学校作为垫底学校。 这样的话,几所学校就形成了梯度 阅读详情

相关推荐

python爬取研究生招生网招生信息

import requests from bs4 import BeautifulSoup from pandas.core.frame import DataFrame import re import time class Graduate: def __init__(self, province, category): self.head = { ...

wwxy1995的博客 7735

高考成绩发布!教你用Python爬取高校数据:看看哪些学校专业更受宠

今天各地的2023年高考成绩陆续马上可以查询了,考生的志愿填报也随即提上日程。俗话说,七分考,三分报。想必同学们一定不想因高分低报而浪费分数,也不想低分高报而与大学失之交臂。**那么历年的高考人数、录取率如何?哪个地区的高校最多?都有什么专业最热门?**今天我们就用数据来聊一聊。

m0_59164520的博客 3322

Python爬虫:全国大学招生信息(二):生源数据分析(matplotlib)

在上一篇博客(https://blog.youkuaiyun.com/qq_39192827/article/details/87136836)中爬取了6W+条json数据,接下来通过2D可视化来分析这些数据。需要使用的是matplotlib模块。 之前我们获取了一个大学名字对应url的txt,通过大学名字去6w条数据中检索同一学校的各地区招生数进行统计 先随便拿一条数据观察一下 data列表是我们...

Jodness' Blogs 3046

python-大学录取数据爬虫程序

python-大学录取数据爬虫程序 其中运用了python3.7编译器,还有利用pip install requests在CMD中安装了requests第三方库,然后就可以进行我们的程序编译了 以下是获取2017年郑州大学在各个省市的录取数据的python爬虫程序 import requests import re def getHTMLText(url): #向网页发送请求,读取网页内容 ...

JinweiGuo 1685

使用Python爬取学校学生信息!(简单爬虫)

很久木有来写博文啦,发现学校的教务系统很多童鞋都木有修改初始密码,于是博主就想写试用python写一个爬虫小脚本,将木有修改初始密码的童鞋资料扒下来,然后嘿嘿嘿~~~通知他们修改!鉴于写博文时未通知这些受害童鞋们,因此隐去关键信息。首先为初始化方法__init__(self) import urllib import urllib2 import cookielib

BlingSmile的博客 1万+

使用Python爬取高校官网教育数据的全流程实战详解

网站结构解析使用 requests + BS 进行基本爬取使用 XPath 提高解析效率使用 Selenium 和 Playwright 处理 JS 页面数据保存至 Excel/MySQL反爬策略应对与工程化封装。

2201_76125261的博客 1142

Python 3爬取全国高校基本信息

最近接了一个单子,是爬取全国高校的基本信息,高校名字,高校层次,高校地区,招生办电话,招生办官网~ 镇楼图如下: 信息源网址是:https://gkcx.eol.cn/ 开发环境:python3.6(最新3.7也可以的) +pycharm 第三放库:request(网页级请求库),pymysql(l操作mysql库),json(python和json互转的库)其余库自带 思路就参...

memory_qianxiao的博客 6546

爬虫实战——网页抓取及信息提取

当我们想要在浏览器中打开一个网页时,需要在浏览器的地址栏中输入该网页的url,例如在地址栏中输入百度搜索网站的首页url:https://www.baidu.com/ ,点击确认后,浏览器将向服务器发出一个对该网的请求;服务器端收到请求后,会返回该网页的超文本文件,浏览器收到服务器端发来的网页超文本文件后,对其进行解析,然后在窗口中显示该超文本文件对应的网页。通过上一关卡,我们学会了如何对文件中的数据进行分析,现已将2016年国防科技大学录取的分数最高的3个省份,存储至本地文件中。

Junds0的博客 7482

国央企创新负责人如何快速掌握本地科技资源分布与匹配情况?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

利用爬虫获取高校及专业录取分数线的python代码

随着高考的落幕,大家也该估分选大学,突然发现中国教育在线的估分选大学系统开始收费了(3G时代是免费的),所以自己就写了爬虫获取高校各个专业历年在河南的录取分数线、排名,以及大学的基本信息。

SpringBoot+Vue+Mysql社区医院信息平台(源码+文档)

SpringBoot+Vue+Mysql学生宿舍信息管理系统 WEB端软件项目(源码+文档) 项目包含:①完整源码(后端+管理后台+用户前台);②数据库脚本文件;③万字论文文档;④开题报告;⑤答辩PPT;⑥项目运行视频教程。 项目架构: B/S 架构、MVC 分层设计 技术栈: Java、MySQL、SpringBoot、MyBatis-Plus、Token 权限校验、管理后台 Vue+ElementUI+ECharts、用户前台 Layui+jQuery、Maven 构建。 一、角色设计 学生用户: 注册登录、浏览宿舍信息、查询宿舍安排、在线报修、缴费查询、失物招领、桶装水预订、评论交流、收藏等。 商家用户: 注册登录、桶装水品牌与商品信息维护、预订订单查看与处理。 维修人员: 注册登录、查看待处理报修、接单维修、填写报修处理结果与进度反馈。 管理员: 学生管理、班级管理、宿舍信息管理、宿舍安排管理、报修处理管理、缴费信息管理、卫生检查与文明宿舍管理、失物招领管理、桶装水与商家管理、维修人员管理、公告资讯与轮播图管理等。 二、前台功能模块(学生端) 1. 首页展示:轮播图、宿舍公告、最新动态推荐、常用服务快速入口 2. 宿舍信息模块:宿舍列表、楼栋与房间条件筛选、宿舍详情查看、我的宿舍安排查询 3. 在线报修模块:在线提交宿舍设施报修申请、上传故障描述、查看报修进度与维修处理结果 4. 缴费模块:水电等缴费记录查询、缴费状态查看 5. 卫生检查与文明宿舍:宿舍卫生检查结果公示、文明宿舍展示 6. 失物招领模块:失物信息发布、招领信息发布、详情查看、评论互动 7. 桶装水预订:桶装水品牌与商品浏览、在线预订、我的预订记录查询 8. 假期留宿:假期留校申请提交、申请记录查询 9. 评论与收藏:信息评论、查看他人评论、评论回复互动、收藏感兴趣的内容 10. 个人中心...

国央企创新负责人如何实现科技成果的精准识别与高效筛选?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

YOLOv8作物病害检测实战源码

这是一套基于YOLOv8n的农田病虫害目标检测系统,对叶片照片做目标检测,识别健康叶片、褐斑病、白粉病、锈病四类,并给出对应防治建议。全流程纯CPU可跑,配套交付完整Python源码、9章实战教程(md/html/pdf三种格式)、100张自造样本及YOLO标签、训练曲线与混淆矩阵截图、Tkinter界面和答辩PPT。 系统围绕五项功能展开:同一套推理逻辑覆盖图像、视频、摄像头三种来源;图形界面选文件即检测,画面叠彩色框标注病名与置信度,右侧面板同步给出症状与防治措施;检测记录自动写入CSV,统计页一键生成四类病害占比饼图;训练结束自动产出损失与精度曲线;内置防治建议文案库,检测命中即回填面板。 选型用YOLOv8n nano档,参数量约320万、权重约6.2MB,CPU单帧几十毫秒,小样本训练5到8分钟完成。数据由data_gen.py程序化绘制100张叶片图,固定随机种子,重跑分布一致,不依赖公开数据集。依赖全部锁版本,教程第7章整理了19条真实报错及排查方法,第8章给出换模型档、换PlantVillage数据集的迁移路径。 适合做计算机视觉方向课程设计或毕业设计的学生,也适合智慧农业方向想了解检测模型如何落进业务面板的学习者。不需要GPU,不需要系统学过目标检测,教程第3章会按需补齐原理。 使用时先pip install -r requirements.txt装好依赖,download_data.py下载预训练权重,data_gen.py生成样本后train.py训练,完成后用main.py infer批量检测或main.py gui启动界面。

高校技术转移办公室人员如何系统化管理科技成果信息?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

数据挖掘实战:决策树 + 聚类 + 关联规则,零 Key 离线可跑(Python 源码)

数据挖掘实战包。用 Python 标准库实现 CART 风格决策树、K-Means 聚类与 Apriori 关联规则,在虚构「星河便利店」会员表和购物篮上跑通分类、分群与购物篮分析,并给出准确率、轮廓系数、支持度/置信度/提升度。零第三方依赖、零 API Key 即可离线跑通。含 4 个样例、离线冒烟、中文 README(含「使用方法」)。均为作者原创可运行源码。MIT。作者 blog.youkuaiyun.com/mtl1994

科技中介服务机构在技术供需对接中如何实现服务流程的自动化与标准化?.docx

科技中介服务机构在技术供需对接中如何实现服务流程的自动化与标准化?

科技中介服务机构如何构建标准化、模块化的智能服务工具?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

产业园区如何高效对接企业技术需求与高校科研成果?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

上一篇: STM32 HAL库驱动微雪1.54寸墨水屏V2全攻略:从硬件连接到动态刷新优化
下一篇: Windows环境下Git入门:从零开始上传本地项目到GitHub(含常见错误解决)
sql99
博客等级 码龄19年 168粉丝 1905原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值