数据采集技术 - 第三次作业:Scrapy框架与数据库存储

作业①

1)、图片爬取实验

1. 实验描述
指定一个网站(以中国气象网为例),爬取该网站下的所有图片。

难点:需分别实现单线程和多线程两种方式,并控制总下载数量不超过学号后3位(130张)。

2. 核心代码

(1) 环境适配与SSL修复
在 MacOS 环境下,Python 的 SSL 模块与系统底层的 OpenSSL 库版本不兼容,会导致 HTTPS 请求失败。必须在代码头部注入 pyopenssl 库。

import urllib3
try:
    import urllib3.contrib.pyopenssl
    urllib3.contrib.pyopenssl.inject_into_urllib3()
except ImportError:
    print("Warning: pyOpenSSL not installed")
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

(2) 学号限制与单线程下载

import os
import time
import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin

# ... (导入SSL修复代码同上)

STUDENT_ID = '102302130'
MAX_IMAGES = int(STUDENT_ID[-3:])  # 限制 130 张
TARGET_URL = '[http://www.weather.com.cn/](http://www.weather.com.cn/)'
SAVE_DIR = 'images'

def main():
    if not os.path.exists(SAVE_DIR):
        os.makedirs(SAVE_DIR)
    
    session = requests.Session()
    session.headers.update({
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
    })

    try:
        response = session.get(TARGET_URL, verify=False)
        response.encoding = 'utf-8'
        soup = BeautifulSoup(response.text, 'html.parser')
        img_tags = soup.find_all('img')
    except Exception as e:
        print(f"请求失败: {e}")
        return

    current_count = 0
    for index, img in enumerate(img_tags):
        # 实时检查是否达到限制
        if current_count >= MAX_IMAGES:
            print(f"【限制触发】已达到学号限制的 {MAX_IMAGES} 张图片,停止下载。")
            break
            
        src = img.get('src')
        if src:
            full_url = urljoin(TARGET_URL, src)
            if not full_url.startswith('http'): continue
            
            ext = os.path.splitext(full_url)[-1]
            if not ext or len(ext) > 5: ext = '.jpg'
            file_name = f"image_{index}{ext}"
            
            # 下载逻辑
            try:
                res = session.get(full_url, timeout=10, verify=False)
                if res.status_code == 200:
                    with open(os.path.join(SAVE_DIR, file_name), 'wb') as f:
                        f.write(res.content)
                    print(f"[下载成功] {file_name}")
                    current_count += 1
            except Exception as e:
                print(f"[下载出错] {e}")


(3) 多线程并发逻辑

from concurrent.futures import ThreadPoolExecutor

# ... (前面的解析代码与单线程类似) ...

# 构建下载任务列表
download_list = []
for index, img in enumerate(img_tags):
    src = img.get('src')
    if src:
        full_url = urljoin(TARGET_URL, src)
        if not full_url.startswith('http'): continue
        ext = os.path.splitext(full_url)[-1]
        if not ext or len(ext) > 5: ext = '.jpg'
        file_name = f"multi_img_{index}{ext}"
        download_list.append((full_url, file_name))

# 关键步骤:根据学号限制截取任务列表
if len(download_list) > MAX_IMAGES:
    print(f"发现图片数量超过限制,仅下载前 {MAX_IMAGES} 张")
    download_list = download_list[:MAX_IMAGES]

# 开启 10 个线程并发下载
MAX_WORKERS = 10
with ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
    executor.map(download_task, download_list)

3. 运行结果

image

image

image

image

image

image

2)、心得体会

效率对比:通过对比实验,我深刻体会到了多线程技术的优势。在处理图片下载这种 IO 密集型任务时,单线程的大部分时间都浪费在等待网络响应上;而多线程可以“以数量换时间”,显著提升了爬取效率。

环境适配:在 MacOS环境下开发时,遇到了严重的 SSL: TLSV1_ALERT_PROTOCOL_VERSION 报错。通过查阅资料,我学会了使用 pyopenssl 库进行底层 SSL 注入的方法,成功解决了跨平台开发中的环境冲突问题。

逻辑控制:将学号逻辑融入代码控制中,增强了程序的定制化能力,也培养了我在编程中对边界条件(Boundary Condition)的控制意识。

作业②

1)、股票数据爬取实验

