
中乘风,生于海南,中国《英雄联盟》电子竞技职业选手,RNG战队上单。 2015年加入RNG战队并帮助队伍获得2017年LPL春季赛亚军、2017年LPL夏季赛亚军、2017年全球总决赛四强、2018年LPL春季赛冠军、2018年MSI季中赛冠军、RNG队伍2018英雄联盟洲际赛冠
我的博客即将入驻“云栖社区”,诚邀技术同仁一同入驻。
前言 对于那些通过JS来渲染数据的网站,我们要解析出它的html来才能取到想要的数据,通常有两种解决办法: 1、通过selenim调用浏览器(如chrome firefox等)来爬取,将解析的任务交给浏览器。 2、通过splash来解析数据,scrapy可以直接从splash的【空间】中拿到渲染后的数据。 这里介绍scrapy_splash 有个坑 根据它的文档,我们可以知道它依赖于Docker服务,所以你想要使用scrapy_splash就需要先安装docker并跑起来。再根据它的文档进行安装、启动等操作(其实这里有个坑): $ docker run -p 8050:8050 scrapinghub/splash 项目启动的时候,如果通过这个命令启动,他其实默认给你启动的是http://127.0.0.1:8050/,你可以用浏览器打开来看,看到这个页面就算正常启动了 splash正常启动页面-通过浏览器访问 所以,它的官方示例代码下一句代码(在settings.py中配置): SPLASH_URL = 'http://192.168.59.103:8050' 你在使用的时候如果按照这句来写,是无法连接splash的,必须写成: "SPLASH_URL": 'http://127.0.0.1:8050' 其他的配置可以按照文档来写。 代码中的使用 如果使用动态settings配置(避免影响其他爬虫)的话,可以在具体的spider文件中新增: custom_settings = { """ 动态settings配置 """ "SPLASH_URL": 'http://127.0.0.1:8050', "DOWNLOADER_MIDDLEWARES": { 'scrapy_splash.SplashCookiesMiddleware': 723, 'scrapy_splash.SplashMiddleware': 725, 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810, }, "SPIDER_MIDDLEWARES": { 'scrapy_splash.SplashDeduplicateArgsMiddleware': 100, }, "DUPEFILTER_CLASS": 'scrapy_splash.SplashAwareDupeFilter', "HTTPCACHE_STORAGE": 'scrapy_splash.SplashAwareFSCacheStorage', } name = 'tsinghua' allowed_domains = ['tv.tsinghua.edu.cn'] start_urls = ['http://tv.tsinghua.edu.cn/publish/video/index.html'] 然后再使用的时候,需要将哪个动态页面解析,就要用SplashRequest来发起请求,而不是之前的scrapy.Request来发起,其他的如callback、meta、url都是一样的(如果不一样请以文档为准)下面我放出清华大学视频站的解析代码: def parse(self, response): """ 获取导航链接, 自动爬取所有导航url, 交给parseList方法 为了获取js渲染的翻页, 这里用scrapy-splash的SplashRequest来构造请求, 以获得js渲染后的的html数据 """ totalNav = response.css('#nav li') for i in totalNav: urls = i.css('a::attr("href")').extract_first() yield SplashRequest(url=parse.urljoin(response.url, urls), args={"wait": 1}, callback=self.parseList) def parseList(self, response): """ 在列表页获取详情页的url以及视频封面, 传递给parseDetails方法, 最终获取视频和封面等信息 """ totalUrl = response.css('.picnewslist2.clearfix .clearfix ') for i in totalUrl: urls = parse.urljoin(response.url, i.css('.contentwraper figcaption a::attr("href")').extract_first()) imagesUrl = parse.urljoin(response.url, i.css('.picwraper img::attr("src")').extract_first()) yield Request(url=urls, meta={"imagesUrl": imagesUrl}, callback=self.parseDetails) """ 翻页操作 借助scrapy-splash来解析js渲染的html 取出"上一页, 下一页"的页码, 通过re正则来匹配其中的数值 对url进行判断, 是否是第一页。根据url构造不同的下一页nexPageUrl 最后借助scrapy-splash继续解析下一页的html """ nextPageList = response.css('a.p::attr("onclick")').extract() if len(nextPageList) >= 2: matchRule = re.search('\d+', nextPageList[1]) if matchRule: nextPageNumber = matchRule.group(0) thisPageRule = re.search('index_\d+', response.url) if thisPageRule: thisPageNumber = thisPageRule.group(0).replace('index_', '') nexPageUrl = response.url.replace(thisPageNumber, nextPageNumber) yield SplashRequest(url=nexPageUrl, callback=self.parseList) else: nexPageUrlJoin = 'index_' + nextPageNumber nexPageUrl = response.url.replace('index', nexPageUrlJoin) yield SplashRequest(url=nexPageUrl, callback=self.parseList) def parseDetails(self, response): """ 抽取视频详情, 交给对应item进行序列化 """ imagesUrl =response.meta['imagesUrl'] loaders = tsinghuaVedioItemLoader(item=tsinghuaVedioItem(), response=response) loaders.add_css("title", "article.article h1::text") # 标题 loaders.add_css("articleContent", '#play_mobile source::attr("src")') # 内容 loaders.add_value("imagesUrl", imagesUrl) # 视频封面地址 loaders.add_value("articleType", 2) # 类型:视频 loaders.add_value("addtime", datetime.now()) loaders.add_value("schoolName", "清华大学") loaders.add_value("schoolID", 6) items = loaders.load_item() yield items
这里增加应用场景,让图片下载结合自动识别,实现识别转换图片中的电话号码。 背景 在爬取广西人才网的过程当中,发现广西人才网企业联系电话那里不是str,而是将电话生成了一张图片,遇到这种情况,通常有三种不同的处理办法: 将图片地址保存下来,只存url 将图片下载到本地,存储url和本地路径path 将图片下载到本地,存储url和本地路径,然后用图片识别的方式将电话识别出来,赋给tel字段存入数据库 图片文字识别 这里先做图片识别的功能,在github上有个pytesseract包,通过pip安装: pip install pytesseract (当然了,前提是我之前安装过pillow包,要不然这里还是要安装pillow的) 然后项目目录下新建一个文件imagetestpy,代码如下: from PIL import Image import pytesseract codes = pytesseract.image_to_string(Image.open('test.jpg')) print(codes) 保存运行即可看到它能够正常识别出图片中的电话号码。 结合到项目中 广西人才网的项目逻辑是: spider获取电话图片-->交给item进行序列化-->配置图片下载将电话号码图片下载到本地-->通过pytesseract识别本地图片-->将识别出来的电话号码装在到item["tel"]中,完成。 所以spider里面不用获取tel的值,只需要获取图片url就行,代码为: loaders.add_css("image_urls", ".address tr:first-child img::attr(src)") 在item中定义好tel字段和path字段: comp_tel = scrapy.Field() image_path = scrapy.Field() 接着根据上一篇文章配置图片下载,settings: ITEM_PIPELINES = { #'scrapy.contrib.pipeline.images.ImagesPipeline': 1, 'rspider.pipelines.ImagePathPipeline': 1,# 开启图片下载 } """ 图片下载配置 """ IMAGES_URLS_FIELD = "image_urls" # 对应item里面设定的字段,取到图片的url prodir = os.path.abspath(os.path.dirname(__file__)) IMAGES_STORE = os.path.join(prodir,"images") # 设置图片保存path 到pipelisne.py中编写新的ImagesPipeline,通过重载item_completed获得图片下载路径,然后自定义get_img_tel方法来识别图片中的号码,将识别出来的号码传递给item["comp_tel"],将下载路径也传递给item["image_path"],整个类的代码为: from scrapy.pipelines.images import ImagesPipeline from PIL import Image import pytesseract import os class ImagePathPipeline(ImagesPipeline): """ 继承ImagesPipeline,重载item_completed方法 以实现处理图片下载路径的目的 """ def get_img_tel(self,image_path): """ 获取广西人才网图像中的电话号码 下载存储人才网企业联系电话的图片 将图片path传进来,通过pytesseract进行解析 由于代码存在些许问题,数字0会识别为u,所以识别后要进行replace替换 返回结果 """ prodir = os.path.abspath(os.path.dirname(__file__)) url_path = os.path.join(prodir, "images/") + image_path # 设置图片保存path tel_nums = pytesseract.image_to_string(Image.open(url_path)) return tel_nums def item_completed(self, results, item, info): """ 循环results,将里面的path取出,然后赋值到item中的image_path字段,最后返回item """ for ok, value in results: image_path = value["path"] item["image_path"] = image_path item["comp_tel"] = self.get_img_tel(image_path) return item 这样即可完成从图片下载到识别号码再传值回来的功能
通过前面两章的熟悉,这里开始实现具体的爬虫代码 广西人才网 以广西人才网为例,演示基础爬虫代码实现,逻辑: 配置Rule规则:设置allow的正则-->设置回调函数 通过回调函数获取想要的信息 具体的代码实现: import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class GxrcSpider(CrawlSpider): name = 'gxrc' allowed_domains = ['www.gxrc.com'] start_urls = ['http://www.gxrc.com/'] rules = ( Rule(LinkExtractor(allow=r'WebPage/Company.*'),follow=True,callback='parse_company'), # 配置公司正则 Rule(LinkExtractor(allow=r'WebPage/JobDetail.*'), callback='parse_item', follow=True), # 配置职位正则 ) def parse_item(self, response): """ 获取职位信息 """ i = {} i['job_name'] = response.css('h1#positionName::text').extract_first("") # 职位名称 return i def parse_company(self, response): """ 获取公司信息 """ i = {} i['company_name'] = response.css('.inner h1::text').extract_first("") # 公司名称 return i 这样即可完成基础的正则和信息抓取工作,至于Item和Pepiline和之前的写法一样。
rules = ( Rule(LinkExtractor(allow=r'WebPage/Company.*'),follow=True,callback='parse_company'), Rule(LinkExtractor(allow=r'WebPage/JobDetail.*'), callback='parse_item', follow=True), ) Rule的参数用法 跟踪Rule代码看它的参数: link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=identity link_extractor完成url的抽取,它就是交给CrawlSpider用 callback是回调函数 cb_kwargs是传递给link_extractor的参数 follow的意思是满足Rule规则的url是否跟进 process_links在Scrapy笔记--通用爬虫Broad Crawls(上)里面有代码演示,主要处理url process_request可以对request进行预处理,就像process_links处理url一样,编写一个函数方法进行处理 LinkExtrator的参数用法,跟踪代码看参数: allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(), tags=('a', 'area'), attrs=('href',), canonicalize=False, unique=True, process_value=None, deny_extensions=None, restrict_css=(), strip=True allow=(r'/jobs/\d+.html')中放置的是一个正则表达式,如果你满足正则,就对其进行提取 deny是allow的反向 allow_domains=('www.lagou.com')是指在指定域名www.lagou.com下的才进入处理 deny_domains是allow_domains的反向 restrict_xpaths、restrict_css可以通过xpath或者css进一步限定url,比如当前页面有很多符合条件的url,但是我希望限定某个范围进行取值,则可以通过它来指定范围区域,如: restrict_css('.jon-info') 是限定 <div class=jon-info>中间的范围</div> tags=('a', 'area'), attrs=('href',)是指默认通过a标签和area标签找到里面的href
通用爬虫(Broad Crawls)介绍 [传送:中文文档介绍],里面除了介绍还有很多配置选项。 通用爬虫一般有以下通用特性: 其爬取大量(一般来说是无限)的网站而不是特定的一些网站。 其不会将整个网站都爬取完毕,因为这十分不实际(或者说是不可能)完成的。相反,其会限制爬取的时间及数量。 其在逻辑上十分简单(相较于具有很多提取规则的复杂的spider),数据会在另外的阶段进行后处理(post-processed) 其并行爬取大量网站以避免被某个网站的限制所限制爬取的速度(为表示尊重,每个站点爬取速度很慢但同时爬取很多站点)。 正如上面所述,Scrapy默认设置是对特定爬虫做了优化,而不是通用爬虫。不过, 鉴于其使用了异步架构,Scrapy对通用爬虫也十分适用。 本篇文章总结了一些将Scrapy作为通用爬虫所需要的技巧, 以及相应针对通用爬虫的Scrapy设定的一些建议。 创建默认工程 scrapy创建工程是通过命令进行,创建一个名为proname的scrapy工程: scrapy startproject proname 然后根据提示: cd proname 然后生成爬虫模板: scrapy genspider lagou www.lagou.com 创建Broad Crawls工程 通用爬虫的创建过程与默认爬虫创建过程一样,只是在生成爬虫模板的时候命令不同,生成不同的爬虫模板: scrapy genspider -t crawl lagou www.lagou.com 只需要增加 -t crawl即可。 源码逻辑解析 主要逻辑: 1.爬虫模板主要使用的类是CrawlSpider,而它继承的是Spider。 2.Spider的入口函数是start_requests()。 3.Spider的默认返回处理函数是parse(),它调用_parse_response(),则允许我们重载parse_start_url()和process_results()方法来对response进行逻辑处理。 4._parse_response()会去调用爬虫模板设置的rules=(Rule(LinkExtractor…)),将response交给LinkExtrator,LinkExtrator会根据我们传进来的参数: allow=(), deny=(), allow_domains=(), deny_domains=(), restrict_xpaths=(), tags=('a', 'area'), attrs=('href',), canonicalize=False, unique=True, process_value=None, deny_extensions=None, restrict_css=(), strip=True 进行处理,其中deny的意思是除了它以外,反向取值,比如deny=('jobs/')则在处理的时候就会略过jobs,只爬取jobs以外的规则。 在项目目录的spiders文件夹下默认生成proname.py: import scrapy from scrapy.linkextractors import LinkExtractor from scrapy.spiders import CrawlSpider, Rule class GxrcSpider(CrawlSpider): name = 'proname' allowed_domains = ['www.proname.com'] start_urls = ['http://www.proname.com/'] rules = ( Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True), ) def parse_item(self, response): i = {} return i 1.通过Ctrl + 鼠标左键的方式跟踪CrawlSpider类,发现它是继承Spider的,往下看到CrawlSpider中有个parse方法,那么就意味着后面写代码的时候不能跟之前一样在代码里自定义parse函数了,最好像模板给出的parse_item这种写法。 2.解析parse函数,它调用了_parse_response方法: def parse(self, response): return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True) 其中的_parse_response可以说是核心函数,里面的参数cb_kwargs代表着参数。通过Ctrl+左键跟进_parse_response: def _parse_response(self, response, callback, cb_kwargs, follow=True): if callback: cb_res = callback(response, **cb_kwargs) or () cb_res = self.process_results(response, cb_res) for requests_or_item in iterate_spider_output(cb_res): yield requests_or_item if follow and self._follow_links: for request_or_item in self._requests_to_follow(response): yield request_or_item 首先它判断是否有callback,就是parse函数中的parse_start_url方法,这个方法是可以让我们重载的,可以在里面加入想要的逻辑; 然后它还会将参数cb_kwargs传入到callback中,往下看它还调用了process_result方法: def process_results(self, response, results): return results 这个方法什么都没做,把从parse_start_url接收到的result直接return回去,所以process_result方法也是可以重载的。 接着看: if follow and self._follow_links: for request_or_item in self._requests_to_follow(response): yield request_or_item 如果存在follow,它就进行循环,跟进_requests_to_follow看一看: def _requests_to_follow(self, response): if not isinstance(response, HtmlResponse): return seen = set() for n, rule in enumerate(self._rules): links = [lnk for lnk in rule.link_extractor.extract_links(response) if lnk not in seen] if links and rule.process_links: links = rule.process_links(links) for link in links: seen.add(link) r = self._build_request(n, link) yield rule.process_request(r) 在 _requests_to_follow中首先判断是否是response,如果不是就直接返回了,如果是就设置一个set,通过set去重;然后把_rules变成一个可迭代的对象,跟进_rules: def _compile_rules(self): def get_method(method): if callable(method): return method elif isinstance(method, six.string_types): return getattr(self, method, None) self._rules = [copy.copy(r) for r in self.rules] for rule in self._rules: rule.callback = get_method(rule.callback) rule.process_links = get_method(rule.process_links) rule.process_request = get_method(rule.process_request) 看到: rule.callback = get_method(rule.callback) rule.process_links = get_method(rule.process_links) rule.process_request = get_method(rule.process_request) 这几个都是前面可以传递过来的,其中rule.process_links是从Rule类中传递过来的: class Rule(object): def __init__(self, link_extractor, callback=None, cb_kwargs=None, follow=None, process_links=None, process_request=identity): self.link_extractor = link_extractor self.callback = callback self.cb_kwargs = cb_kwargs or {} self.process_links = process_links self.process_request = process_request if follow is None: self.follow = False if callback else True else: self.follow = follow 虽然process_links默认为None,但是实际上我们在需要的时候可以设置的,通常出现在前面爬虫模板代码里面的 Rule(LinkExtractor(allow=r'WebPage/JobDetail.*'), callback='parse_item', follow=True,process_links='links_handle') 然后可以在url那里增加各种各样的逻辑,这里只简单的打印输出: def links_handle(self, links): for link in links: url = link.url print(url) return links 可以将url进行其他的预处理,比如可以将url拼接到一起、设置不同的url或者对url进行字符切割等操作。(使用举例:常用于大型分城市的站点,比如58的域名nn.58.com、wh.58.com,就可以通过这个对各个站点的域名进行预匹配) 再来到_requests_to_follow方法中看处理逻辑 def _requests_to_follow(self, response): if not isinstance(response, HtmlResponse): return seen = set() for n, rule in enumerate(self._rules): links = [lnk for lnk in rule.link_extractor.extract_links(response) if lnk not in seen] if links and rule.process_links: links = rule.process_links(links) for link in links: seen.add(link) r = self._build_request(n, link) yield rule.process_request(r) set去重后就yield交给了_build_request处理,build_request则调用_response_downloaded进行页面的下载,下载后的页面交给_parse_response
背景 有时候爬虫爬过的url需要进行指纹核对,比如Scrapy就是进行指纹核对,如果是指纹重复则不再爬取。当然在入库的时候我还是需要做一次核对,否则如果爬虫有漏掉,进入数据库就不合适了。 思路 根据Scrapy的指纹生成方式,这次的指纹生成方式也是用hash的MD5对目标URL进行加密,生成固定长度的字符串,然后在数据库里面将字段设置成unique,这样的话在保证url固定长度的情况下还能够保证入库后的唯一性,进最大努力避免出现重复的数据。 指纹生成代码 新建一个文件,然后在里面编写指纹生成的方法,在使用的时候from import进来,调用方法即可。代码为: import hashlib def get_md5(url): """ 由于hash不处理unicode编码的字符串(python3默认字符串是unicode) 所以这里判断是否字符串,如果是则进行转码 初始化md5、将url进行加密、然后返回加密字串 """ if isinstance(url, str): url = url.encode("utf-8") md = hashlib.md5() md.update(url) return md.hexdigest() 为了验证代码的可用性,再加上代码: if __name__ == "__main__": urls = "http://www.baidus.com" print(get_md5(urls)) 在本地运行无误,再把下面这串删除。等到调用的时候from import get_md5把文件和方法引入,就可以使用了
前言 网上关于mysq时间、python时间与时间戳等文章很多,翻来翻去找不到头绪,根据不同博客的写法,挑了几个来测试,这里记录一下。 况且,不以实际需求为前提的博文,就是瞎写,估计以后自己都看不懂。 Mysql 时间类型 在数据库建表的时候,通常有5中字段类型让人选择: TIME、DATE、DATETIME、TIMESTAMP、YEAR,它们又各自是什么格式呢?要写的让自己容易记: TIME类型 :存储空间[3 bytes] - 时间格式[HH:MM:SS] - 时间范围[-838:59:59 到 ~ 838:59:59] DATE类型 :存储空间[3 bytes] - 时间格式[YYYY-MM-DD] - 时间范围[1000-01-01 到 9999-12-31] (可以理解为年月日) DATETIME类型 :存储空间[8 bytes] - 时间格式[YYYY-MM-DD HH:MM:SS] - 时间范围[1000-01-01 00:00:00 到 9999-12-31 23:59:59] (可以理解为年月日时分秒) TIMESTAMP类型 :存储空间[4 bytes] - 时间格式[YYYY-MM-DD HH:MM:SS] - 时间范围[1970-01-01 00:00:01 到 2038-01-19 03:14:07] (以秒为计算) YEAR类型 :存储空间[1 bytes] - 时间格式[YYYY] - 时间范围1901 到 2155 根据上面的类型得知,YEAR这种类型用的稍微少一点,TIME用的估计也不多,比较多的还是DATE、DATETIME和时间戳TIMESTAMP Python 日期和时间 Python提供了三种时间函数,时间模块time、基本时间日期模块datetime和日历模块Calendar,具体的详细介绍和用法在[菜鸟教程-时间和日期:传送A,传送B]有介绍,这里不复制粘贴了。 日历模块Calendar是用的次数比较少的(在爬虫和Django开发的实际应用较少),出现较多的是time模块和dateteime模块: time模块 -- 比较接近底层的 datetime模块 -- 基于time新增了很过功能,提供了更多函数 使用对比 1、获取当前时间 import datetime,time """ 当前时间 """ print(time.time()) print(datetime.datetime.now()) 得到的输出结果是: 1516200437.9920225 2018-01-17 22:47:17.992047 2、当前时间格式化 import datetime,time """ time当前时间 """ localtime = time.localtime(time.time()) print("本地时间为 :", localtime) gtime = time.strftime('%Y-%m-%d',localtime) print("可以把时间转换为 :",gtime) gltime = time.strftime('%Y-%m-%d %H:%M:%S',localtime) print("可以把时间转换为 :",gltime) #------------------------------------------------ """ datetime当前时间 """ localtime = datetime.datetime.now() gtime = datetime.datetime.now().strftime("%Y-%m-%d") gltime = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") print(localtime) print(gtime) print(gltime) 得到的结果是: 本地时间为 : time.struct_time(tm_year=2018, tm_mon=1, tm_mday=17, tm_hour=22, tm_min=55, tm_sec=46, tm_wday=2, tm_yday=17, tm_isdst=0) 可以把时间转换为 : 2018-01-17 可以把时间转换为 : 2018-01-17 22:55:46 #------------------------------------------------ 2018-01-18 08:03:18.760582 2018-01-18 2018-01-18 08:03:18 这里可以看出,用两个模块获得的当前时间都不是人类容易阅读的,都需要通过strftime函数进行格式化。 3、文本时间转换 这里我指的是爬虫获取的其他网站的时间,通常有几种格式: 长时间 -- 2018-01-06 18:35:05、2018-01-06 18:35 日期 -- 2018-01-06 月时间 -- 2018-01 时间 -- 18:35:05 通常,爬虫得到的时间都是人阅读的,只不过分隔符不同。并且在入库的时候,我希望他们的时间格式是统一的,年月日时分秒或者年月日,如果可以就用时间戳,方便计算(年月日时分秒对应年月日时分秒,年月日不可直接转换为年月日时分秒)。 遇到日期类型2018-01-06的时间格式,是不可以用函数直接转成长时间2018-01-06 18:35:05格式的,报错。当遇到这种情况,而我又想将时间统一,只能进行转换。转换又分为两种,相同时间格式转换与不同时间格式转换: 第一种情形 目标:2018-01-06 18:35:05 转换为2018-01-06 18:35:05 它有两种方法可以满足 方法一的逻辑是不同格式的时间转换要先转成时间数组,然后再由时间数组格式化成想要的类型: import datetime,time a = "2013-10-10 23:40:00" # 想要转换成 a = "2013/10/10 23:40:00" timeArray = time.strptime(a, "%Y-%m-%d %H:%M:%S") otherStyleTime = time.strftime("%Y/%m/%d %H:%M:%S", timeArray) print(timeArray) print(otherStyleTime) 从输出结果: time.struct_time(tm_year=2013, tm_mon=10, tm_mday=10, tm_hour=23, tm_min=40, tm_sec=0, tm_wday=3, tm_yday=283, tm_isdst=-1) 2013/10/10 23:40:00 可以看到,先通过time.strptime把它转换成时间数组,然后通过time.strftime把时间数组格式化成我想要的格式。 方法二,由于最终格式化的时间也是字符串str,所以当遇到这种情况的时候,还可以直接用replace来进行转换: a = "2013-10-10 23:40:00" # 想要转换成 a = "2013/10/10 23:40:00" print(a.replace("-", "/")) 输出结果为: 2013/10/10 23:40:00 第二种情形 目标:2018-01-06 转换为2018-01-06 18:35:05 它也有两种方法可以满足 它的逻辑是将年月日的字符串拼接上时分秒,然后再按照上面的两种方法进行转换,比如: a = "2013-10-10 " # 想要转换成 a = "2013/10/10 23:40:00" ac = a + "00:00:00" print(ac.replace("-", "/")) 得到输出结果 2013/10/10 00:00:00 第三种情形 目标:2018-01-06 18:35:05 转换为2018-01-06 思路与第一种一致,先转换为时间数组,然后再由时间数组进行格式化: import datetime,time a = "2013-10-10 23:40:00" # 想要转换成 a = "2013/10/10" timeArray = time.strptime(a, "%Y-%m-%d %H:%M:%S") otherStyleTime = time.strftime("%Y/%m/%d", timeArray) print(type(timeArray)) print(otherStyleTime) 得到结果输出为(可以看到timeArray的类型是time.struct_time): <class 'time.struct_time'> 2013/10/10 4、时间的比较运算 都知道字符串是不可以进行比较计算的,那么我们就需要用到其他的格式进行。time的strptime转换成时间数组是不可以进行运算的,但是datetime可以。 第一种 ,时间格式相同 import datetime,time d1 = datetime.datetime.strptime('2012-03-05 17:41:20', '%Y-%m-%d %H:%M:%S') d2 = datetime.datetime.strptime('2012-03-05 16:41:20', '%Y-%m-%d %H:%M:%S') delta = d1 - d2 print(type(d1)) print(delta.seconds) print(delta) 得到的输出是: <class 'datetime.datetime'> 3600 1:00:00 从结果上可以看到,格式相同的两种时间,可以通过datetime.datetime.strptime进行转换后再运算,在结果中还可以通过.seconds来计算 相差秒数 和通过.days来计算 相差天数 第二种 ,如果时间格式不一样,但是转换后的类型一样,也是可以比较的: import datetime,time d1 = datetime.datetime.strptime('2012/03/05 17:41:20', '%Y/%m/%d %H:%M:%S') d2 = datetime.datetime.strptime('2012-03-05 16:41:20', '%Y-%m-%d %H:%M:%S') delta = d1 - d2 print(delta.seconds) print(delta) 这段代码里面时间的字符串形式就不一样,但是通过同样的函数进行转换后就可以比较计算了。 第三种 ,年月日时分秒与年月日的计算,其实原理是一样的,转换后他们的格式都一样,所以也是可以计算的,2012/03/05 17:41:20与2012-03-05的时间相差: import datetime,time d1 = datetime.datetime.strptime('2012/03/05 17:41:20', '%Y/%m/%d %H:%M:%S') d2 = datetime.datetime.strptime('2012-03-01', '%Y-%m-%d') delta = d1 - d2 print(delta.days,delta.seconds) print(delta) print(type(delta)) 输出结果是 4 63680 4 days, 17:41:20 <class 'datetime.timedelta'> 通过print的结果可以得到几点信息: 不同格式的时间在转化后是可以进行比较运算的 可以通过.days和.seconds来进行天数与时分秒的展示 计算后得到的数据类型是 'datetime.timedelta' 而不是str类型 比如计算3天后的时间: import datetime,time now = datetime.datetime.now() delta = datetime.timedelta(days=3) n_days = now + delta print(type(n_days)) print(n_days.strftime('%Y-%m-%d %H:%M:%S')) 得到的结果是: <class 'datetime.datetime'> 2018-01-21 10:26:14 用datetime.timedelta取得3天时间,然后将当前时间加上3天,得到的是'datetime.datetime'类型数据,变成人类阅读的格式则需要strftime函数进行格式化,最终得到想要的2018-01-21 10:26:14。 5、时间戳 把字符串时间转换为时间戳: import datetime,time a = "2013-10-10 23:40:00" # 转换为时间数组 timeArray = time.strptime(a, "%Y-%m-%d %H:%M:%S") # 转换为时间戳: timeStamp = time.mktime(timeArray) print(timeArray) print(timeStamp) 输出结果为: time.struct_time(tm_year=2013, tm_mon=10, tm_mday=10, tm_hour=23, tm_min=40, tm_sec=0, tm_wday=3, tm_yday=283, tm_isdst=-1) 1381419600.0 可以看到time的时间数组与时间戳并不是同一样东西,他们是有区别的 6、strftime与strptime 这两个是python中常用的 strftime函数: 函数接收以时间元组,并返回以可读字符串表示的当地时间,格式由参数format决定。 time.strftime(format[, t]) format -- 格式字符串。t -- 可选的参数t是一个struct_time对象。 返回以可读字符串表示的当地时间。 import time t = (2009, 2, 17, 17, 3, 38, 1, 48, 0) t = time.mktime(t) print(time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(t))) 得到结果输出: 2009-02-17 09:03:38 strptime() 函数根据指定的格式把一个时间字符串解析为时间元组。 time.strptime(string[, format]) string -- 时间字符串。format -- 格式化字符串。 返回struct_time对象。 import datetime,time d1 = datetime.datetime.strptime('20120305 17:41:20', '%Y%m%d %H:%M:%S') d2 = datetime.datetime.strptime('2012-03-01', '%Y-%m-%d') print(d1) print(d2) 得到结果: 2012-03-05 17:41:20 2012-03-01 00:00:00 时间格式与入库 前面铺垫了这么多,最终的目的还是需要入库。这里以4种数据库时间类型为例: 字段名 => 数据类型 r_time => time r_date => date r_datetime => datetime r_timestamp => timestamp 根据最上方所写的Mysql时间类型,可以得出对应的时间格式为: 时间格式 => 数据类型 17:35:05 => time 2018-3-1 => date 2018/3/1 17:35 => datetime 2018/3/1 17:35 => timestamp time类型 time类型的格式指定为17:35:05,不可替换为(17-35-05或者17/35/05),会报错 可以简写成17:35,数据库会自动补全后面的00,入库后最终数据17:35:00 如果简写成17,则入库后变成00:00:17 当然,如果更奇葩的写法17:,17:35:这种是会报错的 date类型 date类型的格式指定为2018-3-1与2018/3/1,最终入库格式是(2018-03-01),它会自动补全 可以简写成[18/3/1]、[17/3/1]、[07/3/1]、[97/3/1],数据库会自动补全前面的年份,入库后最终数据2018-03-01、2017-03-01、2007-03-01、1997-03-01 不可简写成[2017]、[2017/3],会报错,必须是完整的日期格式 datetime类型 datetime类型的格式指定为2018-3-1 17:35:00和2018/3/1 17:35:00,最终入库格式是2018-03-01 17:35:00 它是date与time的结合,有很多共同特性 可以简写成[18/3/1 17:35:05]、[17/3/1 17:35]、[07/3/1 17]、[97/3/1 17],数据库会自动补全前面的年份,入库后最终数据2018-03-01 17:35:05、2017-03-01 17:35:00、2007-03-01 17:00:00、1997-03-01 17:00:00。可以看到它自动将时间格式补全成统一格式,这里与time不同的是,如果只写17不写分秒,time会默认将17当成秒,这里则是默认当成小时。 与date一样,年月日不可省略,必须以年月日格式出现 timestamp类型 根据上面的描述,timestamp的入库格式与datetime是一样的,不同的是时间范围和存储空间,它的格式与用法跟datetime一致
前言 大部分情况下,通过request去请求网页,response.text返回来的都是正常值,但是有一些反爬虫比较严重的网站(比如知乎)就不会是这样。知乎会返回转义字符,例如: header = { "User-Agent":"Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0", "HOST":"www.zhihu.com", "Referer":"https://www.zhihu.com/signup?next=%2F", } def rget(): response = session.get("https://www.zhihu.com/signup?next=%2F", headers=header) target_str = response.text print(target_str) rget() 在给知乎的登录页面发请求后,打印返回结果(内容太多,只返回一小部分): &quot;token&quot;:{&quot;xsrf&quot;:&quot;9b6c6406-db1b-45fa-8626-296c037cfc00&quot;,&quot;xUDID&quot;:&quot;ANBsasFlAg2PTgaqB1CHBtsWMijmJ20s89E=&quot;},&quot;account&quot;:{&quot;lockLevel&quot;:{} 发现有很多字符是转义的,登录需要用到的xsrf字段也在这里面,这样做正则匹配就很麻烦。 解决的办法是将html进行转义: import html target_str = html.unescape(response.text) 就能够得到正常的返回信息了: "token":{"xsrf":"9febf0fd-7c47-4695-93b6-f670e518d920","xUDID":"ACDsF5lmAg2PTi2GMwQTl0Cwh88G51BOgzc="}, 正则匹配 匹配xsrf的值 (为了方便测试,只将一小部分值提取出来做匹配): tokens = '"token":{"xsrf":"9febf0fd-7c47-4695-93b6-f670e518d920","xUDID":"ACDsF5lmAg2PTi2GMwQTl0Cwh88G51BOgzc="},:' matchs = re.search(r'xsrf[:"\w-]+', tokens) if matchs: print(matchs.group(0)) else: print("未匹配") 得到输出结果为: xsrf":"9febf0fd-7c47-4695-93b6-f670e518d920" 然后再借用replace将引号替换,然后用split将值用冒号分开: import re tokens = '"token":{"xsrf":"9febf0fd-7c47-4695-93b6-f670e518d920","xUDID":"ACDsF5lmAg2PTi2GMwQTl0Cwh88G51BOgzc="},:' matchs = re.search(r'xsrf[:"\w-]+', tokens) nstr = matchs.group(0).replace("\"", "").split(":") finall = nstr[1] print(finall) 就得到了我需要的xsrf: 9febf0fd-7c47-4695-93b6-f670e518d920 xUDID的提取也是如此,这两个值在知乎登录的时候是需要携带的,所以要提取: import re tokens = '"token":{"xsrf":"9febf0fd-7c47-4695-93b6-f670e518d920","xUDID":"ACDsF5lmAg2PTi2GMwQTl0Cwh88G51BOgzc="},:' matchs = re.search(r'xUDID[:"\w-]+=', tokens) nstr = matchs.group(0).replace("\"", "").split(":") finall = nstr[1] print(finall) ACDsF5lmAg2PTi2GMwQTl0Cwh88G51BOgzc= re正则匹配html的坑 上面的正则可以匹配到字符串了,如果正常登录的话要将请求返回的内容文本进行匹配的,也就是匹配response.text,代码是否就是 matchs = re.search(r'xUDID[:"\w-]+=', response.text) 坑就在这里! re默认匹配的是单行字符串,而response.text的返回值虽然是一个html页面的构成,但是它是分行的,第一行是html头<! DOCUMENT html>不是我想要的整个文本进行匹配。 re是支持整个文本匹配的,需要在正则代码加上参数re.DOTALL即可: matchs = re.search(r'xUDID[:"\w-]+=', response.text, re.DOTALL) 就可以对整个返回的文本进行匹配了
一、前言 需要在阿里云服务器部署Django-restframework框架,一开始不清楚情况,网上找了很多的文章和办法,东拼西凑也没有能够完全实现nginx和uwsgi的互通。 想学会? 你需要找一个干干净净的、没有安装其他东西的Linux服务器(Centos系统)来跟着这篇文章做。 参考过的文章有 -视频:Nginx + uWsgi 部署 Django + Mezzanine 生产服务器 -文章:uWSGI+django+nginx的工作原理流程与部署历程 -文章:uwsgi官方文档 -文章:Django Nginx+uwsgi 安装配置 -文章:centos7 下通过nginx+uwsgi部署django应用 二、网上文章的遗漏 因为是东拼西凑,所以无论是网上的文章还是自己拼凑的配置,都是没有办法打通的。后来红包求助,才了解到有这几个地方: 1、nginx执行权限 2、uwsgi配置 3、uwsgi设置虚拟环境 4、uwsgi安装问题及插件安装问题 5、django静态文件收集处理 三、部署安装记录 1、创建非管理员账户 由于安全需求,还是配置一个非管理员(自己操作,增加sudo授权)账户操作,通过命令创建用户名密码 adduser quinns # 新增用户 passwd quinns # 为quinns设置密码 设置好之后,还需要开启sudo权限,通过命令: vi /etc/sudoers 然后找到有 root ALL=(ALL)那一行,然后在下面增加一行: quinns ALL=(ALL) ALL 保存即可。 下面的操作,用新用户quinns登录来操作。 2、安装依赖 uwsgi和nginx以及anaconda的安装会存在一些报错问题,这里为了避免出现这情况,所以先安装好依赖。 sudo yum install gcc-c++ sudo yum install wget openssl-devel bzip2-devel expat-devel gdbm-devel readline-devel sqlite-devel sudo yum install uwsgi-plugin-python sudo yum install libxml* sudo yum install -y bzip2 sudo yum -y install pcre-devel sudo yum install python-devel sudo yum install build-essential python-dev 3、安装软件 通过quinns账户登录,然后到quinns用户目录(/home/quinns)下新建一个utils目录,把一些软件下载在utils目录下。 3.1安装anaconda 通过wget方式下载anaconda(官网) wget https://repo.continuum.io/archive/Anaconda3-5.0.1-Linux-x86_64.sh 如果想更快,就安装国内源(清华镜像): wget https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/Anaconda3-5.1.0-Linux-x86_64.sh 下载好之后sh安装 sh Anaconda3-5.0.1-Linux-x86_64.sh 一路默认,到之后面安装完的时候会提示是否添加环境变量,输入yes即可。 如果想要后面使用更快,可以更改仓库镜像: conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ conda config --set show_channel_urls yes 这样的话安装软件都是国内源,速度比较快 3.2验证是anaconda否成功安装 通过命令: conda list 来验证是否成功安装并加入环境变量,如果出现list列表则代表成功,如果出现报错提示信息则需要用命令: source ~/.bashrc 来添加,然后重复conda list命令。如果还是不行,则编辑/etc/profile文件,在底部添加环境变量及指向: export PATH=/home/quinns/anaconda3/bin:$PATH 通过文件添加的环境变量需重启服务器才能生效 3.3安装uwsgi 在确认安装好anaconda之后,先不着急新建虚拟环境,直接在linux下输入python,检查默认python是否已自动替换为python3.6。接着通过pip安装uwsgi: pip install uwsgi 待有安装成功的提示出来,再通过命令: uwsgi --version 来确认是否成功安装。 3.4创建虚拟环境 通过anaconda来创建python虚拟环境: conda create --name envname python=3.6.3 (亲身经历 3.6.5无法启动uwsgi,最好还是3.6.3) 观察过程,无报错即完成安装。 3.5安装nginx 直接通过yum来安装nginx即可,如果想安装新版,可以在网上寻找新方法。 sudo yum install nginx 安装完成后应该是自动启动服务,在浏览器输入ip即可访问nginx的欢迎页面。如果没有,通过命令: sudo service nginx start/restart 来启动或者重启nginx服务。 3.6安装mysql 先下载mysql的repo源 wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm 接着安装mysql-community-release-el7-5.noarch.rpm包 sudo rpm -ivh mysql-community-release-el7-5.noarch.rpm 安装这个包后,会获得两个mysql的yum repo源:/etc/yum.repos.d/mysql-community.repo,/etc/yum.repos.d/mysql-community-source.repo。 最后执行安装 sudo yum install mysql-server 3.7重置mysql密码 先授权 sudo chown -R root:root /var/lib/mysql service mysqld restart # 然后重启 接着重置密码 mysql -u root //直接回车进入mysql控制台 mysql > use mysql; mysql > update user set password=password('quinns') where user='root'; mysql > exit; service mysqld restart # 然后再重启一次服务 3.8开启远程访问 mysql默认是不开启远程访问的,想要在本地连接服务器的mysql,必须开启: mysql -u root -p mysql> use mysql; mysql> update user set host = '%' where user = 'root'; service mysqld restart # 这里也要重启一次服务 (过30秒或者1分钟再测试远程连接)如果不行的话,接着重启服务一次。 四、uwsgi服务测试 安装好这些软件后,需要确保独立服务都是正常运行的。 在/home/quinns目录下新建wwwroot目录,然后在里面新建一个测试文件uwsgitest.py def application(env, start_response): start_response('200 OK', [('Content-Type','text/html')]) return [b"Hello World, This uwsgi server is running"] 保存后通过命令来启动 uwsgi --http :8000 --wsgi-file uwsgitest.py 看到服务启动后,就可以在浏览器访问8080端口,如果能够正常显示文字内容,则代表uwsgi单独服务是可以正常运行的。如果没有,根据报错找原因。 五、上传django-rest项目 可以在本地,通过ssh对服务器进行连接,其中也包括上传下载服务。 本地打开终端后输入: scp -r djangoName quinns@47.98.xxx.xxx:/home/quinns/wwwroot 将当前目录的djangoName文件夹通过quinns账户上传到/home/quinns/wwwroot目录内。回车执行后输入quinns的密码即可看到上传到指定的wwwroot目录内。 六、配置django 本地开发环境下的django和服务器的设置有些许不一样。 首先要开放ALLOWED_HOSTS,使得程序可以远程访问,然后再设置静态文件,最后再通过命令来测试是否可以顺利启动。 开启drf远程访问及静态设置 找到django项目的settings.py文件,里面有个ALLOWED_HOSTS,是接收一个空列表,现在要将服务器地址或者域名添加进去(也可以放*号,代表所有都可以指向这里,但是不推荐这么做): ALLOWED_HOSTS = ['47.98.xxx.xxx'] 上面就算是开启了远程访问,接着设置静态(drf有一些样式,如果不设置,通过uwsgi启动是无法加载的)。同样是在settings.py文件中,下部分代码中有个STATIC_URL = '/static/',在它下面新增一行: STATIC_ROOT = os.path.join(BASE_DIR, "static/") 保存文件,然后在虚拟环境下执行命令: python manage.py collectstatic 这样django就会收集静态文件,放到指定目录内,也就是(static目录内) 七、编写uwsgi配置 uwsgi可以通过命令来启动django项目,也可以通过配置文件ini或者xml来启动。这里已ini为例。 在项目根目录(manage.py同目录,其实哪个目录都可以,这里是方便寻找)新建文件夹conf,然后再在conf下新建uwsgi文件夹(这俩文件夹什么名字无所谓)。接着新建uwsgi的配置文件,这里暂且叫做lagou_uwsgi.ini 里面写上uwsgi与项目的配置信息: ite_uwsgi.ini file` [uwsgi] # Django-related settings # the base directory (full path) chdir = /home/quinns/wwwroot/GamesAPI # Django's wsgi file module = GamesAPI.wsgi # the virtualenv (full path) # process-related settings # master master = true # maximum number of worker processes processes = 4 threads = 2 # the socket (use the full path to be safe socket = 127.0.0.1:8001 # ... with appropriate permissions - may be needed # chmod-socket = 664 # clear environment on exit vacuum = true virtualenv = /home/quinns/anaconda3/envs/envgames python-autoreload=1 logto = /home/quinns/wwwroot/GamesAPI/uwsgilog.log stats = %(chdir)/conf/uwsgi/uwsgi.status pidfile = %(chdir)/conf/uwsgi/uwsgi.pid 具体的含义在uwsgi文档都有,这里记录一下: chdir # 项目绝对路径 module # 项目内的uwsgi.py文件,其实与项目同名即可 master processes threads socket # 服务启动地址及端口 vacuum virtualenv # 这个就很重要了,python虚拟环境地址 python-autoreload=1 # python自启动 logto # 自动生成日志文件及存放路径 stats pidfile 这就算是编写好uwsgi的配置文件了,接着编写nginx的配置。 八、单项目nginx配置 最好不要改动原有的ningx,来新建一个新的.conf配置文件吧。同样在项目目录的conf目录内新建nginx文件夹,然后再在nginx文件夹里新建lagou.conf配置文件,里面写上nginx的配置: upstream games { # server unix:///path/to/your/mysite/mysite.sock; # for a file socket server 127.0.0.1:8001; # uwsgi的端口 } # configuration of the server error_log /home/quinns/wwwroot/nginxerror.log;#错误日志 server { # the port your site will be served on listen 8080; # 端口 server_name 47.98.xxx.xxx ; # 服务器ip或者域名 charset utf-8; # max upload size client_max_body_size 75M; # adjust to taste # Django media location /media { alias /home/quinns/wwwroot/GamesAPI/media; # 指向django的media目录 } # Django static location /static { alias /home/quinns/wwwroot/GamesAPI/static; # 指向django的static目录 } # Finally, send all non-media requests to the Django server. location / { uwsgi_pass games; include uwsgi_params; # uwsgi服务 } } 里面都有说明了,我就不写了。其的upstream games中的games是自定义名称,但是要与下面的uwsgi_pass games中games名称相同。 注意: .conf文件建立好后,要与让nginx知道并承认,所以需要通过软连接来链接到/etc/nginx/conf.d/目录下,如果不知道软连接怎么做,可以把这个文件copy到这个目录下。 然后重启服务器 sudo service nginx restart 如果没有报错,应该就是可以了。 如果有报错,没有重启ng服务器,那肯定是配置文件写错了,得去看一下。 九、启动项目 既然uwsgi也配置好了,django项目的虚拟环境也pip install -r requirements.txt过了,ng的配置文件也写好了。那就可以启动服务了。 nginx的服务启停 通过linux命令来进行启停 sudo service nginx restart/start/stop 如果之前启动过,就不用重启了。 uwsgi启动项目 找到刚才编写的lagou_uwsgi.ini配置文件目录,通过命令来启动: uwsgi -i lagou_uwsgi.init & 如果没有报错,就代表启动了。就可以在浏览器访问之前.conf配置文件配置的8080端口了。 十、nginx配置静态 后端api没有问题后,前端也要部署。 前端通过npm run build打包之后,将build文件通过ssh上传到wwwroot目录下: scp -r build quinns@xx.xx.xx.xx:/home/quinns/wwwroot 等到上传完成后,就到nginx那里进行静态的部属配置 cd /etc/nginx 然后打开nginx.conf文件进行编辑。 首先要给nginx文件进行访问授权,否则有些目录是会报错403的。其配置文件中有: user nginx; 这里得给他改成用户的权限,如quinns或者root user quinns; 看到server部分的代码: server { listen 80 default_server; listen [::]:80 default_server; server_name _; root /usr/share/nginx/html; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } …… …… } 是这样的,访问网址80端口默认指向/usr/share/nginx/html目录下的index.html 因为静态打包后build也是由index.html来作为主入口的。所以这里只需要把root的指向改过来即可: server { listen 80 default_server; listen [::]:80 default_server; server_name _; #root /usr/share/nginx/html; root /home/quinns/wwwroot/build; # Load configuration files for the default server block. include /etc/nginx/default.d/*.conf; location / { } …… …… } 将原来的root指向注释掉,增加build文件夹的指向 然后重启nginx服务,打开浏览器访问,就可以看正常的页面了。 心中一阵窃喜,这个坑终于是填上了。
前言 这里尝试用docker做个简单的服务启动,只要能够正常启动scrapyd,并且外部可以对其进行访问即可。 至于项目打包和利用数据卷进行持久化到下一篇文章再写,到时候要将这几样东西结合起来运行一个完整的项目。-- 安装docker 在本地机器上安装docker,只需要输入命令: sudo apt-get install docker-ce 就可以安装docker了(ce是社区免费版),然后通过命令: sudo docker images 可以查看docker是否完整安装并且可运行 项目基本知识 scrapyd是scrapy官方团队为用户提供的用于发布scrapy项目的web服务,通过pip install安装好后输入scrapyd即可启动,但是如果需要外部访问则要将bind_adress设置为0.0.0.0 在安装好docker后,需要编写Dockerfile和docker-compose.yml以构建docker镜像。我这里新建了一个空目录,在里面通过sudo nano 编写Dockerfile: FROM python:3.6 MAINTAINER ranbos RUN pip install scrapyd \ && pip install scrapyd-client COPY default_scrapyd.conf /usr/local/lib/python3.6/site-packages/scrapyd/ CMD ["scrapyd"] 基于python3.6 作者ranbos 安装scrapyd 接着安装scrapyd-client 复制本机上改好的default_scrapyd.conf 到docker python镜像内的 /usr/local/lib/python3.6/site-packages/scrapyd/目录下,以覆盖原来的配置文件,实现外部可访问(里面我只改动了bind地址,将127.0.0.1改成0.0.0.0) 最后执行命令scrapyd来启动服务 更改后的default_scrapyd.conf内容为: [scrapyd] eggs_dir = eggs logs_dir = logs items_dir = jobs_to_keep = 5 dbs_dir = dbs max_proc = 0 max_proc_per_cpu = 4 finished_to_keep = 100 poll_interval = 5.0 bind_address = 0.0.0.0 http_port = 6800 debug = off runner = scrapyd.runner application = scrapyd.app.application launcher = scrapyd.launcher.Launcher webroot = scrapyd.website.Root [services] schedule.json = scrapyd.webservice.Schedule cancel.json = scrapyd.webservice.Cancel addversion.json = scrapyd.webservice.AddVersion listprojects.json = scrapyd.webservice.ListProjects listversions.json = scrapyd.webservice.ListVersions listspiders.json = scrapyd.webservice.ListSpiders delproject.json = scrapyd.webservice.DeleteProject delversion.json = scrapyd.webservice.DeleteVersion listjobs.json = scrapyd.webservice.ListJobs daemonstatus.json = scrapyd.webservice.DaemonStatus 至于为什么复制后的路径是/usr/local/lib/python3.6/site-packages/scrapyd/呢? 在容器启动后,通过命令: sudo docker exec -it a86 bash 其中a86是镜像id可以进入到容器内部,然后在里面通过: pip show scrapyd 就可以找到scrapyd的安装路径。 在编写好Dockerfile后,就需要编写docker-compose.yml文件了: version: '3' services: web: build: . ports: - "6800:6800" 指定compose版本 然后指定服务为web 在当前目录构建 映射端口将开放给外部的端口映射到scrapyd服务端口6800 将Dockerfile和docker-compose.yml文件编写好后,通过命令: sudo docker-compose up 就可以让它自行打包(根据Dockerfile的设定),下载和复制对应的依赖及文件,然后根据docker-compose.yml的设定构建镜像并且运行。 输入图片说明 可以看到容器正常启动,而且外部也可以访问scrapyd服务了。 下次启动 这次的打包构建做好了,那么下一次的呢? 通过命令: sudo docker-images 可以看到本地有一个名为dockerscrapyd_web的镜像,就是刚才我构建的镜像 然后用命令: sudo docker run -p 6800:6800 dockerscrapyd_web 其中dockerscrapyd_web就是容器的名称,就可以看到它又被启动了。 存储到云仓库 构建好的镜像可以通过命令启动,但是这不是最终目的,最终目的应该是将它放到云仓库当中,当自己需要的时候直接run或者pull就可以使用了。 Docker官方为人们提供了这样的服务,网址是hub.docker.com 登录后可以点击create 输入图片说明 输入信息后创建一个云镜像 输入图片说明 用命令登录docker: sudo docker login 根据提示输入用户名和密码就行,登录成功后会回传消息: Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one. Username: ranbovideo Password: Login Succeeded 然后再用命令提交镜像: sudo docker push <hub-user>/<repo-name>:<tag> <hub-user>指的是用户名 <repo-name>指的是建立的仓库名,用图举例: 输入图片说明 后面跟着版本tag,用命令举例: sudo docker push dockerscrapyd_web 如果是这样的命令,直接输入镜像名称是不行的 会得到回传信息: denied: requested access to the resource is denied 要根据刚才的格式,将镜像tag和名称改一下: sudo docker tag dockerscrapyd_web ranbovideo/scrapyd 然后可以用 sudo docker images查看是否改成功。看到镜像存在后用命令进行上传: sudo docker push ranbovideo/scrapyd:latest 就可以看到它在一步步上传镜像了 输入图片说明 注意:推送Docker Hub速度很慢,耐心等待,很有可能失败,之后断开推送(但已推送上去的会保留,保留时间不知道是多久,可以通过刚才的命令继续上传,毕竟一个镜像几百M(我也不知道为什么那么大,700多M,我看了python3.6镜像有690多M,估计就是它),不是那么容易的 最后检查 push上云端之后,为了检查是否正常和正确,我把本地的镜像全都删了,然后从云端将它pull下载到本地运行 阿里云仓库 阿里云也为广大用户提供了仓库,可以传到共有也可以申请私有本地仓库,而且速度肯定比远在国外的hub docker快,而且对于直接在阿里云ECS上部署的话,拉取的速度超快,还不计算公网流量,可以体验一下: $ sudo docker login --username=m152********@163.com registry.cn-beijing.aliyuncs.com $ sudo docker tag [ImageId] registry.cn-beijing.aliyuncs.com/ranbos/scrapyd:[镜像版本号] $ sudo docker push registry.cn-beijing.aliyuncs.com/ranbos/scrapyd:[镜像版本号] 上面由于700多M的数据上传到国外服务器实在是卡得不行,我这次就放在Aliyun的私有仓库中 输入图片说明 速度真是快的没话说,767M的镜像 2分钟左右上传完毕
由于项目需求,要将繁体字转成简体字。网上一直没有头绪,有些说用opencc-python,有些则说用OpenCC。我也找了很久,最后才实现,这里记录一下。 输入图片说明 OpenCC的github地址在这里 根据说明,先将OpenCC项目git clone下来,然后cd进入OpenCC项目目录内,进行编译 make PREFIX=/usr/local sudo make PREFIX=/usr/local install 如果编译过程当中报错,就应该是gcc-c++之类的问题,通过mac的brew来安装gcc或者其他环境即可。 centos则通过yum install gcc-c++和yum install opencc来解决 brew install gcc 代码实现 当时在python虚拟环境安装了opencc和opencc-python 然后实际的代码是: import opencc cc = opencc.OpenCC('t2s') print(cc.convert('Open Chinese Convert(OpenCC)「開放中文轉換」,是一個致力於中文簡繁轉換的項目,提供高質量詞庫和函數庫(libopencc)。')) 其中的t2s来自于不同作用的json包 输入图片说明 也就是说代表了不同的字体转换方式和结果 最后,上面的代码输出结果为: Open Chinese Convert(OpenCC)「开放中文转换」,是一个致力于中文简繁转换的项目,提供高质量词库和函数库(libopencc)。 这算是质量很高的转换结果了。
一、背景 爬取数据过程中,会遇到一些特殊的字符入库出错的问题,比如二进制数据、比如特殊文字(类似QQ表情)等。 Siberian Husky fighting 这样的标题,后面就带有一个表情。 在mysql存储的时候,报错信息如下: [Failure instance: Traceback: <class 'pymysql.err.InternalError'>: (1366, "Incorrect string value: '\\xF0\\x9F\\x90\\xB6' for column 'title' at row 1") 大致意思是指title这个字段无法存储这种字符。 解决办法 经过网上搜索mysql 1366,多方查看后,终于找到了原因和解决的办法。 是因为\xF0\x9F\x90\xB6 它通常是4个字符存储,而mysql的utf8默认是3个字符存储。 1、 更改代码中数据库连接的字符编码charset为utf8mb4 MYSQL_CHARSET = 'utf8mb4' 2、将数据库字符集编码页改成utf8mb4。 3、检查数据表的字符编码,保持同步。 4、最后确认数据表中的字段,存储这种特殊文字的字段字符编码也是utf8mb4 输入图片说明 这样就解决了这个问题。 如果你没有小心处理好,可以尝试新建一个数据库,然后将默认的字符编码设置为utf8mb4.
其实文件下载也差不多 前言 在日常爬取的过程中,图片下载还是挺多的,有时候可能纯粹是爬取图片,比如妹子图、动态图、表情包、封面图等,还有些时候是要进行验证码识别,所以需要用到图片下载功能。很高兴的是Scrapy为用户提供了图片下载功能,具体使用方法这里记录一下,它的逻辑是: spider获取图片url --> 交给item进行处理 --> 然后根据setting的配置(开启图片下载以及设置路径)进行下载 具体的代码实现为: Spider代码获取url,传递到item: telnums = response.css('.address tr:first-child img::attr(src)').extract_first("") loaders.add_value("image_urls", telnums) # 企业联系电话(下载图片) item对值进行预处理: default_output_processor = TakeFirst() # itemloader image_urls = scrapy.Field() #item 然后根据settings.py的配置: IMAGES_URLS_FIELD = "image_urls" # 对应item里面设定的字段,取到图片的url prodir = os.path.abspath(os.path.dirname(__file__)) IMAGES_STORE = os.path.join(prodir,"images") # 设置图片保存path 进行下载,即可完成下载功能。其中IMAGES_URLS_FIELD是固定写法,通过它来获取item中设定的图片url;IMAGES_STORE也是固定写法,设置图片保存路径。 这是一个坑 保存运行后得到一个报错: Traceback (most recent call last): File "/home/ranbo/anaconda3/envs/pspiders/lib/python3.6/site-packages/twisted/internet/defer.py", line 653, in _runCallbacks current.result = callback(current.result, *args, **kw) File "/home/ranbo/anaconda3/envs/pspiders/lib/python3.6/site-packages/scrapy/pipelines/media.py", line 79, in process_item requests = arg_to_iter(self.get_media_requests(item, info)) File "/home/ranbo/anaconda3/envs/pspiders/lib/python3.6/site-packages/scrapy/pipelines/images.py", line 155, in get_media_requests return [Request(x) for x in item.get(self.images_urls_field, [])] File "/home/ranbo/anaconda3/envs/pspiders/lib/python3.6/site-packages/scrapy/pipelines/images.py", line 155, in <listcomp> return [Request(x) for x in item.get(self.images_urls_field, [])] File "/home/ranbo/anaconda3/envs/pspiders/lib/python3.6/site-packages/scrapy/http/request/__init__.py", line 25, in __init__ self._set_url(url) File "/home/ranbo/anaconda3/envs/pspiders/lib/python3.6/site-packages/scrapy/http/request/__init__.py", line 62, in _set_url raise ValueError('Missing scheme in request url: %s' % self._url) ValueError: Missing scheme in request url: h 网上查资料得知: return [Request(x) for x in item.get(self.images_urls_field, [])] scrapy下载图片的类传参数是list,而不是'http://www.baidu.com/123.jpg'这种str格式,在item传递的时候默认是所有数据都是list,但是之前在itemloaders加了TakeFirst: class GxrcItemLoader(ItemLoader): """ 广西人才网企业信息itemloader """ default_output_processor = TakeFirst() 所以才会被处理成str。 脱坑 现在只需要将它变成list即可,在item编写一个预处理的方法: def keep_list(value): """ scrapy的图片下载url默认处理列表list格式,str会报错 由于下方itemloaders设置了default_output_processor为TakeFirst 所以这里需要定义一个直接返回的函数来覆盖output_processor,这样就可以保持list格式 """ return value 然后在item代码里覆盖output_processor: image_urls = scrapy.Field(output_processor=MapCompose(keep_list)) 同时spider也得到解放,代码改成: loaders.add_css("image_urls", ".address tr:first-child img::attr(src)") # 企业联系电话(下载图片) 就可以下载图片了,而且代码风格有条理,舒服。 文件名和路径 下载后要获得图片的文件名和存放路径,毕竟下载完了之后不能不管。 scrapy下载的时候默认用hash加密图片url得到固定长度的值作为文件名,我要是想获得的话有两个方式,第一是自己加密图片url,生成文件名,路径就用setting里面配置的路径即可;第二个方式是重载ImagePipeline,在生成的时候就进行处理,这里演示第二种: 这个ImagePipeline写在pipelines.py文件中 它重载的是: from scrapy.pipelines.images import ImagesPipeline 并且里面有很多的方法可以让我们在settings中进行配置,比如过滤图片的格式、图片大小等(文档有记录,以后翻文档)。 在pipelines.py文件中编写代码: class ImagePathPipeline(ImagesPipeline): """ 继承ImagesPipeline,重载方法 以实现处理图片下载路径的目的 """ def item_completed(self, results, item, info): pass # (这里打断点,等下调试) 然后到settings.py中将之前使用的默认ImagesPipeline替换成自定的: ITEM_PIPELINES = { #'scrapy.contrib.pipeline.images.ImagesPipeline': 1, 'rspider.pipelines.ImagePathPipeline': 1,# 开启图片下载 } 然后debug运行,进行调试,看看 item_completed方法里面的results是什么样的数据结构: <class 'list'>: [(True, {'url': 'http://vip.gxrc.com/Public/Phone/F4D1D4B0-DABB-4E83-BC54-5153BC44AA33', 'path': 'full/82cf733279ebc25c4de892e531018644e77aed67.jpg', 'checksum': 'b14c3fcdeb93209bc835d2d3dfdefb57'})] 可以看到,results是一个list,然后里面嵌套了一个tuple,再嵌套一个dict,dict存放有三个值,url/path/checksum,其中的path就是文件的路径,所以整个代码为: class ImagePathPipeline(ImagesPipeline): """ 继承ImagesPipeline,重载item_completed方法 以实现处理图片下载路径的目的 """ def item_completed(self, results, item, info): """ 循环results,将里面的path取出,然后赋值到item中的image_path字段,最后返回item """ for ok, value in results: image_path = value["path"] item["image_path"] = image_path return item 总而言之 要给Scrapy增加图片下载功能,只需要编写Pipelines并且在Settings.py文件中开配置item中对应的图片url字段、设置图片保存地址以及开启pipeliens即可
常见报错信息 报错信息: Operand should contain 1 column(s) 意思是只能插入单行,不能插入多行数据 报错信息: data too long 意思是数据库字段长度不够 报错信息: [Failure instance: Traceback: <class 'KeyError'>: 'job_name' 意思是键值错误,情况一般是CSS选择器在页面获取不到对应的值(比如页面有变化导致不是常规页面)、spider的取值与item的键对应不上、还有后面跟colum的一般是数据库字段与item键对不上 常见数据处理方法 1.文章内容、简介等多行多段的文本数据 [思路来源:传送] 这种数据通常的做法是全部取下来,保留文章里面的html标签(如 \n \r等),在item里面把值转成str类型,存入数据库即可。这里有个新的发现,在爬取广西人才网的时候,它的文本是这样的: 1.负责员工饭堂菜品的烹饪 2.员工饭堂物料的购买 3.员工食堂卫生 对应的html结构如下: <p id="examineSensitiveWordsContent" style="display: block;"> 1.负责员工饭堂菜品的烹饪 <br>2.员工饭堂物料的购买 <br>3.员工食堂卫生 </p> 问题就在这里! 这样的值看似一个完整的文本,但是传递到item后由于标签的存在,就会变成一条一条的数据: 1.负责员工饭堂菜品的烹饪 2.员工饭堂物料的购买 3.员工食堂卫生 而不是我想象中的一个完整文本: 1.负责员工饭堂菜品的烹饪,2.员工饭堂物料的购买,3.员工食堂卫生 这样的数据存入数据库,就会报错 Operand should contain 1 column(s) 应对这种问题,解决的办法就是在数据传递到item之前,在spider取值的时候就对进行处理,可以用replace把它替换掉。 在广西人才网这个爬虫中,我的做法是用.join()方法来清除,.join()的介绍如下: 描述:Python join() 方法用于将序列中的元素以指定的字符连接生成一个新的字符串。 语法:str.join(sequence) 参数:sequence -- 要连接的元素序列 返回值:返回通过指定字符连接序列中元素后生成的新字符串。 它的代码示例: str = "-"; seq = ("a", "b", "c"); # 字符串序列 print str.join( seq ); 得到输出是 a-b-c 所以广西人才网爬虫这里,我在spider文件新建一个方法,用于清除br: def clear_br(self, value): """ 文本中包含有<br>标签的话,传值到itme中就不会是整个文本,而是一条一条的数据 保存到数据库的时候会报错:Operand should contain 1 column(s) 那就要将文本里面的<br>换成其他,由于传递过来的value是一个列表list,所以用for循环把元素replace也可以 这里用.join()方法把列表里的所有元素用逗号拼接成字符串 """ value = ','.join(value) return value 然后itemloader赋值的时候这样写: p_centent = response.css('#examineSensitiveWordsContent::text').extract() iloaders.add_value("job_content", self.clear_br(p_centent)) # 工作内容及要求 意思是在取到文本后调用clear_br函数将list列表里面的元素用逗号拼接,最后返回一个字符串。这样就能达到正常入库的需求了。
一、背景 之前有记录过普通的scrapy模拟登录,这种方法可以满足了日常爬虫的登录需求。 但是技术一直在进步,近几年前后端分离的趋势越来越好,很多web都采用前后端分离的技术。那么登录后的用户权限验证就会出现jwt的形式。(主要是token方式的验证,在模拟登录中要解决的问题) 这里记录一下。 二、登录操作 前后端分离的项目,一般都是react、vue等js语言编写的(没有这方面经验的同志,可以不用往下看了) 有些会采用成型的前端框架,如AntDesign,ElementUI等,它们写出来的web页面,如果用css定位或者xpath定位,是很不准确的。所以最好的办法就是观察数据流,找到api和发送的参数进行构造。 输入图片说明 以这里的登录为例,通过css定位其实也可以,但是有不稳定的风险。所以还是看api和参数比较稳妥,毕竟css怎么变,api都不会随意改变。 输入图片说明 选中post那条数据流,看到右侧的请求地址、请求头和参数 输入图片说明 [图片上传失败...(image-9401fa-1531469273677)] 这样就可以根据请求地址、请求头和参数来构造登录用的代码: def start_requests(self): """ 重载start_requests方法 通过is_login方法判断是否成功登录 """ login_url = "http://xxx.yyy.ccc.aa/api/v1/oauth/login" login_data = { "username": "abcd@easub.com", "password": "faabbccddeeffggd5", "type": "email" } return [scrapy.FormRequest(url=login_url, formdata=login_data, callback=self.is_login)] def is_login(self, response): """ 根据返回值中的message值来判断是否登录成功 如果登录成功则对数据传输页发起请求,并将结果回传给parse方法 如果登录失败则提示 由于后面的用户权限验证需要用到token信息,所以这里取到登录后返回的token并传递给下一个方法 """ results = json.loads(response.text) if results['message'] == "succeed": urls = 'http://xxx.yyy.ccc.aa' access_token = results['data']['access_token'] print("登录成功,开始调用方法") yield Request(url=urls, callback=self.parse, meta={"access_token": access_token}) else: print("登录失败,请重新检查") 如果返回信息的json里面message值为succeed及认为登录成功,并调用parse方法。 三、用户权限验证 登录完毕后,我想执行其他的操作,比如上传(post)数据,跟刚才一样,需要观察api的地址和所需参数请求头信息等。 输入图片说明 输入图片说明 同样是根据返回的参数和请求头,来构造代码 然而这次却不行,返回的状态码是401,由于scrapy默认只管200和300的状态码,4开头和5开头的都不处理。但是又需要观察401状态返回的东西,可以在settings.py中空白处新增代码: """ 状态码处理 """ HTTPERROR_ALLOWED_CODES = [400, 401] 然后在下一个方法中观察response回来的数据。 ====================================== 后来又查询了401的意思,就是未获得授权,也就是用户权限验证不通过,经过多方资料查找,发现请求头中有这么一条: [图片上传失败...(image-1322be-1531469273677)] 它就是用于用户权限验证的,authorization的值分为两部分<type>和<credentials>,前者是验证采用的类型,后者是具体的参数值。这里的类型可以看到用的是Bearer类型,(传说值默认是用户名+密码的base64字符串,但这个这么长,显然不是64)。 我又去观察登录时候的返回值,发现登录成功后的返回值除了succeed之外,还有其他的一些返回值,里面包括了一个叫access_token的字段,它是用于JWT登录方式用来鉴权的token信息,而且authorization用的也正好就是这个token作为值。 那么代码就应该在第一次登录时候,取出access_token的值,并传递下去,用于后面请求的鉴权: def is_login(self, response): """ 根据返回值中的message值来判断是否登录成功 如果登录成功则对数据传输页发起请求,并将结果回传给parse方法 如果登录失败则提示 由于后面的用户权限验证需要用到token信息,所以这里取到登录后返回的token并传递给下一个方法 """ results = json.loads(response.text) if results['message'] == "succeed": urls = 'http://xxx.yyy.ccc.aa' access_token = results['data']['access_token'] print("登录成功,开始调用方法") yield Request(url=urls, callback=self.parse, meta={"access_token": access_token}) else: print("登录失败,请重新检查") 下面的pase方法中,将authorization设定到header中以对数据进行请求: header = { "authorization": "Bearer " + access_token } 这样就解决了用户权限的问题,不再出现401 四、postman发送请求特殊格式数据(json) 在parse方法中,根据浏览器观察到的参数,进行构造: datas = { "url": "https://www.youtube.com/watch?v=eWeACm7v01Y", "title": "看上去可爱其实很笨的狗#动物萌宠#", "share_text": "看上去可爱其实很笨的狗#动物萌宠#[doge]", "categories": {'0': '00e2e120-37fd-47a8-a96b-c6fec7eb563d'} } 由于categories里面是个数组,所以在构造的时候也可以直接写数据,然后用scrapy.Formdata来进行post。发现返回的状态是这次是400,并且提示:categories必须是数组 再次观察请求头信息,发现请求头信息中还有: [图片上传失败...(image-e2574e-1531469273677)] 叫做content-type的参数,我将它加入到header中: header = { "authorization": "Bearer " + access_token, "content-type": "application/json", } 这样关于categories的提示就没有了。但是返回的状态码依然是400,而且提示变成了url不能为空,这到底又是怎么一回事? 多方探查都没有结果。 真是伤心 后来我又想起了,既然这里的文本类型是application/json,那么提交出去的文本应该是json类型数据,而不是python的dict字典类型数据。 于是打开json在线解析,对传递的参数进行观察,发现这样的数据并不满足json格式: 输入图片说明 后来尝试对它进行更改: 输入图片说明 在外层增加了一对{},然后又将categories的值加上了双引号,才是正确的json格式。 但是如果这样,拿到postman中进行测试,是不行的,后来经过反复测试,最终确定了postman的请求格式为: 输入图片说明 输入图片说明 输入图片说明 对Auth、Headers和Raw进行设置,才终于成功发送post,返回正确的信息!!! 五、scrapy发送Json格式数据 在postman测试通过后,说明这样的做法是可行的,但是代码上怎么编写呢? 用之前的scrapy.Formdata是不行的,它的formdat=默认使用dict格式,如果强行转成json格式也是会报错的。经过群里咨询和搜索,发现要用scrapy.http的Requst方法(平时常用的这个): access_token = response.meta['access_token'] urls = "http://aaa.bbb.xxx.yy/api/v1/material/extract" datas = { "url": "https://www.youtube.com/watch?v=eWeACm7v01Y", "title": "看上去可爱其实很笨的狗#动物萌宠#", "share_text": "看上去可爱其实很笨的狗#动物萌宠#[doge]", "categories": {'0': '00e2e120-37fd-47a8-a96b-c6fec7eb563d'} } header = { "authorization": "Bearer " + access_token, "content-type": "application/json", } yield Request(url=urls, method='POST', body=json.dumps(datas), headers=header, callback=self.parse_details) 这样才发送请求,终于成功了!!!
前言 分布式爬虫,总归是要上到服务器的。 这里先讲解如何在服务器上配置和部署scrapyd,主要的点还是在scrapyd和redis的conf配置文件上。其实到末尾我已经实现了分布式,本机的爬虫访问远程redis,从里面拿数据,但是由于是测试,没有放入start_urls,所以也没有启动并爬出结果,但是redis远程连接确实是做到了。下一篇结合Docker再搭建分布式爬虫。 环境配置 为了从头配置,我在阿里云上重新更换了纯净的Centos7.4,新盘,什么都没有。 创建环境 这里比较简单,参考自己写的《Aliyun-安装Anaconda记录 》就可以成功安装Anaconda了 创建环境的话用命令: conda create --name pspiders python=3.6 创建一个名为pspiders并且版本是python3.6的虚拟环境。 进入环境的命令: source activate pspiders 当前面出现(pspiders) (pspiders) [root@iZqmg63rkase8aZ SRspider]# 就代表着成功进入了虚拟环境。 安装scrapyd 首先,通过本地sftp将写好的代码上传到服务器,我这里是新建目录SRspider,然后将代码上传到目录中。 然后cd进入SRspider目录,与.cfg文件同目录内,通过命令开启虚拟环境: source activate pspiders 然后安装scrapyd以及scrapd-client (pspiders) [root@ixxx SRspider]# pip install scrapyd (pspiders) [root@ixxx SRspider]# pip install scrapyd-client 这样就完成了他们的安装。 导入环境依赖 接着导入本地机器的依赖包,在本地的虚拟环境下,通过命令导出requirements.txt: pip freeze > requirements.txt 就会在当前目录下生成此文件,打开后将文件内容复制,并在服务器的虚拟环境下vim新建同名文件,写入内容(刚才导出的文件内容): six==1.11.0 Twisted==17.9.0 urllib3==1.22 w3lib==1.18.0 you-get==0.4.1011 zope.interface==4.4.3 …… …… …… 这里面记录的就是之前的虚拟环境的依赖,如果不导入的话,就要手动在虚拟环境下通过pip安装这些依赖(建议导入)。 然后在服务器通过命令: pip install -r requirements.txt 安装依赖,可以看到它依次下载并安装。 服务器安装redis 这个比较简单: yum install redis 跟着提示就行了,然后用命令启动redis: service redis start 设置为开启启动的话: chkconfig redis on 配置scrapyd远程访问 到这里,scrapyd也安装好了、依赖也装好了、代码也上传了、redis也安装好了,是不是可以打包然后就开启scrapyd服务了呢? 当初我也是这么认为的,直到scrapyd启动服务后,在浏览器打开http://59.110.xxx.xxx:6800,但是始终无法访问,这个问题困扰了很久,后来在群里面有人告诉我应该开启阿里云安全组配置(我去检查了,我已经开启6800端口),又有人说scrapyd默认绑定地址是127.0.0.1,要将它绑定的地址改为0.0.0.0就可以开启外部访问了。文件路径是在: /root/anaconda3/envs/pspiders/lib/python3.6/site-packages/scrapyd 下面有一个default_scrapyd.conf 文件,vi打开后找到里面有一句: bind_address = 127.0.0.1 将它改成0.0.0.0保存,就可以远程访问了。 这时候到SRspider目录下打开虚拟环境,然后输入命令scrapyd启动它,再用浏览器打开就可以访问到了。 配置redis远程访问 redis同样,默认设定是本地访问,如果想开启远程访问就要改动bind 但是为了安全起见(redis默认没有密码,开启远程后别人可以操作),就需要给redis设置密码,通过命令redis-cli启动redis的命令行: [root@iZqmg63rkase8aZ scrapyd]# redis-cli 127.0.0.1:6379> 然后查看是否有密码: 127.0.0.1:6379> config get requirepass 1) "requirepass" 2) "" 127.0.0.1:6379> 如果是密码为空,则需要设置密码: CONFIG set requirepass "ranbos" 再次查看的时候就会提示需要密码: (error) NOAUTH Authentication required. 用命令登录: auth ranbos 即可登录。这里就完成了密码的设置,通过命令quit退出命令行。下面更改bind 进入redis.conf目录下(默认在/etc下): cd /etc 然后用ls查看目录下的文件,发现有redis.conf文件 输入图片说明 用vim打开它,找到里面的bind: bind 127.0.0.1 将它改为: #bind 127.0.0.1 bind 0.0.0.0 我怕有错,所以先注释127.0.0.1,然后添加bind0.0.0.0 然后重启redis: service redis restart 就可以了,redis可以远程访问了。 连接redis的代码 但是写代码的时候如何连接呢? 这里有区分master和slaver,如果是主机,就在settings.py中增加连接配置: # 指定redis数据库的连接参数 REDIS_HOST = "127.0.0.1" REDIS_PORT = "6379" REDIS_PARAMS ={ 'password': 'ranbos', } 主机就连接本地127.0.0.1的redis就行了,写上端口号和密码。 如果是slaver端,同样是这么设置,然后将REDIS_HOST改为服务器ip即可,比如我的deepin机器就这么配置: REDIS_HOST = "59.110.xxx.xxx" REDIS_PORT = "6379" REDIS_PARAMS ={ 'password': 'ranbos', } 这样就完成了所有的配置,现在去开启服务测试。 测试 首先,在服务器上开启scrapyd服务(在项目工程.cfg同目录进入虚拟环境): source avticate pspiders 接着开启scrapyd服务: scrapyd 会收到如下信息: (pspiders) [root@iZqmg63rkase8aZ SRspider]# scrapyd 2018-01-23T12:56:27+0800 [-] Loading /root/anaconda3/envs/pspiders/lib/python3.6/site-packages/scrapyd/txapp.py... 2018-01-23T12:56:27+0800 [-] Scrapyd web console available at http://0.0.0.0:6800/ 2018-01-23T12:56:27+0800 [-] Loaded. 2018-01-23T12:56:27+0800 [twisted.scripts._twistd_unix.UnixAppLogger#info] twistd 17.9.0 (/root/anaconda3/envs/pspiders/bin/python 3.6.4) starting up. 2018-01-23T12:56:27+0800 [twisted.scripts._twistd_unix.UnixAppLogger#info] reactor class: twisted.internet.epollreactor.EPollReactor. 2018-01-23T12:56:27+0800 [-] Site starting on 6800 2018-01-23T12:56:27+0800 [twisted.web.server.Site#info] Starting factory <twisted.web.server.Site object at 0x7f05aea75d68> 2018-01-23T12:56:27+0800 [Launcher] Scrapyd 1.2.0 started: max_proc=4, runner='scrapyd.runner' 代表服务正常开启,服务开启后再开启一个窗口,连接到服务器。然后也是进入虚拟环境,到.cfg同目录下,打包代码,发送到: scrapyd-deploy SRspider -p Jobbole 如果成功就会返回信息: (pspiders) [root@iZqmg63rkase8aZ SRspider]# scrapyd-deploy SRspider -p Jobbole Packing version 1516683538 Deploying to project "Jobbole" in http://localhost:6800/addversion.json Server response (200): {"node_name": "iZqmg63rkase8aZ", "status": "ok", "project": "Jobbole", "version": "1516683538", "spiders": 1} 就代表打包成功,打开页面即可看到: 输入图片说明 然后通过命令启动爬虫,开始爬取: curl http://localhost:6800/schedule.json -d project=Jobbole -d spider=jobbole 最后通过Log可以查看到爬虫的运行情况
scrapy的去重原理 信号无处不在 【知其然且知其所以然,才能够更好的理解这个框架,而且在使用和改动的时候也能够想出更合理的方法。】 (开始测试前,到settings.py中注释掉下载中间件的设置,这里用jobbole爬虫来测试,所以之前写的调用chrome的两个方法init和spider_closed都要注释掉。) 这里你们可以用自己的爬虫来测试,不一定要按我的来测试。 到scrapy源码包 [项目\Lib\site-packages\scrapy\dupefilters.py] 里面找去重的代码,RFPDupeFilter类就是去重器,里面有个方法叫做request_seen,它在scheduler(发起请求的第一时间)的时候被调用。它代码里面调用了request_fingerprint方法(就是给request生成一个指纹),连续两次跟进代码就进入到了request.py文件的request_fingerprint方法中,方法中有一句代码: fp = hashlib.sha1() … … cache[include_headers] = fp.hexdigest() 就是给每一个传递过来的url生成一个固定长度的唯一的哈希值。这种量级千万到亿的级别内存是可以应付的。 然后看到init方法: def __init__(self, path=None, debug=False): self.file = None self.fingerprints = set() self.logdupes = True self.debug = debug self.logger = logging.getLogger(__name__) if path: self.file = open(os.path.join(path, 'requests.seen'), 'a+') self.file.seek(0) self.fingerprints.update(x.rstrip() for x in self.file) 里面有一句代码 self.fingerprints = set(),就是通过set集合的特点(set不允许有重复值)进行去重。 可以用断点调试的方法进行跟踪查看。 Telnet Telnet协议是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用telnet程序,用它连接到服务器。终端使用者可以在telnet程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个telnet会话,必须输入用户名和密码来登录服务器。Telnet是常用的远程控制Web服务器的方法。 可以用这个在本地操控远程的scrapy,telnet是默认开启的,当scrapy运行的时候,会自动开启端口,运行框会有显示: [scrapy.extensions.telnet] DEBUG: Telnet console listening on 127.0.0.1:6023 我们可以通过cmd连接测试。 要测试,就要开启windows电脑的telnet功能。在控制面板-程序与功能-启用或关闭windows功能,找到Telnet服务和客户端,打上勾即可。 linux系统也是默认没有Telnet的,需要安装,比如我的Deepin系统就需要用命令安装: sudo apt-get install telnet 先运行scrap有,然后在cmd(linux在终端输入)中输入: telnet localhost 6023 连接成功后会显示: >>> 符号,我们输入est()可以查看当前scrapy的运行状态等属性。官方文档有Telnet的相关介绍,里面包括有一些可用的变量/查看引擎状态/暂停,恢复和停止scrapy/终端信号/设置等内容。 Telnet的源码在site-package/scrapy/extensions目录下的telnet.py文件中。 数据收集器 官方文档中有对数据收集器的介绍. 数据收集器可以应用在很多地方,举例子:如果你想知道scrapy总共发出了多少个request请求;或者你想记录总共发起了多少次yeild,都可以用数据收集器记录,它不用打开,默认可以直接使用。 在jobbole爬虫的JobboleSpider类里面新增代码: # 收集jobbole.com所有的500、404页面及页面数量 handle_httpstatus_list = [500,404] def __init__(self): self.fail_urls = [] 然后到parse方法中新增代码: if response.status == 500 or response.status == 404: self.fail_urls.append(response.url) self.crawler.stats.inc_value("failed_url") 就可以实现对页面数量和页面url的收集,可以自定义保存. 信号 scrapy的中间件与通信都是通过信号来传递的,官网有文档 可以在您的Scrapy项目中捕捉一些信号(使用 extension)来完成额外的工作或添加额外的功能,扩展Scrapy。 信号提供了一些参数,不过处理函数不用接收所有的参数 - 信号分发机制(singal dispatching mechanism)仅仅提供处理器(handler)接受的参数。 代码演示: from scrapy.xlib.pydispatch import dispatcher from scrapy import signals def parse(self, response): """ 正式进入爬取区域 """ dispatcher.connect(self.handler_spider_closed, signals.spider_closed) def handler_spider_closed(self, spider, reason): print("这个名为:" + spider.name + "的爬虫已经关闭了,原因是:" + reason) 得到的输出结果是在爬虫关闭后: 'start_time': datetime.datetime(2018, 1, 20, 3, 5, 49, 791339)} 2018-01-20 11:05:51 [scrapy.core.engine] INFO: Spider closed (finished) 这个名为:dongmeng的爬虫已经关闭了,原因是:finished dispatcher.connect监听爬虫signals(信号),当收到爬虫关闭(signals.spider_closed)的信号时,调用handler_spider_closed方法。而handler_spider_closed我只是简单的编写了一个关闭的原因而已,还可以做更深入的操作。 由此可以看出,爬虫的信号监听和状态操作可以做很多的事情,比如打开爬虫时、爬虫空闲时可以收集当前request请求队列、记录404页面数量或者200状态的页面有那些、多少条等。 扩展 官方文档对扩展有介绍: 扩展框架提供一个机制,使得你能将自定义功能绑定到Scrapy。 扩展只是正常的类,它们在Scrapy启动时被实例化、初始化。 scrapy里面的中间件都是一种扩展。 为了更好的理解扩展,这里用源码跟踪的形式来理解,到[项目/Lib/site-packages/scrapy/extensions]目录下找到corestats.py文件。 from_crawler方法里面记录了很多的信号量: @classmethod def from_crawler(cls, crawler): o = cls(crawler.stats) crawler.signals.connect(o.spider_opened, signal=signals.spider_opened) crawler.signals.connect(o.spider_closed, signal=signals.spider_closed) crawler.signals.connect(o.item_scraped, signal=signals.item_scraped) crawler.signals.connect(o.item_dropped, signal=signals.item_dropped) crawler.signals.connect(o.response_received, signal=signals.response_received) return o 下面对应都写着具体的执行方法。比如spider_opened爬虫启动的时候就会: def spider_opened(self, spider): self.stats.set_value('start_time', datetime.datetime.utcnow(), spider=spider) 记录爬虫启动的时间。 还有spider_closed关闭爬虫的时候就会: def spider_closed(self, spider, reason): self.stats.set_value('finish_time', datetime.datetime.utcnow(), spider=spider) self.stats.set_value('finish_reason', reason, spider=spider) 记录下关闭时间和关闭的原因等。 到[项目/Lib/site-packages/scrapy/extensions]memusage.py文件是监控内存使用的。里面有一串代码: crawler.signals.connect(self.engine_started, signal=signals.engine_started) crawler.signals.connect(self.engine_stopped, signal=signals.engine_stopped) 是主要的绑定项 比如这个engine_started方法开始就记录内存使用信息。 self.crawler = crawler self.warned = False self.notify_mails = crawler.settings.getlist('MEMUSAGE_NOTIFY_MAIL') self.limit = crawler.settings.getint('MEMUSAGE_LIMIT_MB')*1024*1024 self.warning = crawler.settings.getint('MEMUSAGE_WARNING_MB')*1024*1024 self.check_interval = crawler.settings.getfloat('MEMUSAGE_CHECK_INTERVAL_SECONDS') self.mail = MailSender.from_settings(crawler.settings) 里面的大概意思就是设置了监控内存的定时时间/不同状态的操作等,可以在engine_started里面加逻辑,想对内存干什么就干什么。
scrapy是不支持分布式的。分布式爬虫应该是在多台服务器(A B C服务器),他们不会重复交叉爬取(需要用到状态管理器)。 有主从之分的分布式结构图 重点 一、我的机器是Linux系统或者是MacOSX系统,不是Windows 二、区别,事实上,分布式爬虫有几个不同的需求,会导致结构不一样,我举个例子: 1、我需要多台机器同时爬取目标url并且同时从url中抽取数据,N台机器做一模一样的事,通过redis来调度、中转,也就是说它根本没有主机从机之分。 2、我有n台机器负责爬取目标URL,另外有M台机器负责容url中抽取数据,N和M做的事不一样,他们也是通过redis来进行调度和中转,它有主机从机之分。 这里演示的,是第1中,N台机器做一模一样的事。这点大家要搞清楚。 分布式爬虫优点: ① 充分利用多台机器的带宽速度爬取数据 ② 充分利用多台机器的IP爬取 通过状态管理器来调度scrapy,就需要改造一下scrapy,要解决两个问题: ① request之前是放在内存的,现在两台服务器就需要对队列进行集中管理。 ② 去重也要进行集中管理 redis安装和命令 参考菜鸟教程的安装以及命令介绍(由于安装时候是下载压缩包后进行解压再安装,所以会留下压缩包和文件夹。需要找一个指定的文件夹存放这些东西,我的电脑一般是放在home/ranbos/Programe File目录下,打开终端,执行以下命令) $ wget http://download.redis.io/releases/redis-4.0.6.tar.gz $ tar xzf redis-4.0.6.tar.gz $ cd redis-4.0.6 $ make 这次笔记时候的redis版本是4.0.6。 make完后 redis-4.0.6目录下会出现编译后的redis服务程序redis-server,还有用于测试的客户端程序redis-cli,两个程序位于安装目录 src 目录下,下面启动redis服务: $ cd src $ ./redis-server 就可以看到redis的启动画面了。但是这只是启动服务,如果想输入命令的话,还需要打开另一个终端,同样进入到src目录下,运行./redis-cli命令,才能进入命令交互界面。 设置密码 redis是可以匿名访问的,所以需要设置连接密码,在cli窗口通过命令查看密码设置状态: CONFIG get requirepass 可以得到一个结果,那就是没有设置密码 "requirepass" 通过命令设置密码: CONFIG set requirepass "ranbos" 再次查看的时候就会提示: (error) NOAUTH Authentication required. 需要登录才行,登录的命令是: AUTH "ranbos" 只要密码对了,就可以连接上去了。 redis基础知识 redis是一个key-value存储系统,它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted sets)等类型。 Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。 Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。 Redis支持数据的备份,即master-slave模式的数据备份。 Redis 优势 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。 基础操作 一些基础操作就根据菜鸟教程的文章进行学习吧 通过scrapy-redis搭建分布式爬虫 在github上搜索scrapy-redis,里面有具体的文档及介绍。 ① 安装redis 通过pycharm安装redis ② 配置scrapy-redis 根据文档的说明,到settings.py中更改配置,在空白地方新增代码: """ scrapy-redis配置 """ # Enables scheduling storing requests queue in redis. SCHEDULER = "scrapy_redis.scheduler.Scheduler" # Ensure all spiders share same duplicates filter through redis. DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" 然后到ITEM_PIPELINES中新增: # Store scraped item in redis for post-processing. 分布式redispipeline 'scrapy_redis.pipelines.RedisPipeline': 300, 即可完成配置。 但是在写代码的时候跟之前的写法不一样,文档这里介绍到: from scrapy_redis.spiders import RedisSpider class MySpider(RedisSpider): name = 'myspider' def parse(self, response): # do stuff pass 在爬虫里面引入scrapy_redis的包,以及在类继承的不能继承scrapy.Spider了,而是继承RedisSpider 还有另外两点 run the spider: scrapy runspider myspider.py push urls to redis: push urls to redis: redis-cli lpush myspider:start_urls http://google.com Note 要预先放置url在redis当中才行,否则爬虫会一直在等待。 开始搭建分布式爬虫 ① 新建项目 为了更好的测试scrapy-redis,需要新建一个项目,但是可以选择之前爬虫的虚拟环境,这样就可以不用重复装那么多外部包了 用pycharm新建ScrapyRedis项目,在选择虚拟环境的时候选择之前jobbole-test那个虚拟环境,路径在C盘Admin用户下的Jobbole-test/Script/python.exe。 ② 新建scrapy项目 用scrapy startproject ScrapyRedisTest命令来新建项目,建好项目后不着急建工程。 然后到github上下载scrapy-redis(实际上是要用pip安装scrapy-redis外部包)。解压后,复制文件夹下面的src目录下的scrapy_redis放到项目目录下,与项目的Spider目录同级。 接着在spider目录下新建jobbole.py文件,将使用说明里的示例代码粘贴进去,覆盖默认的爬虫类: from scrapy_redis.spiders import RedisSpider class MySpider(RedisSpider): name = 'myspider' def parse(self, response): # do stuff pass 由于路径问题,自己改一下: from ..scrapy_redis.spiders import RedisSpider 注意 这里复制进来只是为了更好的观察和跟踪代码,实际上是要用Pycharm安装scrapy-redis外部包的,一定要装。 跟踪RedisSpider代码可以发现它是继承了两个类: class RedisSpider(RedisMixin, Spider) scrapy的默认Spider以及对redis操作的RedisMixin。 然后跟踪代码RedisMixin。可以看到它用setup_redis给每个爬虫设置了一个redis的key,方法里面包含: self.redis_key = settings.get( 'REDIS_START_URLS_KEY', defaults.START_URLS_KEY, ) 意思就是不同的爬虫会自己设置一个默认的key,可以进行覆盖,也可以让它自动生成。覆盖的方法就是在scrapy-redis包的源码中,路径是 scrapy-redis\example-project\example\spiders 里面有两个文件: mycrawler_redis.py myspider_redis.py 对应两种不同的爬虫。 这里以myspider.py为例,将jobbole.py的代码改成: from ..scrapy_redis.spiders import RedisSpider class JobboleSpider(RedisSpider): name = 'jobbole' allowd_domains = ["blog.jobbole.com"] redis_key = 'jobbole:start_urls' def parse(self, response): # do stuff pass 现在先放着,看下一个RedisMixin中还有一个方法next_requests: def next_requests(self): """Returns a request to be scheduled or none.""" use_set = self.settings.getbool('REDIS_START_URLS_AS_SET', defaults.START_URLS_AS_SET) fetch_one = self.server.spop if use_set else self.server.lpop # XXX: Do we need to use a timeout here? found = 0 # TODO: Use redis pipeline execution. while found < self.redis_batch_size: data = fetch_one(self.redis_key) if not data: # Queue empty. break req = self.make_request_from_data(data) if req: yield req found += 1 else: self.logger.debug("Request not made from data: %r", data) if found: self.logger.debug("Read %s requests from '%s'", found, self.redis_key) 之前的scrapy获取下一个队列next_requests,是从本机Schedule获取的,这里是通过redis获取的。 准备测试 将之前写的jobbole的逻辑代码拿到这个jobbole中: from scrapy.http import Request from urllib import parse from ..scrapy_redis.spiders import RedisSpider class JobboleSpider(RedisSpider): name = 'jobbole' allowd_domains = ["blog.jobbole.com"] redis_key = 'jobbole:start_urls' def parse(self, response): """ 逻辑分析 1.通过抓取下一页的链接,交给scrapy实现自动翻页,如果没有下一页则爬取完成 2.将本页面的所有文章url爬下,并交给scrapy进行深入详情页的爬取 """ node_urls = response.css('#archive .floated-thumb .post-thumb a') for node_url in node_urls: title_url = node_url.css('::attr(href)').extract_first("") title_img = node_url.css('img::attr(src)').extract_first("") yield Request(url=parse.urljoin(response.url, title_url), meta={"title_img": title_img}, callback=self.parse_detail) # 实现下一页的翻页爬取 next_pages = response.css('.next.page-numbers::attr(href)').extract_first("") # 在当前列表页获取下一页链接 if next_pages: yield Request(url=parse.urljoin(response.url, next_pages), callback=self.parse) # 如果存在下一页,则将下一页交给parse自身处理 def parse_detail(self, response): """ 将爬虫爬取的数据送到item中进行序列化 这里通过ItemLoader加载item """ pass 然后根据说明文档,到settings.py中进行配置: ITEM_PIPELINES = { 'scrapy_redis.pipelines.RedisPipeline': 300, } # Obey robots.txt rules ROBOTSTXT_OBEY = False """ scrapy-redis配置 """ # Enables scheduling storing requests queue in redis. SCHEDULER = "scrapy_redis.scheduler.Scheduler" # Ensure all spiders share same duplicates filter through redis. DUPEFILTER_CLASS = "scrapy_redis.dupefilter.RFPDupeFilter" 也就是将scrapy_redis的item_pipeline、scheduler、dupefilter_class配置进来,并且关闭robots协议设置 为了调试,需要在项目写一个main.py文件,里面的代码跟之前的一样: import os,sys from scrapy.cmdline import execute sys.path.append(os.path.dirname(os.path.abspath(__file__))) execute(["scrapy","crawl","jobbole"]) 保存即可运行,这时候如果redis之前有设置登录密码的话,是会报错的。这里可以用命令,到redis里面取消登录密码,到redis/src目录下打开终端: ./redis-cli 进入redis命令终端: config get requirepass 查看是否有密码,如果结果显示: 1) "requirepass" 2) "ranbos" 那就说明requirepass对应的密码是ranbos,要取消就输入命令: config set requirepass '' 即可。这时候保存运行,发现爬虫启动了,但是没有开始爬取,是因为scrapy_redis现在的start_urls是从redis里面取的,所以在redis里面设置key : redis-cli lpush jobbole:start_urls http://blog.jobbole.com/all-posts 也就是在redis中设置一个Jobbole的初始url,这样爬虫开始爬取的时候就会取这个url开始,如果没有则报错。 然后在jobbole.py的paser方法和paser_detail方法里面打断点,以便调试。 Debug运行,发现可以运行了,也正确的进入了paser方法和paser_detail方法里面。其他操作跟之前的jobbole爬虫一模一样即可。 观察过程 为了更好的观察过程,需要在scrapy-redis源码包 [项目jobbole-test\Lib\site-packages\scrapy_redis\scheduler.py] 中的next_request方法里面: request = self.queue.pop(block_pop_timeout) 这句代码打一个断点,然后恢复断点继续(继续才会从redis里面取starturls,它的取值方法是pop,所以取完后redis是不会有这条记录的),等程序运行到parse里面的时候,断点暂停,不要点继续,在暂停的时候到redis中用命令查看: redis-cli keys * 就会得到这些数据: 1) "myKey" 2) "jobbole:dupefilter" 3) "jobbole:requests" 4) "runoobkey" 5) "mykey" 之前的那些是我插入的,真正的是: 2) "jobbole:dupefilter" 3) "jobbole:requests" 凡是在spider(这里是jobbole爬虫)中用yield出去的记录,都通过scheduler.py里面的enqueue_request方法发push送到这jobbole:requests里面记录着,然后jobbole:dupefilter是个过滤器里面记录的是指纹。 通过命令: redis-cli zrangebyscore jobbole:requests 0 100 可以看到redis里面存储的requests数据,这样爬虫发送的所有请求都会在requests中存有记录,分布式爬虫通过里面的记录和dupefilter里面的指纹实现的去重,不会造成交叉重复爬取。 远程redis 既然它是一个分布式爬虫,就会存在多个服务器。但是负责去重和调度的只能是其中1个服务器,其他的都根据它的redis来抽取request。主要的机器一般叫做master,其他的机器称为slave。 数据库密码连接 之前有提到过,redis可以是不用密码的,但是这显然很危险。还是要根据命令设定好密码。但是如果设定好密码后,爬虫不进行配置就会报错。那如何进行密码配置呢(基于单机情况),在settings.py中新增配置: REDIS_PARAMS ={ 'password': 'ranbos', } 就是这么简单,保存后运行即可。 远程服务器redis 如果是远程服务器上面的redis是如何连接的呢? 还好有台阿里云服务器,在上面根据之前的redis安装方法将它安装上,然后设置好密码。 在阿里云服务器安全配置规则里面把6379端口打开 (有可能需要将bind地址从127.0.0.1改成0.0.0.0)这个我忘了 在本地settings配置中新增配置即可 新增的配置代码为: # 指定redis数据库的连接参数 REDIS_HOST = "59.110.xxx.xxx" REDIS_PORT = "6379" REDIS_PARAMS ={ 'password': 'ranbospider', # 服务器的redis对应密码 } 然后开启爬虫,再用命令在服务器的redis上把start_urls添加进去: lpush jobbole:start_urls http://blog.jobbole.com/all-posts 就完成了scrapy及远程服务器的连接设置。(多个sleva连接master都可以这么设置) 动态配置 在动态配置知识中,可以通过在具体的spider里面重载custom_settings来实现动态配置。这里的redis同样适合动态配置,现将setting里面之前写的配置注释掉,到具体的spider代码中(这里用jobbole演示): class JobboleSpider(RedisSpider): name = 'jobbole' allowd_domains = ["blog.jobbole.com"] redis_key = 'jobbole:start_urls' custom_settings = { # 指定redis数据库的连接参数 'REDIS_HOST':"59.110.xxx.xxx", 'REDIS_PORT':"6379", 'REDIS_PARAMS': { 'password': 'ranbospider', }, } def parse(self, response): pass 同样可以实现远程redis,而且还可以根据不同的爬虫设定不同的服务器地址、配置。 小知识 在我们测试的时候,手动停止爬虫(爬虫自动爬取完毕是finish),手动停止是killed。待下次开启爬虫测试的时候,它总是会再爬取几条信息。 原因是上一次手动关闭爬虫,但request队列里面还有记录,所以打开它就会爬完上次的数据。然后就进入等待阶段,等待我用命令将start_urls添加到redis里面。 去重源码讲解 在源码包里面有: dupefilter.py 文件,它的功能主要是去重。它的源码里面用到的方法与scrapy的源码和功能基本上是一致的: def __init__(self, server, key, debug=False): self.server = server self.key = key self.debug = debug self.logdupes = True 在初始化的时候就自动连接了server,而这个server 在from_settings方法里面生成,跟踪代码可以发现它是自动连接到redis的,而且把key也传到了dupefilter里面。 然后看到request_seen方法: def request_seen(self, request): fp = self.request_fingerprint(request) # This returns the number of values added, zero if already exists. added = self.server.sadd(self.key, fp) return added == 0 意思是会根据request生成一个指纹,然后把指纹添加到redis中,如果成功则返回1,如果失败则返回0。返回0代表里面已经有一个相同的指纹了。 这样就完成了去重的任务 打开源码包里面的pipelines.py,里面有一个RedisPipeline类。首先,它的入口是from_settings方法,里面也有一句代码: 'server': connection.from_settings(settings), 这个server指向的也是上面介绍的那个server,也就是我们的redis。 接着看process_item方法,这里面这是pipelines里面的重要方法,数据传到pipeline都会经过process_item的处理 def process_item(self, item, spider): return deferToThread(self._process_item, item, spider) 它调用了deferToThread方法(一个异步化的方法),放到另外一个线程中去做。然后它还调用了_process_item方法: def _process_item(self, item, spider): key = self.item_key(item, spider) data = self.serialize(item) self.server.rpush(key, data) return item 它首先调用spider的name去redis中找到对应的变量,然后通过rpush放置到队列的队尾。 源码包里面还有个queue.py文件 里面有几个类要讲解一下 FifoQueue 就是先进先出的有序队列
前言 爬虫写完了,很多时候本机部署就可以了,但是总有需要部署到服务器的需求,网上的文章也比较多,复制的也比较多,从下午3点钟摸索到晚上22点,这里记录一下。 环境情况 我的系统是Deepin 开发环境也是Deepin,python 环境用的是Anaconda建立的虚拟环境(python3.6) 部署系统是本机的Deepin 部署环境由于在本机部署,所以跟开发环境一致(就是这里有个坑) 用到的服务是scrapyd 参考文章 网上对于scrapy部署的文章真是很多,一搜就很多页结果,但是我看很多都是复制粘贴,命令错了也没改。我的是综合多个文章来实际执行的,这里列一下我看过的文章: https://www.jianshu.com/p/694a56b2199a 【scrapyd部署scrapy项目】 https://www.jianshu.com/p/495a971233b5 【scrapyd+supervisor在ubuntu部署scrapy项目】 https://www.jianshu.com/p/f0077adb74bb 【使用Scrapyd部署爬虫】 http://blog.csdn.net/qq_40717846/article/details/79014132 【搭建分布式架构爬取知乎用户信息】 介绍 Scrapyd是scrapinghub官方提供的爬虫管理、部署、监控的方案,文档传送 安装scrapyd 对于它的安装,网上的说法层出不穷,有可能是老版本吧? 我的安装很简单,在本机虚拟环境中 pip isntall scrapyd,就完成了 没有安装scrapyd-client也没有安装scrapyd-deploy,就是这么简简单单。 使用 它的使用有3个步骤 1、为了检查是否安装正确,在电脑任意一个地方打开终端(再次强调,我的电脑系统是linux),输入scrapyd,如果没有报错,请打开 http://localhost:6800 ,看到如下画面则代表服务启动成功,看到启动成功后就可以关闭了。 输入图片说明 2、到scrapy工程目录内打开的scrapy.cfg文件,将原代码改为: [settings] default = future.settings [deploy] url = http://localhost:6800/ project = Gxrcpro 保存即可 如果是在服务器上面部署(重要,服务器跟本机设置不同),除了改bind端口为0.0.0.0外 bind端口更改地址: anaconda3/envs/pspiders/lib/python3.6/site-packages/scrapyd 里面有个名为default_scrapyd.conf的文件,其中有一个设置是: bind_address = 127.0.0.1 要将它改为 0.0.0.0 还需要 [settings] default = future.settings [deploy] url = http://0.0.0.0:6800/ project = Gxrcpro 设置成0.0.0.0,否则会提示name or service not known 3、由于之前在本地随意目录下打开scrapyd,导致爬虫启动失败,后来经过群友[wally小馒头]的帮助才知道失败是因为虚拟环境的问题,这次需要用到pycharm。 在pycharm里面打开teminal,然后输入命令scrapyd 接着看到服务启动,通过teminal左上角的绿色+号打开新的teminal窗口,在窗口输入命令: scrapyd-deploy -p Gxrcpro 这里的Gxrc和Gxrcpro跟上面的cfg文件设置有关 如果收到一下信息就代表这次命令成功执行: Packing version 1516456705 Deploying to project "Gxrcpro" in http://localhost:6800/addversion.json Server response (200): {"node_name": "ranbo-PC", "status": "ok", "project": "Gxrcpro", "version": "1516456705", "spiders": 1} 接着执行启动爬虫的命令: curl http://localhost:6800/schedule.json -d project=Gxrcpro -d spider=gxrc 这里的Gxrcpro跟上面的cfg文件设置有关,而gxrc是你写代码时候填写的爬虫名字 收到如下信息: {"node_name": "ranbo-PC", "status": "ok", "jobid": "5a6c4016fdec11e7ad5800e070785d37"} 则代表这次成功启动爬虫,可以通过localhost:6800/Jobs来查看爬虫运行基本情况。 输入图片说明 通过log可以看到爬虫的信息,如果是正在跑数据,则应该可以在log里面看到爬出来的数据;如果是出错,则会看到报错信息(之前我的环境路径不对,就是能启动,但是报错,爬虫就停止了) 输入图片说明
前言 (备注一下,我的开发环境不是Linux就是MacOSX,Windows很多写法不是这样的) 在爬取数据的过程中,有时候需要用到定时、增量爬取。定时这里暂且不说,先说增量爬取。 我想要的增量爬取目前只是简单的,根据url请求来判断是否爬过,如果爬过则不再爬。 复杂一些的增量则是重复爬取,根据指定的几个字段判断是否值有变化,值有变化也算作增量,应当爬取且只更新变化部分(比如天猫商品数据,商品的价格有变化则更新价格,但是url是重复的,也应当爬取) 网上增量爬取的文章很多,包括看过慕课网Scrapy课的笔记,但是它还是不完善,我将在这个基础上进行实际集成。 布隆简介 Bloom Filter是一种空间效率很高的随机数据结构,它利用位数组很简洁地表示一个集合,并能判断一个元素是否属于这个集合。Bloom Filter的这种高效是有一定代价的:在判断一个元素是否属于某个集合时,有可能会把不属于这个集合的元素误认为属于这个集合(false positive)。因此,Bloom Filter不适合那些“零错误”的应用场合。而在能容忍低错误率的应用场合下,Bloom Filter通过极少的错误换取了存储空间的极大节省。 输入图片说明 具体的bloomfilter概念和原理应该查看这篇文章:传送,还有《海量数据处理算法》以及《大规模数据处理利器》 布隆优点 相比于其它的数据结构,布隆过滤器在空间和时间方面都有巨大的优势。布隆过滤器存储空间和插入/查询时间都是常数。另外, Hash 函数相互之间没有关系,方便由硬件并行实现。布隆过滤器不需要存储元素本身,在某些对保密要求非常严格的场合有优势。 布隆过滤器可以表示全集,其它任何数据结构都不能; k 和 m 相同,使用同一组 Hash 函数的两个布隆过滤器的交并差运算可以使用位操作进行。 布隆缺点 但是布隆过滤器的缺点和优点一样明显。误算率(False Positive)是其中之一。随着存入的元素数量增加,误算率随之增加。但是如果元素数量太少,则使用散列表足矣。 另外,一般情况下不能从布隆过滤器中删除元素. 我们很容易想到把位列阵变成整数数组,每插入一个元素相应的计数器加1, 这样删除元素时将计数器减掉就可以了。然而要保证安全的删除元素并非如此简单。首先我们必须保证删除的元素的确在布隆过滤器里面. 这一点单凭这个过滤器是无法保证的。另外计数器回绕也会造成问题。 总的来说,布隆很适合来处理海量的数据,而且速度优势很强。 redis与bloom 去重”是日常工作中会经常用到的一项技能,在爬虫领域更是常用,并且规模一般都比较大。参考文章《基于Redis的Bloomfilter去重》,作者【九茶】还有另一篇文章可以参考《scrapy_redis去重优化,已有7亿条数据》 去重需要考虑两个点:去重的数据量、去重速度。 为了保持较快的去重速度,一般选择在内存中进行去重。 数据量不大时,可以直接放在内存里面进行去重,例如python可以使用set()进行去重。 当去重数据需要持久化时可以使用redis的set数据结构。 当数据量再大一点时,可以用不同的加密算法先将长字符串压缩成 16/32/40 个字符,再使用上面两种方法去重; 当数据量达到亿(甚至十亿、百亿)数量级时,内存有限,必须用“位”来去重,才能够满足需求。Bloomfilter就是将去重对象映射到几个内存“位”,通过几个位的 0/1值来判断一个对象是否已经存在。 然而Bloomfilter运行在一台机器的内存上,不方便持久化(机器down掉就什么都没啦),也不方便分布式爬虫的统一去重。如果可以在Redis上申请内存进行Bloomfilter,以上两个问题就都能解决了。 Bloomfilter算法如何使用位去重,这个百度上有很多解释。简单点说就是有几个seeds,现在申请一段内存空间,一个seed可以和字符串哈希映射到这段内存上的一个位,几个位都为1即表示该字符串已经存在。插入的时候也是,将映射出的几个位都置为1。 需要提醒一下的是Bloomfilter算法会有漏失概率,即不存在的字符串有一定概率被误判为已经存在。这个概率的大小与seeds的数量、申请的内存大小、去重对象的数量有关。下面有一张表,m表示内存大小(多少个位),n表示去重对象的数量,k表示seed的个数。例如我代码中申请了256M,即1<<31(m=2^31,约21.5亿),seed设置了7个。看k=7那一列,当漏失率为8.56e-05时,m/n值为23。所以n = 21.5/23 = 0.93(亿),表示漏失概率为8.56e-05时,256M内存可满足0.93亿条字符串的去重。同理当漏失率为0.000112时,256M内存可满足0.98亿条字符串的去重。 基于Redis的Bloomfilter去重,其实就是利用了Redis的String数据结构,但Redis一个String最大只能512M,所以如果去重的数据量大,需要申请多个去重块(代码中blockNum即表示去重块的数量)。 代码中使用了MD5加密压缩,将字符串压缩到了32个字符(也可用hashlib.sha1()压缩成40个字符)。它有两个作用,一是Bloomfilter对一个很长的字符串哈希映射的时候会出错,经常误判为已存在,压缩后就不再有这个问题;二是压缩后的字符为 0~f 共16中可能,我截取了前两个字符,再根据blockNum将字符串指定到不同的去重块进行去重 总结:基于Redis的Bloomfilter去重,既用上了Bloomfilter的海量去重能力,又用上了Redis的可持久化能力,基于Redis也方便分布式机器的去重。在使用的过程中,要预算好待去重的数据量,则根据上面的表,适当地调整seed的数量和blockNum数量(seed越少肯定去重速度越快,但漏失率越大)。 编写代码 安装依赖 根据github上的资源《BloomFilter_imooc》以及思路来编写bloomfilter的代码。 先前说过,bloom是一种算法,而不是插件也不是软件,它依赖于mmh3,所以需要在虚拟环境中安装mmh3. 然而当我在本机的anaconda虚拟环境内安装时,出现了报错: g++: error trying to exec 'cc1plus': execvp: 没有那个文件或目录 网上查阅了很多文章,找到一个适合我的:传送,大致原因是电脑上的gcc版本与g++版本不一致引起的。可以打开终端用命令: gcc -v g++ -v 来查看两个东西的版本,最终发现用g++的时候报错,于是我安装它: sudo apt-get install g++ 如果是在阿里云服务器,命令改成: yum install gcc-c++ 安装成功后,再次到anaconda虚拟环境中安装mmh3,才成功安装。 编写bloom代码 根据文章《将bloomfilter(布隆过滤器)集成到scrapy-redis中》的指引,作者是将github代码下载到本地目录。 而我为了省事,我在site-package里面写。 在site-package下新建bloofilter_scrapy_redis的package包(带init那种),然后在里面新建文件bloomfilter.py,编写代码: # -*- coding: utf-8 -*- # 18-1-21 下午2:22 # RanboSpider import mmh3 import redis import math import time class PyBloomFilter(): #内置100个随机种子,种子越多需要的内存就越大,内存小的服务器用30个种子就行了 SEEDS = [543, 460, 171, 876, 796, 607, 650, 81, 837, 545, 591, 946, 846, 521, 913, 636, 878, 735, 414, 372, 344, 324, 223, 180, 327, 891, 798, 933, 493, 293, 836, 10, 6, 544, 924, 849, 438, 41, 862, 648, 338, 465, 562, 693, 979, 52, 763, 103, 387, 374, 349, 94, 384, 680, 574, 480, 307, 580, 71, 535, 300, 53, 481, 519, 644, 219, 686, 236, 424, 326, 244, 212, 909, 202, 951, 56, 812, 901, 926, 250, 507, 739, 371, 63, 584, 154, 7, 284, 617, 332, 472, 140, 605, 262, 355, 526, 647, 923, 199, 518] #capacity是预先估计要去重的数量 #error_rate表示错误率 #conn表示redis的连接客户端 #key表示在redis中的键的名字前缀 def __init__(self, capacity=1000000000, error_rate=0.00000001, conn=None, key='BloomFilter'): self.m = math.ceil(capacity*math.log2(math.e)*math.log2(1/error_rate)) #需要的总bit位数 self.k = math.ceil(math.log1p(2)*self.m/capacity) #需要最少的hash次数 self.mem = math.ceil(self.m/8/1024/1024) #需要的多少M内存 self.blocknum = math.ceil(self.mem/512) #需要多少个512M的内存块,value的第一个字符必须是ascii码,所有最多有256个内存块 self.seeds = self.SEEDS[0:self.k] self.key = key self.N = 2**31-1 self.redis = conn # print(self.mem) # print(self.k) def add(self, value): name = self.key + "_" + str(ord(value[0])%self.blocknum) hashs = self.get_hashs(value) for hash in hashs: self.redis.setbit(name, hash, 1) def is_exist(self, value): name = self.key + "_" + str(ord(value[0])%self.blocknum) hashs = self.get_hashs(value) exist = True for hash in hashs: exist = exist & self.redis.getbit(name, hash) return exist def get_hashs(self, value): hashs = list() for seed in self.seeds: hash = mmh3.hash(value, seed) if hash >= 0: hashs.append(hash) else: hashs.append(self.N - hash) return hashs pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=0) conn = redis.StrictRedis(connection_pool=pool) 这里的pool和conn都是单独连接的,实际上在分布式爬虫中是比较不友好的,多台机器的配置就会烦人,这里暂且这样,后期我再改。 是否配置密码 至于是否配置密码,如何配置密码,在bloomfilter.py文件中,有一句: pool = redis.ConnectionPool(host='127.0.0.1', port=6379, db=0) conn = redis.StrictRedis(connection_pool=pool) 其中redis.StrictRedis方法,跟踪(ctrl+左键点击)进去,可以看到init初始化方法里面有个password=None def __init__(self, host='localhost', port=6379, db=0, password=None, socket_timeout=None, socket_connect_timeout=None, socket_keepalive=None, socket_keepalive_options=None, connection_pool=None, unix_socket_path=None, encoding='utf-8', encoding_errors='strict', charset=None, errors=None, decode_responses=False, retry_on_timeout=False, ssl=False, ssl_keyfile=None, ssl_certfile=None, ssl_cert_reqs=None, ssl_ca_certs=None, max_connections=None): 这里应该是设置password,也就是将服务器redis的权限密码auth设置进来。 pool = redis.ConnectionPool(host='47.98.110.67', port=6379, db=0, password='quinns') conn = redis.StrictRedis(connection_pool=pool) 即可完成密码的设置。 集成到scrapy_redis中 上面的布隆过滤器代码写好后,需要集成到scrapy_redis中。完成去重任务的是dupefilter.py文件,就要对它进行改造,路径是site-package/scrapy_redis/目录内: 现将刚才编写的布隆选择器导入此文件 from bloomfilter_scrapy_redis.bloomfilter import conn,PyBloomFilter # 从源码包导入布隆 然后在init方法中初始化布隆选择器(这里贴上整个init代码): def __init__(self, server, key, debug=False): """Initialize the duplicates filter. Parameters ---------- server : redis.StrictRedis The redis server instance. key : str Redis key Where to store fingerprints. debug : bool, optional Whether to log filtered requests. """ self.server = server self.key = key self.debug = debug self.logdupes = True """ 集成布隆过滤器,通过连接池连接redis """ self.bf = PyBloomFilter(conn=conn, key=key) 接下来改动request_seen方法,在里面对request进行判断,如果此次request请求在redis中存在,则直接返回,如果不存在则添加到redis的队列里面去,让爬虫去爬: def request_seen(self, request): """ …… """ fp = self.request_fingerprint(request) """ 集成布隆过滤 判断redis是否存在此指纹,如果存在则直接返回true 如果不存在添加指纹到redis,同时返回false """ if self.bf.is_exist(fp): return True else: self.bf.add(fp) return False """ 集成布隆过滤器,将下方2行代码注释 """ # This returns the number of values added, zero if already exists. # added = self.server.sadd(self.key, fp) # return added == 0 到这里即完成了scrapy_redis对布隆过滤器的集成。 测试 在爬虫代码中编写: # -*- coding: utf-8 -*- import scrapy from scrapy_redis.spiders import RedisSpider from scrapy.http import Request from urllib import parse class JobboleSpider(RedisSpider): name = 'jobbole' allowd_domains = ["www.gxnhyd.com"] redis_key = 'jobbole:start_urls' def parse(self, response): """ 将当前列表页的每条标的链接拿到 并传给detail进行深入爬取 通过已知列表页码数量 进行循环爬取 就不用翻页了 """ total = response.css('.item .tl.pl10 a') for x in total: title = x.css('::text').extract_first("") title_url = x.css('::attr(href)').extract_first("") yield Request(url=parse.urljoin(response.url, title_url), callback=self.parse_detail) for i in range(1, 10): next_pages = "http://www.gxnhyd.com/deals/p-%s" % (i) yield Request(url=next_pages, callback=self.parse) def parse_detail(self, response): """ 获取当前详情页的标的信息 包括金额 收益 期限 借款人 投资人列表 - 投资人用户名/投资人投资金额/投资方式/投资时间等 :param response: :return: """ print(response.url) 通过print对爬取情况做观察 开启爬虫后,由于scrapy_redis的特性,需要给redis里面添加start_urls: lpush jobbole:start_urls http://www.gxnhyd.com/deals [value ...] 爬虫监听到值之后,立即开始爬取,这一步没问题 但是爬完后它空跑了,不会结束,一直空跑。(事实证明,跑空了也不要紧) 二次测试 在第一次测试通过后,我加大了循环次数for i in range(1, 30),看看是否会出现重复的值,结果报错了。 报错信息与bloom是否重复无关,原因是我之前看到空跑,就主动停止了代码,导致redis报错: MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist 解决办法在这里《redis异常解决:MISCONF Redis 》,在redis-cli用命令解决这个权限问题: config set stop-writes-on-bgsave-error no 二次测试后,发现可以正常运行了。然后观察到bloom也生效了,但是还是有空跑的问题 解决空跑(这个办法其实不太好,不推荐) 空跑就是爬虫在爬取完所有的队列有,不会自动停止,而是一直请求请求,然后观察redis-server窗口有memory的提示一直在进行。 解决这个空跑问题参考了一些资料《scrapy-redis所有request爬取完毕,如何解决爬虫空跑问题? 》 输入图片说明 根据scrapy-redis分布式爬虫的原理,多台爬虫主机共享一个爬取队列。当爬取队列中存在request时,爬虫就会取出request进行爬取,如果爬取队列中不存在request时,爬虫就会处于等待状态. 可是,如果所有的request都已经爬取完毕了呢?这件事爬虫程序是不知道的,它无法区分结束和空窗期状态的不同,所以会一直处于上面的那种等待状态,也就是我们说的空跑。 那有没有办法让爬虫区分这种情况,自动结束呢? 从背景介绍来看,基于scrapy-redis分布式爬虫的原理,爬虫结束是一个很模糊的概念,在爬虫爬取过程中,爬取队列是一个不断动态变化的过程,随着request的爬取,又会有新的request进入爬取队列。进进出出。 爬取速度高于填充速度,就会有队列空窗期(爬取队列中,某一段时间会出现没有request的情况),爬取速度低于填充速度,就不会出现空窗期。所以对于爬虫结束这件事来说,只能模糊定义,没有一个精确的标准。 可以通过限定爬虫自动关闭时间来完成这个任务,在settings配置: # 爬虫运行超过23.5小时,如果爬虫还没有结束,则自动关闭 CLOSESPIDER_TIMEOUT = 84600 特别注意 :如果爬虫在规定时限没有把request全部爬取完毕,此时强行停止的话,爬取队列中就还会存有部分request请求。那么爬虫下次开始爬取时,一定要记得在master端对爬取队列进行清空操作。 想象一下,爬虫已经结束的特征是什么? 那就是爬取队列已空,从爬取队列中无法取到request信息。那着手点应该就在从爬取队列中获取request和调度这个部分。查看scrapy-redis源码,我们发现了两个着手点,调度器site-packages\scrapy_redis\schedluer.py和site-packages\scrapy_redis\spiders.py爬虫。 但是爬虫在爬取过程中,队列随时都可能出现暂时的空窗期。想判断爬取队列为空,一般是设定一个时限,如果在一个时段内,队列一直持续为空,那我们可以基本认定这个爬虫已经结束了。 我选择更改调度器,site-packages\scrapy_redis\schedluer.py所以有了如下的改动: 首先在init里面设定一个初始次数 import datetime def __init__(self, server, …… …… """ """ 为解决空跑问题:设定倒计次数 下方根据次数决定何时关闭爬虫,避免空跑""" self.lostGetRequest = 0 if idle_before_close < 0: …… …… 完整的init方法代码为: def __init__(self, server, persist=False, flush_on_start=False, queue_key=defaults.SCHEDULER_QUEUE_KEY, queue_cls=defaults.SCHEDULER_QUEUE_CLASS, dupefilter_key=defaults.SCHEDULER_DUPEFILTER_KEY, dupefilter_cls=defaults.SCHEDULER_DUPEFILTER_CLASS, idle_before_close=0, serializer=None): """ 为解决空跑问题:设定倒计次数 下方根据次数决定何时关闭爬虫,避免空跑""" self.lostGetRequest = 0 if idle_before_close < 0: raise TypeError("idle_before_close cannot be negative") self.server = server self.persist = persist self.flush_on_start = flush_on_start self.queue_key = queue_key self.queue_cls = queue_cls self.dupefilter_cls = dupefilter_cls self.dupefilter_key = dupefilter_key self.idle_before_close = idle_before_close self.serializer = serializer self.stats = None 然后到next_request方法中进行修改: def next_request(self): block_pop_timeout = self.idle_before_close request = self.queue.pop(block_pop_timeout) if request and self.stats: """ 解决空跑问题,这里判断如果获取到request则重置倒计时lostGetRequest """ self.lostGetRequest = 0 self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider) if request is None: """ scrapy_reids跑完数据后不会自动停止,会产生空跑情况,一直空跑 每次调度Schedule时如果队列没有数据 则倒计时+1 50次空跑大约费时5分钟,根据项目需求设定次数,满足空跑次数则主动停止并填写停止原因 """ self.lostGetRequest += 1 if self.lostGetRequest > 10: self.spider.crawler.engine.close_spider(self.spider, 'Queue is empty,So active end') return request 这样就可以解决空跑的问题了。(事实证明,高兴得太早) 真正解决空跑(这个也不好,不建议。因为scrapy_redis已处理空跑问题(我也不确定)) 真是太年轻,不懂事,我以为按照别人的想法实施,就可以解决空跑的问题了。然后当自己亲自测试的时候,发现并不是那么回事。 scrapy是异步的,而且request队列确实会有空闲状态,如果有空闲状态就会+1,用数字进行累加的话,虽然上编写了重置为0的操作,但貌似是不行的,测试没有那么细致,反正当空闲状态达到N次(关闭条件)的时候,就会自动关闭(request队列还在抽取,也会被关闭),那这就是个bug。 首先 思路是对的,然而用+1的方式出错了。我换了个思路,用时间差来决定是否关闭爬虫。逻辑: 时间差是不会存在累加的情况,所以不会有刚才的bug 先初始化一个起始时间 在每次请求队列的时候刷新起始时间 在每次队列为空的时候开始计时 计算时间差,如果队列为空的时间减去起始时间的秒数结果大于设定值,则判定为空跑,关闭爬虫 优点 通过时间差来判断空跑,解决了刚才的bug; 可以根据时间来关闭爬虫,而不是次数,这样对于日后爬虫的监控更精准 具体的代码如下: 现在init方法设定起始时间 为解决空跑问题:设定起始时间 下方根据记录空跑时间end_times与起始时间的时间差来决定何时关闭爬虫,避免空跑 """ self.strat_times = datetime.datetime.now() 然后到next_request方法进行具体的时间差计算和空跑判断,还有爬虫的关闭操作: def next_request(self): block_pop_timeout = self.idle_before_close request = self.queue.pop(block_pop_timeout) if request and self.stats: """ 解决空跑问题,这里判断如果获取到request则重置起始时间strat_times """ self.strat_times = datetime.datetime.now() self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider) if request is None: """ scrapy_reids跑完数据后不会自动停止,会产生空跑情况,一直空跑 每次调度Schedule时如果队列没有数据 则计算end_times 当end_times与start_times的时间差close_times超过N秒,就判定为空跑且进行关闭爬虫的操作 """ self.end_times = datetime.datetime.now() self.close_times = (self.end_times - self.strat_times).seconds print("tihs close_times is : ") print(self.close_times) if self.close_times > 180: self.spider.crawler.engine.close_spider(self.spider, 'Queue is empty,So active end') return request 看到下图,跑完数据后会根据时间差关闭爬虫 输入图片说明 这样才是真正的解决了空跑的问题 最后运行,可以正常关闭爬虫了。但是结束的时候还会有报错信息: builtins.AttributeError: 'NoneType' object has no attribute 'start_requests' 2017-12-14 16:18:56 [twisted] CRITICAL: Unhandled Error Traceback (most recent call last): File "E:\Miniconda\lib\site-packages\scrapy\commands\runspider.py", line 89, in run self.crawler_process.start() File "E:\Miniconda\lib\site-packages\scrapy\crawler.py", line 285, in start reactor.run(installSignalHandlers=False) # blocking call File "E:\Miniconda\lib\site-packages\twisted\internet\base.py", line 1243, in run self.mainLoop() File "E:\Miniconda\lib\site-packages\twisted\internet\base.py", line 1252, in mainLoop self.runUntilCurrent() --- <exception caught here> --- File "E:\Miniconda\lib\site-packages\twisted\internet\base.py", line 878, in runUntilCurrent call.func(*call.args, **call.kw) File "E:\Miniconda\lib\site-packages\scrapy\utils\reactor.py", line 41, in __call__ return self._func(*self._a, **self._kw) File "E:\Miniconda\lib\site-packages\scrapy\core\engine.py", line 137, in _next_request if self.spider_is_idle(spider) and slot.close_if_idle: File "E:\Miniconda\lib\site-packages\scrapy\core\engine.py", line 189, in spider_is_idle if self.slot.start_requests is not None: builtins.AttributeError: 'NoneType' object has no attribute 'start_requests' 当通过engine.close_spider(spider, ‘reason’)来关闭spider时,有时会出现几个错误之后才能关闭。可能是因为scrapy会开启多个线程同时抓取,然后其中一个线程关闭了spider,其他线程就找不到spider才会报错。 注意事项 编写代码的schedule.py有个next_request方法有这么一句代码: request = self.queue.pop(block_pop_timeout) 打开同目录的queue.py文件 输入图片说明 所以,PriorityQueue和另外两种队列FifoQueue,LifoQueue有所不同,特别需要注意。 如果会使用到timeout这个参数,那么在setting中就只能指定爬取队列为FifoQueue或LifoQueue # 指定排序爬取地址时使用的队列, # 默认的 按优先级排序(Scrapy默认),由sorted set实现的一种非FIFO、LIFO方式。 # 'SCHEDULER_QUEUE_CLASS': 'scrapy_redis.queue.SpiderPriorityQueue', # 可选的 按先进先出排序(FIFO) 'SCHEDULER_QUEUE_CLASS': 'scrapy_redis.queue.SpiderQueue', # 可选的 按后进先出排序(LIFO) # SCHEDULER_QUEUE_CLASS = 'scrapy_redis.queue.SpiderStack' 数据入库测试 经过多次 的mysql入库测试,发现bloomfilter是生效的,而且增量开始之前,对于那么重复的数据对比过滤是非常快的(仅用了500条数据测试),正常爬取500条数据大约1分钟多一点。在爬取过500多数据后,bloomfilter的略过只用了几秒钟,很短的时间。 这个还是很强的,我很高兴
登录的需求 有些数据,必须在登录之后才能查看,所以我们在爬取过程中就会产生模拟登录的需求,它有两个点: 1、未登录的情况下无法查看数据,或者直接弹出登录框提示你先登录 2、登录后登录状态的保持(通常可以理解为cookie的处理) 登录的逻辑 访问登录页面(部分网站会在登录页面设定token或标识来反爬虫,根据Network查看post数据来确认) 构造登录所需数据,并携带伪造的数据发送登录请求(如token或标识、User-Agent/HOST/Referer等数据,向登录地址POST数据。) 根据某个状态码或者登录后跳转url判断是否登录成功 登录成功后获取start_urls,并且调用parse进行数据爬取 模拟登录注意的地方 登录爬取有几个特点,比如浏览器不能换,可能UserAgent也不能换、要用到cookie、页面可能会有重定向,通常表现为登录后跳转、页面需要发送token或其他标识,所以正则是个关键。 最好不要用自动切换UserAgent的功能(未测试) 必须在配置开启cookie,COOKIES_ENABLED = True 关闭重定向禁止开关 #REDIRECT_ENABLED = False # 禁止重定向 robots协议也关掉 ROBOTSTXT_OBEY = False 代码实现 这里以东盟贷为例子, 示例网站 这是最简单的一种例子(登录时没有验证码、不用携带token、不用携带其他标识),仅仅需要把用户名和密码发送过去即可。 当你需要爬取投资列表的数据时,要到【我要投资】页面去爬,你想要打开那个页面他就会判断你是否登录,如果没登录就会给你直接跳转到登录界面 被强制跳转到登录页面 审查元素,看input name 那我们面对这种情况,就必须先登录后请求页面再爬取数据(我这里仅演示到登录完成),示例代码: # -*- coding: utf-8 -*- import scrapy class DongmengSpider(scrapy.Spider): name = 'dongmeng' allowed_domains = ['www.dongmengdai.com'] start_urls = ['https://www.dongmengdai.com/view/Investment_list_che.php?page=1'] # 根据浏览器Network返回值来构造header,这是比较简单的header,复杂的还会有很多信息 header = { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:57.0) Gecko/20100101 Firefox/57.0", "HOST": "www.dongmengdai.com", "Referer": "https://www.dongmengdai.com/index.php?user&q=action/login", } def parse(self, response): """ 正式进入爬取区域 """ pass def start_requests(self): """ 重载start_requests方法 待登录成功后,再进入parse进行数据爬取 访问登录页面 并调用do_login方法进行登录 """ return [scrapy.Request('https://www.dongmengdai.com/index.php?user&q=action/login', headers=self.header, callback=self.do_login)] def do_login(self, response): """ 根据Network的信息 向登录地址发送请求 携带用户名和密码 如果需要token或者其他标识则需要用正则进行匹配,然后放到login_data中 调用is_login方法判断是否登录成功 """ login_url = "https://www.dongmengdai.com/index.php?user&q=action/login" login_data = { "keywords": "18077365555", "password": "123456789" } return [scrapy.FormRequest(url=login_url,formdata=login_data,headers=self.header,callback=self.is_login)] def is_login(self, response): """ 这个网站登陆后会自动跳转到用户中心 可根据返回的url判断是否登录成功 其他网站可以依靠状态码进行判断 如果登录成功则从 start_urls中抽取url进行爬取 这里不用设置callback回调parse 因为它默认调用parse 如果是在crawl模板的爬虫,可能需要设置callback调用 """ if "index.php?user" in response.url: for url in self.start_urls: yield scrapy.Request(url, dont_filter=True, headers=self.header) else: print("登录失败") 具体逻辑整理 具体的逻辑我已经写在代码中了,这里再整理一下: 1、先确定起始url和设置domains 2、根据观察浏览器Network返回值来构造header(因为它也识别请求头信息) 3、重载start_requests方法并带上请求头信息来发起请求 4、do_login方法中执行具体的登录操作,(keywords和password是登录框的input name,通过右键审查元素可以看到html结构,通过input name来定位输入框)以及发起请求 5、is_login方法来判断是否登录成功,并且指定了下一步操作的方法(可以开始爬数据了) Cookie的问题 可以看到,上面的代码里面只是发送了用户名和密码,但是常规的登录请求是需要保存和发送cookie的,我们在代码中并没有保存cookie和二次请求携带cookie的操作,那Scrapy是如何完成这个行为的呢? 在源码目录site-packages/scrapy/downloadermiddlewares/cookies.py文件中,可以看到具体的源码: CookiesMiddleware的第前面两个个方法是重载from_crawler: def __init__(self, debug=False): self.jars = defaultdict(CookieJar) self.debug = debug @classmethod def from_crawler(cls, crawler): if not crawler.settings.getbool('COOKIES_ENABLED'): raise NotConfigured return cls(crawler.settings.getbool('COOKIES_DEBUG')) init在加载的时候初始化CookieJar,from_crawler则是检查settings里面的cookie配置情况。 接着到process_request方法: def process_request(self, request, spider): if request.meta.get('dont_merge_cookies', False): return cookiejarkey = request.meta.get("cookiejar") jar = self.jars[cookiejarkey] cookies = self._get_request_cookies(jar, request) for cookie in cookies: jar.set_cookie_if_ok(cookie, request) # set Cookie header request.headers.pop('Cookie', None) jar.add_cookie_header(request) self._debug_cookie(request, spider) 它完成的任务大致就是设置cookie,请求的时候就带上。 而process_response方法又完成了什么任务呢: def process_response(self, request, response, spider): if request.meta.get('dont_merge_cookies', False): return response # extract cookies from Set-Cookie and drop invalid/expired cookies cookiejarkey = request.meta.get("cookiejar") jar = self.jars[cookiejarkey] jar.extract_cookies(response, request) self._debug_set_cookie(response, spider) 它是完成cookie的筛选,提取cookie和删除废弃的cookie 下面还有几个方法_debug_cookie、_debug_set_cookie、_format_cookie、_get_request_cookies他们几个完成了cookie的获取、生成和格式化等任务。 cookie小结 可以得出结论,Scrapy框架会自动帮我们处理cookie的问题,在常规的使用当中我们不需要关心它的切换和更新问题。只有在一些逻辑处理的时候,有可能涉及到登录逻辑的改动,才需要了解底层原理并对某个方法进行重载,以实现逻辑的变化。
之前的驱动版本和浏览器版本对不上,在deepin下吃了这个亏……,记录一下 chrome 安装selenium 打开终端,通过命令进入虚拟环境(当然,不用虚拟环境的可以不用这个命令): source activate pspiders (pspiders是虚拟环境名称)激活当前虚拟环境,然后在里面通过pip命令安装selenuim: pip install selenium 即可完成。 下载Chromedirver 就是这里被坑的,事实上是要先下载chromedirver的,传送门,还有另一个传送门然后根据电脑上的chrome浏览器版本找到dirver版本(坑就在这里,他的版本不按顺序,没有文档找不到,事实上v63版本对应的驱动在2.35dirver目录),进去下载linux64位的压缩包chromedriver_linux64.zip就行。 解压安装 在目录直接可以解压(里面就一个文件) 然后通过命令来对他进行安装: chmod +x chromedriver sudo mv -f chromedriver /usr/local/share/chromedriver sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver sudo ln -s /usr/local/share/chromedriver /usr/bin/chromedriver 安装后确认/usr/bin目录下是否有chromedriver文件 测试 随地(没有错,就是随地)新建一个py文件(当然,你的Pycharm环境配置必须配置到你安装selenium的虚拟环境,其中要注意的是Chromedriver是安装到系统的,与虚拟环境无关),编写代码: from selenium import webdriver browser = webdriver.Chrome() # 由于设置过了路径,它会自己去调用,不用写路径(windows下才要写) # 通过get方法可以获取到指定url的网页 并且自动加载和渲染js/css等内容 browser.get("http://www.baidu.com/") browser.save_screenshot(browser.title) 发现可以正常调用浏览器、打开制定页面就对了
使用背景 有时候为了做测试,不想去数据库设置字段,设置musql字段实在是太麻烦了,这种情况下可以先把存储到json文件中,并观察数据的正确性,如果数据没问题了,再改成mysql存储即可。 有时候任务本身就是要存储进json文件中。 有时候为了更好的阅读数据,看结构,json文件是一个不错的选择 json 在pipeline写json存储 存储的好处与逻辑: 在pipeline写json存储,代码分离性比较好 写文件涉及到打开关闭,在init进行打开操作,close进行关闭操作 scrapy中数据流过process_item方法,所以对它进行重载,进行数据的写入 通过信号量进行close操作 具体的代码实现 根据整理好的逻辑来编写代码(在pipelines.py中新增) import codecs,json class JsonCreatePipeline(object): """ 将数据保存到json文件,由于文件编码问题太多,这里用codecs打开,可以避免很多编码异常问题 在类加载时候自动打开文件,制定名称、打开类型(只读),编码 重载process_item,将item写入json文件,由于json.dumps处理的是dict,所以这里要把item转为dict 为了避免编码问题,这里还要把ensure_ascii设置为false,最后将item返回回去,因为其他类可能要用到 调用spider_closed信号量,当爬虫关闭时候,关闭文件 """ def __init__(self): self.file = codecs.open('spiderdata.json', 'w', encoding="utf-8") def process_item(self, item, spider): lines = json.dumps(dict(item), ensure_ascii=False) + "\n" self.file.write(lines) return item def spider_closed(self, spider): self.file.close() 启用pipelines 写好代码后,还需要在settings中启用,然后在settings的ITEM_PIPELINES处将JsonCreatePipeline配置进去就可以运行了: 'rspider.pipelines.JsonCreatePipeline':200, # 开启json文件保存 数字任意 它会自动为你创建一个名为spiderdata.json的文件,里面写满了你爬到的数据,你可以在Pycharm中直接打开它,并且通过快捷键格式化数据(这样你才能更好的阅读数据和校验数据)。 结语 爬虫编写过程中,很重要的一步就是校验数据,因为数据是可变的,如果你不去校验它,那么入库的时候就会产生很多的麻烦。我的做法是边写边校验,以确保能够及时的修正代码。
一、需求 邮件发送功能,作为一个【通知】或者说【知晓】的方式,在实际应用中会经常使用的,试想一个场景: 你掌握着公司半数以上的爬虫,并且你每天都要监控它们(他们在服务器上),你作为一个爬虫技术从业者,你肯定会想(偷懒),因为不偷懒的工程师不会进步。你希望当它们触发某个状况的时候,你的邮箱会收到对应的提醒,这样你可以及时的处理这些状况,当然你也可以集成微信来开发,让通知发送到你的微信,但是互联网行业,邮箱还是经常用的。 可以根据实际需求,在不同的时机发送不同的提醒邮件,以对爬虫状态进行监控。 这里我以监控爬虫的停止信息来作为示例。 timg (1).jpg 二、scrapy文档 scrapy官网文档有提供邮件发送的资料: 发送email 虽然Python通过 smtplib 库使得发送email变得很简单,Scrapy仍然提供了自己的实现。 该功能十分易用,同时由于采用了 Twisted非阻塞式(non-blocking)IO ,其避免了对爬虫的非阻塞式IO的影响。 另外,其也提供了简单的API来发送附件。 通过一些 settings 设置,您可以很简单的进行配置。 简单例子 有两种方法可以创建邮件发送器(mail sender)。 您可以通过标准构造器(constructor)创建: fromscrapy.mailimportMailSendermailer = MailSender() 或者您可以传递一个Scrapy设置对象,其会参考 settings: mailer= MailSender.from_settings(settings) 这是如何来发送邮件(不包括附件): mailer.send(to=["someone@example.com"], subject="Some subject", body="Some body", cc=["another@example.com"]) MailSender类参考手册 在Scrapy中发送email推荐使用MailSender。其同框架中其他的部分一样,使用了 Twisted非阻塞式(non-blocking)IO 。 classscrapy.mail.MailSender(smtphost=None, mailfrom=None, smtpuser=None, smtppass=None, smtpport=None) 参数由以下组成: smtphost (str) – 发送email的SMTP主机(host)。如果忽略,则使用 MAIL_HOST 。 mailfrom (str) – 用于发送email的地址(address)(填入 From:) 。 如果忽略,则使用 MAIL_FROM 。 smtpuser – SMTP用户。如果忽略,则使用 MAIL_USER 。 如果未给定,则将不会进行SMTP认证(authentication)。 smtppass (str) – SMTP认证的密码 smtpport (int) – SMTP连接的端口 smtptls – 强制使用STARTTLS smtpssl(boolean)– 强制使用SSL连接 classmethodfrom_settings(settings) 使用Scrapy设置对象来初始化对象。其会参考 这些Scrapy设置. 参数: settings (scrapy.settings.Settings object) – the e-mail recipients send(to, subject, body, cc=None, attachs=(), mimetype='text/plain') 发送email到给定的接收者。 参数: to (list) – email接收者 subject (str) – email内容 cc (list) – 抄送的人 body (str) – email的内容 attachs (iterable) – 可迭代的元组 (attach_name, mimetype, file_object)。 attach_name 是一个在email 的附件中显示的名字的字符串, mimetype 是附件的mime类型, file_object 是包含附件内容的可读的文件对象。 mimetype (str) – email的mime类型 三、实际写法 根据上面官网文档的一些介绍和语法(更多语法请上官网翻阅) 这里编写一段示例代码(结合信号量) 3.1 设置邮箱pop 登录QQ邮箱 找到设置-账户 然后生成授权码(以前是生成密码,现在用授权码) 3.2 编码 在具体的爬虫文件中编写: from scrapy.mail import MailSender from scrapy.xlib.pydispatch import dispatcher from scrapy import signals 接着在class类的上方编写emial的链接配置信息: mailers= MailSender(smtphost="smtp.qq.com", # 发送邮件的服务器 mailfrom="abcdefg@qq.com", # 邮件发送者 smtpuser="abcdefg@qq.com", # 用户名 smtppass="qtpzvxxyyxxyyxxyyxde", # 发送邮箱的密码不是你注册时的密码,而是授权码!!!切记! smtpport=25 # 端口号 ) #初始化邮件模块 然后再到class类中编写信号量监听和具体的邮件发送代码: def __init__(self): """ 监听信号量 """ super(YoutubeapiSpider, self).__init__() # 当收到spider_closed信号的时候,调用下面的close方法来发送通知邮件 dispatcher.connect(self.close, signals.spider_closed) def close(spider, reason): """ 执行邮件发送操作 """ body ="爬虫[%s]已经关闭,原因是: %s"% (spider.name, reason) subject ="[%s]爬虫关闭提醒"% spider.name mailers.send(to={"admin@qq.com","quinns@aliyun.com"}, subject=subject, body=body) 这样就会在收到爬虫关闭信号的时候,通过abgdefg@qq.com给指定的admin@qq.com和quinns@aliyun.com发送邮件(实际应用的时候可以考虑给1个或多个邮箱发送),邮件内容是body,邮件标题是subject。 3.3 在邮件里添加爬虫停止信息 毕竟停止信息里面对爬虫状态和记录比较详细,所以邮件中应当发送停止信息。 当然了,写法很多,除了def close 还可以: def __init__(self): """ 监听信号量 """ super(YoutubeapiSpider, self).__init__()# 当收到spider_closed信号的时候,调用下面的close方法来发送通知邮件 dispatcher.connect(self.spider_closed, signals.spider_closed) def spider_closed(self, spider, reason): # 上方的信号量触发这个方法 stats_info = self.crawler.stats._stats # 爬虫结束时控制台信息 body = "爬虫[%s]已经关闭,原因是: %s.\n以下为运行信息:\n %s" % (spider.name, reason, stats_info) subject = "[%s]爬虫关闭提醒" % spider.name mailers.send(to={"513720453@qq.com"}, subject=subject, body=body) 只要满足触发条件,就可以发送指定内容的邮件。 然后收到的邮件是这样的: 收到的邮件 3.4 邮件配置的坑 在使用过程中,如果发送邮件后,scrapy报出如下错误信息: 报错 通过多方排查,依旧没有资料信息。 后来无意中改动了邮件发送配置中的端口,就解决了这个问题 25-465.png 只要将原来的25端口改成465即可。(猜测跟邮件服务器pop3的ssl有关,未亲自确认) 这里虽然只是以爬虫关闭来举例,实际上可以监控更多的行为操作,比如百度翻译的接口超量、捕获一些特殊的异常等。
360截图20180712215548501.jpg 一、前言 在爬虫行当,每天都要面对目标反爬虫技术,我们想要拿到数据,就需要针对它们的反爬虫来制定绕过方法,比如它识别你的UserAgent,那你可能就需要伪造、它限制IP请求次数,你可能就需要限速或者改变ip、它用验证码来识别你是人是机,你就需要模拟人的操作并且正确填写它给你弹出的验证码等等。 这里我以实际项目举例:有个项目需要爬取中国证券协会,对方使用的反爬虫手段中就有IP请求次数的限制,因为我需要的数据比较多,而且目标的数据很绕,所以我紧紧降低我爬虫的速度我觉得对我影响很大,所以我选择通过动态ip切换来应对,我需要在scrapy中实现ip自动切换,才能够在客户要求的时间内完成爬取任务。 在此之前,我用过第三方库scrapy-proxys加上芝麻ip的代理api接口,可能是之前代码没有调整好,导致的没有能够成功。(后面有机会再测试)。 二、阿布云范例 阿布云官方给出了python和scrapy的示例代码 文档python3示例 from urllib import request # 要访问的目标页面 targetUrl = "http://test.abuyun.com/proxy.php" # 代理服务器 proxyHost = "http-dyn.abuyun.com" proxyPort = "9020" # 代理隧道验证信息 proxyUser = "H01234567890123D" proxyPass = "0123456789012345" proxyMeta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % { "host" : proxyHost, "port" : proxyPort, "user" : proxyUser, "pass" : proxyPass, } proxy_handler = request.ProxyHandler({ "http" : proxyMeta, "https" : proxyMeta, }) #auth = request.HTTPBasicAuthHandler() #opener = request.build_opener(proxy_handler, auth, request.HTTPHandler) opener = request.build_opener(proxy_handler) request.install_opener(opener) resp = request.urlopen(targetUrl).read() print (resp) 上面的是官方提供的python原生写法,下面还有官方提供的scrapy写法: 文档scrapy示例 import base64 # 代理服务器 proxyServer = "http://http-dyn.abuyun.com:9020" # 代理隧道验证信息 proxyUser = "H012345678901zyx" proxyPass = "0123456789012xyz" # for Python2 proxyAuth = "Basic " + base64.b64encode(proxyUser + ":" + proxyPass) # for Python3 #proxyAuth = "Basic " + base64.urlsafe_b64encode(bytes((proxyUser + ":" + proxyPass), "ascii")).decode("utf8") class ProxyMiddleware(object): def process_request(self, request, spider): request.meta["proxy"] = proxyServer request.headers["Proxy-Authorization"] = proxyAuth 这里在scrapy项目中的Middleware里面写即可。 三、正式集成 将它集成到scrapy框架中,那就在项目工程的middlewares.py中新增类及key等信息: import base64 """ 阿布云ip代理key配置 """ proxyServer = "http://http-dyn.abuyun.com:9020" proxyUser = "HWFHQ5YP14Lxxx" proxyPass = "CB8D0AD56EAxxx" # for Python3 proxyAuth = "Basic " + base64.urlsafe_b64encode(bytes((proxyUser + ":" + proxyPass), "ascii")).decode("utf8") class ABProxyMiddleware(object): """ 阿布云动态ip代理中间件 """ def process_request(self, request, spider): request.meta["proxy"] = proxyServer request.headers["Proxy-Authorization"] = proxyAuth 然后再到settings.py中启用刚才编写的中间件: DOWNLOADER_MIDDLEWARES = { #'Securities.middlewares.SecuritiesDownloaderMiddleware': None, 'Securities.middlewares.ABProxyMiddleware': 1, } 四、注意事项 阿布云动态ip默认是1秒钟请求5次,(可以加钱,购买多次)。所以,当他是默认5次的时候,我需要对爬虫进行限速,还是在settings.py中,空白处新增如下代码: """ 启用限速设置 """ AUTOTHROTTLE_ENABLED = True AUTOTHROTTLE_START_DELAY = 0.2 # 初始下载延迟 DOWNLOAD_DELAY = 0.2 # 每次请求间隔时间 当然了,如果你加钱增加每秒次数的话,可以不用考虑限速的问题。 最后 即可完成阿布云动态代理ip在scrapy中的的集成,尽情的爬吧! 学习是一通百通的,既然阿布云平台的示例代码可以集成到框架中,那么其他平台的示例代码同样可以集成到框架中使用,我就不多写了。 如果你想真的学会这个技巧,你就应该自己去实践,自己动手编写代码并测试,才能够掌握在自己手中。
我也是一名爬虫爱好者,深知在爬虫学习过程中的汗水和艰辛,之前的笔记、记录和绕坑手段都写在了博客中,但是未开放。 由于生活焦灼,我在爬虫的道路上渐行渐远,所以我想将我之前的记录和经验整理成有规律的、便于其他人理解、学习的文章记录且开放出来。 其中分为python编程记录与scrapy爬虫框架记录两个大的部分。其中python编程记录会写一些函数式编程、闭包相关知识及记录,而scrapy爬虫框架主要编写一些实际应用及绕坑手段,再带上些许服务器相关的记录。 生活也需要绕坑,我觉得我可以把这些知识放在这里,这样我再去绕坑就会轻松一些。