数据采集作业3

这个作业属于哪个课程 2024数据采集与融合技术实践
这个作业要求在哪里 作业3
这个作业的目标 1.爬虫设计 2.掌握scrapy爬虫框架,并实际应用 3.掌握scrapy爬取动态页面(实现scrapy与selenium的对接)
学号 102202123

Gitee🔗:作业3

作业整体思路:

  • 创建一个scrapy项目(scrapy startproject scrapy_name)
  • 新建一个爬虫(scrapy genspider spider_name spider_domain)
  • items.py(需要爬取的数据项)
  • 爬虫项目修改(页面url变化、解析响应逻辑、爬取数据)
  • piplines.py(下载、存储)
  • settings.py(以下是一些配置说明👇)
    • 最大并发请求下载数量CONCURRENT_REQUESTS
    • 数据管道ITEM_PIPELINES
    • 图片下载地址IMAGES_STORE 实验3.1
      • 下载路径DEFAULT_IMAGES_URLS_FIELD
      • 本地存储路径DEFAULT_IMAGES_RESULT_FIELD
      • 注:这些都在items有定义
    • 自动延迟AUTOTHROTTLE 温和仿真模拟爬取网站
    • 是否遵循爬虫协议ROBOTSTXT_OBEY 本次作业通常为False,不然没有权限爬下来x
    • 其他默认创建项目即配置(项目名称、爬虫模块定义等)

注:由于此处标明了具体过程,博客撰写重心为关键代码实现思路

作业①

