爬虫实践之线程池与多线程
1.首先,选择想要爬取的目标网站
例:我选择的是北京新发地(xinfadi.com.cn/priceDetail…) 上面有蔬菜的实时价格,来看一下页面。
简单的爬虫爬取页面的内容的方法有好几种,简单列举两点:
- 使用request库获取网页源码,然后使用etree进行xpath解析或者beautiful解析文本
- 如果网站使用的是ajax加载,可以找到网页的通讯请求,直接通过请求获取通讯数据包,然后直接分析数据包的内容
2.分析页面类型
因为我们爬取的内容为分页展示,所以首先我们需要知道网页翻页使用的是重定位还是ajax局部刷新,我们翻到第二页来看,看看网址是否发生改变:
可以清晰的看到,网址没有任何变化,这个时候,传统的request+etree来获取数据会有一点困难,因为网址没变,只是ajax进行了刷新,故可以开始尝试抓包。
点击F12进入调试模式,不用刷新页面,然后我们点击下面的翻页栏,例:点击第5页
我们可以看到网页给服务器发了一个请求,点击查看具体的请求内容
里面有一个url,我们复制这个url,新打开一个页面,输入这个网址
发现里面就是服务器发回来的json包,是用来更新网页ajax渲染的部分。
但是,有一个问题,这个json文件的第一个属性,current=1,返回的也是第1页的内容,但是网页只发了这一个请求啊,我们点击的是第五页,而且页面上刷新出来的也是第五页的内容,那为什么信息不对呢?而且既然url中没包含页面信息,那服务器是怎么知道我点击了第几页,从而返回正确的信息呢?
实际上,请求中并不是没包含这些信息,只不过它隐藏起来了。我们看一下网络监听中的负载信息
网页发送请求的同时,也把表单中的这些信息一并发回去了,理解了这个,那我们怎么实现对于不同页的接口访问呢,其实很简单,在url后加上 ?current=5 就可以访问到第五页的json通讯文件了,其他页面只需要改动这个数字即可。
3.线程池+多线程的爬取
确定了页面的类型和爬取的方法,那么可以开始写代码了。json文件的处理不多赘述,比较简单,另headers此网站不需要,其他网站按需进行添加属性。
因为需要爬取的页面均为一种类型,可以不用继承的线程类,直接直接定义方法,通过线程池的调用,实现多线程。
通过ThreadPoolExecutor创建线程池,参数代表线程池的大小,即同时运行多少个线程。
通过t.submit(方法,方法所需的参数(可以为多个))启动线程
from threading import Lock, Thread
from itsdangerous import json
import requests
import csv
from concurrent.futures import ThreadPoolExecutor
def get_price(url,page_info):
headers = {}
resp = requests.get(url,headers)
res = json.loads(resp.text)
price_list = res['list']
for i in price_list:
name = i['prodName']
avgprice = i['avgPrice']
detail = [name] + [avgprice]
with open('菜价.csv','a+',encoding='utf-8-sig',newline='') as fi:
fi = csv.writer(fi)
fi.writerow(detail)
print('第{}页爬取成功!'.format(page_info))
if __name__ == '__main__':
with ThreadPoolExecutor(50) as t:
for i in range(1,200):
t.submit(get_price,url = 'http://xinfadi.com.cn/getPriceData.html?current={}'.format(i),page_info = i)
写完程序,我们来看看运行结果怎么样:
好像都没问题,但是,我们打开文件看一下
多线程的问题之一,如何处理共享资源的问题暴露出来,最简单的处理方式就是加锁,在一个进程写入的时候,不允许其他线程对文件进行写入或修改,这样的锁叫做互斥锁,其他种类的锁这里就不介绍了,感兴趣的可以自己去查一下。
看一下加了互斥锁后的代码
from threading import Lock, Thread
from itsdangerous import json
import requests
import csv
from concurrent.futures import ThreadPoolExecutor
def get_price(url,page_info):
headers = {}
resp = requests.get(url,headers)
res = json.loads(resp.text)
price_list = res['list']
with lock:
for i in price_list:
name = i['prodName']
avgprice = i['avgPrice']
detail = [name] + [avgprice]
with open('菜价.csv','a+',encoding='utf-8-sig',newline='') as fi:
fi = csv.writer(fi)
fi.writerow(detail)
print('第{}页爬取成功!'.format(page_info))
if __name__ == '__main__':
lock = Lock()
with ThreadPoolExecutor(50) as t:
for i in range(1,200):
t.submit(get_price,url = 'http://xinfadi.com.cn/getPriceData.html?current={}'.format(i),page_info = i)
爬下来的数据,看起来没有啥子问题。