沉迷于经过高效算法及经典数据结构来优化程序的时候并不理解,为何多线程能够优化爬虫运行速度?原来是程序特性所决定的:传统算法的程序复杂度主要来源于计算,但网络程序的计算时间能够忽略不计,网络程序所面临的挑战打开不少很慢的连接,或者说,是如何有效的等待大量网络事件。css
直接下载一个页面html
import socket def threaded_method(): sock = socket.socket() sock.connect(('xkcd.com', 80)) request = 'GET /353/ HTTP/1.0\r\nHost: xkcd.com\r\n\r\n' sock.send(request.encode('ascii')) response = b'' chunk = sock.recv(4096) while chunk: response += chunk chunk = sock.recv(4096) #print(chunk) print(response) threaded_method()
每次经过recv向字节流读取4096字节的数据。固然默认状况下recv和connect都是阻塞的。这样咱们每下载一个页面就须要一个线程。线程开销是昂贵的,极可能在页面处理完以前就用光了线程。这是裸的套接字写法,为了接下来的编写和学习舒服一点,果真仍是来用几个python包吧。java
官方文档:Scrapy框架简单介绍node
这里记录下scrapy的基本操做python
在你的工做文件夹下输入此命令,tutorial文件夹就出现了。第一感受文件不多很清爽。web
编辑tutorial中的items文件。items是你保存爬取数据的容器。咱们须要根据你程序获取的内容对item进行建模,好比咱们须要title,link,desc这三个字段算法
import scrapy class DmozItem(scrapy.Item): title = scrapy.Field() link = scrapy.Field() desc = scrapy.Field()
在tutorial/spiders目录下建立dmoz_spider.py,好比代码写成这样sql
import scrapy class DmozSpider(scrapy.Spider): name = "dmoz" allowed_domains = ["dmoz.org"] start_urls = [ "https://www.amazon.cn/%E6%96%87%E5%AD%A6%E5%9B%BE%E4%B9%A6/b/ref=sa_menu_books_l2_b658394051?ie=UTF8&node=658394051" ] def parse(self, response): filename = response.url.split("/")[-2] with open(filename, 'wb') as f: f.write(response.body)
你必须继承自scrapy.Spider,并定义以下三个属性chrome
name
: 用于区别Spider。 该名字必须是惟一的,您不能够为不一样的Spider设定相同的名字。shell
start_urls
: 包含了Spider在启动时进行爬取的url列表。 所以,第一个被获取到的页面将是其中之一。 后续的URL则从初始的URL获取到的数据中提取。
parse()
是spider的一个方法。 被调用时,每一个初始URL完成下载后生成的 Response
对象将会做为惟一的参数传递给该函数。 该方法负责解析返回的数据(response data),提取数据(生成item)以及生成须要进一步处理的URL的 Request
对象
进入项目根目录执行命令启动爬虫:scripy crawl dmoz
你也能够加个scripy crawl dmoz --nolog关闭日志信息,这样输出页面就变得清爽了
(python是讲究格式的,start_url列表里格式里有坑)
谷歌浏览器插件:xpath-helper
安装好以后,咱们从新打开浏览器,按ctrl+shift+x就能调出xpath-helper框了。
若是咱们要查找某一个、或者某一块元素的xpath路径,能够按住shift,并移动到这一块中,上面的框就会显示这个元素的xpath路径,右边则会显示解析出的文本内容,而且咱们能够本身改动xpath路径,程序也会自动的显示对应的位置,能够很方便的帮助咱们判断咱们的xpath语句是否书写正确。
啊忘记说了scrapy的选择器是基于css+xpath的,可能在应用scrapy的时候大部分时间都用在xpath的构建上。而这个插件颇有用
虽然这个小插件使用很是方便,但它也不是万能的,有两个问题:
1.XPath Helper 自动提取的 XPath 都是从根路径开始的,这几乎必然致使 XPath 过长,不利于维护;
2.当提取循环的列表数据时,XPath Helper 是使用的下标来分别提取的列表中的每一条数据,这样并不适合程序批量处理,仍是须要人为修改一些相似于*标记等。
scrapy shell “https://www.amazon.cn/%E6%96%87%E5%AD%A6%E5%9B%BE%E4%B9%A6/b/ref=sa_menu_books_l2_b658394051?ie=UTF8&node=658394051”
以上命令执行后,会使用Scrapy downloader下载指定url的页面数据,而且打印出可用的对象和函数列表
这对于想要练习xpath使用的人来讲实在是棒得不行,不须要每次浪费时间爬一次才发现xpath没写对,你能够充分的验证你xpath的正确性而后再开启爬虫显然省了不少时间
到底怎么编写xpath才好呢我也思索了半天。谷歌浏览器有一个copy xpath功能(在f12模式下选中元素右键能够看到),多玩了几回发现它老是优先找路径上是否有Id选择器,显然copy xpath的制做者利用了一个html页面的设计规则--id选择器的属性是惟一对应的,找指定元素优先找id选择器是很是方便的。好比
import scrapy from tutorial.items import TutorialItem class DmozSpider(scrapy.Spider): name = "amoz" allowed_domains = ["amazon.cn"] start_urls = [ "https://www.amazon.cn/%E6%96%87%E5%AD%A6%E5%9B%BE%E4%B9%A6/b/ref=sa_menu_books_l2_b658394051?ie=UTF8&node=658394051" ] def parse(self, response): for sel in response.xpath('//*[@id="a-page"]/div[4]/div/div[2]/div/div[1]/div[1]/ul[2]'): item = TutorialItem() item['link'] = sel.xpath('li[2]/a/@href').extract() #link = sel.xpath('a/@href').extract() #print title,price,desc #print item['title'][0],item['price'][0],item['desc'][0] print item['link'][0] #yield item
这里的path就是谷歌浏览器拷下来的一个路径,而后xpath嵌套使用从而提取路径。
可是显然咱们只抓一个指定元素这种状况用得不多,而后再尝试抓取一组类似的目标。这里选取了亚马孙文学图书页面,试图抓取标题,价格,评论数。
而后发现这些元素的属性会随着分类变化,好比电子书跟实体书标题属性不同,折扣商品的价格属性又和不折扣的商品不同(我真是曰了苟了这源码这么乱的吗,彷佛只有分类讨论来解决?这样的话又会写不少,暂时没有想到好方法)
import scrapy from tutorial.items import TutorialItem from scrapy.http import Request class DmozSpider(scrapy.Spider): name = "amoz" allowed_domains = ["amazon.cn"] start_urls = [ "https://www.amazon.cn/b/ref=amb_link_9?ie=UTF8&node=659379051&pf_rd_m=A1AJ19PSB66TGU&pf_rd_s=merchandised-search-leftnav&pf_rd_r=04N93678VRYT3BZZZWFA&pf_rd_r=04N93678VRYT3BZZZWFA&pf_rd_t=101&pf_rd_p=4b8b99d4-2fd8-44c5-96ff-dbc77bd19498&pf_rd_p=4b8b99d4-2fd8-44c5-96ff-dbc77bd19498&pf_rd_i=658394051" ] def parse(self, response): #item['link'] = response.xpath('//a[@target="_blank"]/@href').extract() url_list = response.xpath('//a[@target="_blank"]/@href').extract() for url in url_list: yield Request(url,callback=self.parse_name) #print title,price,desc #print item['title'][0],item['price'][0],item['desc'][0] for i in range(2,75): page_url = 'https://www.amazon.cn/s/ref=lp_659379051_pg_{}?rh=n%3A658390051%2Cn%3A%21658391051%2Cn%3A658394051%2Cn%3A658511051%2Cn%3A659379051&page=2&ie=UTF8&qid=1497358151&spIA=B01FTXJZV2'.format(i) yield Request(page_url, callback=self.parse_name) def parse_name(self,response): item = TutorialItem() item['title'] = response.xpath('//*[@id="productTitle" or @id="ebooksProductTitle"]/text()').extract() item['descnum'] = response.xpath('//*[@id="acrCustomerReviewText"]/text()').extract() item['price'] = response.xpath('//span[@class="a-size-medium a-color-price inlineBlock-display offer-price a-text-normal price3P" or @class="a-color-price a-size-medium a-align-bottom"]/text()').extract() item['link'] = response.url yield item
当Item在Spider中被收集以后,它将会被传递到Item Pipeline,一些组件会按照必定的顺序执行对Item的处理。
每一个item pipeline组件(有时称之为“Item Pipeline”)是实现了简单方法的Python类。他们接收到Item并经过它执行一些行为,同时也决定此Item是否继续经过pipeline,或是被丢弃而再也不进行处理。
如下是item pipeline的一些典型应用:
显然在pipeline中对数据进行筛选并存储很是的稳
出于各类考虑(安全性,维护性),咱们通常不将数据库的信息写在源码里,而是像这样写在settings.py里
#Mysql 配置 MYSQL_HOST = '127.0.0.1' MYSQL_DBNAME = 'amazon' #数据库名字,请修改 MYSQL_USER = 'root' #数据库帐号,请修改 MYSQL_PASSWD = '123456' #数据库密码,请修改 MYSQL_PORT = 3306
而后从setting中加载数据库信息
# -*- coding: utf-8 -*- # Define your item pipelines here # # Don't forget to add your pipeline to the ITEM_PIPELINES setting # See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html from twisted.enterprise import adbapi from scrapy.exceptions import DropItem import MySQLdb import MySQLdb.cursors import codecs import json class TutorialPipeline(object): def __init__(self,dbpool): self.dbpool=dbpool self.ids_seen=set() def process_item(self, item, spider): #print '----------------' #print u'标题' + item['title'][0] #print u'评论' + item['descnum'][0] #print u'价格' + item['price'][0] if item['title'] and item ['descnum'] and item['price']: query = self.dbpool.runInteraction(self._conditional_insert, item) # 调用插入的方法 query.addErrback(self._handle_error, item, spider) # 调用异常处理方法 return item else: raise DropItem("Missing price in %s" % item) @classmethod def from_settings(cls, settings): #一、@classmethod声明一个类方法,而对于日常咱们见到的则叫作实例方法。 #二、类方法的第一个参数cls(class的缩写,指这个类自己),而实例方法的第一个参数是self,表示该类的一个实例 #三、能够经过类来调用,就像C.f(),至关于java中的静态方法 dbparams = dict( host=settings['MYSQL_HOST'], # 读取settings中的配置 db=settings['MYSQL_DBNAME'], user=settings['MYSQL_USER'], passwd=settings['MYSQL_PASSWD'], charset='utf8', # 编码要加上,不然可能出现中文乱码问题 cursorclass=MySQLdb.cursors.DictCursor, use_unicode=False, ) dbpool = adbapi.ConnectionPool('MySQLdb', **dbparams) # **表示将字典扩展为关键字参数,至关于host=xxx,db=yyy.... return cls(dbpool) # 至关于dbpool付给了这个类,self中能够获得 def _conditional_insert(self, tx, item): # print item['name'] sql = "insert into testtable(title,descnum,price) values(%s,%s,%s)" params = (item["title"], item["descnum"],item['price']) tx.execute(sql, params) def _handle_error(self, failue, item, spider): print '--------------database operation exception!!-----------------' print '-------------------------------------------------------------' print failue
如此就成功存储了