数据采集作业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路径编写了,经常观察错了或者写非法了,就会爬取失败,这两题也应用了很多调试日志,来观察自己在编码上的错误。
上课做得不好就只能课后总结了。做到这里,基本理顺了这次实践的任务和方法,可能报告上还有不足,一些口语化表达不够规范等等,也欢迎批评指正。