1. 实验描述
使用 Scrapy + MySQL 技术路线,爬取东方财富网的股票相关信息(代码、名称、最新价、涨跌幅等),并存储至本地数据库。

2. 核心代码

(1) Item 定义 (items.py)

import scrapy

class StockItem(scrapy.Item):
    stock_code = scrapy.Field()      # 股票代码
    stock_name = scrapy.Field()      # 股票名称
    latest_price = scrapy.Field()    # 最新报价
    change_percent = scrapy.Field()  # 涨跌幅
    change_amount = scrapy.Field()   # 涨跌额
    volume = scrapy.Field()          # 成交量
    turnover = scrapy.Field()        # 成交额
    amplitude = scrapy.Field()       # 振幅
    high = scrapy.Field()            # 最高
    low = scrapy.Field()             # 最低
    open_price = scrapy.Field()      # 今开
    prev_close = scrapy.Field()      # 昨收

(2) 爬虫逻辑 (spiders/stock.py)
采用 API 逆向分析法,直接请求 JSON 接口并清洗数据。

import scrapy
import json
from homework2.items import StockItem

class StockSpider(scrapy.Spider):
    name = 'stock'
    allowed_domains = ['eastmoney.com']
    # 真实的 JSON 数据接口
    start_urls = [
        '[http://82.push2.eastmoney.com/api/qt/clist/get?cb=jQuery1124&pn=1&pz=50&po=1&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&invt=2&wbp2u=](http://82.push2.eastmoney.com/api/qt/clist/get?cb=jQuery1124&pn=1&pz=50&po=1&np=1&ut=bd1d9ddb04089700cf9c27f6f7426281&fltt=2&invt=2&wbp2u=)|0|0|0|web&fid=f3&fs=m:0+t:6,m:0+t:80,m:1+t:2,m:1+t:23,m:0+t:81+s:2048&fields=f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f12,f13,f14,f15,f16,f17,f18,f20,f21,f23,f24,f25,f22,f11,f62,f128,f136,f115,f152'
    ]

    def parse(self, response):
        # 清洗 jQuery 包裹字符,提取 JSON
        data_text = response.text
        start_idx = data_text.find('(')
        end_idx = data_text.rfind(')')
        
        if start_idx != -1 and end_idx != -1:
            json_str = data_text[start_idx+1 : end_idx]
            data_json = json.loads(json_str)
            
            if 'data' in data_json and 'diff' in data_json['data']:
                for stock in data_json['data']['diff']:
                    item = StockItem()
                    item['stock_code'] = stock.get('f12')
                    item['stock_name'] = stock.get('f14')
                    item['latest_price'] = self.safe_float(stock.get('f2'))
                    item['change_percent'] = str(stock.get('f3')) + '%'
                    item['change_amount'] = self.safe_float(stock.get('f4'))
                    item['volume'] = str(stock.get('f5'))
                    item['turnover'] = str(stock.get('f6'))
                    item['amplitude'] = str(stock.get('f7')) + '%'
                    item['high'] = self.safe_float(stock.get('f15'))
                    item['low'] = self.safe_float(stock.get('f16'))
                    item['open_price'] = self.safe_float(stock.get('f17'))
                    item['prev_close'] = self.safe_float(stock.get('f18'))
                    yield item

    def safe_float(self, value):
        try:
            return float(value)
        except (ValueError, TypeError):
            return 0.0

(3) 数据库存储 (pipelines.py)

import pymysql

class MySQLPipeline:
    def __init__(self):
        self.host = 'localhost'
        self.port = 3306
        self.user = 'root'
        self.password = 'LJ100328'
        self.db = 'spider_homework'

    def open_spider(self, spider):
        self.conn = pymysql.connect(
            host=self.host, port=self.port, user=self.user,
            password=self.password, database=self.db, charset='utf8mb4'
        )
        self.cursor = self.conn.cursor()

    def process_item(self, item, spider):
        sql = """
            INSERT INTO stock_data (
                stock_code, stock_name, latest_price, change_percent, 
                change_amount, volume, turnover, amplitude, 
                high, low, open_price, prev_close
            ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
        """
        params = (
            item.get('stock_code'), item.get('stock_name'), item.get('latest_price'),
            item.get('change_percent'), item.get('change_amount'), item.get('volume'),
            item.get('turnover'), item.get('amplitude'), item.get('high'),
            item.get('low'), item.get('open_price'), item.get('prev_close')
        )
        try:
            self.cursor.execute(sql, params)
            self.conn.commit()
        except Exception as e:
            self.conn.rollback()
        return item

