第三次作业:Scrapy 框架数据序列化与存储实践

第三次作业:Scrapy 框架数据序列化与存储实践

本次作业通过 Scrapy 框架结合 XPath、MySQL 数据库,完成了网页图片的多线程爬取以及结构化数据的提取与持久化存储,深入实践了 Item 与 Pipeline 的序列化处理机制。

作业①

1)、爬取气象网图片实验

实验说明:
本次实验指定中国气象网为目标网站,爬取网页中的所有图片。根据要求,我在爬虫中设置了 max_pages(限制总页数为13页)和 max_images(限制总图片数为113张)的计数器。程序在控制台输出下载的图片 URL,并将图片以二进制流的形式保存到本地的 images 子文件夹中。
同时,结合课堂知识,我对比了单线程与多线程的差异,最终采用了 threading.Thread 多线程的方式,将每个图片的下载任务放入子线程中执行,避免了因网络波动导致单张图片下载阻塞主线程的问题。

相应代码:

imageSpider.py

import scrapy
import os
import urllib.request
import threading

class ImagespiderSpider(scrapy.Spider):
    name = "imageSpider"
    allowed_domains = ["weather.com.cn"]
    start_urls = ["http://www.weather.com.cn/weather/101280601.shtml"]

    max_pages = 13
    max_images = 113
    count = 0
    current_page = 1
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 6.0 x64; en-US; rv:1.9pre) Gecko/2008072421 Minefield/3.0.2pre"
    }

    def parse(self, response):
        if not os.path.exists("images"):
            os.makedirs("images")

        img_srcs = response.xpath("//img/@src").extract()
        for src in img_srcs:
            if self.count >= self.max_images:
                break
            url = response.urljoin(src)
            print("下载URL:", url)
            
            self.count += 1
            T = threading.Thread(target=self.download, args=(url, self.count))
            T.setDaemon(False)
            T.start()

        if self.current_page < self.max_pages and self.count < self.max_images:
            self.current_page += 1
            print(f"准备爬取第 {self.current_page} 页")
            yield scrapy.Request(url=self.start_urls[0], callback=self.parse, dont_filter=True)

    def download(self, url, count):
        try:
            ext = url[len(url)-4:] if url[len(url)-4] == "." else ".jpg"
            req = urllib.request.Request(url, headers=self.headers)
            data = urllib.request.urlopen(req, timeout=100).read()
            fobj = open("images\\" + str(count) + ext, "wb")
            fobj.write(data)
            fobj.close()
            print("downloaded " + str(count) + ext)
        except Exception as err:
            print("下载失败:", err)

settings.py

BOT_NAME = "weather_spider"
SPIDER_MODULES = ["weather_spider.spiders"]
NEWSPIDER_MODULE = "weather_spider.spiders"
ROBOTSTXT_OBEY = False

运行结果截图:

控制台输出 URL 与下载提示:

屏幕截图 2026-11-06 103655

屏幕截图 2026-11-06 103718

下载的图片存储在 images 文件夹:

屏幕截图 2026-11-06 104046

屏幕截图 2026-11-06 104101

Gitee文件夹链接:https://gitee.com/lilyluna/2026shujucaiji/tree/master/3/1

2)、心得体会

通过本次实验,我深刻理解了多线程在实际爬虫应用中的重要性。引入多线程后,下载任务互不干扰,大大提高了爬取的效率和程序的可靠性。同时,通过计数器限制爬取数量,也让我掌握了如何给爬虫设置合理的“刹车”,避免对目标网站造成过大压力。


作业②

1)、爬取股票信息实验

实验说明:
本实验采用 Scrapy + Selenium + XPath + MySQL 技术路线。首先在数据库中创建了 StockDB 数据库及包含各项股票指标(股票代码、名称、最新报价、涨跌幅等)的 stocks 表。
由于东方财富网的数据是动态渲染的,我编写了 SeleniumDownloaderMiddleware 中间件,让 Scrapy 能够获取到渲染后的完整 HTML。随后在 Spider 中通过精准的 XPath 提取数据,最后利用 StockPipeline 实现了数据的异步入库。

