爬虫第三次作业

gitee: https://gitee.com/jadevoice/data-collection-tas/tree/hw3/

作业① - 图片爬虫实验

一、实验内容

本实验要求爬取中国气象网(http://www.weather.com.cn)的所有图片,并实现单线程和多线程两种爬取方式。

技术栈:

  • Python 3.x
  • requests 库(HTTP请求)
  • BeautifulSoup 库(HTML解析)
  • threading 模块(多线程)
  • concurrent.futures(线程池)

二、实验代码

1. 单线程图片爬虫

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
作业①:单线程图片爬虫
功能:爬取中国气象网的所有图片
"""

import os
import requests
from urllib.parse import urljoin, urlparse
from bs4 import BeautifulSoup


class SingleThreadImageCrawler:
    """单线程图片爬虫类"""

    def __init__(self, base_url, save_dir='images'):
        """
        初始化爬虫
        :param base_url: 目标网站URL
        :param save_dir: 图片保存目录
        """
        self.base_url = base_url
        self.save_dir = save_dir
        self.image_urls = set()  # 已下载的图片URL
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }

        # 创建保存图片的目录
        if not os.path.exists(self.save_dir):
            os.makedirs(self.save_dir)

    def get_html(self, url):
        """获取网页HTML内容"""
        try:
            response = requests.get(url, headers=self.headers, timeout=10)
            response.encoding = response.apparent_encoding
            return response.text
        except Exception as e:
            print(f"获取页面失败 {url}: {str(e)}")
            return None

    def extract_images(self, html, page_url):
        """从HTML中提取图片URL"""
        if not html:
            return []

        soup = BeautifulSoup(html, 'html.parser')
        img_tags = soup.find_all('img')

        image_urls = []
        for img in img_tags:
            img_url = img.get('src') or img.get('data-src')
            if img_url:
                # 将相对URL转换为绝对URL
                img_url = urljoin(page_url, img_url)
                image_urls.append(img_url)

        return image_urls

    def download_image(self, img_url, index):
        """下载单张图片"""
        if img_url in self.image_urls:
            return False

        try:
            print(f"正在下载: {img_url}")
            response = requests.get(img_url, headers=self.headers, timeout=10)

            if response.status_code == 200:
                # 从URL中提取文件扩展名
                parsed_url = urlparse(img_url)
                path = parsed_url.path
                ext = os.path.splitext(path)[1]

                # 如果没有扩展名,默认使用jpg
                if not ext or len(ext) > 5:
                    ext = '.jpg'

                # 保存图片
                filename = f"image_{index:04d}{ext}"
                filepath = os.path.join(self.save_dir, filename)

                with open(filepath, 'wb') as f:
                    f.write(response.content)

                self.image_urls.add(img_url)
                print(f"下载成功: {filename}")
                return True

        except Exception as e:
            print(f"下载失败 {img_url}: {str(e)}")
            return False

    def crawl(self):
        """开始爬取"""
        print("=" * 60)
        print("单线程图片爬虫开始运行")
        print(f"目标网站: {self.base_url}")
        print("=" * 60)

        # 获取首页
        html = self.get_html(self.base_url)
        if not html:
            print("无法获取首页内容")
            return

        # 提取首页图片
        image_urls = self.extract_images(html, self.base_url)
        print(f"找到 {len(image_urls)} 张图片")

        # 下载图片
        download_count = 0
        for i, img_url in enumerate(image_urls, 1):
            if self.download_image(img_url, i):
                download_count += 1

        print(f"成功下载: {download_count} 张图片")


def main():
    """主函数"""
    target_url = "http://www.weather.com.cn"
    crawler = SingleThreadImageCrawler(target_url, save_dir='images/single_thread')
    crawler.crawl()


if __name__ == "__main__":
    main()

运行命令:

python assignment1_single_thread.py

运行结果截图:

image

下载的图片截图:
image


2. 多线程图片爬虫

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
作业①:多线程图片爬虫
功能:使用多线程爬取中国气象网的所有图片
"""

import os
import requests
from urllib.parse import urljoin, urlparse
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading


class MultiThreadImageCrawler:
    """多线程图片爬虫类"""

    def __init__(self, base_url, save_dir='images', max_workers=5):
        """
        初始化爬虫
        :param max_workers: 最大线程数
        """
        self.base_url = base_url
        self.save_dir = save_dir
        self.max_workers = max_workers
        self.image_urls = set()
        self.lock = threading.Lock()  # 线程锁
        self.download_count = 0

        if not os.path.exists(self.save_dir):
            os.makedirs(self.save_dir)

    # ... 其他方法与单线程版本类似 ...

    def download_image(self, img_url, index):
        """下载单张图片(线程安全)"""
        # 使用线程锁检查URL是否已下载
        with self.lock:
            if img_url in self.image_urls:
                return {'success': False, 'message': '已下载'}
            self.image_urls.add(img_url)

        try:
            thread_id = threading.current_thread().name
            print(f"[{thread_id}] 正在下载: {img_url}")

            response = requests.get(img_url, headers=self.headers, timeout=10)

            if response.status_code == 200:
                filename = f"image_{index:04d}.jpg"
                filepath = os.path.join(self.save_dir, filename)

                with open(filepath, 'wb') as f:
                    f.write(response.content)

                with self.lock:
                    self.download_count += 1

                print(f"[{thread_id}] 下载成功: {filename}")
                return {'success': True}

        except Exception as e:
            print(f"下载失败: {str(e)}")
            return {'success': False}

    def crawl(self):
        """使用多线程开始爬取"""
        print(f"多线程图片爬虫开始运行(线程数: {self.max_workers})")

        html = self.get_html(self.base_url)
        image_urls = self.extract_images(html, self.base_url)

        # 使用线程池下载图片
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            futures = []
            for i, img_url in enumerate(image_urls, 1):
                future = executor.submit(self.download_image, img_url, i)
                futures.append(future)

            # 等待所有任务完成
            for future in as_completed(futures):
                future.result()

        print(f"成功下载: {self.download_count} 张图片")


def main():
    target_url = "http://www.weather.com.cn"
    crawler = MultiThreadImageCrawler(target_url, save_dir='images/multi_thread', max_workers=5)
    crawler.crawl()


if __name__ == "__main__":
    main()

运行命令:

python assignment1_multi_thread.py

运行结果截图:
image

下载的图片截图:
image


三、心得体会

通过本次图片爬虫实验,我深入学习了以下内容:

  1. HTTP请求处理:掌握了使用requests库发送HTTP请求,处理响应数据,设置请求头等技巧。

  2. HTML解析:学会使用BeautifulSoup解析HTML文档,定位和提取img标签,处理相对URL转绝对URL的问题。

  3. 多线程编程

    • 理解了Python的GIL(全局解释器锁)机制
    • 学会使用ThreadPoolExecutor创建线程池
    • 掌握了线程锁(threading.Lock)保护共享资源
    • 了解了线程安全的重要性
  4. 性能优化

    • 单线程版本简单但效率低
    • 多线程版本能充分利用IO等待时间,显著提升效率
    • 合理设置线程数很重要(本例中5个线程效果较好)
  5. 异常处理:网络爬虫需要处理各种异常情况,如网络超时、连接失败、文件读写错误等。

  6. 遇到的问题和解决方案

    • 问题1:部分图片URL是相对路径,导致下载失败

      • 解决:使用urljoin将相对URL转换为绝对URL
    • 问题2:多线程版本出现重复下载

      • 解决:使用线程锁保护共享的image_urls集合
    • 问题3:某些图片没有文件扩展名

      • 解决:从Content-Type响应头判断文件类型

总结: 本次实验让我对爬虫的基本原理和多线程编程有了更深入的理解,为后续学习更复杂的爬虫技术打下了良好基础。


作业② - 股票数据爬虫实验

一、实验内容

本实验要求使用Scrapy框架爬取股票相关信息,并将数据存储到MySQL数据库。

目标网站: 东方财富网(https://www.eastmoney.com/)

技术栈:

  • Scrapy框架
  • XPath选择器
  • PyMySQL(MySQL驱动)
  • Pipeline数据处理

数据字段:

  • 序号(id)
  • 股票代码(bStockNo)
  • 股票名称(bStockName)
  • 最新报价(newPrice)
  • 涨跌(upDown)
  • 涨跌幅(upDownRate)
  • 成交量(traVol)
  • 成交额(traAmount)
  • 振幅(amplitude)
  • 最高(high)
  • 最低(low)
  • 今开(open)
  • 昨收(preClose)

二、实验代码

1. Items定义(items.py)

import scrapy

class StockItem(scrapy.Item):
    """股票信息Item类"""
    id = scrapy.Field()              # 序号
    bStockNo = scrapy.Field()        # 股票代码
    bStockName = scrapy.Field()      # 股票名称
    newPrice = scrapy.Field()        # 最新报价
    upDown = scrapy.Field()          # 涨跌
    upDownRate = scrapy.Field()      # 涨跌幅
    traVol = scrapy.Field()          # 成交量
    traAmount = scrapy.Field()       # 成交额
    amplitude = scrapy.Field()       # 振幅
    high = scrapy.Field()            # 最高
    low = scrapy.Field()             # 最低
    open = scrapy.Field()            # 今开
    preClose = scrapy.Field()        # 昨收

2. Pipeline定义(pipelines.py)

import pymysql
from itemadapter import ItemAdapter

class StockMySQLPipeline:
    """MySQL数据库Pipeline"""

    def __init__(self, mysql_host, mysql_port, mysql_user, mysql_password, mysql_db):
        """初始化数据库连接参数"""
        self.mysql_host = mysql_host
        self.mysql_port = mysql_port
        self.mysql_user = mysql_user
        self.mysql_password = mysql_password
        self.mysql_db = mysql_db

    @classmethod
    def from_crawler(cls, crawler):
        """从settings中获取数据库配置"""
        return cls(
            mysql_host=crawler.settings.get('MYSQL_HOST', 'localhost'),
            mysql_port=crawler.settings.get('MYSQL_PORT', 3306),
            mysql_user=crawler.settings.get('MYSQL_USER', 'root'),
            mysql_password=crawler.settings.get('MYSQL_PASSWORD', ''),
            mysql_db=crawler.settings.get('MYSQL_DB', 'stock_db')
        )

    def open_spider(self, spider):
        """爬虫启动时调用,建立数据库连接并创建表"""
        self.conn = pymysql.connect(
            host=self.mysql_host,
            port=self.mysql_port,
            user=self.mysql_user,
            password=self.mysql_password,
            charset='utf8mb4'
        )
        self.cursor = self.conn.cursor()

        # 创建数据库和表
        self.cursor.execute(f"CREATE DATABASE IF NOT EXISTS {self.mysql_db}")
        self.cursor.execute(f"USE {self.mysql_db}")

        create_table_sql = """
        CREATE TABLE IF NOT EXISTS stock_data (
            id INT AUTO_INCREMENT PRIMARY KEY,
            bStockNo VARCHAR(20) NOT NULL,
            bStockName VARCHAR(50) NOT NULL,
            newPrice DECIMAL(10, 2),
            upDown DECIMAL(10, 2),
            upDownRate VARCHAR(20),
            traVol VARCHAR(50),
            traAmount VARCHAR(50),
            amplitude VARCHAR(20),
            high DECIMAL(10, 2),
            low DECIMAL(10, 2),
            open DECIMAL(10, 2),
            preClose DECIMAL(10, 2),
            create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        """
        self.cursor.execute(create_table_sql)
        self.conn.commit()

    def close_spider(self, spider):
        """爬虫关闭时调用,关闭数据库连接"""
        self.cursor.close()
        self.conn.close()

    def process_item(self, item, spider):
        """处理每个Item,将数据插入数据库"""
        adapter = ItemAdapter(item)

        insert_sql = """
        INSERT INTO stock_data
        (bStockNo, bStockName, newPrice, upDown, upDownRate,
         traVol, traAmount, amplitude, high, low, open, preClose)
        VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
        """

        data = (
            adapter.get('bStockNo', ''),
            adapter.get('bStockName', ''),
            adapter.get('newPrice', None),
            adapter.get('upDown', None),
            adapter.get('upDownRate', ''),
            adapter.get('traVol', ''),
            adapter.get('traAmount', ''),
            adapter.get('amplitude', ''),
            adapter.get('high', None),
            adapter.get('low', None),
            adapter.get('open', None),
            adapter.get('preClose', None)
        )

        self.cursor.execute(insert_sql, data)
        self.conn.commit()

        return item

3. Spider爬虫(spiders/stock_spider.py)

import scrapy
import json
from stock_spider.items import StockItem

class EastMoneyStockSpider(scrapy.Spider):
    """东方财富网股票爬虫"""
    name = 'eastmoney_stock'
    allowed_domains = ['eastmoney.com']

    # 东方财富网API接口
    start_urls = ['http://82.push2.eastmoney.com/api/qt/clist/get?...']

    def parse(self, response):
        """解析API响应数据"""
        try:
            # 提取JSON数据
            text = response.text
            start = text.find('(') + 1
            end = text.rfind(')')
            json_str = text[start:end]
            data = json.loads(json_str)

            # 获取股票列表
            if data and 'data' in data:
                stock_list = data['data']['diff']

                for index, stock in enumerate(stock_list, 1):
                    item = StockItem()
                    item['id'] = index
                    item['bStockNo'] = stock.get('f12', '')
                    item['bStockName'] = stock.get('f14', '')
                    item['newPrice'] = stock.get('f2', 0)
                    item['upDown'] = stock.get('f4', 0)
                    item['upDownRate'] = f"{stock.get('f3', 0)}%"
                    item['traVol'] = self._format_volume(stock.get('f5', 0))
                    item['traAmount'] = self._format_amount(stock.get('f6', 0))
                    item['amplitude'] = f"{stock.get('f7', 0)}%"
                    item['high'] = stock.get('f15', 0)
                    item['low'] = stock.get('f16', 0)
                    item['open'] = stock.get('f17', 0)
                    item['preClose'] = stock.get('f18', 0)

                    yield item

        except Exception as e:
            self.logger.error(f"解析数据出错: {str(e)}")

    def _format_volume(self, volume):
        """格式化成交量"""
        try:
            vol = float(volume)
            if vol >= 100000000:
                return f"{vol / 100000000:.2f}亿手"
            elif vol >= 10000:
                return f"{vol / 10000:.2f}万手"
            return f"{vol:.2f}手"
        except:
            return str(volume)

    def _format_amount(self, amount):
        """格式化成交额"""
        try:
            amt = float(amount)
            if amt >= 100000000:
                return f"{amt / 100000000:.2f}亿元"
            elif amt >= 10000:
                return f"{amt / 10000:.2f}万元"
            return f"{amt:.2f}元"
        except:
            return str(amount)

4. Settings配置(settings.py)

BOT_NAME = 'stock_spider'
SPIDER_MODULES = ['stock_spider.spiders']
ROBOTSTXT_OBEY = False
USER_AGENT = 'Mozilla/5.0 ...'

# Pipeline配置
ITEM_PIPELINES = {
    'stock_spider.pipelines.StockMySQLPipeline': 300,
}

# MySQL配置
MYSQL_HOST = 'localhost'
MYSQL_PORT = 3306
MYSQL_USER = 'root'
MYSQL_PASSWORD = 'your_password'
MYSQL_DB = 'stock_db'

LOG_LEVEL = 'INFO'

三、运行结果

运行命令:

cd assignment2
scrapy crawl eastmoney_stock

控制台输出截图:
image

MySQL数据库截图:
image


四、心得体会

通过本次Scrapy股票爬虫实验,我收获颇丰:

  1. Scrapy框架理解

    • Scrapy是一个强大的爬虫框架,提供了完整的爬虫解决方案
    • 框架采用异步IO机制,性能远超普通爬虫
    • Item、Spider、Pipeline职责分明,代码结构清晰
  2. XPath和JSON解析

    • 本实验使用了JSON API接口,避免了复杂的XPath解析
    • 学会了JSONP格式的处理(去除回调函数)
    • 理解了现代网站多使用API接口返回数据
  3. Pipeline数据处理

    • Pipeline是Scrapy的核心组件,负责数据的后处理
    • from_crawler方法可以从settings读取配置
    • open_spider和close_spider管理资源的生命周期
  4. MySQL数据库操作

    • 掌握了PyMySQL的基本用法
    • 学会了动态创建数据库和表
    • 理解了事务的重要性(commit/rollback)
    • 索引的设置可以优化查询性能
  5. 数据格式化

    • 成交量、成交额需要转换为易读格式(亿、万)
    • 涨跌幅需要添加百分号
    • 数据类型的选择很重要(DECIMAL vs VARCHAR)
  6. 遇到的问题和解决方案

    • 问题1:JSONP格式无法直接解析

      • 解决:使用字符串操作提取JSON部分
    • 问题2:MySQL连接失败

      • 解决:检查数据库服务状态和密码配置
    • 问题3:中文乱码

      • 解决:设置charset='utf8mb4'

总结: Scrapy框架大大简化了爬虫开发流程,通过本次实验,我对Scrapy的架构和使用有了深入理解,为今后开发更复杂的爬虫项目打下了基础。


作业③ - 外汇数据爬虫实验

一、实验内容

本实验要求使用Scrapy框架爬取中国银行外汇牌价数据,并存储到MySQL数据库。

目标网站: 中国银行(https://www.boc.cn/sourcedb/whpj/)

技术栈:

  • Scrapy框架
  • XPath选择器
  • PyMySQL
  • Pipeline数据处理

数据字段:

  • Currency(货币名称)
  • Time(时间)
  • TBP(现汇买入价)
  • CBP(现钞买入价)
  • TSP(现汇卖出价)
  • CSP(现钞卖出价)

二、实验代码

1. Items定义(items.py)

import scrapy

class ForexItem(scrapy.Item):
    """外汇牌价Item类"""
    Currency = scrapy.Field()        # 货币名称
    Time = scrapy.Field()            # 时间
    TBP = scrapy.Field()             # 现汇买入价
    CBP = scrapy.Field()             # 现钞买入价
    TSP = scrapy.Field()             # 现汇卖出价
    CSP = scrapy.Field()             # 现钞卖出价
    MiddleRate = scrapy.Field()      # 中行折算价
    PublishTime = scrapy.Field()     # 发布时间

2. Pipeline定义(pipelines.py)

import pymysql
from itemadapter import ItemAdapter

class ForexMySQLPipeline:
    """MySQL数据库Pipeline"""

    def open_spider(self, spider):
        """建立数据库连接并创建表"""
        self.conn = pymysql.connect(
            host=self.mysql_host,
            port=self.mysql_port,
            user=self.mysql_user,
            password=self.mysql_password,
            charset='utf8mb4'
        )
        self.cursor = self.conn.cursor()

        # 创建数据库和表
        self.cursor.execute(f"CREATE DATABASE IF NOT EXISTS {self.mysql_db}")
        self.cursor.execute(f"USE {self.mysql_db}")

        create_table_sql = """
        CREATE TABLE IF NOT EXISTS forex_data (
            id INT AUTO_INCREMENT PRIMARY KEY,
            Currency VARCHAR(50) NOT NULL,
            Time VARCHAR(50),
            TBP DECIMAL(10, 4),
            CBP DECIMAL(10, 4),
            TSP DECIMAL(10, 4),
            CSP DECIMAL(10, 4),
            MiddleRate DECIMAL(10, 4),
            PublishTime VARCHAR(50),
            create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )
        """
        self.cursor.execute(create_table_sql)
        self.conn.commit()

    def process_item(self, item, spider):
        """处理Item并插入数据库"""
        adapter = ItemAdapter(item)

        insert_sql = """
        INSERT INTO forex_data
        (Currency, Time, TBP, CBP, TSP, CSP, MiddleRate, PublishTime)
        VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
        """

        data = (
            adapter.get('Currency', ''),
            adapter.get('Time', ''),
            adapter.get('TBP', None),
            adapter.get('CBP', None),
            adapter.get('TSP', None),
            adapter.get('CSP', None),
            adapter.get('MiddleRate', None),
            adapter.get('PublishTime', '')
        )

        self.cursor.execute(insert_sql, data)
        self.conn.commit()

        return item

3. Spider爬虫(spiders/forex_spider.py)

import scrapy
from forex_spider.items import ForexItem

class BOCForexSpider(scrapy.Spider):
    """中国银行外汇牌价爬虫"""
    name = 'boc_forex'
    allowed_domains = ['boc.cn']
    start_urls = ['https://www.boc.cn/sourcedb/whpj/']

    def parse(self, response):
        """解析外汇牌价页面"""
        # 获取发布时间
        publish_time = response.xpath('//div[@class="publish"]/span/text()').get()
        if publish_time:
            publish_time = publish_time.strip()

        # 定位表格
        table = response.xpath('//div[@class="BOC_main publish"]//table')
        rows = table.xpath('.//tr')[1:]  # 跳过表头

        for row in rows:
            # 提取各列数据
            cols = row.xpath('.//td/text()').getall()
            cols = [col.strip() if col else '' for col in cols]

            if len(cols) >= 6:
                item = ForexItem()
                item['Currency'] = cols[0]          # 货币名称
                item['TBP'] = self._parse_price(cols[1])  # 现汇买入价
                item['CBP'] = self._parse_price(cols[2])  # 现钞买入价
                item['TSP'] = self._parse_price(cols[3])  # 现汇卖出价
                item['CSP'] = self._parse_price(cols[4])  # 现钞卖出价
                item['MiddleRate'] = self._parse_price(cols[5])  # 中行折算价
                item['Time'] = cols[6] if len(cols) > 6 else ''  # 时间
                item['PublishTime'] = publish_time

                yield item

    def _parse_price(self, price_str):
        """解析价格字符串"""
        try:
            if price_str and price_str.strip():
                return float(price_str.strip())
            return None
        except:
            return None

三、运行结果

运行命令:

cd assignment3
scrapy crawl boc_forex

控制台输出截图:
image

MySQL数据库截图:

image

四、心得体会

通过本次外汇爬虫实验,我进一步巩固和提升了爬虫技能:

  1. XPath选择器的应用

    • 掌握了XPath的基本语法和使用方法
    • 学会了使用//定位元素,使用[@class]筛选
    • 理解了相对路径和绝对路径的区别
    • text()方法可以提取文本内容
  2. 表格数据提取

    • 网页表格是常见的数据呈现方式
    • 使用XPath定位表格和行://table//tr
    • 跳过表头:rows = table.xpath('.//tr')[1:]
    • 提取单元格:cols = row.xpath('.//td/text()').getall()
  3. 数据清洗

    • 原始数据往往包含空格、换行等无用字符
    • 使用strip()方法清理字符串
    • 需要进行类型转换(字符串→浮点数)
    • 异常处理很重要,避免程序崩溃
  4. 数据库设计

    • 价格字段使用DECIMAL类型,精确到小数点后4位
    • 时间字段使用VARCHAR存储,也可以用DATETIME
    • 建立索引提高查询效率
    • 添加create_time字段记录数据插入时间
  5. Scrapy最佳实践

    • 遵守robots.txt协议(实验环境可以关闭)
    • 设置合理的下载延迟,避免被封IP
    • 使用正确的User-Agent
    • 启用AutoThrottle自动限速
  6. 遇到的问题和解决方案

    • 问题1:XPath表达式不匹配

      • 解决:使用浏览器开发者工具检查HTML结构
    • 问题2:数据中有空值

      • 解决:使用try-except处理,设置默认值None
    • 问题3:价格字符串无法转换

      • 解决:先判断是否为空,再进行float转换

总结: 外汇爬虫实验让我对XPath选择器和表格数据提取有了深入理解。相比作业②使用API接口,本次使用XPath解析HTML更贴近实际爬虫场景。通过三次作业的学习,我已经能够独立完成各种类型的网络爬虫开发。

posted @ 2025-11-12 16:08  wjord2023  阅读(8)  评论(0)    收藏  举报