数据采集与融合技术实践3

数据采集与融合技术实践3

任务一

实验要求

指定一个网站,爬取这个网站中的所有的所有图片,例如:中国气象网(http://www.weather.com.cn)。实现单线程和多线程的方式爬取。
–务必控制总页数(学号尾数2位)、总下载的图片数量(尾数后3位)等限制爬取的措施,我的学号是132301146,所以我的总页数应该控制在46页之内(一般达不到),下载的图片量最多为146张。

实验思路

这次任务分成了单线程和多线程模式,用到的方法略有不同。

单线程模式

单线程模式下,我从中国天气网首页开始,用一个队列 to_visit 存待访问网址,用 visited 记录已经访问过的页面,按类似 BFS 的方式一页一页爬,限制最多 46 页。每访问一页就用 requests 把 HTML 拿下来,然后再用 BeautifulSoup 解析出所有链接继续扩展爬取范围,同时找出页面里的所有 img 标签,把图片的 src 拼成完整 URL。用一个 downloaded_imgs 集合给图片链接去重,保证同一张图片只下载一次,然后按顺序把最多 146 张图片保存到本地的叫作 images 的文件夹里。但是这样做的话,程序的运行时间会比较就,因为照片是一张一张爬的。

%%{init: {"themeVariables": { "fontSize": "10px"}}}%% flowchart LR A([开始]) B[初始化参数<br/>start_url / 上限值<br/>visited / to_visit / 计数器] C{to_visit 非空<br/>且页数和图片数<br/>都未达上限?} D[从 to_visit 取出 url<br/>检查并加入 visited] E[requests.get 请求页面<br/>获取 HTML] F[BeautifulSoup 解析页面<br/>提取链接加入 to_visit] G[遍历 img 标签生成 img_url<br/>过滤重复图片] H[下载并保存图片<br/>img_cnt++] I([打印总页数和图片数<br/>结束]) A --> B --> C C -->|否| I C -->|是| D --> E --> F --> G G --> H --> C

多线程模式

多线程模式下,我先只做“收集链接”的工作,同样从起始页开始遍历页面,用 BeautifulSoup 找出所有图片 URL,利用 collected_set 去重,把不重复的图片地址依次放进 image_urls 列表中,直到收集到 146 张或者页面上限。收集完后,再开启多线程下载:为每个图片 URL 创建一个线程,调用 urllib.request.urlretrieve 把图片保存到本地,并用 join() 等所有线程结束,这样就实现了图片的并发下载,提高了整体下载效率。根据后面测试,比单线程快了将近10倍。

flowchart LR M0([开始]) M1[初始化参数<br/>start_url / 上限值<br/>visited / to_visit / image_urls] M2[遍历页面<br/>requests 请求 + BeautifulSoup 解析] M3[收集图片链接<br/>去重后加入 image_urls] M4{达到页数或图片上限?} M5[为每个图片创建线程<br/>urlretrieve 下载] M6[等待所有线程结束<br/>] M7([打印总图片数<br/>结束]) M0 --> M1 --> M2 --> M3 --> M4 M4 -->|否| M2 M4 -->|是| M5 --> M6 --> M7

代码和图片

任务要求指定一个网站,爬取这个网站中的所有的所有图片,例如:中国气象网(http://www.weather.com.cn)。实现单线程和多线程的方式爬取。
–务必控制总页数(学号尾数2位)、总下载的图片数量(尾数后3位)等限制爬取的措施,我的学号是132301146,所以我的总页数应该控制在46页之内(一般达不到),下载的图片量最多为146张。

单线程方法:

1.进入一个页面,并抓取所有图片并马上下载。
这是中国气象网站的部分数据,其中,我们可以发现,其主要的图片链接保存在<img>块中:
image

因此,我使用soup.find_all("img")方法,获得所有<img>块,并下载其中内容。
相关代码展示:

        for img in soup.find_all("img"):
            if img_cnt>=max_images:
                break
            src=img.get("src")
            if not src:
                continue
            img_url=urljoin(url,src)
            if not img_url.startswith("http"):
                continue
            #这里的downloaded_imgs为已经下载过的链接的集合,如果这个链接已经下过,就跳过
            if img_url in downloaded_imgs:
                continue
            downloaded_imgs.add(img_url)
            img_cnt+=1
            print("第",img_cnt, "张图片:",img_url)
            try:
                ir=requests.get(img_url,headers=headers,timeout=10)
            except:
                continue
            #下载图片,并将其格式全部转为jpg
            if ir.status_code==200:
                file_path=os.path.join(image_dir, f"single_{img_cnt:03d}.jpg")
                with open(file_path, "wb") as f:
                    f.write(ir.content)

2.收集页面中的链接,并加入待访问队列。
一张页面一般不会有146张图片,所以我们要多找几个页面爬取,我以主页面为出发点,爬取主页面中的链接并访问,如此递归
我们可以发现,url数据存在<a>块的herf中:
image

因此,我使用soup.find_all("a")a.get("href")来提取url
相关代码展示:

        #加入新的页面链接
        for a in soup.find_all("a"):
            href=a.get("href")
            #没有就跳过
            if not href:
                continue
            new_url=urljoin(url, href)
            if "weather.com.cn" in new_url and new_url.startswith("http"):
                #保证链接不重,防止环状结构
                if new_url not in visited and new_url not in to_visit:
                    to_visit.append(new_url)

部分结果图片展示:
images文件夹:
image
终端:
image

可见,146张图片的爬取耗时是23.81秒,速度较慢,这是因为单线程方法是一张一张地爬取图片,速率较低。

多线程方法:

1.先遍历页面,仅负责收集图片URL,但是并不下载图片
这里的代码和单线程差不多,唯一不同就是先不下载图片,只是收集图片的url存入集合中并去重。故不重复展示代码
2.使用threading.Thread启动多个线程并发下载
收集完146张图片的url后,使用threading.Thread并发下载图片:
相关代码展示:

    #多线程下载
    threads=[]
    idx=0
    for img_url in image_urls:
        idx+=1
        file_path=os.path.join(image_dir, f"multi_{idx:03d}.jpg")
        print("准备下载:", img_url)
        t=threading.Thread(target=urllib.request.urlretrieve, args=(img_url, file_path))
        t.start()
        threads.append(t)
    for t in threads:
        t.join()

结果截图:
images文件夹:
image
终端:
image

可见这次只用了2.8秒就下载完了146张图片,速度明显加快,这是因为多线程是并行下载图片,效率较高。

作业心得

通过这次作业,我完整实现了从爬取页面到解析HTML到提取图片到保存文件的完整数据采集流程,感觉收获非常大。在单线程模式里,我能比较清楚地看到爬虫的执行顺序,但速度确实慢;在多线程模式中,我真正体会到了并发带来的效率提升。尤其是在图片较多的情况下,多线程的下载速度可以说是成倍增长。同时我也意识到了爬虫开发中需要注意的几个关键点,比如url和图片链接需要去重,在一开始的时候,由于我没考虑到去重限制,导致爬了很多相同的照片进来,同时也会在几个网页链接之间来回打转,后面我设置了一个集合来去除相同照片和链接后,就不会一直爬同样的图片和链接了。

任务二

任务要求

熟练掌握 scrapy 中 Item、Pipeline 数据的序列化输出方法;使用scrapy框架+Xpath+MySQL数据库存储技术路线爬取股票相关信息。

实验思路

本次实验的思路是用 Scrapy + Xpath + MySQL 完成东方财富网股票数据的自动爬取和入库。先在 items.py 里定义 StockSpiderItem,把股票代码、名称、最新价等字段都写成 Item 属性;然后在 pipelines.py 中用 pymysql 连接 MySQL,准备好插入语句,让后面 yield 出来的数据都能直接写进表。爬虫文件 eastmoney_spider.py 里,在 start_requests 中按页数循环拼接东方财富网的接口 URL 发送请求,在 parse 中先用 Selector + xpath("string(.)") 取出整段 JSON 字符串,再用 json.loads 解析,遍历 diff 列表,把每只股票的各个字段填进 StockSpiderItem,最后用 yield item 交给 Pipeline 入库。这样就完成了“接口请求 → Xpath 提取 JSON → 解析字段 → 写入 MySQL”的完整流程。

flowchart LR A([开始]) B[定义 Item 和 Pipeline<br/>连接 MySQL] C[Spider 按页请求<br/>东方财富接口] D[使用 Xpath 提取 JSON<br/>json.loads 解析数据] E[封装 StockSpiderItem<br/>yield item] F[Pipeline 执行插入语句<br/>写入 MySQL] G([在数据库中查看结果<br/>结束]) A --> B --> C --> D --> E --> F --> G

代码和图片

首先,在终端创建scrapy项目stock_spider
image

接着,修改items.py里面的代码
改成如下形式:

import scrapy

class StockSpiderItem(scrapy.Item):
    # 股票代码
    bStockNo=scrapy.Field()#股票名称
    bStockName=scrapy.Field()#最新报价
    bNewPrice=scrapy.Field()#涨跌幅
    bChangeRate=scrapy.Field()#涨跌额
    bChangeAmt=scrapy.Field()#成交量
    bVolume=scrapy.Field()#成交额
    bTurnover=scrapy.Field()#振幅
    bAmplitude=scrapy.Field()#最高
    bHigh=scrapy.Field()#最低
    bLow=scrapy.Field()#今开
    bOpenToday=scrapy.Field()#昨收
    bCloseYesterday=scrapy.Field()

接着修改pipeline.py的代码,让其在爬虫开始时链接数据库

        self.conn=pymysql.connect(
            host=self.host,
            port=self.port,
            user=self.user,
            password=self.password,
            db=self.dbname,
            charset='utf8mb4'
        )
        self.cursor = self.conn.cursor()
        print("已连接 MySQL 数据库")

并设置插入语句

        sql = """
        INSERT INTO {table}
        (bStockNo, bStockName, bNewPrice, bChangeRate, bChangeAmt,
         bVolume, bAmplitude, bHigh, bLow, bOpenToday, bCloseYesterday)
        VALUES (%s, %s, %ss, %s, %s, %s, %s, %s, %s, %s, %s)
        """.format(table=self.table)

然后,修改settings.py中的配置,设置头部等信息。
最后编写eastmoney_spider.py脚本
对于网站的解析和上一次实践博客中的大致相同,网站结构为
image

image
关于解析提取数据方面,本次实验用 Xpath 取出整个响应体内容,再用再用 json.loads 把这个字符串解析成 Python 字典,并且在提取数据后,程序会将每只股票的数据填充到item里,最后用yield item将数据交给Scrapy的Pipeline
相关代码:
Xpath部分:

        sel=Selector(text=response.text)
        #用xpath取出文本
        json_text=sel.xpath("string(.)").get()

        try:
            data=json.loads(json_text)
        except Exception as e:
            print("解析出错:", e)

item部分:

for d in diff_list:
    item=StockSpiderItem()

    item["bStockNo"]=d.get("f12")
    item["bStockName"]=d.get("f14")
    item["bNewPrice"]=d.get("f2")
    item["bChangeRate"]=d.get("f3")
    item["bChangeAmt"]=d.get("f4")
    item["bVolume"]=d.get("f5")
    item["bTurnover"]=d.get("f6")
    item["bAmplitude"]=d.get("f7")
    item["bHigh"]=d.get("f15")
    item["bLow"]=d.get("f16")
    item["bOpenToday"]=d.get("f17")
    item["bCloseYesterday"]=d.get("f18")

最后,提前在mysql中建好表,并在stock_spider路径下运行scrapy crawl eastmoney_spider开始爬取数据
结果截图:
终端:
image
sql查询结果:
image

作业心得

这次实验主要使用Scrapy+Xpath+MySQL来完成东方财富网股票数据的爬取。整个过程让我熟悉了Scrapy的基本结构,包括Spider用来发送请求、Xpath用来提取数据、Pipeline负责把数据保存到 MySQL中。相比之前用requests的方式,Scrapy的流程更清晰,也更适合做规模化的爬虫。在编写实验代码时,我遇到了一些问题,比如数据库连接失败、数据表不存在、Xpath取值为空等,因为我刚的时候开始直接用 response.text 去 json.loads,发现有时候会解析失败,或者 data.get("diff") 为空。后来改成先用sel = Selector(text=response.text),再json_text = sel.xpath("string(.)").get(),最后json.loads(json_text)解决了问题

任务三

任务要求

熟练掌握 scrapy 中 Item、Pipeline 数据的序列化输出方法;使用scrapy框架+Xpath+MySQL数据库存储技术路线爬取外汇网站数据。这里,我选择的目标网站是中国银行网(https://www.boc.cn/sourcedb/whpj/)

实验思路

本次实验的思路是用 Scrapy + Xpath + MySQL 爬取中国银行外汇牌价数据。先观察网页结构,发现每条外汇记录都在表格的 里,对应的各个字段在一行中的多个 里,所以在 parse 中先用 //table//tr 取出所有行,跳过表头,再用 ./td/text() 拿到每一行的单元格内容,按下标把货币名称和各类买入价、卖出价以及发布时间对应起来。数值字段通过 to_float 做一下转换,清洗掉空值或“-”,最后把这些数据封装进 BocFxItem,用 yield item 交给 Pipeline 写入 MySQL,这样就完成了从网页表格到数据库的自动化采集。

flowchart LR A([开始]) B[请求中国银行外汇牌价页面] C[用 Xpath 获取所有 tr 行] D[逐行提取 td 文本<br/>过滤空行] E[解析出货币名称和各类汇率<br/>做简单清洗/类型转换] F[封装 BocFxItem 并 yield item] G[Pipeline 写入 MySQL] H([结束]) A --> B --> C --> D --> E --> F --> G --> H

代码和图片

首先,观察网站结构:
image

由图片可知,每一组外汇数据储存在一个个<tr>块里,而每个<tr>块里又会有一组<td>储存具体数值
因此我先用 Xpath 找到整张外汇牌价表的每一行,然后对于每一行,用 Xpath 取出所有单元格文本,其中,货币名称,现汇买入价,现钞买入价,现汇卖出价,现钞卖出价,发布时间分别对应第0,1,2,3,4,6个<td>块,在爬取完数据之后,把数据组装成item,通过pipline存储到数据库里。
相关代码展示:

        rows=response.xpath("//table//tr")

        #第一行通常是表头,跳过
        for tr in rows[1:]:
            tds=tr.xpath("./td/text()").getall()
            #有些tr可能是空行,这种就不要了
            if len(tds) < 7:
                continue

            currency=tds[0].strip()
            tbp=tds[1].strip()
            cbp=tds[2].strip()
            tsp=tds[3].strip()
            csp=tds[4].strip()
            time_str=tds[6].strip()

            if not currency:
                continue
            item=BocFxItem()
            item["Currency"]=currency
            item["TBP"]=self.to_float(tbp)
            item["CBP"]=self.to_float(cbp)
            item["TSP"]=self.to_float(tsp)
            item["CSP"]=self.to_float(csp)
            item["Time"]=time_str
            print("抓到:", item["Currency"], item["TBP"], item["CBP"], item["Time"])
            yield item

步骤和任务2差不多。
结果展示:
终端:
image

数据库:
image

作业心得

这次实验我遇到了一些问题,不过解决以后也感觉收获颇丰,一开始我只是照着网页看见一堆 <table><tr><td>,但用 Xpath 的时候总是选不准,要么把表头也算进去,要么把空行也爬进来,导致 tds 长度不够、代码直接报错。后来我在循环里加了 rows[1:] 跳过表头,又对 len(tds) < 7 和货币名称是否为空做了判断,这样就把无效行过滤掉了。还有一个问题是汇率字段里有时候是空串或者“-”,一转 float 就异常,我就单独写了一个 to_float 函数,先判断是不是空或者“-”,是的话就返回 None,否则再转成浮点数。处理完这些坑之后,把数据封装成 BocFxItem,交给 Pipeline 写入 MySQL,终端输出和数据库查询都正常,感觉自己对 Scrapy + Xpath + MySQL 这一整套流程比之前清晰多了。

gitee链接

https://gitee.com/hhz07734/parse/tree/master/practice_3

posted @ 2025-11-17 12:39  正三???  阅读(26)  评论(0)    收藏  举报