F12调试:

屏幕截图 2026-11-06 114239

数据库创建截图:

DBeaver建库
屏幕截图 2026-11-06 105140_edit_1455082385819146

DBeaver建表
屏幕截图 2026-11-06 105151_edit_1455101738437316

代码截图:
items.py

import scrapy

class StockItem(scrapy.Item):
    bStockNo = scrapy.Field()
    bStockName = scrapy.Field()
    bLatestPrice = scrapy.Field()
    bChangePercent = scrapy.Field()
    bChangeAmount = scrapy.Field()
    bVolume = scrapy.Field()
    bAmplitude = scrapy.Field()
    bHigh = scrapy.Field()
    bLow = scrapy.Field()
    bOpen = scrapy.Field()
    bPreClose = scrapy.Field()

pipelines.py

import pymysql

class StockPipeline(object):
    def open_spider(self, spider):
        
        try:
            self.con = pymysql.connect(
                host="127.0.0.1",
                port=3306,
                user="root",
                passwd="......",
                db="StockDB",
                charset="utf8"
            )
            self.cursor = self.con.cursor()
            self.opened = True
            print("数据库连接成功")
        except Exception as err:
            print("数据库连接失败:", err)
            self.opened = False

    def close_spider(self, spider):
        if self.opened:
            self.con.commit()
            self.con.close()
            print("数据库连接已关闭")

    def process_item(self, item, spider):
        try:
            if self.opened:
                sql = """
                    INSERT INTO stocks 
                    (bStockNo, bStockName, bLatestPrice, bChangePercent, 
                     bChangeAmount, bVolume, bAmplitude, bHigh, bLow, bOpen, bPreClose) 
                    VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
                """
                self.cursor.execute(sql, (
                    item["bStockNo"], item["bStockName"], item["bLatestPrice"],
                    item["bChangePercent"], item["bChangeAmount"], item["bVolume"],
                    item["bAmplitude"], item["bHigh"], item["bLow"], item["bOpen"], item["bPreClose"]
                ))
                print(f"成功插入数据: {item['bStockName']} ({item['bStockNo']})")
        except Exception as err:
            print("插入数据失败:", err)
        return item

middlewares.py

import logging
logging.getLogger('selenium').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
from scrapy import signals

from itemadapter import ItemAdapter


from scrapy.http import HtmlResponse
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time

class SeleniumDownloaderMiddleware:
    def __init__(self):
        print("====== 正在初始化 Selenium 浏览器 ======")
        chrome_options = Options()
        chrome_options.add_argument('--headless=new')
        chrome_options.add_argument('--no-sandbox')
        chrome_options.add_argument('--disable-dev-shm-usage')
        self.driver = webdriver.Chrome(options=chrome_options)

    def process_request(self, request, spider):
        print(f"====== Selenium 正在访问: {request.url} ======")
        self.driver.get(request.url)
        
        time.sleep(5) 
        
        body = self.driver.page_source
        print(f"====== 获取到页面源码,长度: {len(body)} ======")
        
        return HtmlResponse(
            url=self.driver.current_url,
            body=body,
            encoding='utf-8',
            request=request
        )

stockSpider.py

import scrapy
from stock_spider.items import StockItem

