[关闭]
@1007477689 2020-04-28T05:12:42.000000Z 字数 6561 阅读 428

通过案例对比这几种爬虫方式的差异和优劣

爬虫


Python 爬虫的方式有多种,从爬虫框架到解析提取,再到数据存储,各阶段都有不同的手段和类库支持。虽然不能一概而论哪种方式一定更好,毕竟不同案例需求和不同应用场景会综合决定采取哪种方式,但对比之下还是会有很大差距。

(I)概况

以安居客杭州二手房信息为爬虫需求,分别对比实验了三种爬虫框架、三种字段解析方式和三种数据存储方式,旨在全方面对比各种爬虫方式的效率高低。

安居客平台没有太强的反爬措施,只要添加 headers 模拟头即可完美爬取,而且不用考虑爬虫过快的问题。选中杭州二手房之后,很容易发现 url 的变化规律。值得说明的是平台最大开放 50 页房源信息,每页 60 条。为使爬虫简单便于对比,我们只爬取房源列表页的概要信息,而不再进入房源详情页进行具体信息的爬取,共 3000 条记录,每条记录包括 10 个字段:标题,户型,面积,楼层,建筑年份,小区/地址,售卖标签,中介,单价,总价。

(II)3种爬虫框架

1. 常规爬虫

实现 3 个函数,分别用于解析网页、存储信息,以及二者的联合调用。在主程序中,用一个常规的循环语句逐页解析。

  1. import requests
  2. from lxml import etree
  3. import pymysql
  4. import time
  5. def get_info(url):
  6. pass
  7. return infos
  8. def save_info(infos):
  9. pass
  10. def getANDsave(url):
  11. pass
  12. if __name__ == '__main__':
  13. urls = [f'https://hangzhou.anjuke.com/sale/p{page}/' for page in range(1,51)]
  14. start = time.time()
  15. #常规单线程爬取
  16. for url in urls:
  17. getANDsave(url)
  18. tt = time.time() - start
  19. print("共用时:",tt, "秒。")
  1. >>> 耗时64.9秒。

2. Scrapy框架

Scrapy 框架是一个常用的爬虫框架,非常好用,只需要简单实现核心抓取和存储功能即可,而无需关注内部信息流转,而且框架自带多线程和异常处理能力。

  1. class anjukeSpider(scrapy.Spider):
  2. name = 'anjuke'
  3. allowed_domains = ['anjuke.com']
  4. start_urls = [f'https://hangzhou.anjuke.com/sale/p{page}/' for page in range(1, 51)]
  5. def parse(self, response):
  6. pass
  7. yield item
  1. >>> 耗时14.4秒。

3. 多线程爬虫

对于爬虫这种 IO 密集型任务来说,多线程可明显提升效率。实现多线程 python 的方式有多种,这里我们应用 concurrent 的 futures 模块,并设置最大线程数为 8。

  1. from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED
  2. def get_info(url):
  3. pass
  4. return infos
  5. def save_info(infos):
  6. pass
  7. def getANDsave(url):
  8. pass
  9. if __name__ == '__main__':
  10. urls = [f'https://hangzhou.anjuke.com/sale/p{page}/' for page in range(1, 51)]
  11. start = time.time()
  12. executor = ThreadPoolExecutor(max_workers = 8)
  13. future_tasks = [executor.submit(getANDsave, url) for url in urls]
  14. wait(future_tasks, return_when = ALL_COMPLETED)
  15. tt = time.time() - start
  16. print("共用时:",tt, "秒。")
  1. >>> 耗时8.1秒。

对比来看,多线程爬虫方案耗时最短,相比常规爬虫而言能带来数倍的效率提升,Scrapy 爬虫也取得了不俗的表现。需要指出的是,这里 种框架都采用了 Xpath 解析和 MySQL 存储。

(III)3种解析方式#

