数据采集与融合技术实践作业三
gitee链接:https://gitee.com/wei-yuxuan6/myproject/tree/master/作业3
作业①
Scrapy爬取图片实验
-
要求:指定一个网站,爬取这个网站中的所有的所有图片,例如:中国气象网(http://www.weather.com.cn)。使用scrapy框架分别实现单线程和多线程的方式爬取。
-
务必控制总页数(学号尾数2位)、总下载的图片数量(尾数后3位)等限制爬取的措施。
-
输出信息: 将下载的Url信息在控制台输出,并将下载的图片存储在images子文件中,并给出截图。
-
过程
通过在网页HTML中查找图片元素发现图片的链接都在<img>
元素的src属性中(这个网站的格式比较统一)
就可以直接使用 Selector 类解析 HTML 并使用XPATH定位<img>
和获取属性src
pics = selector.xpath("//img/@src")
使用extract()提取图片URL
picurl = pic.extract()
使用Scrapy需要使用命令创建爬虫,scrapy startproject
命令创建爬虫项目
scrapy genspider <爬虫名称> <域名>
创建爬虫程序
可以在本地文件夹中查看是否创建成功
至于实现多线程和单线程,Scrapy本身并不直接支持多线程,因为它是基于异步IO的。但是可以通过改变settings中CONCURRENT_REQUESTS的值实现类似多线程的效果,设置并发请求数,为1近似单线程,也可以设置成其他值近似多线程(默认16)CONCURRENT_REQUESTS = 1
-
部分代码
1)先写items.py文件定义数据格式和字段个数import scrapyclass Job1Item(scrapy.Item):num=scrapy.Field() #图片编号picurl = scrapy.Field() #图片URL
2)再写爬虫程序picspider.py文件
必须导入定义的Item类from job1.items import Job1Item
构造初始URL并解析响应,Job1Item 是用于存储爬取结果的对象,通过 yield 将每个包含图片 URL 和编号的 item 传递给 ItemPipelines处理
# 定义一个爬虫类,继承自scrapy.Spider class PicspiderSpider(scrapy.Spider):name = "picspider" # 爬虫的名称,用于命令行启动start_urls = ["http://www.weather.com.cn"] # 初始请求的URL列表num = 0 # 用于计数图片的编号# 解析响应的方法def parse(self, response):try:# 使用UnicodeDammit来处理响应体的编码,自动检测并转换为合适的编码dammit = UnicodeDammit(response.body, ["utf-8", "gbk"])data = dammit.unicode_markup # 获取转换后的HTML内容# 使用Selector解析转换后的HTML数据selector = Selector(text=data)# 使用XPath选择所有图片的src属性pics = selector.xpath("//img/@src")# 遍历所有图片的srcfor pic in pics:self.num += 1 # 每次循环编号加1picurl = pic.extract() # 提取图片URLitem = Job1Item() # 创建一个Item实例item['num'] = self.num # 设置Item的num属性item['picurl'] = picurl # 设置Item的picurl属性# 使用yield生成Item对象yield item# 异常处理,打印错误信息except Exception as e:print(e)
3)写pipelines.py文件(如何处理scrapy爬取到的数据)
这里使用了urllib.request.urlretireve方法从获取到的图片URL下载图片并保存到本地class Job1Pipeline():def process_item(self, item, spider):print("正在下载 "+item['picurl'])urllib.request.urlretrieve(item['picurl'], f'./images/第{item["num"]}张图片.jpg')
4)配置settings.py文件
添加浏览器代理USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 " \"Safari/537.36"
不遵守robots.txt
ROBOTSTXT_OBEY = False
CONCURRENT_REQUESTS = 1
设置download_delay,防止访问过快被拒绝访问
DOWNLOAD_DELAY = 3
打开item_pipelines
4)run.py文件
使得scrapy可以直接在Pycharm中输出from scrapy import cmdline cmdline.execute("scrapy crawl picspider -s LOG_ENABLED=False".split())
-
结果
gitee链接:https://gitee.com/wei-yuxuan6/myproject/tree/master/作业3/job1
还尝试在pipelines.py中继承ImagesPipeline,并重写get_media_requests、file_path 和 item_completed方法,以实现自定义的图片下载逻辑和文件存储路径class Job1Pipeline(ImagesPipeline):def get_media_requests(self, item, info):yield scrapy.Request(item['picurl'])def file_path(self, request, response=None, info=None, *, item=None):print("正在下载 "+request.url)num = item['num']return f'第{num}张图片.jpg'def item_completed(self, results, item, info):return item
在settings.py中指定图片保存文件夹
# 指定文件保存位置 IMAGES_STORE = "./images2"
可以看到它不会再次访问已经访问过的URL,即重复的图片只下载一次
但是比使用urllib.request爬取到的图片还少了几张,主要是云图、雷达、降水量预报图(不知道是什么原因,尝试了很多次都是少这几张)
-
爬取京东的商品图片实现翻页,从第50页开始爬取150项
gitee链接:https://gitee.com/wei-yuxuan6/myproject/tree/master/作业3/jdpic
京东图片URL不仅在元素的src属性中,还有部分在data-lazy-img中,所以需要分别提取这两个属性的值,需要在访问完一页后构造新的URL并发出请求class SpiderJdSpider(scrapy.Spider):name = "picspider" # 爬虫名称source_url = "https://search.jd.com/" # JD商品搜索的基础URLq = "电脑" # 要搜索的商品关键词num = 150 # 用户输入的商品数量# 计算需要抓取的页面数量,每页显示30个商品if num % 30 == 0 and num != 0:page = num // 30 # 如果num是30的倍数else:page = num // 30 + 1 # 否则,加一页begin = 50 # 初始化页面计数器,表示当前抓取的页面(从第50页开始)i = 0 # 初始化抓取页数计数器count = 0 # 初始化商品计数器,记录成功抓取的商品数量def start_requests(self):# 构建请求URL,包含搜索关键词和其他参数url = self.source_url + "Search?keyword=" + urllib.parse.quote(self.q) + \"&wq=" + urllib.parse.quote(self.q) + \" f731e1b710764540abce5caf912eeb7a & isList = 0 & page = " + \str(self.begin) + "&s = 56 & click = 0 & log_id = 1730513540494.9221"yield scrapy.Request(url=url, callback=self.parse) # 发送请求,回调解析函数def parse(self, response):print(response.url) # 打印当前请求的URLdammit = UnicodeDammit(response.body, ["utf-8", "gbk"]) # 处理响应体的编码data = dammit.unicode_markup # 获取处理后的HTML内容selector = Selector(text=data) # 使用Selector解析HTMLitems = selector.xpath("//ul[@class='gl-warp clearfix']/li") # 选择商品列表项for item in items:url = item.xpath(".//div[@class='p-img']//img/@src").extract_first()if not url:url = item.xpath(".//div[@class='p-img']//img/@data-lazy-img").extract_first()picurl="https:"+urlpicurl = re.sub(r'\.avif$', '', picurl)self.count += 1 # 商品计数器加1item = JdpicItem() # 创建JdItem实例item['num'] = self.countitem['picurl'] = picurlyield item # 发送抓取到的商品信息if self.count == self.num: # 如果已抓取到所需数量break # 退出循环self.i += 1 # 页数计数器加1if self.i < self.page: # 如果还有页面需要抓取# 构建下一页的请求URLurl = self.source_url + "Search?keyword=" + urllib.parse.quote(self.q) + \"&wq=" + urllib.parse.quote(self.q) + \" f731e1b710764540abce5caf912eeb7a & isList = 0 & page = " + \str(self.begin+self.i) + "&s = 56 & click = 0 & log_id = 1730513540494.9221"yield scrapy.Request(url=url, callback=self.parse) # 发送请求,继续抓取下一页
在settings中设置
COOKIES_ENABLED = False
爬取京东的pipelines.py,还是使用urllib.request
因为尝试使用继承ImagePipelines只能下载30张图片,可能被限制访问了
150张图片使用urllib.request.urlretrieve下载很慢,要十几分钟,所以可以在这里使用多线程下载class JdpicPipeline:def __init__(self):# 创建线程池,线程数量可根据需要调整self.executor = ThreadPoolExecutor(max_workers=5) # 5 个线程并发下载def process_item(self, item, spider):# 提交下载任务到线程池self.executor.submit(self.download_image, item)return itemdef download_image(self, item):try:print(f"正在下载 {item['picurl']}")urllib.request.urlretrieve(item['picurl'], f'./images3/第{item["num"]}张图片.jpg')print(f"下载完成 {item['picurl']}")except Exception as e:print(f"下载失败 {item['picurl']} - 错误: {e}")def close_spider(self, spider):# 关闭线程池并等待所有线程完成self.executor.shutdown(wait=True)print("所有下载任务已完成")
结果:
一共150张图片
心得体会
初步实践了使用Scrapy下载图片,在Scrapy框架中传递图片URL数据并在ItemPipelines实现图片下载
巩固了翻页访问网页,以及在spider中构造新的URL请求并获得和解析响应,还有Scrapy的工作流程
两个实践都想采用继承ImagePipelines的方式下载图片但是都没有得到很好的结果,可能是对该方法的掌握和理解还不够深入,需要进一步加强学习。
而且在爬取天气网的图片时还有一些图片url在html中没有被访问和下载,可能是因为这些图片是动态的需要动态加载。
作业②
动态爬取股票相关信息实验
-
要求:熟练掌握 scrapy 中 Item、Pipeline 数据的序列化输出方法;使用scrapy框架+Xpath+MySQL数据库存储技术路线爬取股票相关信息。
-
候选网站:东方财富网:https://www.eastmoney.com/
-
输出信息:MySQL数据库存储和输出格式如下:
-
过程
访问东方财富网,找到需要爬取的股票html,看到股票信息都在表格<tbody>
中,每个股票在一个<tr>
元素中,字段分别在不同的<td>
中,需要找到自己需要的字段(如序号在第一个td
中……)
而且因为这个网站的数据是实时加载的,但是Scrapy是静态的,不能获取JavaScript执行过的html,所以如果不与Selenium配合使用就不能获得数据,而且Selenium相关的处理程序需要写在中间件中,因为downloader会得到spider请求响应的html,需要在中间件中使用Selenium重新处理html使得是JavaScript执行过的,并封装成新的响应发送给spider解析from scrapy.http import HtmlResponse from selenium import webdriverclass SeleniumMiddleware(object):def process_request(self,request,spider):url = request.urlbrowser = webdriver.Chrome()browser.get(url)time.sleep(5)html = browser.page_sourcebrowser.close()return HtmlResponse(url=url, body=html, request=request, encoding="utf-8", status=200)
同时需要在settings.py中配置这个中间件
SELENIUM_ENABLED = True DOWNLOADER_MIDDLEWARES = {"job2.middlewares.SeleniumMiddleware": 543, }
而且题目要求将爬取到的数据存储到MySQL数据库中,所以新建一个数据库crawl,再在其中建一个表stocks
-
部分代码
items.py设置字段class Job2Item(scrapy.Item):# define the fields for your item here like:no = scrapy.Field()code= scrapy.Field()name = scrapy.Field()zxj = scrapy.Field()zdf = scrapy.Field()zde = scrapy.Field()cjl = scrapy.Field()zf = scrapy.Field()zg = scrapy.Field()zd = scrapy.Field()jk = scrapy.Field()zs = scrapy.Field()
spi_stock.py
主要是解析响应部分,因为获得的是JavaScript处理过的html所以可以直接使用Xpath获取所需文本,使用position定位需要字段对应的<td>
标签下的文本,再使用extract_first提取出文本值def parse(self, response):try:dammit=UnicodeDammit(response.body,["utf-8","gbk"])data=dammit.unicode_markupselector=Selector(text=data)items = selector.xpath("//table[@id='table_wrapper-table']//tbody/tr")for i in items:no = i.xpath(".//td[position()=1]//text()")name = i.xpath(".//td[position()=3]//text()")code = i.xpath(".//td[position()=2]//text()")zxj = i.xpath(".//td[position()=5]//text()")zdf = i.xpath(".//td[position()=6]//text()")zde = i.xpath(".//td[position()=7]//text()")cjl = i.xpath(".//td[position()=8]//text()")zf = i.xpath(".//td[position()=10]//text()")zg = i.xpath(".//td[position()=11]//text()")zd = i.xpath(".//td[position()=12]//text()")jk = i.xpath(".//td[position()=13]//text()")zs = i.xpath(".//td[position()=14]//text()")item=Job2Item()item["no"]=no.extract_first()item["code"]=code.extract_first()item["name"]=name.extract_first()item["zxj"]=zxj.extract_first()item["zdf"]=zdf.extract_first()item["zde"]=zde.extract_first()item["cjl"]=cjl.extract_first()item["zf"]=zf.extract_first()item["zg"]=zg.extract_first()item["zd"]=zd.extract_first()item["jk"]=jk.extract_first()item["zs"]=zs.extract_first()yield itemexcept Exception as e:print(e)
pipelines.py
也是一个重要的部分,在这里需要打开数据库,将解析后的数据存储到数据库,最后还要关闭数据库
open_spider在开启爬虫时执行,连接MySQL数据库def open_spider(self, spider):print("opened") # 打印"opened",表示爬虫已启动try:# 连接到 MySQL 数据库self.con = pymysql.connect(host="127.0.0.1", port=3306, user="root", passwd="Tnt191123!", db="crawl",charset="utf8")self.cursor = self.con.cursor(pymysql.cursors.DictCursor) # 创建一个游标,使用字典游标以便返回字典格式的数据self.cursor.execute("delete from stocks") # 清空 stocks 表self.opened = True # 设置状态为已打开# 打印表头print("{:<5}{:<10}{:<10}{:<8}{:<8}{:<10}{:<10}{:<8}{:<10}{:<8}{:<8}{:<8}".format("序号", "代码", "名称", "最新价", "涨跌幅", "涨跌额", "成交量", "振幅", "最高", "最低", "今开", "昨收"))except Exception as e:print(e) # 打印异常信息self.opened = False # 如果出错,设置状态为未打开
close_spider在爬虫结束时执行,关闭数据库
def close_spider(self, spider):if self.opened: # 检查爬虫是否已打开self.con.commit() # 提交数据库事务self.con.close() # 关闭数据库连接self.opened = False # 设置状态为未打开print("closed") # 打印"closed",表示爬虫已关闭
process_item在每次接收到spider解析后的数据执行,先打印出数据再将数据插入到stocks表中
def process_item(self, item, spider):try:# 格式化并打印每个项的内容print("{:<5}{:<13}{:<10}{:<10}{:<10}{:<10}{:<10}{:<15}{:<10}{:<10}{:<10}{:<10}".format(item["no"], item["code"], item["name"], item["zxj"], item["zdf"], item["zde"], item["cjl"], item["zf"],item["zg"], item["zd"], item["jk"], item["zs"]))if self.opened: # 检查爬虫是否已打开# 执行插入操作,将项数据插入到 stocks 表中self.cursor.execute("insert into stocks(no,code,name,zxj,zdf,zde,cjl,zf,zg,zd,jk,zs) values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)",(item["no"], item["code"], item["name"], item["zxj"], item["zdf"], item["zde"], item["cjl"],item["zf"],item["zg"], item["zd"], item["jk"], item["zs"]))except Exception as e:print(e) # 打印异常信息return item # 返回处理后的项
-
结果
gitee链接:https://gitee.com/wei-yuxuan6/myproject/tree/master/作业3/job2
在运行时会出现一个浏览器窗口,加载网页
打印出的数据
存入数据库的数据
心得体会
学会了将Selenium与Scrapy结合爬取动态网页,在处理JavaScript动态渲染页面时,需要提前使用Selenium加载页面,使Scrapy能够抓取到完整的HTML内容,这样在Scrapy接收到响应时,页面已经被完全渲染。还学会了在Scrapy框架中将爬取到的数据存入MySQL数据库,进一步掌握了Scrapy与数据库交互的方式。
作业③
爬取银行外汇网站并存储实验
-
要求:熟练掌握 scrapy 中 Item、Pipeline 数据的序列化输出方法;使用scrapy框架+Xpath+MySQL数据库存储技术路线爬取外汇网站数据。
-
候选网站:中国银行网:https://www.boc.cn/sourcedb/whpj/
-
输出信息:
-
过程
分析网页发现数据都在表格中,在<table>
下的<tbody>
中,每一行数据在<tr>
中,每个字段在不同的<td>
中,其实后面spider获取的响应没有<tbody>
元素。
在MySQL创建一个新的表currency
-
部分代码
爬虫程序spi_bank.py,解析html获取数据,这里限定爬取100个数据,在后面执行了翻页操作class SpiBankSpider(scrapy.Spider):name = "spi_bank"start_urls = ["https://www.boc.cn/sourcedb/whpj/"]num=100count=0def parse(self, response):dammit = UnicodeDammit(response.body, ["utf-8", "gbk"])data = dammit.unicode_markupselector = Selector(text=data)items = selector.xpath("//table[position()=1]/tr[position()>1]")for i in items:item=Job3Item()name = i.xpath("./td[position()=1]/text()")tbp = i.xpath("./td[position()=2]/text()")cbp = i.xpath("./td[position()=3]/text()")tsp = i.xpath("./td[position()=4]/text()")csp = i.xpath("./td[position()=5]/text()")time = i.xpath("./td[position()=8]/text()")item["name"]=name.extract_first() if name else ""item["tbp"]=tbp.extract_first() if tbp else ""item["cbp"]=cbp.extract_first() if cbp else ""item["tsp"]=tsp.extract_first() if tsp else ""item["csp"]=csp.extract_first() if csp else ""item["time"]=time.extract_first() if time else ""yield itemself.count+=1if self.count==self.num:breakif self.count%27==0 and self.count!=self.num:url="https://www.boc.cn/sourcedb/whpj/index_"+str(self.count//27)+".html"yield scrapy.Request(url=url, callback=self.parse)
pipelines.py的代码与上题基本一致,都是打开、插入和关闭数据库
-
结果
gitee链接:https://gitee.com/wei-yuxuan6/myproject/tree/master/作业3/job3
直接输出结果
数据库数据,共100项
心得体会
遇到了网页上的html与获取到的响应的html元素不一致的情况,响应到的html没有<tbody>
元素,所以刚开始使用<tobody>
定位,没有输出但是又不知道错误在哪里……
还学会了使用position()>1
,position不仅可以等于,因为第一个tr是表头,而position()>1
可以直接获取后面所有的tr
巩固了使用scrapy框架+Xpath+MySQL数据库存储技术路线