Python爬虫实战ThreadPoolExecutor多线程采集书籍信息与图片下载完整代码在最后Python 爬虫和多线程使用 BooksToScrape 网站作为练习项目实现获取所有书籍详情页链接获取图片链接多线程采集书籍信息保存 CSV 数据多线程下载图片项目不大但在开发过程中踩到了不少坑本文记录整个开发过程中的经验、问题以及解决方案。项目目标实现以下功能获取列表页 ↓ 提取书籍详情页链接 ↓ 提取图片链接 ↓ 线程池采集详情页 ↓ 保存CSV ↓ 线程池下载图片项目使用技术requests BeautifulSoup ThreadPoolExecutor csv os urllib.parse.urljoin第一个坑标签选择错误最开始写的是articlessoup.find_all(div,class_product_pod)结果print(len(articles))# 输出0检查网页结构后发现articleclassproduct_pod正确写法articlessoup.find_all(article,class_product_pod)写爬虫时不要想当然一定要检查网页真实结构第二个坑图片地址获取错误最开始使用img[href]结果报错KeyError:href因为imgsrcmedia/cache/...jpg图片标签使用的是src而不是href正确写法img_srcimg[src]经验a标签一般使用href img标签一般使用src第三个坑线程池没有真正并发刚学习线程池时写法如下forbook_urlinbook_urls:futurepool.submit(save_books,book_url)writer.writerow(future.result())看起来使用了线程池ThreadPoolExecutor实际上提交任务 ↓ 等待结果 ↓ 提交下一个任务效果接近单线程。正确写法先提交所有任务futures[]forbook_urlinbook_urls:futurepool.submit(save_books,book_url)futures.append(future)再统一获取结果forfutureinfutures:writer.writerow(future.result())这样线程池才能真正发挥作用。第四个坑Future对象不是结果最开始理解错误futurepool.submit(save_books,book_url)print(future)输出Future at0x123456staterunning发现拿到的不是书籍信息。原因submit()返回的是Future对象它表示未来某个时间的结果真正获取结果future.result()第五个坑文件提前关闭最开始写法withopen(books.csv,w)asf:writercsv.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_workers10)aspool:...提交任务futurepool.submit(func,arg1,arg2)等价于func(arg1,arg2)获取结果resultfuture.result()等待所有任务完成forfutureinfutures:future.result()捕获异常try:resultfuture.result()exceptExceptionase:print(e)本次项目结构get_list() ↓ book_urls img_urls ↓ ThreadPoolExecutor ↓ save_books() ↓ CSV ------------------- ThreadPoolExecutor ↓ download() ↓ images完整代码importosimportrequestsfrombs4importBeautifulSoupfromconcurrent.futuresimportThreadPoolExecutorfromurllib.parseimporturljoinimportcsv urlhttps://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/xhtmlxml,application/xml;q0.9,*/*;q0.8,Accept-Language:zh-CN,zh;q0.9,en;q0.8,Accept-Encoding:gzip, deflate, br,Connection:keep-alive} 一次性获取所有书籍信息下载图片并且保存数据 多线程实现 book_urls[]img_urls[]#获取所有书籍详情页地址defget_list():current_urlurl num0whileTrue:resrequests.get(current_url,headersheaders,timeout10)soupBeautifulSoup(res.text,html.parser)articlessoup.find_all(article,class_product_pod)forarticleinarticles:num1print(f找到第{num}条数据)book_hrefarticle.find(div,class_image_container).find(a)[href]img_srcarticle.find(div,class_image_container).find(img)[src]book_urlurljoin(current_url,book_href)img_urlurljoin(current_url,img_src)book_urls.append(book_url)img_urls.append(img_url)break#爬取第一页测试即可next_lisoup.find(li,class_next)ifnext_liisNone:breaknext_urlurljoin(current_url,next_li.find(a)[href])current_urlnext_url#保存数据defsave_books(book_url):resrequests.get(book_url,headersheaders,timeout10)soupBeautifulSoup(res.text,html.parser)titlesoup.find(div,class_col-sm-6 product_main).find(h1).text.strip()pricesoup.find(p,class_price_color).text.strip()instocksoup.find(p,class_instock availability).text.strip()return[title,price,instock]#下载图片defdownload(img_url,i):makedirdownloados.makedirs(makedir,exist_okTrue)resrequests.get(img_url,headersheaders,timeout10)filenameos.path.join(makedir,fimage_{i}.png)withopen(filename,wb)asf:f.write(res.content)if__name____main__:get_list()print(f共找到{len(book_urls)}本书开始爬取...)withThreadPoolExecutor(max_workers10)aspool:# 1. 先提交所有任务真正并行futures[pool.submit(save_books,book_url)forbook_urlinbook_urls]# 2. 再统一写入 CSV避免边爬边写时异常中断withopen(books.csv,w,newline,encodingutf-8-sig)asf:# 用 utf-8-sig 防止 Windows 乱码writercsv.writer(f)writer.writerow([书名,价格,库存])forfutureinfutures:try:rowfuture.result(timeout10)# 增加超时保护writer.writerow(row)exceptExceptionase:print(f爬取书籍信息失败:{e})writer.writerow([爬取失败,N/A,N/A])# 3. 下载图片和上面使用同一个 poolprint(开始下载图片...)download_futures[]fori,img_urlinenumerate(img_urls,start1):futpool.submit(download,img_url,i)download_futures.append(fut)# 等待图片下载完成forfutindownload_futures:try:fut.result(timeout30)exceptExceptionase:print(f下载图片失败:{e})print(全部完成)
Python爬虫实战:ThreadPoolExecutor多线程采集书籍信息与图片下载
发布时间:2026/6/10 17:54:33
Python爬虫实战ThreadPoolExecutor多线程采集书籍信息与图片下载完整代码在最后Python 爬虫和多线程使用 BooksToScrape 网站作为练习项目实现获取所有书籍详情页链接获取图片链接多线程采集书籍信息保存 CSV 数据多线程下载图片项目不大但在开发过程中踩到了不少坑本文记录整个开发过程中的经验、问题以及解决方案。项目目标实现以下功能获取列表页 ↓ 提取书籍详情页链接 ↓ 提取图片链接 ↓ 线程池采集详情页 ↓ 保存CSV ↓ 线程池下载图片项目使用技术requests BeautifulSoup ThreadPoolExecutor csv os urllib.parse.urljoin第一个坑标签选择错误最开始写的是articlessoup.find_all(div,class_product_pod)结果print(len(articles))# 输出0检查网页结构后发现articleclassproduct_pod正确写法articlessoup.find_all(article,class_product_pod)写爬虫时不要想当然一定要检查网页真实结构第二个坑图片地址获取错误最开始使用img[href]结果报错KeyError:href因为imgsrcmedia/cache/...jpg图片标签使用的是src而不是href正确写法img_srcimg[src]经验a标签一般使用href img标签一般使用src第三个坑线程池没有真正并发刚学习线程池时写法如下forbook_urlinbook_urls:futurepool.submit(save_books,book_url)writer.writerow(future.result())看起来使用了线程池ThreadPoolExecutor实际上提交任务 ↓ 等待结果 ↓ 提交下一个任务效果接近单线程。正确写法先提交所有任务futures[]forbook_urlinbook_urls:futurepool.submit(save_books,book_url)futures.append(future)再统一获取结果forfutureinfutures:writer.writerow(future.result())这样线程池才能真正发挥作用。第四个坑Future对象不是结果最开始理解错误futurepool.submit(save_books,book_url)print(future)输出Future at0x123456staterunning发现拿到的不是书籍信息。原因submit()返回的是Future对象它表示未来某个时间的结果真正获取结果future.result()第五个坑文件提前关闭最开始写法withopen(books.csv,w)asf:writercsv.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_workers10)aspool:...提交任务futurepool.submit(func,arg1,arg2)等价于func(arg1,arg2)获取结果resultfuture.result()等待所有任务完成forfutureinfutures:future.result()捕获异常try:resultfuture.result()exceptExceptionase:print(e)本次项目结构get_list() ↓ book_urls img_urls ↓ ThreadPoolExecutor ↓ save_books() ↓ CSV ------------------- ThreadPoolExecutor ↓ download() ↓ images完整代码importosimportrequestsfrombs4importBeautifulSoupfromconcurrent.futuresimportThreadPoolExecutorfromurllib.parseimporturljoinimportcsv urlhttps://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/xhtmlxml,application/xml;q0.9,*/*;q0.8,Accept-Language:zh-CN,zh;q0.9,en;q0.8,Accept-Encoding:gzip, deflate, br,Connection:keep-alive} 一次性获取所有书籍信息下载图片并且保存数据 多线程实现 book_urls[]img_urls[]#获取所有书籍详情页地址defget_list():current_urlurl num0whileTrue:resrequests.get(current_url,headersheaders,timeout10)soupBeautifulSoup(res.text,html.parser)articlessoup.find_all(article,class_product_pod)forarticleinarticles:num1print(f找到第{num}条数据)book_hrefarticle.find(div,class_image_container).find(a)[href]img_srcarticle.find(div,class_image_container).find(img)[src]book_urlurljoin(current_url,book_href)img_urlurljoin(current_url,img_src)book_urls.append(book_url)img_urls.append(img_url)break#爬取第一页测试即可next_lisoup.find(li,class_next)ifnext_liisNone:breaknext_urlurljoin(current_url,next_li.find(a)[href])current_urlnext_url#保存数据defsave_books(book_url):resrequests.get(book_url,headersheaders,timeout10)soupBeautifulSoup(res.text,html.parser)titlesoup.find(div,class_col-sm-6 product_main).find(h1).text.strip()pricesoup.find(p,class_price_color).text.strip()instocksoup.find(p,class_instock availability).text.strip()return[title,price,instock]#下载图片defdownload(img_url,i):makedirdownloados.makedirs(makedir,exist_okTrue)resrequests.get(img_url,headersheaders,timeout10)filenameos.path.join(makedir,fimage_{i}.png)withopen(filename,wb)asf:f.write(res.content)if__name____main__:get_list()print(f共找到{len(book_urls)}本书开始爬取...)withThreadPoolExecutor(max_workers10)aspool:# 1. 先提交所有任务真正并行futures[pool.submit(save_books,book_url)forbook_urlinbook_urls]# 2. 再统一写入 CSV避免边爬边写时异常中断withopen(books.csv,w,newline,encodingutf-8-sig)asf:# 用 utf-8-sig 防止 Windows 乱码writercsv.writer(f)writer.writerow([书名,价格,库存])forfutureinfutures:try:rowfuture.result(timeout10)# 增加超时保护writer.writerow(row)exceptExceptionase:print(f爬取书籍信息失败:{e})writer.writerow([爬取失败,N/A,N/A])# 3. 下载图片和上面使用同一个 poolprint(开始下载图片...)download_futures[]fori,img_urlinenumerate(img_urls,start1):futpool.submit(download,img_url,i)download_futures.append(fut)# 等待图片下载完成forfutindownload_futures:try:fut.result(timeout30)exceptExceptionase:print(f下载图片失败:{e})print(全部完成)