数据采集与融合技术实践--作业三

数据采集与融合技术作业三

📌1.相关信息及链接

名称 信息及链接
学号姓名 102202108 王露洁
本次作业要求链接 https://edu.cnblogs.com/campus/fzu/2024DataCollectionandFusiontechnology/homework/13287
作业①所在码云链接 https://gitee.com/wanglujieeee/crawl_project/tree/master/作业3.1
作业②所在码云链接 https://gitee.com/wanglujieeee/crawl_project/tree/master/作业3.2
作业③所在码云链接 https://gitee.com/wanglujieeee/crawl_project/tree/master/作业3.3

📝2.作业内容

作业①:

✒️要求:指定一个网站,爬取这个网站中的所有的所有图片,例如:中国气象网(http://www.weather.com.cn)。使用scrapy框架分别实现单线程和多线程的方式爬取。 –务必控制总页数(学号尾数2位)、总下载的图片数量(尾数后3位)等限制爬取的措施。

🗃️输出信息:将下载的Url信息在控制台输出,并将下载的图片存储在images子文件中,并给出截图。 Gitee文件夹链接

🌏解决思路及代码实现

1.前情提要:

本人学号:102202108,所以按题目要求,应爬取8个页面,一共爬取图片的数目为108张(理论上,出现的具体问题后再详细解释)

2.spider.py文件(主要板块)

-->设置计数器:

total_images:统计已下载的图片数量。
max_images:设置最大下载的图片数量(108张)。
max_pages:设置最大爬取的页面数量(8页)。
pages_crawled:记录已爬取的页面数量。

import scrapy
from scrapy.exceptions import CloseSpider  # 导入CloseSpider异常,用于关闭爬虫
from urllib.parse import urljoin  # 导入urljoin用于处理URL拼接

class WeatherSpider(scrapy.Spider):
    name = 'weather_spider'  # 爬虫名称
    allowed_domains = ['weather.com.cn']  # 允许的域名
    start_urls = ['http://www.weather.com.cn']  # 起始URL,爬虫从这里开始

    custom_settings = {
        'ITEM_PIPELINES': {
            'weather_images.pipelines.ImagePipeline': 1,  # 启用图片下载管道
        },
        'IMAGES_STORE': 'images',  # 图片存储路径
        'LOG_LEVEL': 'INFO',  # 日志级别
        'CONCURRENT_REQUESTS': 1,  # 设置为单线程
    }

    total_images = 0  # 统计下载的图片数量
    max_images = 108  # 设置最大下载数量
    max_pages = 8     # 设置最大爬取页面数量
    pages_crawled = 0 # 已爬取页面数量


-->解析响应的 parse 方法:

1.使用CSS选择器提取页面中所有的图片URL。
2.利用 urljoin 将相对URL转换为绝对URL,并过滤出有效的HTTP URL。
3.如果没有找到有效的图片URL,记录日志并返回。

 def parse(self, response):
        # 从响应中提取所有图片的src属性
        image_urls = response.css('img::attr(src)').getall()
        # 将相对URL转换为绝对URL
        image_urls = [urljoin(response.url, url) for url in image_urls]
        # 过滤出有效的HTTP URL
        image_urls = [url for url in image_urls if url.startswith('http')]

        if not image_urls:  # 如果没有找到有效的图片URL
            self.log('No valid image URLs found on this page.')
            return  # 直接返回,不进行后续操作

-->处理有效的图片URL:

1.遍历每个有效的图片URL。
2.如果已下载的图片数量小于最大限制,产出一个包含图片URL的字典,并记录下载的URL。
3.增加已下载的图片数量计数。

# 遍历有效的图片URL
        for url in image_urls:
            if self.total_images < self.max_images:  # 如果还未达到最大图片数量
                yield {'image_urls': [url]}  # 产出包含图片URL的字典
                self.log(f'Download URL: {url}')  # 记录下载的URL
                self.total_images += 1  # 增加已下载的图片数量

-->检查是否达到最大图片数量:

如果达到最大图片数量,记录日志并抛出 CloseSpider 异常,关闭爬虫。

 # 检查是否达到最大图片数量
        if self.total_images >= self.max_images:
            self.log(f'Reached max image limit: {self.max_images}. Closing spider.')
            raise CloseSpider(reason='Reached max image limit')  # 关闭爬虫

-->处理分页:

1.增加已爬取的页面数量计数。
2.如果已爬取的页面数量小于最大数量,查找下一个页面的链接,并使用 response.follow 方法继续爬取

# 处理分页,这里假设有一个简单的分页机制
        self.pages_crawled += 1  # 增加已爬取页面数量
        if self.pages_crawled < self.max_pages:  # 如果还未达到最大页面数量
            next_page = response.css('a::attr(href)').get()  # 获取下一个页面的链接
            if next_page:  # 如果找到下一个页面的链接
                yield response.follow(next_page, self.parse)  # 继续爬取下一个页面

3.pipeline.py文件(主要板块)

-->定义 ImagePipeline 类:

继承自 ImagesPipeline,使其具备图片下载的基本功能。

import scrapy
from scrapy.pipelines.images import ImagesPipeline  # 导入Scrapy的图片下载管道
import logging  # 导入日志模块,用于记录下载错误信息
from scrapy.exceptions import DropItem  # 导入DropItem异常,用于丢弃无法处理的项目

class ImagePipeline(ImagesPipeline):

-->get_media_requests 方法:

该方法负责生成图片下载请求。
1.遍历 item['image_urls'] 中的所有图片URL。
2.检查URL是否有效(非 None)。
3.使用 scrapy.Request 创建请求,并添加浏览器的 User-Agent 头,以避免被反爬虫机制拒绝。

def get_media_requests(self, item, info):
        # 遍历每个图片URL
        for image_url in item['image_urls']:
            if image_url:  # 确保 URL 不为 None
                # 返回一个新的请求,设置 User-Agent 伪装成浏览器
                yield scrapy.Request(image_url, headers={
                    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3'
                })

-->item_completed 方法:

1.该方法在请求完成后被调用,处理下载结果。
2.初始化一个列表 failed_urls 用于记录下载失败的URL。
3.遍历结果 results,检查每个请求的状态:
如果下载失败(ok 为 False),则将失败的URL添加到 failed_urls 列表中。
4.如果有失败的URL,记录错误日志,并抛出 DropItem 异常,表示该项目由于下载失败而被丢弃。
5.如果所有下载都成功,返回包含成功下载的项目。

# 处理请求完成后的结果
    def item_completed(self, results, item, info):
        failed_urls = []  # 用于存储下载失败的URL
        # 遍历请求结果
        for ok, x in results:
            if not ok:  # 如果下载失败
                failed_urls.append(x.value)  # 将失败的URL加入列表

        if failed_urls:  # 如果有失败的URL
            # 记录错误日志
            logging.error(f'Failed to download images for URLs: {failed_urls}')
            # 丢弃包含下载失败的项目
            raise DropItem(f"Image download failed for: {failed_urls}")

        return item  # 返回成功下载的项目

4.item.py文件

-->定义数据项类 WeatherImageItem:

这个类继承自 scrapy.Item,用于定义爬取到的数据结构。Scrapy中的Item相当于一个容器,用于存放爬虫提取的数据。

-->定义字段:

image_urls:使用 scrapy.Field() 创建一个字段,专门用于存储图片的URL列表。这使得爬虫在处理数据时可以方便地存取与图片相关的信息。

import scrapy

class WeatherImageItem(scrapy.Item):
    image_urls = scrapy.Field()  # 定义一个字段,用于存储图片的URL列表

5.setting.py文件

DOWNLOAD_TIMEOUT = 15

BOT_NAME = 'weather_images'

SPIDER_MODULES = ['weather_images.spiders']
NEWSPIDER_MODULE = 'weather_images.spiders'

# 存储图片路径
IMAGES_STORE = r'C:\example_scrapy\weather_images\images'

# 启用图片下载管道
ITEM_PIPELINES = {
    'weather_images.pipelines.ImagePipeline': 1,
}

# 日志级别
LOG_LEVEL = 'INFO'

# 设置并发请求数
CONCURRENT_REQUESTS = 16  # 单线程或者多线程只需修改这里

# 控制请求延迟和并发请求
DOWNLOAD_DELAY = 1

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# 启用重试
RETRY_ENABLED = True
MEDIA_ALLOW_REDIRECTS = True  # 允许重定向

🌞运行结果截图(这里不满108张,详情见问题思考模块)

🌈问题思考和心得体会

我这里的代码反反复复修改和运行了很多次,主要是修改pipeline.py文件,因为在终端运行的结果如下所示:

可以看到这里显示很多图片都下载失败,这可能是导致爬取的图片不满108张的原因,然后我问了chatGPT,上面说问题可能出在文件路径问题,文件权限问题,图片 URL 的有效性,网络问题等,我一一检查并按照要求修改之后,仍然出现上述问题,所以我觉得可能网站使用了反爬虫技术,或者真的网络也有问题导致图片下载失败。爬虫程序的编写应该基本上没什么问题,基本上都定位到了图片所在的url,只是下载出现了问题。

本次的心得体会有:

我刚开始做这道题的时候,就被题目迷惑住了,因为我打开这个天气网发现它根本不能翻页,而且看上去也没有多少张图片可以爬取,因此我在课上疑惑了半天。后来才后知后觉,我们可以从网页的html文件中检索出带有href属性的a标签,并获取属性的值就可以得到一个新的网址,经过检查和处理之后就可以继续进行访问,这就是我们之前学到的对网站进行爬取的过程。
除此之外,我还学到一个scrapy框架中用于处理图像下载的一个内置管道类:ImagesPipeline。在该类中可以直接调用方法:get_media_requests(self, item, info)来接收一个 item(包含图像 URL 的项目)并生成用于下载每个图像的请求。当所有请求完成之后会自动调用另一个函数:item_completed(self, results, item, info),它会检查哪些图像下载成功,哪些图像下载失败。所以这个类极大地简化了图像下载的过程。
还有关于单线程和多线程的设置,我本来以为很复杂,需要分别建立两个项目,但是在这里只需要修改配置文件中的CONCURRENT_REQUESTS的值就可以了,真的很简单方便。
最后是关于我个人的思考,我总是过于依赖AI,对很多代码逻辑缺乏自己的思考。我对一些带有反爬机制的网站也是束手无策,只能让AI帮我想办法,如果它解决不了,那就是真的解决不了了。另外不同网站的爬取应该按照实际情况采取不同的爬取数据的方法,比如静态网页和动态网页需要不同的方法,我现在还是有点迷糊,希望能通过做题慢慢理解吧。

作业②:

✒️要求:熟练掌握 scrapy 中 Item、Pipeline 数据的序列化输出方法;使用scrapy框架+Xpath+MySQL数据库存储技术路线爬取股票相关信息。 候选网站:东方财富网:https://www.eastmoney.com/

🗃️MySQL数据库存储和输出格式如下: 表头英文命名例如:序号id,股票代码:bStockNo……,由同学们自行定义设计

🚀解决思路及代码实现

1.前情提要:本次作业使用scrapy框架,selenium方法爬取数据,使用MySQL存储数据,最后把爬取的数据导出为csv文件放在gitee仓库里了

2.items.py文件

--> 这里定义了一个关于股票信息的数据项,方便存放爬虫提取的关于股票的数据。

import scrapy

class StockScraperItem(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()
    bPreviousClose = scrapy.Field()
    pass

3.spider.py文件

--> 这里的start_requests方法定义了一个初始请求生成方式 。发起请求的方式使用了SeleniumRequest,这是一个特殊的请求方式,可以在scrapy中使用selenium。目的是使用selenium等待页面完全加载之后再进行解析操作(因为有些页面是通过JavaScript动态加载的)。该方法的参数:url=url 指定了请求的目标 URL;callback=self.parse 指定了响应到达后的回调函数(这里是 parse 方法,下面有定义),用于处理响应;wait_time=10 指定了等待时间。

from scrapy_selenium import SeleniumRequest
from scrapy.spiders import Spider
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time

class StockSpider(Spider):
    name = 'stock_spider'
    start_urls = ['https://quote.eastmoney.com/center/gridlist.html#hs_a_board']

    def start_requests(self):
        for url in self.start_urls:
            yield SeleniumRequest(url=url, callback=self.parse, wait_time=10)

--> 这个 clean_decimal 方法的作用是清理和转换包含百分号、单位(“亿”或“万”)或千位分隔符的字符串,并将其转换为适当的浮点数格式。

    def clean_decimal(self, value):
        if value:
            value = value.strip().replace(',', '')
            if '%' in value:
                try:
                    return float(value.replace('%', '')) / 100
                except ValueError:
                    return None
            if '亿' in value:
                try:
                    return float(value.replace('亿', '')) * 1e8
                except ValueError:
                    return None
            elif '万' in value:
                try:
                    return float(value.replace('万', '')) * 1e4
                except ValueError:
                    return None
            try:
                return float(value)
            except ValueError:
                return None
        return None

--> 查看网页原html文档,找到所要爬取数据的标签元素,以便进行定位和爬取。

--> 该 parse 方法用于解析网页表格中的股票数据,并将提取的信息存储在 item 字典中。它使用 Selenium 来等待表格元素加载,然后提取并清理数据。“driver = response.request.meta['driver']”获取 Selenium 的 WebDriver 实例,用于执行等待和动态加载。使用 WebDriverWait 等待页面中的表格行加载完成,以确保解析时数据已经在页面上。最后进行字段的提取和生成item项。

    def parse(self, response):
        driver = response.request.meta['driver']

        WebDriverWait(driver, 15).until(
            EC.presence_of_element_located((By.XPATH, '//table//tr'))
        )

        rows = response.xpath('//table//tr')
        for row in rows:
            item = {}
            item['bStockNo'] = row.xpath('./td[2]/a/text()').get()
            item['bStockName'] = row.xpath('./td[@class="mywidth"]/a/text()').get()
            item['bLatestPrice'] = self.clean_decimal(row.xpath('./td[@class="mywidth2"][1]/span/text()').get())
            item['bChangePercent'] = self.clean_decimal(row.xpath('./td[@class="mywidth2"][2]/span/text()').get())
            item['bChangeAmount'] = self.clean_decimal(row.xpath('./td[7]/span/text()').get())
            item['bVolume'] = self.clean_decimal(row.xpath('./td[8]/text()').get())
            item['bAmplitude'] = self.clean_decimal(row.xpath('./td[10]/text()').get())
            item['bHigh'] = self.clean_decimal(row.xpath('./td[11]/span/text()').get())
            item['bLow'] = self.clean_decimal(row.xpath('./td[12]/span/text()').get())
            item['bOpen'] = self.clean_decimal(row.xpath('./td[13]/span/text()').get())
            item['bPreviousClose'] = self.clean_decimal(row.xpath('./td[14]/text()').get())
            yield item

--> 这里是进行一个翻页机制。刚开始执行前面的代码发现只能爬取到一页的数据,所以就使用selenium来模拟翻页的动作,以便进行下一页数据的爬取。首先也是查看原html文件来查看包含“下一页”按钮的元素的位置。

--> 使用显示等待,等到“下一页”这个按钮可以被点击时,通过 next_button.is_displayed() 和 next_button.is_enabled() 进一步验证按钮的可见性和启用状态。如果“下一页”按钮可用,则点击它,并记录操作日志。之后再次使用显示等待使得表格数据加载完成,使用 SeleniumRequest 发送请求,调用 parse 方法递归处理新页面数据,避免过滤掉相同的 URL。

        try:
            next_button = WebDriverWait(driver, 10).until(
                EC.element_to_be_clickable((By.XPATH, '//a[contains(@class, "next paginate_button")]'))
            )
            if next_button.is_displayed() and next_button.is_enabled():
                self.logger.info("Clicking 'Next' button for pagination.")
                next_button.click()

                # 增加延迟或等待下一页数据加载完成
                WebDriverWait(driver, 10).until(
                    EC.presence_of_element_located((By.XPATH, "//table//tr"))
                )

                # 重新调用 parse 函数继续爬取
                yield SeleniumRequest(
                    url=driver.current_url,
                    callback=self.parse,
                    wait_time=10,
                    dont_filter=True
                )
            else:
                self.logger.info("No more pages to crawl or next button disabled.")
        except Exception as e:
            self.logger.error(f"Error during pagination: {e}")

4.pipeline.py文件

--> 打开MySQL,建立数据库和表格,以便后续插入数据。

--> open_spider方法会在 Scrapy 爬虫启动时自动调用,使管道与数据库初始化,这里主要是进行 MySQL 数据库的连接,并且创建一个游标对象 self.cursor,用于执行 SQL 语句。

import mysql.connector
import csv


class MySQLPipeline:
    def open_spider(self, spider):
        try:
            # 连接 MySQL 数据库
            self.conn = mysql.connector.connect(
                host='localhost',  # 数据库主机
                user='root',  # 用户名
                password='Wlj98192188?',  # 密码,替换为你自己的密码
                database='stock_data'  # 数据库名
            )
            self.cursor = self.conn.cursor()
            print("MySQL connection established")
        except mysql.connector.Error as err:
            print(f"Error: {err}")
            raise

--> close_spider 方法,在 Scrapy 爬虫结束时自动调用,负责在爬虫关闭前导出数据并关闭数据库连接。调用管道的 export_to_csv 方法(下面有定义),将爬取的数据从数据库导出为 CSV 文件通过 self.conn.commit() 提交事务,确保数据库中保存了爬虫获取的所有数据。

    def close_spider(self, spider):
        try:
            # 在关闭数据库连接前导出数据到 CSV 文件
            self.export_to_csv()

            # 提交数据并关闭数据库连接
            if hasattr(self, 'conn'):
                self.conn.commit()
                self.cursor.close()
                self.conn.close()
                print("MySQL connection closed")
        except AttributeError:
            print("No database connection to close.")
        except mysql.connector.Error as err:
            print(f"Error closing MySQL connection: {err}")

--> process_item 方法,用于将每个爬取的数据项插入到 MySQL 数据库中的 stocks 表中。每次插入后提交事务,可以确保数据在每次插入后立即写入数据库,避免批量插入时因程序中断导致数据丢失。

    def process_item(self, item, spider):
        try:
            # 插入数据到 MySQL
            insert_query = """
            INSERT INTO stocks (bStockNo, bStockName, bLatestPrice, bChangePercent, bChangeAmount, bVolume, bAmplitude, bHigh, bLow, bOpen, bPreviousClose)
            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
            """
            values = (
                item['bStockNo'],
                item['bStockName'],
                item['bLatestPrice'],
                item['bChangePercent'],
                item['bChangeAmount'],
                item['bVolume'],
                item['bAmplitude'],
                item['bHigh'],
                item['bLow'],
                item['bOpen'],
                item['bPreviousClose']
            )
            self.cursor.execute(insert_query, values)
            self.conn.commit()  # Commit after each insert
            return item
        except mysql.connector.Error as err:
            print(f"Error inserting data: {err}")
            return None

--> export_to_csv 方法,用于将 stocks 表中的数据导出到 CSV 文件中。rows = self.cursor.fetchall() 从查询结果中获取所有行数据,结果保存在 rows 列表中。self.cursor.description 返回字段描述信息,用于动态获取表头。[i[0] for i in self.cursor.description] 提取每个字段的名称,并存入 headers 列表中。使用 csv.writer(file) 创建 CSV 写入器 writer,最后将表头和内容写入文件。

    def export_to_csv(self):
        # 从数据库中查询所有数据并导出到 CSV 文件
        export_query = "SELECT * FROM stocks"
        try:
            self.cursor.execute(export_query)
            rows = self.cursor.fetchall()
            headers = [i[0] for i in self.cursor.description]  # 获取表头

            with open('exported_stock_data.csv', mode='w', newline='', encoding='utf-8') as file:
                writer = csv.writer(file)
                writer.writerow(headers)  # 写入表头
                writer.writerows(rows)  # 写入数据行
            print("Data exported to exported_stock_data.csv")
        except mysql.connector.Error as err:
            print(f"Error exporting data: {err}")

5.middlewares.py文件

--> 定义 CustomSeleniumMiddleware 类,用于在 Scrapy 中自定义 Selenium 中间件,以便在爬取过程中使用 Chrome 浏览器自动化,从而使Scrapy 可以加载 JavaScript 内容的网页,并在无界面环境下稳定地执行页面爬取。

from scrapy_selenium import SeleniumMiddleware
from selenium.webdriver.chrome.options import Options
from selenium import webdriver


class CustomSeleniumMiddleware(SeleniumMiddleware):
    @classmethod
    def from_crawler(cls, crawler):
        # 设置 Chrome 选项
        chrome_options = Options()
        chrome_options.add_argument("--headless")
        chrome_options.add_argument("--disable-gpu")
        chrome_options.add_argument("--no-sandbox")

        # 直接在 ChromeDriver 中指定 executable_path
        driver = webdriver.Chrome(executable_path="D:/chromedriver-win64/chromedriver.exe", options=chrome_options)

        # 设置超时
        driver.set_page_load_timeout(30)
        driver.implicitly_wait(10)

        # 返回自定义的 SeleniumMiddleware
        middleware = cls(driver_name='chrome', driver_executable_path="D:/chromedriver-win64/chromedriver.exe",
                         driver_arguments=chrome_options.arguments, browser_executable_path=None)
        return middleware

6.setting.py文件

--> 配置文件为 Scrapy 项目 stock_scraper 设置了相关的抓取和数据存储设置,特别是在爬虫中集成了 Scrapy-Selenium,使其能够处理需要浏览器渲染的动态内容。

BOT_NAME = "stock_scraper"

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

# 激活 MySQL Pipeline
ITEM_PIPELINES = {
   'stock_scraper.pipelines.MySQLPipeline': 1,
}
REDIRECT_ENABLED = False

# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = "stock_scraper (+http://www.yourdomain.com)"

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

# settings.py

# 启用 Scrapy-Selenium

# 导入 Service 类
from selenium.webdriver.chrome.service import Service

# 设置 Chrome 驱动服务
SELENIUM_DRIVER_NAME = 'chrome'
SELENIUM_DRIVER_EXECUTABLE_PATH = 'D:/chromedriver-win64/chromedriver.exe'
SELENIUM_DRIVER_ARGUMENTS = ['--headless', '--disable-gpu', '--no-sandbox']

# 使用 Service 类指定 ChromeDriver 路径
from scrapy_selenium.middlewares import SeleniumMiddleware

class CustomSeleniumMiddleware(SeleniumMiddleware):
    def __init__(self, *args, **kwargs):
        service = Service(executable_path=SELENIUM_DRIVER_EXECUTABLE_PATH)
        kwargs['service'] = service
        super().__init__(*args, **kwargs)

# 使用自定义的中间件
DOWNLOADER_MIDDLEWARES = {
    'stock_scraper.middlewares.CustomSeleniumMiddleware': 800,
}

🌞运行结果截图

--> 检查MySQL中的数据(这里只显示前时行,有些数据空了(不知啥原因),不过无伤大雅):

--> 导出的csv文件(结果有两万多行,这里只截了最后的一部分):

🌈心得体会

--> 之前在理论课上刚做了关于selenium用法的作业,然后这次就使用scapy+selenium的方式来做,我本来以为可以直接使用,但是实践之后才知道在scrapy中使用selenium要定义专门的Scrapy 的中间件--scrapy_selenium,它允许 Scrapy 和 Selenium WebDriver 一起工作,从而支持抓取包含 JavaScript 动态渲染内容的网页。这也是第一次编写middlewares.py这个文件(之前都是只编写其他四个文件就行了,因为scrapy框架默认只处理静态页面的爬取),在这个文件中,我们要定义一个中间件的类,这个类继承于SeleniumMiddleware类--scrapy_selenium 中的默认 Selenium 中间件基类,用来自定义启动谷歌浏览器的参数设置。这道题花了很长的时间,中间出现了很多各种各样的问题,由于问题实在太多了,没有办法截图一一在这里展示,在爬虫,管道,中间件这三个文件的每个模块几乎都出现了问题,不过好在最后都一一解决了。所以感觉做爬虫的作业真的很需要耐心,每次数据是否爬取成功都是由多个因素来决定的,这种情况下唯一的好处就是在你做出来的那一刻会觉得非常惊喜和开心。当然熟练了以后肯定就能把速度提上去了,所以再接再厉吧。

作业③:

✒️要求:熟练掌握 scrapy 中 Item、Pipeline 数据的序列化输出方法;使用scrapy框架+Xpath+MySQL数据库存储技术路线爬取外汇网站数据。 候选网站:中国银行网:https://www.boc.cn/sourcedb/whpj/

🗃️输出信息: Gitee文件夹链接

🚀解决思路和代码实现

1.前情提要

--> 这道题和上一题类似,这里我也是使用了Scrapy框架+Selenium方法+MySQL数据库的方式,不过不同的是在这个网页的html文件中我没有办法直接找到所要爬取数据的标签元素,这就意味着这些数据是由JavaScript脚本动态生成的,没有办法直接查看。因此就只能在请求发起后收到的响应中来找经过渲染后的页面信息,以便进行后面数据的定位和提取。

2.item.py文件

--> 定义相关数据项

import scrapy

class ForexScraperItem(scrapy.Item):
    currency = scrapy.Field()
    tbp = scrapy.Field()  # 现钞买入价
    cbp = scrapy.Field()  # 现钞卖出价
    tsp = scrapy.Field()  # 现汇买入价
    csp = scrapy.Field()  # 现汇卖出价
    time = scrapy.Field()
    pass

3.spider.py文件

--> 在发起请求并获取响应后打印出响应中的page_source,观察和找出数据所在的标签的定位方法。

--> 这是找到的关于表格数据的标签格式(因为之前在终端运行时打印出的数据被后面的数据顶上去找不到了,只能在与gpt的聊天记录中找了)

--> 在分析过它的结构之后就能写爬虫代码了(内容跟前面类似这里就不多说了)。

from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import scrapy
from forex_scraper.items import ForexScraperItem
from selenium import webdriver


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

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        options = webdriver.ChromeOptions()
        options.add_argument("--headless")
        self.driver = webdriver.Chrome(options=options)

    def parse(self, response):
        self.driver.get(response.url)
        try:
            WebDriverWait(self.driver, 10).until(
                EC.presence_of_element_located((By.XPATH, '//table[@width="100%"]'))
            )
            page_source = self.driver.page_source
            response = scrapy.Selector(text=page_source)

            rows = response.xpath('//table[@width="100%"]/tbody/tr')
            for row in rows[1:]:  # Skip the header row
                item = ForexScraperItem()
                # Use default values if any field is missing
                item['currency'] = row.xpath('.//td[1]/text()').get(default="").strip()
                item['tsp'] = row.xpath('.//td[2]/text()').get(default=None)
                item['tbp'] = row.xpath('.//td[3]/text()').get(default=None)
                item['csp'] = row.xpath('.//td[4]/text()').get(default=None)
                item['cbp'] = row.xpath('.//td[5]/text()').get(default=None)
                item['time'] = row.xpath('.//td[8]/text()').get(default=None)

                yield item
        finally:
            self.driver.quit()

4.pipeline.py文件

--> 在这里也是首先连上MySQl数据库,不过不同的是这次表格是直接在这里创建的(提前建好了db)

import pymysql
from pymysql import IntegrityError
import pandas as pd
from sqlalchemy import create_engine

class ForexScraperPipeline:
    def open_spider(self, spider):
        # 连接数据库
        self.conn = pymysql.connect(
            host='localhost',
            user='root',
            password='Wlj98192188?',
            db='forex_data',
            charset='utf8mb4',
            cursorclass=pymysql.cursors.DictCursor
        )
        self.cursor = self.conn.cursor()

        # 使用 SQLAlchemy 创建引擎
        self.engine = create_engine('mysql+pymysql://root:Wlj98192188?@localhost/forex_data')

        # 创建表格,如果表格不存在
        self.cursor.execute("""
            CREATE TABLE IF NOT EXISTS forex_data (
                id INT AUTO_INCREMENT PRIMARY KEY,
                currency VARCHAR(255) NOT NULL,
                tbp FLOAT DEFAULT NULL,
                cbp FLOAT DEFAULT NULL,
                tsp FLOAT DEFAULT NULL,
                csp FLOAT DEFAULT NULL,
                time TIME DEFAULT NULL
            )
        """)
        self.conn.commit()

--> 插入数据并最终导入到csv文件中

    def process_item(self, item, spider):
        try:
            # 检查字段是否有数据,避免空字段
            if item.get('currency'):
                # 插入数据
                self.cursor.execute("""
                    INSERT INTO forex_data (currency, tbp, cbp, tsp, csp, time) 
                    VALUES (%s, %s, %s, %s, %s, %s)
                """, (
                    item.get('currency'),
                    item.get('tbp') or None,
                    item.get('cbp') or None,
                    item.get('tsp') or None,
                    item.get('csp') or None,
                    item.get('time') or None
                ))
                self.conn.commit()
        except IntegrityError as e:
            spider.logger.error(f"Database integrity error: {e}")
        except Exception as e:
            spider.logger.error(f"Failed to insert item: {e}")

        return item

    def close_spider(self, spider):
        # 导出数据库数据到 CSV 文件
        try:
            query = "SELECT currency, tbp, cbp, tsp, csp, time FROM forex_data"
            data = pd.read_sql(query, self.engine)
            data.to_csv('forex_data_export.csv', index=False, encoding='utf-8-sig')
            spider.logger.info("Data successfully exported to forex_data_export.csv")
        except Exception as e:
            spider.logger.error(f"Failed to export data: {e}")
        finally:
            # 关闭数据库连接
            self.cursor.close()
            self.conn.close()

5.middlewares.py文件

--> 这里与之前不同的是导入了一个scrapy.http.HtmlResponse,用于创建一个 HtmlResponse 对象,以便将 Selenium 获取的网页内容返回给 Scrapy。process_request方法是 Scrapy 的一个钩子方法,用于处理每个请求,覆盖了默认的 Scrapy 请求处理逻辑。将页面源代码通过 HtmlResponse 包装并返回,使 Scrapy 将此页面作为一个普通的响应对象处理。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

from scrapy.http import HtmlResponse

class SeleniumMiddleware:
    def __init__(self):
        chrome_options = Options()
        chrome_options.add_argument("--headless")
        self.driver = webdriver.Chrome(options=chrome_options)

    def process_request(self, request, spider):
        self.driver.get(request.url)
        try:
            # 等待表格加载完成
            WebDriverWait(self.driver, 10).until(
                EC.presence_of_element_located((By.XPATH, '//table'))
            )
        except Exception as e:
            spider.logger.warning("页面加载超时或找不到表格")
        body = self.driver.page_source
        return HtmlResponse(self.driver.current_url, body=body, encoding='utf-8', request=request)

    def __del__(self):
        self.driver.quit()



6.setting.py文件

--> 配置文件如下:

BOT_NAME = "forex_scraper"

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

DOWNLOADER_MIDDLEWARES = {
    'forex_scraper.middlewares.SeleniumMiddleware': 543,
}
ITEM_PIPELINES = {
    'forex_scraper.pipelines.ForexScraperPipeline': 300,
}

# Crawl responsibly by identifying yourself (and your website) on the user-agent
#USER_AGENT = "forex_scraper (+http://www.yourdomain.com)"

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

🌞运行结果截图

--> MySQl数据库查询(不好意思有点糊糊):

--> 导出的csv文件的内容:

🌈心得体会

--> 在做这道题查看网页代码的时候,整个人都愣住了,因为不管怎么样都找不到所要数据的标签元素,后来才知道它是动态加载的,需要JavaScript渲染填充后才会出现,上一题虽然也是动态加载,不过在原网页代码还是能够找到对应标签的。这里就体现出了使用selenium的重要性,如果只按照静态页面爬取数据的方式来爬取这次的数据根本什么都爬取不到,因为根本不知道标签是啥样的。所以这道题相比上道题可能难度又加了一点点。不过我在这道题上花费的时间是比上道题要短一些的,可能因为有上次的一点点“基础”和“经验”吧。在这里竟然一道题的题量都能积累可受益的经验,那就说明我更应该多多做题了,那些平时作业做得很快的同学估计平时都没闲着,才能越做越快,越做越好。

posted @ 2024-11-10 11:54  bushiwanglujie  阅读(16)  评论(0编辑  收藏  举报