——编辑:大牧莫邪html
爬虫程序,主要是用与数据采集处理的一种网络程序,在操做过程当中针对指定的url地址进行数据请求并根据须要采集数据,可是在实际项目开发过程当中,常常会遇到目标url地址数量不明确的状况,如以前的章节中提到的智联招聘项目,不一样的岗位搜索到的岗位数量不必定一致,也就意味着每一个工做搜索到的工做岗位列表页面的数量不必定一致,爬虫工程师工做可能搜索到了10页,Django工做有可能都索到了25页数据,那么针对这样的数据要所有进行爬取,应该怎么处理呢?答案就是:深度爬虫python
深度爬虫:针对其实url地址进行数据采集,在响应数据中进行数据筛选获得须要进行数据采集的下一波url地址,并将url地址添加到数据采集队列中进行二次爬取..以此类推,一致到全部页面的数据所有采集完成便可完成深度数据采集,这里的深度指代的就是url地址的检索深度。正则表达式
深度爬虫能够经过不一样的方式实现,在urllib2和requesets模块中经过轮询数据筛选获得目标url地址,而后进行循环爬取数据便可,在scrapy中主要经过两种方式进行处理:算法
首先完成深度爬虫以前,先了解Scrapy框架底层的一些操做模式,Scrapy框架运行爬虫项目,默认调用并执行parse()函数进行数据的解析,可是此时已经由框架完成了请求解析调度和下载的过程,那么Scrapy到底作了哪些事情呢?shell
咱们首先观察一下scrapy.Spider源代码数据库
class Spider(object_ref): """Base class for scrapy spiders. All spiders must inherit from this class. """ name = None custom_settings = None # 初始化函数,主要进行程序的名称、起始地址等数据初始化工做 def __init__(self, name=None, **kwargs): if name is not None: self.name = name elif not getattr(self, 'name', None): raise ValueError("%s must have a name" % type(self).__name__) self.__dict__.update(kwargs) if not hasattr(self, 'start_urls'): self.start_urls = [] ... ... # 程序启动,发送请求的函数 def start_requests(self): cls = self.__class__ # 默认没有重写直接调用,重写的时候根据子类重写的方式从新定义发送处理方式 # 默认状况下发送get请求获取数据,若是要发送Post请求能够重写start_reuqests函数进行请求的处理 if method_is_overridden(cls, Spider, 'make_requests_from_url'): warnings.warn( "Spider.make_requests_from_url method is deprecated; it " "won't be called in future Scrapy releases. Please " "override Spider.start_requests method instead (see %s.%s)." % ( cls.__module__, cls.__name__ ), ) for url in self.start_urls: yield self.make_requests_from_url(url) else: # 没有重写该方法,直接根据初始地址包装请求对象发送请求 for url in self.start_urls: yield Request(url, dont_filter=True)
咱们能够从源代码中查看到,咱们定义的爬虫处理类继承的scrapy.Spider类型中,对于初始化的name和start_urls初始地址进行了初始化,而后自动调用start_requests函数包装Request请求对象,而后经过协程调用的方法将请求交给调度器进行后续的处理markdown
这里就须要了解请求对象中到底作了哪些事情?!cookie
(1) Request对象 Request请求对象是scrapy框架中的核心对象,经过将字符串url地址包装成请求对象交给调度器进行调度管理,以后交给下载模块进行数据采集的操做网络
Request底层操做部分源码以下:框架
# scrapy中的Request请求对象 class Request(object_ref): # 默认构建时,method="GET"包装的是GET请求的采集方式 # 参数url:请求地址字符串 # 参数callback:请求的回调函数 # 参数headers:默认的请求头 # 参数body: 请求体 # 参数cookies:请求中包含的cookie对象 # 参数encoding:请求编码方式 def __init__(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, dont_filter=False, errback=None, flags=None): self._encoding = encoding # this one has to be set first self.method = str(method).upper() self._set_url(url) self._set_body(body) assert isinstance(priority, int), "Request priority not an integer: %r" % priority self.priority = priority if callback is not None and not callable(callback): raise TypeError('callback must be a callable, got %s' % type(callback).__name__) if errback is not None and not callable(errback): raise TypeError('errback must be a callable, got %s' % type(errback).__name__) assert callback or not errback, "Cannot use errback without a callback" self.callback = callback self.errback = errback self.cookies = cookies or {} self.headers = Headers(headers or {}, encoding=encoding) self.dont_filter = dont_filter self._meta = dict(meta) if meta else None self.flags = [] if flags is None else list(flags)
那么在实际操做中,咱们经过以下三点详细说明:
直接编写爬虫程序,定义strat_urls中的初始地址和爬虫的name名称,而后重写父类中的parse()函数便可,请求的发送默认就是get()方式进行数据采集:
import scrapy # 定义本身的爬虫处理类 class MySpider(scrapy.Spider): # 定义爬虫名称 name = 'myspider' # 定义初始化url地址列表 start_urls = ("http://www.baidu.com", ) # 定义域名限制 allowed_domains = ["baidu.com"] # 定义数据处理方式 def parse(self, response): # 数据处理部分 pass
由于scarpy默认的Request是get方式发送请求,若是要经过post方式发送请求采集数据,须要从新编写start_requests()函数覆盖父类中的请求包装方式
import scrapy class MySpider(scrapy.Spider): # 定义爬虫名称 name = 'myspider' # 定义初始化url地址列表 start_urls = ("http://www.baidu.com", ) # 定义域名限制 allowed_domains = ["baidu.com"] # 重写父类请求初始化发送方式 def start_requests(self, response): # 循环初始话地址,发送post请求 for url in self.start_urls: yield scrapy.FormRequest( url = url, formdata = {post参数字典}, callback = self.parse_response, ) # 从新编写响应数据处理函数 def parse_response(self, response): # 处理采集到的response数据 pass
同时,也能够经过响应对象构建一个POST请求从新发送,以下:
import scrapy class MySpider(scarpy.Spider): # 定义爬虫名称 name = 'myspider' # 定义初始化url地址列表 start_urls = ("http://www.baidu.com", ) # 定义域名限制 allowed_domains = ["baidu.com"] # 重写父类请求初始化发送方式 def parse(self, response): # 经过响应对象从新构建一个POST请求再次发送 return scrapy.FormRequest.from_response( response, formdata = {"post参数字典数据"}, callback = self.parse_response ) # 从新编写响应数据处理函数 def parse_response(self, response): # 处理采集到的response数据 pass
(2) Response对象 Response对象在项目中的直接操做并非不少,参考源代码以下:
# 部分代码 class Response(object_ref): def __init__(self, url, status=200, headers=None, body='', flags=None, request=None): self.headers = Headers(headers or {}) self.status = int(status) # 响应码 self._set_body(body) # 响应体 self._set_url(url) # 响应url self.request = request # 请求对象 self.flags = [] if flags is None else list(flags) @property def meta(self): try: return self.request.meta except AttributeError: raise AttributeError("Response.meta not available, this response " \ "is not tied to any request")
(3)案例操做:模拟CSDN登陆
scrapy startproject csdnspider
# coding:utf-8 import scrapy class CsdnSpider(scrapy.Spider): ''' CSDN登陆爬虫处理类 ''' # 爬虫名称 name = "cs" # 初始登陆地址 start_urls = ["https://passport.csdn.net/account/login"] def parse(self, response): # 匹配登陆流水号 lt = response.xpath("//form[@id='fm1']/input[@type='hidden']/@value").extract()[1] # 发送post请求完成登陆 return scrapy.FormRequest.from_response( response, formdata = { "username": "15682808270", "password": "DAMUpython2016", "lt": lt, # "execution": "e2s1", # "_eventId": "submit" }, callback=self.parse_response ) def parse_response(self, response): # 获得登陆后的数据,进行后续处理 with open("csdn.html", "w") as f: f.write(response.body)
(4). 深度采集数据:爬取智联某工做岗位全部页面工做数据
scrapy startproject zlspider
# -*- coding: utf-8 -*- # Define here the models for your scraped items # # See documentation in: # https://doc.scrapy.org/en/latest/topics/items.html import scrapy class ZhilianItem(scrapy.Item): ''' 定义采集数据的类型,该类型中,会封装采集到的数据 继承scrapy.Item类型,scrapy框架才会调用内建函数继续自动化操做 ''' # 经过scrapy.Field()定义属性字段,每一个字段都是采集数据的一部分 job_name = scrapy.Field() company = scrapy.Field() salary = scrapy.Field() 建立数据库,定义数据表,用于存储数据 # 建立数据库 DROP DATABASE py1709_spider; CREATE DATABASE py1709_spider DEFAULT CHARSET 'utf8'; USE py1709_spider; # 建立数据表 CREATE TABLE jobs( id INT AUTO_INCREMENT PRIMARY KEY, job_name VARCHAR(200), company VARCHAR(200), salary VARCHAR(50) ); SELECT COUNT(1) FROM jobs; SELECT * FROM jobs; TRUNCATE TABLE jobs;
在zlspider/zlspider/spider/文件夹中,建立zhilianspider.py文件,编辑爬虫程序以下:
# coding:utf-8 # 引入scrapy模块 import scrapy from ..items import ZhilianItem class ZhilianSpider(scrapy.Spider): ''' 智联招聘数据采集爬虫程序 须要继承scrapy.Spider类型,让scrapy负责调度爬虫程序进行数据的采集 ''' # name属性:爬虫名称 name = "zl" # allowed_domains属性:限定采集数据的域名 allowed_domains = ["zhaopin.com"] # 起始url地址 start_urls = [ #"http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E5%8C%97%E4%BA%AC&kw=%E7%88%AC%E8%99%AB&sm=0&sg=cab76822e6044ff4b4b1a907661851f9&p=1", "http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E5%8C%97%E4%BA%AC%2b%E4%B8%8A%E6%B5%B7%2b%E5%B9%BF%E5%B7%9E%2b%E6%B7%B1%E5%9C%B3&kw=python&isadv=0&sg=7cd76e75888443e6b906df8f5cf121c1&p=1", ] def parse(self, response): ''' 采集的数据解析函数(响应数据解析函数) 主要用于进行响应数据的筛选:筛选目标数据分装成Item对象 :param response: :return: ''' # # 再次从响应中获取要进行下一次爬取的url地址[其余页面请求] # next_page = response.xpath("//div[@class='pagesDown']/ul/li/a/@href").extract() # # 循环处理请求 # for page in next_page: # page = response.urljoin(page) # # 从新发起请求采集下一组url地址的数据[第一个参数:发起的请求地址,第二个参数:请求数据一旦被采集~交个哪一个函数进行处理] # yield scrapy.Request(page, callback=self.parse_response) url = response.urljoin(self.start_urls[0]) yield scrapy.Request(url, callback=self.parse_response) def parse_response(self, response): # 筛选获得工做列表 job_list = response.xpath("//div[@id='newlist_list_content_table']/table[position()>1]/tr[1]") # 循环获取采集的字段信息 for job in job_list: # 岗位名称 job_name = job.xpath("td[@class='zwmc']/div/a").xpath("string(.)").extract()[0] # 公司名称 company = job.xpath("td[@class='gsmc']/a").xpath("string(.)").extract()[0] # 薪水 salary = job.xpath("td[@class='zwyx']").xpath("string(.)").extract()[0] # 封装成item对象 item = ZhilianItem() item['job_name'] = job_name item['company'] = company item['salary'] = salary # 经过协程的方式移交给pipeline进行处理 yield item # 再次从响应中获取要进行下一次爬取的url地址[其余页面请求] next_page = response.xpath("//div[@class='pagesDown']/ul/li/a/@href").extract() # 循环处理请求 for page in next_page: page = response.urljoin(page) # 从新发起请求采集下一组url地址的数据[第一个参数:发起的请求地址,第二个参数:请求数据一旦被采集~交个哪一个函数进行处理] yield scrapy.Request(page, callback=self.parse_response) 运行测试程序 在终端命令行窗口中,运行程序 scrapy crawl zl
查看数据库中的数据记录
备注:在这样的深度采集数据时,首页数据颇有可能会重复,因此,将数据解析函数分红了两个步骤执行,第一步经过parse()函数处理首页地址增长到response.urljoin()中,而后经过parse_response()函数进行实际的数据采集工做,达到首页数据去重的目的!
Scrapy框架针对深度爬虫,提供了一种深度爬虫的封装类型scrapy.CrawlSpider,咱们本身定义开发的爬虫处理类须要继承该类型,才能使用scrapy提供封装的各项深度爬虫的功能
scrapy.CrawlSpider是从scrapy.Spider继承并进行功能扩展的类型,在该类中,经过定义Url地址的提取规则,跟踪链接地址,从已经采集获得的响应数据中继续提取符合规则的地址进行跟踪爬取数据
部分源代码以下:
class CrawlSpider(Spider): rules = () def __init__(self, *a, **kw): super(CrawlSpider, self).__init__(*a, **kw) self._compile_rules() # 1. 调用重写父类的parse()函数来处理start_urls中返回的response对象 # 2. parse()则将这些response对象再次传递给了_parse_response()函数处理 # 2.1. _parse_response()函数中设置follow为True,该参数用于打开是否跟进连接提取 # 3. parse将返回item和跟进了的Request对象 def parse(self, response): return self._parse_response(response, self.parse_start_url, cb_kwargs={}, follow=True) # 定义处理start_url中返回的response的函数,须要重写 def parse_start_url(self, response): return [] # 结果过滤函数 def process_results(self, response, results): return results # 从response中抽取符合任一用户定义'规则'的连接,并构形成Resquest对象返回 def _requests_to_follow(self, response): if not isinstance(response, HtmlResponse): return seen = set() # 循环获取定义的url地址提取规则 for n, rule in enumerate(self._rules): # 获得全部的提取规则列表 links = [l for l in rule.link_extractor.extract_links(response) if l not in seen] # 使用用户指定的process_links处理每一个链接 if links and rule.process_links: links = rule.process_links(links) #将连接加入seen集合,为每一个连接生成Request对象,并设置回调函数为_repsonse_downloaded() for link in links: seen.add(link) # 构造Request对象,并将Rule规则中定义的回调函数做为这个Request对象的回调函数 r = Request(url=link.url, callback=self._response_downloaded) r.meta.update(rule=n, link_text=link.text) # 对每一个Request调用process_request()函数。该函数默认为indentify,即不作任何处理,直接返回该Request. yield rule.process_request(r) # 采集数据连接处理,从符合规则的rule中提取连接并返回item和request def _response_downloaded(self, response): rule = self._rules[response.meta['rule']] return self._parse_response(response, rule.callback, rule.cb_kwargs, rule.follow) # 解析response对象,经过callback回调函数解析处理,并返回request或Item对象 def _parse_response(self, response, callback, cb_kwargs, follow=True): # 首先判断是否设置了回调函数。(该回调函数多是rule中的解析函数,也多是 parse_start_url函数) #若是设置了回调函数(parse_start_url()),那么首先用parse_start_url()处理response对象, # 而后再交给process_results处理。返回cb_res的一个列表 if callback: #若是是parse调用的,则会解析成Request对象 #若是是rule callback,则会解析成Item 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 # 若是须要跟进,那么使用定义的Rule规则提取并返回这些Request对象 if follow and self._follow_links: #返回每一个Request对象 for request_or_item in self._requests_to_follow(response): yield request_or_item # 规则过滤 def _compile_rules(self): def get_method(method): if callable(method): return method elif isinstance(method, basestring): 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) # 连接跟踪全局配置设置 def set_crawler(self, crawler): super(CrawlSpider, self).set_crawler(crawler) self._follow_links = crawler.settings.getbool('CRAWLSPIDER_FOLLOW_LINKS', True)
(1) LinkExtractor连接提取对象
LinkExtract类型,主要目的是用于定义连接的提取匹配方式
该类中的方法extract_link()用于从响应对象response中提取符合定义规则的连接
该类型只会被实例化一次,可是在每次采集获得数据时重复调用
class scrapy.linkextractors.LinkExtractor( allow = (), # 正则表达式,符合规则的连接会提取 deny = (), # 正则表达式,负责规则的连接会排除 allow_domains = (), # 容许的域名 deny_domains = (), # 禁止的域名 deny_extensions = None, # 是否容许扩展 restrict_xpaths = (), # xpath表达式,和allow配合使用精确提取数据 tags = ('a','area'), # 标签~ attrs = ('href'), # 指定提取的属性 canonicalize = True, unique = True, # 惟一约束,是否去重 process_value = None )
上述的参数中,咱们能够看到经过一个linkextractors.LinkExtractor对象,能够定义各类提取规则,而且不须要考虑是否会将重复的连接添加到地址列表中
经过srapy shell作一个简单的测试,首先打开智联工做列表页面,终端命令行执行以下命令:
scrapy shell "http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E5%8C%97%E4%BA%AC%2b%E4%B8%8A%E6%B5%B7%2b%E5%B9%BF%E5%B7%9E%2b%E6%B7%B1%E5%9C%B3&kw=python&isadv=0&sg=5b827b7808f548ad8261595837624f24&p=4"
此时scrapy就会自动从指定的地址中采集数据,并包含在response变量中,打开了python命令行,导入LinkExtractor类型并定义提取规则:
# 导入LinkExtractor类型 >>> from linkextractors import LinkExtractor # 定义提取规则,包含指定字符的连接被提取 >>> links = LinkExtractor(allow=('7624f24&p=\d+'))
接下来,从响应数据中提取符合规则的超连接,执行extract_links()函数以下:
next_urls = links.extract_links(response)
打印next_urls,获得以下结果:
[Link(url='http://sou.zhaopin.com/jobs/searchresult.ashx ?jl=%E5%8C%97%E4%BA%AC%2b%E4%B8%8A%E6%B5%B7%2b%E5%B9%BF%E 5%B7%9E%2b%E6%B7%B1%E5%9C%B3&kw=python&isadv=0&sg=5b827b7 808f548ad8261595837624f24&p=4',text=u'\u767b\u5f55', frag ment='', nofollow=True), Link(url='http://sou.zhaopin.com /jobs/searchresult.ashx?j l=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5% b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b780 8f548ad8261595837624f24&p=3', text=u'\u4e0a\u4e00\u9875', fragment='', nofollow=False), Link(url='http://sou.zhaopi n.com/jobs/searchresult.ashx?j l=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5%b 7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b7808 f548ad8261595837624f24&p=1', text='1', fragment='', nofoll ow=False), Link(url='http://sou.zhaopin.com/jobs/searchre sult.ashx?jl=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e 5%b9%bf%e5%b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&s g=5b827b7808f548ad8261595837624f24&p=2', text='2', fragme nt='', nofollow=False), Link(url='http://sou.zhaopin.com/ jobs/searchresult.ashx?j l=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5% b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b780 8f548ad8261595837624f24&p=5', text='5', fragment='', nofo llow=False), Link(url='http://sou.zhaopin.com/jobs/search result.ashx?jl=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b %e5%b9%bf%e5%b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0 &sg=5b827b7808f548ad8261595837624f24&p=6', text='6', frag ment='', nofollow=False), Link(url='http://sou.zhaopin.co m/jobs/searchresult.ashx?j l=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5% b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b780 8f548ad8261595837624f24&p=7', text='7', fragment='', nofo llow=False), Link(url='http://sou.zhaopin.com/jobs/search result.ashx?jl=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b %e5%b9%bf%e5%b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0 &sg=5b827b7808f548ad8261595837624f24&p=8', text='8', frag ment='', nofollow=False), Link(url='http://sou.zhaopin.co m/jobs/searchresult.ashx?j l=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5% b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b780 8f548ad8261595837624f24&p=9', text='...', fragment='', no follow=False)]
咱们能够很直观的看到,全部符合规则的链接所有被提取了出来
(2) Rule规则对象
Rule对象是连接操做规则对象,主要定义了对于LinkExtractor类型提取的超连接url地址的操做行为,能够在一个爬虫程序中定义多个Rule对象,包含在一个rules列表中便可
class scrapy.spiders.Rule( # LinkExtractor对象 link_extractor, # 回调函数,获得数据库以后调用的函数 callback = None, # 回调函数调用时传递的参数列表 cb_kwargs = None, # 是否从返回的响应数据中根据LinkExtractor继续提取,通常选择True follow = None, # 从LinkExtractor中提取的链接,会自动调用该选项指定的函数,用来进行超连接的筛选 process_links = None, # 指定每一个请求封装处理时要调用的函数 process_request = None )
(3) 案例操做
智联招聘深度爬虫操做案例:
scrapy startproject zhilianspider2
# coding:utf-8 # 引入CrawlSpider, Rule, LinkExtractor模块 from scrapy.linkextractors import LinkExtractor from scrapy.spider import CrawlSpider, Rule class ZhilianSpider(CrawlSpider): """ 智联招聘深度爬虫处理类 继承scrapy.spiders.CrawlSpider类型 """ # 定义爬虫名称 name = "cs2" # 定义域名限制 allowed_domains = ["zhaopin.com"] # 定义起始地址 start_urls = ("http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E5%8C%97%E4%BA%AC%2b%E4%B8%8A%E6%B5%B7%2b%E5%B9%BF%E5%B7%9E%2b%E6%B7%B1%E5%9C%B3&kw=python&isadv=0&sg=5b827b7808f548ad8261595837624f24&p=1",) # 定义提取规则 links = LinkExtractor( allow=("5837624f24&p=\d+") ) # 定义操做规则 rules = [ # 定义一个操做规则 Rule(links, follow=True, callback='parse_response'), ] # 定义数据处理函数 def parse_response(self, response): # 提取数据 job_list = response.xpath("//div[@id='newlist_list_content_table']/table[@class='newlist'][position()>1]") # 循环筛选数据 for job in job_list: job_name = job.xpath("tr[1]/td[@class='zwmc']/div/a").xpath("string(.)").extract()[0] print job_name print("*************************************************")
在终端命令行中执行以下命令运行爬虫程序
scrapy crawl cs2
能够在控制台看到具体的爬取信息,对于提取的数据所有进行了跟踪处理
.. [scrapy.core.engine] DEBUG: Crawled (200) <GET http://sou.zhaopin.com/jobs/searchresult.ashx?jl=%e5%8c%97%e4%ba%ac%2 b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5%b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python&isadv=0&sg=5b827b7808f548ad8261595837624f24&p=13> (referer: http ://sou.zhaopin.com/jobs/searchresult.ashx?jl=%e5%8c%97%e4%ba%ac%2b%e4%b8%8a%e6%b5%b7%2b%e5%b9%bf%e5%b7%9e%2b%e6%b7%b1%e5%9c%b3&kw=python &isadv=0&sg=5b827b7808f548ad8261595837624f24&p=9) .... 图像算法工程师 软件测试工程师 ******************************************************************** 软件测试经理 高级软件测试工程师 ...... 'scheduler/enqueued/memory': 17, 'spider_exceptions/IOError': 3, 'spider_exceptions/UnicodeEncodeError': 1, 'start_time': datetime.datetime(2018, 1, 17, 4, 33, 38, 441000)} 2018-01-17 12:35:56 [scrapy.core.engine] INFO: Spider closed (shutdown)