在明确爬虫框架的基础上,如何对字段进行解析提取就是第二个需要考虑的问题,常用的解析方式有 种,一般而言,论解析效率 Re >= Xpath > Bs4;论难易程度,Bs4 则最为简单易懂。

因为前面已经对比得知,多线程爬虫有着最好的执行效率,我们以此为基础,对比 种不同解析方式,解析函数分别为:

1. Xpath

  1. from lxml import etree
  2. def get_info(url):
  3. response = requests.get(url, headers = headers)
  4. html = response.text
  5. html = etree.HTML(html)
  6. items = html.xpath("//li[@class = 'list-item']")
  7. infos = []
  8. for item in items:
  9. try:
  10. title = item.xpath(".//div[@class='house-title']/a/text()")[0].strip()
  11. houseType = item.xpath(".//div[@class='house-details']/div[2]/span[1]/text()")[0]
  12. area = item.xpath(".//div[@class='house-details']/div[2]/span[2]/text()")[0]
  13. floor = item.xpath(".//div[@class='house-details']/div[2]/span[3]/text()")[0]
  14. buildYear = item.xpath(".//div[@class='house-details']/div[2]/span[4]/text()")[0]
  15. adrres = item.xpath(".//div[@class='house-details']/div[3]/span[1]/text()")[0]
  16. adrres = "|".join(adrres.split())
  17. tags = item.xpath(".//div[@class='tags-bottom']//text()")
  18. tags = '|'.join(tags).strip()
  19. broker = item.xpath(".//div[@class='broker-item']/span[2]/text()")[0]
  20. totalPrice = item.xpath(".//div[@class='pro-price']/span[1]//text()")
  21. totalPrice = "".join(totalPrice).strip()
  22. price = item.xpath(".//div[@class='pro-price']/span[2]/text()")[0]
  23. values = (title, houseType, area, floor, buildYear, adrres, tags, broker, totalPrice, price)
  24. infos.append(values)
  25. except:
  26. print('1条信息解析失败')
  27. return infos
  1. 耗时8.1秒。

2. Re

  1. import re
  2. def get_info(url):
  3. response = requests.get(url, headers = headers)
  4. html = response.text
  5. html = html.replace('\n','')
  6. pattern = r'<li class="list-item" data-from="">.*?</li>'
  7. results = re.compile(pattern).findall(html)##先编译,再正则匹配
  8. infos = []
  9. for result in results:
  10. values = ['']*10
  11. titles = re.compile('title="(.*?)"').findall(result)
  12. values[0] = titles[0]
  13. values[5] = titles[1].replace('&nbsp;','')
  14. spans = re.compile('<span>(.*?)</span><em class="spe-lines">|</em><span>(.*?)</span><em class="spe-lines">|</em><span>(.*?)</span><em class="spe-lines">|</em><span>(.*?)</span>').findall(result)
  15. values[1] =''.join(spans[0])
  16. values[2] = ''.join(spans[1])
  17. values[3] = ''.join(spans[2])
  18. values[4] = ''.join(spans[3])
  19. values[7] = re.compile('<span class="broker-name broker-text">(.*?)</span>').findall(result)[0]
  20. tagRE = re.compile('<span class="item-tags tag-others">(.*?)</span>').findall(result)
  21. if tagRE:
  22. values[6] = '|'.join(tagRE)
  23. values[8] = re.compile('<span class="price-det"><strong>(.*?)</strong>万</span>').findall(result)[0]+'万'
  24. values[9] = re.compile('<span class="unit-price">(.*?)</span>').findall(result)[0]
  25. infos.append(tuple(values))
  26. return infos
  1. >>> 耗时8.6秒。

