Python爬虫实战:ThreadPoolExecutor多线程采集书籍信息与图片下载
2026/6/10 17:54:32 网站建设 项目流程

Python爬虫实战:ThreadPoolExecutor多线程采集书籍信息与图片下载

完整代码在最后

Python 爬虫和多线程,使用 BooksToScrape 网站作为练习项目,实现:

  • 获取所有书籍详情页链接
  • 获取图片链接
  • 多线程采集书籍信息
  • 保存 CSV 数据
  • 多线程下载图片

项目不大,但在开发过程中踩到了不少坑

本文记录整个开发过程中的经验、问题以及解决方案。


项目目标

实现以下功能:

获取列表页 ↓ 提取书籍详情页链接 ↓ 提取图片链接 ↓ 线程池采集详情页 ↓ 保存CSV ↓ 线程池下载图片

项目使用技术

requests BeautifulSoup ThreadPoolExecutor csv os urllib.parse.urljoin

第一个坑:标签选择错误

最开始写的是:

articles=soup.find_all("div",class_="product_pod")

结果:

print(len(articles))# 输出0

检查网页结构后发现:

<articleclass="product_pod">

正确写法:

articles=soup.find_all("article",class_="product_pod")

写爬虫时不要想当然,一定要检查网页真实结构


第二个坑:图片地址获取错误

最开始使用:

img["href"]

结果报错:

KeyError:'href'

因为:

<imgsrc="media/cache/...jpg">

图片标签使用的是:

src

而不是:

href

正确写法:

img_src=img["src"]

经验:

a标签一般使用href img标签一般使用src

第三个坑:线程池没有真正并发

刚学习线程池时写法如下:

forbook_urlinbook_urls:future=pool.submit(save_books,book_url)writer.writerow(future.result())

看起来使用了线程池:

ThreadPoolExecutor

实际上:

提交任务 ↓ 等待结果 ↓ 提交下一个任务

效果接近单线程。


正确写法:

先提交所有任务:

futures=[]forbook_urlinbook_urls:future=pool.submit(save_books,book_url)futures.append(future)

再统一获取结果:

forfutureinfutures:writer.writerow(future.result())

这样线程池才能真正发挥作用。


第四个坑:Future对象不是结果

最开始理解错误:

future=pool.submit(save_books,book_url)print(future)

输出:

<Future at0x123456state=running>

发现拿到的不是书籍信息。

原因:

submit()

返回的是:

Future对象

它表示:

未来某个时间的结果

真正获取结果:

future.result()

第五个坑:文件提前关闭

最开始写法:

withopen("books.csv","w")asf:writer=csv.writer(f)writer.writerow(data)

结果:

ValueError:I/O operation on closedfile

原因:

with

结束后文件自动关闭。

必须保证:

writer.writerow()

在 with 代码块内部执行。


第六个坑:线程写CSV

最开始想让多个线程直接写 CSV。

后来发现容易出现:

数据错乱 缺失 覆盖

正确思路:

线程负责采集 ↓ 主线程统一写文件

即:

returndata

最后:

writer.writerows(data_list)

第七个坑:下载图片没有返回值

下载函数:

defdownload(url):...

没有:

return

因此:

future.result()

返回:

None

但这并不代表 Future 没用。

Future还有两个重要作用:

等待任务结束 捕获异常

例如:

future.result()

可以检查:

requests.exceptions.Timeout

等异常。


ThreadPoolExecutor常用知识点

创建线程池

fromconcurrent.futuresimportThreadPoolExecutorwithThreadPoolExecutor(max_workers=10)aspool:...

提交任务

future=pool.submit(func,arg1,arg2)

等价于:

func(arg1,arg2)

获取结果

result=future.result()

等待所有任务完成

forfutureinfutures:future.result()

捕获异常

try:result=future.result()exceptExceptionase:print(e)

本次项目结构

get_list() ↓ book_urls img_urls ↓ ThreadPoolExecutor ↓ save_books() ↓ CSV ------------------- ThreadPoolExecutor ↓ download() ↓ images


完整代码

importosimportrequestsfrombs4importBeautifulSoupfromconcurrent.futuresimportThreadPoolExecutorfromurllib.parseimporturljoinimportcsv url="https://books.toscrape.com/"headers={"User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36","Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","Accept-Language":"zh-CN,zh;q=0.9,en;q=0.8","Accept-Encoding":"gzip, deflate, br","Connection":"keep-alive"}''' 一次性获取所有书籍信息,下载图片,并且保存数据 多线程实现 '''book_urls=[]img_urls=[]#获取所有书籍详情页地址defget_list():current_url=url num=0whileTrue:res=requests.get(current_url,headers=headers,timeout=10)soup=BeautifulSoup(res.text,"html.parser")articles=soup.find_all("article",class_="product_pod")forarticleinarticles:num+=1print(f"找到第{num}条数据")book_href=article.find("div",class_="image_container").find("a")["href"]img_src=article.find("div",class_="image_container").find("img")["src"]book_url=urljoin(current_url,book_href)img_url=urljoin(current_url,img_src)book_urls.append(book_url)img_urls.append(img_url)break#爬取第一页测试即可next_li=soup.find("li",class_="next")ifnext_liisNone:breaknext_url=urljoin(current_url,next_li.find("a")["href"])current_url=next_url#保存数据defsave_books(book_url):res=requests.get(book_url,headers=headers,timeout=10)soup=BeautifulSoup(res.text,"html.parser")title=soup.find("div",class_="col-sm-6 product_main").find("h1").text.strip()price=soup.find("p",class_="price_color").text.strip()instock=soup.find("p",class_="instock availability").text.strip()return[title,price,instock]#下载图片defdownload(img_url,i):makedir="download"os.makedirs(makedir,exist_ok=True)res=requests.get(img_url,headers=headers,timeout=10)filename=os.path.join(makedir,f"image_{i}.png")withopen(filename,"wb")asf:f.write(res.content)if__name__=="__main__":get_list()print(f"共找到{len(book_urls)}本书,开始爬取...")withThreadPoolExecutor(max_workers=10)aspool:# 1. 先提交所有任务(真正并行)futures=[pool.submit(save_books,book_url)forbook_urlinbook_urls]# 2. 再统一写入 CSV(避免边爬边写时异常中断)withopen("books.csv","w",newline="",encoding="utf-8-sig")asf:# 用 utf-8-sig 防止 Windows 乱码writer=csv.writer(f)writer.writerow(["书名","价格","库存"])forfutureinfutures:try:row=future.result(timeout=10)# 增加超时保护writer.writerow(row)exceptExceptionase:print(f"爬取书籍信息失败:{e}")writer.writerow(["爬取失败","N/A","N/A"])# 3. 下载图片(和上面使用同一个 pool)print("开始下载图片...")download_futures=[]fori,img_urlinenumerate(img_urls,start=1):fut=pool.submit(download,img_url,i)download_futures.append(fut)# 等待图片下载完成forfutindownload_futures:try:fut.result(timeout=30)exceptExceptionase:print(f"下载图片失败:{e}")print("全部完成!")

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询