class StockspiderSpider(scrapy.Spider):
    name = "stockSpider"
    allowed_domains = ["eastmoney.com"]
    
    start_urls = ["https://quote.eastmoney.com/center/gridlist.html#hs_a_board"]

    def parse(self, response):
        rows = response.xpath("//table[contains(@class, 'default-table')]//tbody/tr")
        if not rows:
            rows = response.xpath("//tbody/tr")
            
        print(f"\n====== [数据提取] 本次共找到 {len(rows)} 行数据 ======\n")

        for row in rows:
            item = StockItem()
            
            stock_no = row.xpath("string(./td[2])").get()
            if not stock_no:
                continue
            stock_no = stock_no.strip()
            
            if len(stock_no) != 6 or not stock_no.isdigit():
                continue
            
            item["bStockNo"] = stock_no
            
            name = row.xpath("./td[3]//a/text()").get()
            if not name:
                name = row.xpath("string(./td[3])").get()
            item["bStockName"] = name.strip() if name else "-"
            
            item["bLatestPrice"] = row.xpath("string(./td[7])").get().strip()
            item["bChangePercent"] = row.xpath("string(./td[8])").get().strip()
            item["bChangeAmount"] = row.xpath("string(./td[9])").get().strip()
            item["bVolume"] = row.xpath("string(./td[10])").get().strip()
            item["bAmplitude"] = row.xpath("string(./td[12])").get().strip()
            item["bHigh"] = row.xpath("string(./td[13])").get().strip()
            item["bLow"] = row.xpath("string(./td[14])").get().strip()
            item["bOpen"] = row.xpath("string(./td[15])").get().strip()
            item["bPreClose"] = row.xpath("string(./td[16])").get().strip()
            
            if item["bLatestPrice"] and item["bLatestPrice"] != "-":
                yield item
        ```
`settings.py` 
```python
BOT_NAME = "stock_spider"

SPIDER_MODULES = ["stock_spider.spiders"]
NEWSPIDER_MODULE = "stock_spider.spiders"

ADDONS = {}

ITEM_PIPELINES = {
   'stock_spider.pipelines.StockPipeline': 300,
}

ROBOTSTXT_OBEY = False
DEFAULT_REQUEST_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',
}

DOWNLOADER_MIDDLEWARES = {
    'stock_spider.middlewares.SeleniumDownloaderMiddleware': 543,
}

运行结果截图:

控制台输出“成功插入数据”:

屏幕截图 2026-11-06 110022

屏幕截图 2026-11-06 114506

屏幕截图 2026-11-06 114514

DBeaver 中查询 SELECT * FROM stocks; 的数据:

屏幕截图 2026-11-06 114729

Gitee文件夹链接:https://gitee.com/lilyluna/2026shujucaiji/tree/master/3/2

2)、心得体会

本次实验让我真正体会到了 Scrapy “5+2” 结构中 Pipeline 的作用。Scrapy 将数据爬取与数据存储完全解耦,Spider 爬取到数据后直接 yield 抛出;而 Pipeline 则像一个流水线工人,专门负责异步接收 Item 并写入数据库,非常高效。
同时,面对 JS 动态渲染的网页,我结合了学习的 Selenium 知识。在编写 XPath 时,我发现页面中存在大量的隐藏干扰列(如“股吧”、“资金流”等),导致了数据错位。我通过 F12 仔细分析 DOM 树,精准定位 td 索引,并结合 isDigit() 过滤脏数据,最终提取到了完美的数据。


作业③

1)、爬取外汇数据实验

实验说明:
本实验目标为中国银行外汇牌价页面。与作业②不同,该页面是纯静态的 HTML 表格。为了防止中文乱码,我运用了学习中提到的 UnicodeDammit 库对 response.body 进行编码识别与转换。
利用 scrapy.Selector 构建选择器后,直接使用 response.xpath('//table//tr') 并利用 position()>1 过滤掉表头,精确提取了表格每一行的 6 个字段(货币、现汇买入价、现钞买入价、现汇卖出价、现钞卖出价、时间),并通过 Pipeline 顺利写入 MySQL 的 ForexDB 数据库中。

F12调试:
image

DBeaver建库建表

屏幕截图 2026-11-06 114955

代码截图:

items.py

import scrapy