3. 运行结果
image

image

image

2)、心得体会

动态网页处理:东方财富网的数据是通过 Ajax 动态加载的,直接使用 XPath 无法提取。通过分析浏览器 Network 请求,我找到了真实的 JSON 数据接口。这让我明白了爬虫不仅是解析 HTML,更重要的是分析数据源(Source Analysis)。

Scrapy 架构:深入理解了 Scrapy 的模块化设计:Spider 负责抓取,Item 定义结构,Pipeline 负责存储。这种分层架构使得代码逻辑清晰,维护性远超单脚本爬虫。

数据库连接:在使用 DBeaver 连接 MySQL 8.0 时,遇到了 Public Key Retrieval is not allowed 错误。通过修改驱动属性 allowPublicKeyRetrieval=true,我成功解决了这一安全策略限制,积累了数据库配置经验。

作业③

1)、外汇牌价爬取实验

1. 实验描述
爬取中国银行外汇牌价(货币名称、现汇/现钞买入卖出价、发布时间),使用 XPath 技术解析数据并存储至 MySQL。

2. 核心代码

(1) 数据库建表 (SQL)

CREATE TABLE IF NOT EXISTS forex_data (
    id INT AUTO_INCREMENT PRIMARY KEY COMMENT '序号',
    currency VARCHAR(50) COMMENT '货币名称',
    tbp DECIMAL(10, 2) COMMENT '现汇买入价',
    cbp DECIMAL(10, 2) COMMENT '现钞买入价',
    tsp DECIMAL(10, 2) COMMENT '现汇卖出价',
    csp DECIMAL(10, 2) COMMENT '现钞卖出价',
    pub_time DATETIME COMMENT '发布时间',
    crawl_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '爬取时间'
) COMMENT '外汇牌价表';

(2) XPath 解析逻辑 (spiders/boc.py)

import scrapy
from homework3.items import ForexItem

class BocSpider(scrapy.Spider):
    name = 'boc'
    allowed_domains = ['boc.cn']
    start_urls = ['[https://www.boc.cn/sourcedb/whpj/](https://www.boc.cn/sourcedb/whpj/)']

    def parse(self, response):
        # 使用 XPath 定位表格中的所有行 (排除表头)
        rows = response.xpath('//div[@class="publish"]/div/table/tr')
        
        for row in rows[1:]:
            item = ForexItem()
            # 相对路径提取单元格数据
            # 货币名称 (第1列)
            item['currency'] = row.xpath('./td[1]/text()').get()
            # 现汇买入价 (第2列)
            item['tbp'] = self.safe_float(row.xpath('./td[2]/text()').get())
            # 现钞买入价 (第3列)
            item['cbp'] = self.safe_float(row.xpath('./td[3]/text()').get())
            # 现汇卖出价 (第4列)
            item['tsp'] = self.safe_float(row.xpath('./td[4]/text()').get())
            # 现钞卖出价 (第5列)
            item['csp'] = self.safe_float(row.xpath('./td[5]/text()').get())
            # 发布时间 (第7列)
            item['pub_time'] = row.xpath('./td[7]/text()').get()
            
            if item['currency']:
                yield item

    def safe_float(self, value):
        try:
            return float(value)
        except (ValueError, TypeError):
            return 0.0

3. 运行结果
image

image

2)、心得体会

XPath 实践:相比于作业②的 JSON 解析,作业③回归了经典的静态网页爬取。通过使用 //tr 定位行容器,再结合 ./td 相对路径提取单元格数据,我熟练掌握了 DOM 树的遍历与节点定位技巧。

数据清洗:网页上的外汇数据可能存在空值或特殊字符(如“-”),直接存入数据库会报错。我在代码中编写了 safe_float 辅助函数进行预处理,这让我体会到了数据清洗在数据采集流程中的重要性。

全流程贯通:本次作业完整贯穿了“数据库设计 -> 爬虫项目构建 -> 数据采集 -> 持久化存储”的全流程,极大地提升了我的数据采集工程能力。

代码地址:https://gitee.com/wsmlhqqwwn/LH/tree/master

posted @ 2025-11-25 18:30  七年qn  阅读(2)  评论(0)    收藏  举报