3. Bs4

  1. from bs4 import BeautifulSoup
  2. def get_info(url):
  3. response = requests.get(url, headers = headers)
  4. html = response.text
  5. soup = BeautifulSoup(html, "html.parser")
  6. items = soup.find_all('li', attrs={'class': "list-item"})
  7. infos = []
  8. for item in items:
  9. try:
  10. title = item.find('a', attrs={'class': "houseListTitle"}).get_text().strip()
  11. details = item.find_all('div',attrs={'class': "details-item"})[0]
  12. houseType = details.find_all('span')[0].get_text().strip()
  13. area = details.find_all('span')[1].get_text().strip()
  14. floor = details.find_all('span')[2].get_text().strip()
  15. buildYear = details.find_all('span')[3].get_text().strip()
  16. addres = item.find_all('div',attrs={'class': "details-item"})[1].get_text().replace(' ','').replace('\n','')
  17. tag_spans = item.find('div', attrs={'class':'tags-bottom'}).find_all('span')
  18. tags = [span.get_text() for span in tag_spans]
  19. tags = '|'.join(tags)
  20. broker = item.find('span',attrs={'class':'broker-name broker-text'}).get_text().strip()
  21. totalPrice = item.find('span',attrs={'class':'price-det'}).get_text()
  22. price = item.find('span',attrs={'class':'unit-price'}).get_text()
  23. values = (title, houseType, area, floor, buildYear, addres, tags, broker, totalPrice, price)
  24. infos.append(values)
  25. except:
  26. print('1条信息解析失败')
  27. return infos
  1. >>> 耗时23.2秒。

XpathRe 执行效率相当,Xpath 甚至要略胜一筹,Bs4 效率要明显低于前两者(此案例中,相当远前两者效率的 ),但写起来则最为容易。

(IV)存储方式

在完成爬虫数据解析后,一般都要将数据进行本地存储,方便后续使用。小型数据量时可以选用本地文件存储,例如:CSV、txt或者json文件;当数据量较大时,则一般需采用数据库存储,这里,我们分别选用关系型数据库的代表MySQL和文本型数据库的代表MongoDB加入对比。

1. MySQL

  1. import pymysql
  2. def save_info(infos):
  3. #####infos为列表形式,其中列表中每个元素为一个元组,包含10个字段
  4. db= pymysql.connect(host="localhost",user="root",password="123456",db="ajkhzesf")
  5. sql_insert = 'insert into hzesfmulti8(title, houseType, area, floor, buildYear, adrres, tags, broker, totalPrice, price) values(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)'
  6. cursor = db.cursor()
  7. cursor.executemany(sql_insert, infos)
  8. db.commit()
  1. >>> 耗时8.1秒。

2. MongoDB

  1. import pymongo
  2. def save_info(infos):
  3. # infos为列表形式,其中列表中的每个元素为一个字典,包括10个字段
  4. client = pymongo.MongoClient()
  5. collection = client.anjuke.hzesfmulti
  6. collection.insert_many(infos)
  7. client.close()
  1. >>> 耗时8.4秒。
  1. CSV文件
  2. 4.
  1. import csv
  2. def save_info(infos):
  3. # infos为列表形式,其中列表中的每个元素为一个列表,包括10个字段
  4. with open(r"D:\PyFile\HZhouse\anjuke.csv", 'a', encoding='gb18030', newline="") as f:
  5. writer = csv.writer(f)
  6. writer.writerows(infos)
  1. >>> 耗时8.8秒。

可见,在爬虫框架和解析方式一致的前提下,不同存储方式间并不会带来太大效率上的差异。

(V)结论

不同爬虫执行效率对比

易见,爬虫框架对耗时影响最大,甚至可带来数倍的效率提升;解析数据方式也会带来较大影响,而数据存储方式则不存在太大差异。

对此,个人认为可以这样理解:类似于把大象装冰箱需要3步,爬虫也需要3步:
网页源码爬取,
目标信息解析,
数据本地存储。

其中,爬取网页源码最为耗时,这不仅取决于你的爬虫框架和网络负载,还受限于目标网站的响应速度和反爬措施;信息解析其次,而数据存储则最为迅速,尤其是在磁盘读取速度飞快的今天,无论是简单的文件写入还是数据库存储,都不会带来太大的时间差异。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注