要求:指定一个网站,爬取这个网站中的所有的所有图片,例如:中国气象网(http://www.weather.com.cn)。使用scrapy框架分别实现单线程和多线程的方式爬取。
务必控制总页数(学号尾数2位)、总下载的图片数量(尾数后3位)等限制爬取的措施。
输出信息: 将下载的Url信息在控制台输出,并将下载的图片存储在images子文件中,并给出截图。

详细代码见gitee🔗作业3-实验3.1

实践过程

1.items.py

最终目标需要下载图片,所以这里定义了两个数据项目类:

  • 爬取网站图片url
  • 最后下载图片本地路径
class WeatherImageItem(scrapy.Item):
    image_urls = scrapy.Field()  # 用于存储图片的 URL 列表
    images = scrapy.Field()       # 用于存储下载后的图片信息

2.修改爬虫文件

①检测爬取网站
若直接爬取题目中所指的网站,检测ELEMENT可知:

  • 元素节点混乱,不统一
  • 不能满足题目的翻页
  • 图片数不可能达到学号后三位

自己摸索找到了天气网-图片这个网站

url检查:

  • index会随翻页改变
  • 中间有字段是你选择模块的拼音缩写
        for page in range(start, end + 1):
            if page == 1:
                url = f'https://p.weather.com.cn/{block}/index.shtml'
                yield scrapy.Request(url=url, callback=self.parse)
            else:
                url = f'https://p.weather.com.cn/{block}/index_{page}.shtml'
                yield scrapy.Request(url=url, callback=self.parse)

网页检查:

pictures = response.xpath('//div[@class="oi"]')
item = WeatherImageItem()
url = picture.xpath('.//div[@class="tu"]/a/img/@src').extract()[0]  # 提取图片链接
item["image_urls"] = [url]

②设置最大爬取数

    def __init__(self, max_items=None, *args, **kwargs):
        super(WeatherSpider, self).__init__(*args, **kwargs)
        self.max_items = int(max_items) if max_items else None
        self.item_count = 0  # 引入计数器,每提取一个图片路径就+1,直到达到max——items

同时在循环获取图片下载路径时,要判别item_count的大小,达到最大就截断:

        for picture in pictures:
            if self.max_items and self.item_count >= self.max_items:
                self.log(f"Reached maximum items limit: {self.max_items}", level=scrapy.log.DEBUG)
                print(f"Reached maximum items limit: {self.max_items}")
                return

3.piplines.py

管道类本身带有下载图片的功能,这里使用了内置管道ImagesPipeline来处理和下载图片

  • 图片 URL 的提取: 它能够从 item 中提取图片 URL,并为每个 URL 发送下载请求。
  • 图片存储: 下载的图片会按照配置的存储路径保存到本地。
  • 下载结果的处理: 下载完成后,可以在 item_completed 方法中处理下载结果。
  • 下载结果默认存在{指定路径}/full/下
from scrapy.pipelines.images import ImagesPipeline

class WeatherImagesPipeline(ImagesPipeline):
    def item_completed(self, results, item, info):
        for ok, result in results:
            if ok:
                image_url = result['url']
                print(f"Downloaded image: {image_url}")
        return item

4.settings.py

主要解释标题下的配置说明

IMAGES_STORE = 'images'  # 存储图片的目录
DEFAULT_IMAGES_URLS_FIELD = 'image_urls'  # urls存放路径
DEFAULT_IMAGES_RESULT_FIELD = 'images'  # 下载后图片所处本地路径

CONCURRENT_REQUESTS = 16  # 设置并发请求数

题目要求单线程和多线程分别编写
在scrapy中体现为并发请求数的设置,即CONCURRENT_REQUESTS

  • 单线程爬取:将 CONCURRENT_REQUESTS 设置为 1,这意味着 Scrapy 只会在任何时刻处理一个请求,尽管它仍然是异步的。
  • 多线程爬取:将 CONCURRENT_REQUESTS 设置为更高的值(例如 16),这样 Scrapy 将同时处理多个请求,尽管它仍然是在异步环境中。

实践结果

本次实践爬取的是天气现场2-5页

实践心得

这次实验是我自己在天气网里面摸索找到这个模块进行作业的,好像更符合题意,但美中不足的是,我检查了所有模块,这个网站图片数量比较少,都没法爬下我学号后三位那么多图片,这次没有直接截停图片下载的体验。
我是做完所有作业再来写博客,我考虑了一下,可能用selenium,模拟用户点击网站,就能进一步爬取更多图片了。在天气网-图片中,我选取的模块页面只将一张图片作为封面,其实点进去,还有多张图片,如果能动态模拟点击并进行下一页翻页,可以获得更多图片。
从这题我也了解到了scrapy管道类的内置方法ImagesPipeline,用于高效下载图片,它有很完善的框架,不用像先前还需要自己写下载图片的代码。

作业②

要求:熟练掌握 scrapy 中 Item、Pipeline 数据的序列化输出方法;使用scrapy框架+Xpath+MySQL数据库存储技术路线爬取股票相关信息。
候选网站:东方财富网:https://www.eastmoney.com/
输出信息:MySQL数据库存储和输出:序号、股票代码、股票名称、最新报价、涨跌幅、涨跌额、成交量、振幅、最高、最低、今开、昨收,表头英文命名

详细代码见gitee🔗作业3-实验3.2

实践过程

1.实现scrapy与selenium对接

由于东方财富网的行情模块是动态渲染的,所以需要完成以上对接才能成功爬取数据

    def __init__(self):
        self.driver = webdriver.Chrome()  # 使用 Chrome 浏览器
        self.max_stocks = 123  # 最大爬取数量
        self.current_count = 0  # 当前已爬取数量
        super().__init__()

    def parse(self, response):
        self.driver.get(response.url)  # 打开网站
        time.sleep(2)  # 等待页面加载

包括后面Selector类型也是通过selenium驱动获得网页源代码

sel = Selector(text=self.driver.page_source)
# ...(数据爬取在第二点)

爬取完毕也要退出浏览器

    def closed(self, reason):
        self.driver.quit()  # 确保退出浏览器

2.网页检查与数据爬取




所有数据都在元素对里面,
再观察网页可以注意到,部分带链接的数据项会嵌套在a元素中,带颜色(红表示涨,绿表示跌了)在span里面

            stocks = sel.xpath('//table/tbody//tr')  # 选择股票数据的容器
            self.logger.info(f"Found {len(stocks)} stocks.")  # 输出找到的股票数量

            for stock in stocks:
                if self.current_count >= self.max_stocks:
                    break  # 达到最大爬取数量,停止爬取

                item = StockItem()
                try:
                    item['stock_id'] = stock.xpath('./td[1]/text()').get()  # 序号
                    item['stock_code'] = stock.xpath('./td[2]/a/text()').get()  # 股票代码
                    item['stock_name'] = stock.xpath('./td[3]/a/text()').get()  # 股票名称
                    item['latest_price'] = stock.xpath('./td[5]/span/text()').get()  # 最新报价
                    item['change_percent'] = stock.xpath('./td[6]/span/text()').get()  # 涨跌幅
                    item['change_amount'] = stock.xpath('./td[7]/span/text()').get()  # 涨跌额
                    item['volume'] = stock.xpath('./td[8]/text()').get()  # 成交量
                    item['amplitude'] = stock.xpath('./td[10]/text()').get()  # 振幅
                    item['high'] = stock.xpath('./td[11]/span/text()').get()  # 最高
                    item['low'] = stock.xpath('./td[12]/span/text()').get()  # 最低
                    item['open'] = stock.xpath('./td[13]/span/text()').get()  # 今开
                    item['prev_close'] = stock.xpath('./td[14]/text()').get()  # 昨收

3.翻页爬取

翻页观察发现,url并不会改变
所以这里用selenium模拟用户使用浏览器点击下一页翻页,达成多页股票数据爬取

            # 查找并点击“下一页”按钮
            try:
                next_button = self.driver.find_element("link text", "下一页")
                if "disabled" in next_button.get_attribute("class"):
                    break  # 如果“下一页”按钮被禁用,停止翻页
                next_button.click()
                time.sleep(2)  # 等待页面加载
            except Exception as e:
                self.logger.error(f"Error clicking next page: {e}")
                break  # 如果出现错误,停止翻页

4.连接并将数据存储在MySQL

这里已经在编辑数据管道类
思路

  • 连接数据库(数据库用户登录、要连接的提前创建好的数据库)
  • 创建表(create)
  • 插入数据(insert into)
  • 关闭数据库
创建表代码
    def create_table(self):
        # 创建股票信息表
        create_table_sql = """
        CREATE TABLE IF NOT EXISTS stocks (
            stock_id VARCHAR(10) PRIMARY KEY,
            stock_code VARCHAR(10) NOT NULL,
            stock_name VARCHAR(50) NOT NULL,
            latest_price REAL,
            change_percent REAL,
            change_amount REAL,
            volume REAL,
            amplitude REAL,
            high REAL,
            low REAL,
            open REAL,
            prev_close REAL
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
        """
        try:
            self.cursor.execute(create_table_sql)
            self.connection.commit()
            print("Table 'stocks' created successfully.")
        except pymysql.MySQLError as e:
            print(f"Error creating table: {e}")
            self.connection.rollback()  # 出现错误时回滚
插入数据代码

sql = """
        INSERT INTO stocks (stock_id, stock_code, stock_name, latest_price, change_percent, change_amount, volume, amplitude, high, low, open, prev_close) 
        VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
        """
        try:
            spider.logger.info(f"Inserting data: {item}")
            self.cursor.execute(sql, (
                item['stock_id'], item['stock_code'], item['stock_name'], item['latest_price'],
                item['change_percent'], item['change_amount'], item['volume'],
                item['amplitude'], item['high'], item['low'],
                item['open'], item['prev_close']
            ))
            self.connection.commit()
            spider.logger.info("Data inserted successfully.")
        except pymysql.MySQLError as e:
            spider.logger.error(f"Error inserting data: {e}")
            self.connection.rollback()  # 出现错误时回滚
        except Exception as e:
            spider.logger.error(f"Unexpected error: {e}")

5.数据清洗

因为MySQL数据库数据类型限制,例如涨跌幅的百分号和成交量的,都需要进行展开转换

        try:
            item['latest_price'] = float(item['latest_price'])
            item['change_percent'] = float(item['change_percent'].replace('%', '').strip()) / 100.0  # 转换为小数
            item['change_amount'] = float(item['change_amount'])

            # 处理 volume,将万转换为实际数值
            item['volume'] = float(item['volume'].replace('万', '').strip()) * 10000

            # 处理 amplitude,假设其值是百分比形式,去掉百分号并转换
            item['amplitude'] = float(item['amplitude'].replace('%', '').strip()) / 100.0 if item['amplitude'] else 0.0

            # 处理其他字段
            item['high'] = float(item['high']) if item['high'] else 0.0
            item['low'] = float(item['low']) if item['low'] else 0.0
            item['open'] = float(item['open']) if item['open'] else 0.0
            item['prev_close'] = float(item['prev_close']) if item['prev_close'] else 0.0

        except ValueError as ve:
            spider.logger.error(f"Value conversion error: {ve}")
            return item  # 如果出现错误,可以选择跳过该条目

6.settings设置

这里爬取必须进行以下设置,不然没法爬

# Obey robots.txt rules
ROBOTSTXT_OBEY = False

实践结果


实践心得

这题是在老师的提示下才能完成的。在此之前我也没想到要动态爬取网页信息,要让scrapy和selenium对接。
针对此题url的特殊性,我提前应用selenium的方式实现了翻页爬取数据操作,成功爬下了123项股票数据,收获颇丰。
因为是第一次连接MySQL数据库,很不熟练,也进行了一定的学习,但发现和sqlite3其实比较相似,只是在连接上,MySQL更强调c-s模式交互。

作业③

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

详细代码见gitee🔗作业3-实验3.3

实践过程

1.实现与selenium的对接

中国银行网的货币数据也是动态渲染的,所以需要应用标题方法爬取数据
虽为核心方法,但方法同上,不多展示

2.网页检查

翻页时url会变化:

        for page in range(start, end + 1):
            if page == 1:
                url = f'https://www.boc.cn/sourcedb/whpj/index.html'  # 根据实际网站结构生成 URL
                yield scrapy.Request(url=url, callback=self.parse)
            else:
                url = f'https://www.boc.cn/sourcedb/whpj/index_{page - 1}.html'  # 根据实际网站结构生成 URL
                yield scrapy.Request(url=url, callback=self.parse)



由此可见在爬取的时候要跳过第一个tr(装的是标题,但是属性也为odd)
并且要注意每两个tr为一组,一个没有class属性,一个class属性值为odd
结合起来,爬取没有属性的tr就对了

    def parse(self, response):
        self.driver.get(response.url)
        time.sleep(2)  # 等待页面加载
        sel = Selector(text=self.driver.page_source)
        currencies = sel.xpath('//table[@width="100%"]/tbody//tr[not(@class)]')  # 选择货币数据的容器

        for index, currency in enumerate(currencies):
            item = CurrencyItem()
            try:
                item['currency'] = currency.xpath('./td[1]/text()').get()
                item['tbp'] = currency.xpath('./td[2]/text()').get()
                item['cbp'] = currency.xpath('./td[3]/text()').get()
                item['tsp'] = currency.xpath('./td[4]/text()').get()
                item['csp'] = currency.xpath('./td[5]/text()').get()
                item['time'] = currency.xpath('./td[8]/text()').get()

                yield item

            except IndexError as e:
                self.logger.error(f"Error processing stock: {e} - Stock data may be incomplete.")

进行翻页观察,每五个小时更新一次,并且只需要爬取两页就能爬出一个时间上全部货币情况
所以我选取爬取前两页

3.piplines和settings配置

piplines.py:MySQL数据库连接与存入应用
settings.py:不遵循爬虫协议
都同第二题

实践结果

实践心得

通过后面两道题我也直接接触了scrapy和selenium对接的方法。在做的过程中除了方法掌握以外,更让人纠结的应该就是数据爬取的xpath路径编写了,经常观察错了或者写非法了,就会爬取失败,这两题也应用了很多调试日志,来观察自己在编码上的错误。
上课做得不好就只能课后总结了。做到这里,基本理顺了这次实践的任务和方法,可能报告上还有不足,一些口语化表达不够规范等等,也欢迎批评指正。

posted @ 2024-11-03 17:52  okiqiiii  阅读(26)  评论(0编辑  收藏  举报