class ForexItem(scrapy.Item):
    Currency = scrapy.Field()
    TBP = scrapy.Field()
    CBP = scrapy.Field()
    TSP = scrapy.Field()
    CSP = scrapy.Field()
    Time = scrapy.Field()

pipelines.py

import pymysql

class ForexPipeline(object):
    def open_spider(self, spider):
        try:
            self.con = pymysql.connect(host="127.0.0.1", port=3306, user="root", 
                                       passwd="......", db="ForexDB", charset="utf8")
            self.cursor = self.con.cursor()
            self.opened = True
        except Exception as err:
            print("数据库连接失败:", err)
            self.opened = False

    def close_spider(self, spider):
        if self.opened:
            self.con.commit()
            self.con.close()

    def process_item(self, item, spider):
        try:
            if self.opened:
                sql = """INSERT INTO forex (Currency, TBP, CBP, TSP, CSP, Time) 
                         VALUES (%s, %s, %s, %s, %s, %s)"""
                self.cursor.execute(sql, (
                    item["Currency"], item["TBP"], item["CBP"], 
                    item["TSP"], item["CSP"], item["Time"]
                ))
                print(f"成功插入: {item['Currency']} | 现汇买入价: {item['TBP']} | 时间: {item['Time']}")
        except Exception as err:
            print("插入失败:", err)
        return item

forexSpider.py

import scrapy
from forex_spider.items import ForexItem
from bs4 import UnicodeDammit

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

    def parse(self, response):
        try:
            dammit = UnicodeDammit(response.body, ["utf-8", "gbk"])
            data = dammit.unicode_markup
            selector = scrapy.Selector(text=data)
            
            rows = selector.xpath('//div[contains(@class,"BOC_main")]//table//tr[position()>1]')
            
            for row in rows:
                item = ForexItem()
                item["Currency"] = row.xpath("./td[1]/text()").get(default="").strip()
                item["TBP"]      = row.xpath("./td[2]/text()").get(default="").strip()
                item["CBP"]      = row.xpath("./td[3]/text()").get(default="").strip()
                item["TSP"]      = row.xpath("./td[4]/text()").get(default="").strip()
                item["CSP"]      = row.xpath("./td[5]/text()").get(default="").strip()
                item["Time"]     = row.xpath("./td[7]/text()").get(default="").strip()
                
                if item["Currency"]:
                    yield item
        except Exception as err:
            print("解析出错:", err)

settings.py

BOT_NAME = "forex_spider"
SPIDER_MODULES = ["forex_spider.spiders"]
NEWSPIDER_MODULE = "forex_spider.spiders"

ITEM_PIPELINES = {
   'forex_spider.pipelines.ForexPipeline': 300,
}

ROBOTSTXT_OBEY = False
DEFAULT_REQUEST_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',
}

运行结果截图:

控制台输出“成功插入”的日志:

屏幕截图 2026-11-06 115126

屏幕截图 2026-11-06 115429

屏幕截图 2026-11-06 115439

DBeaver 中查询 SELECT * FROM forex; 的数据:

屏幕截图 2026-11-06 115535

Gitee文件夹链接:https://gitee.com/lilyluna/2026shujucaiji/tree/master/3/3

2)、心得体会

本次作业是对整个 Scrapy 框架使用的综合检验。在处理外汇数据时,XPath 的灵活性得到了充分体现,通过 position()>1 可以轻松跳过表头。通过对比作业②和作业③,我明白了在爬虫开发中,根据目标网站的类型灵活选择解析策略是十分重要的。
同时,我也注意到部分小众币种因银行未公布现汇买入价,导致数据库中对应字段为空,而这其实是符合实际网页情况的,让我明白了爬虫不仅要能抓取数据,更要保证数据是真实与正确的。整个作业流程从建库建表,到定义 Item,再到编写 Spider 和 Pipeline,让我建立了一套完整的数据爬取思维。

posted @ 2026-11-09 20:50  102402113  阅读(4)  评论(0)    收藏  举报