scrapy,它是一个整合了的爬虫框架, 有着很是健全的管理系统. 并且它也是分布式爬虫, 它的管理体系很是复杂. 可是特别高效.用途普遍,主要用于数据挖掘、检测以及自动化测试。css
本项目实现功能:模拟登陆、分页爬取、持久化至指定数据源、定时顺序执行多个spiderhtml
首先须要有环境,本案例使用
python 2.7,macOS 10.12,mysql 5.7.19java
下载scrapypython
pip install scrapy
下载Twistedmysql
pip install Twisted
下载MySQLdbreact
pip install MySQLdb
建立项目sql
*****@localhost:~$ scrapy startproject scrapy_school_insurance
在对应的目录下面就会生成以下目录格式数据库
scrapy_school_insurance/ spiders/ _init_.py _init_.py items.py ---- 实体(存储数据信息) middlewares.py ---- 中间件(初级开发无需关心) pipelines.py ---- 处理实体,页面被解析后的数据会发送到此(持久化、验证明体有效性,去重) setting.py ---- 设置文件 scrapy.cfg ---- configuration file
在spiders下建立school_insurance_spider.py 编写具体爬取页面信息的代码编程
首先,要作的是定义items,也就是你须要的数据项,数据存储的地方。json
定义Item很是简单,只须要继承scrapy.Item类,并将全部字段都定义为scrapy.Field类型便可。
Field对象用来对每一个字段指定元数据。
定义子项目的items.py
# -*- coding: utf-8 -*— import scrapy class ScrapySchoolInsuranceItem(scrapy.Item): # 班级名称 classroom = scrapy.Field() # 学生身份证 id_card = scrapy.Field() # 学生姓名 student_name = scrapy.Field() # 家长姓名 parent_name = scrapy.Field() # 练习电话 phone = scrapy.Field() # 是否缴费 is_pay = scrapy.Field() # 学校名称 school_id = scrapy.Field()
下面进行网络爬取步骤:
school_insurance_spider.py 文件
此类须要继承scrapy.Spider类
先分析一下基本结构和工做流程:
import scrapy class SchoolInsuranceSpider(scrapy.Spider): name = "school_insurance" def start_requests(self): urls = [ 'http://quotes.toscrape.com/page/1/', 'http://quotes.toscrape.com/page/2/', ] for url in urls: yield scrapy.Request(url=url, callback=self.parse) def parse(self, response): pass
这是spider最基本的结构:
name:是你爬虫的名字,后面启动爬虫的时候使用就是这个参数。
start_requests():是初始请求,爬虫引擎会自动调取。
urls:是你定义须要爬取的url,能够是一个也能够是多个。
parse():函数主要进行网页分析,爬取数据。当你的返回没有指定回调函数的时候,默认回调parse()函数;
先来分析一下在迭代器中的这句话:
yield scrapy.Request(url=url, callback=self.parse)
yield 是一个python关键字,表明这个函数返回的是个生成器。
理解yield,你必须理解:当你仅仅调用这个函数的时候,函数内部的代码并不立马执行,这个函数只是返回一个生成器对象。只有当你进行迭代此对象的时候才会真正的执行其中的代码,而且之后的迭代会从在函数内部定义的那个循环的下一次,再返回那个值,直到没有能够返回的。
这里的start_requests函数就会被当作一个生成器使用,而scrapy引擎能够被看做是迭代器。scrapy会逐一获取start_requests方法中生成的结果,并判断该结果是一个什么样的类型。当返回Request时会被加入到调度队列中,当返回items时候会被pipelines调用。很显然,此时迭代返回的是request对象。
引擎会将此请求发送到下载中间件,经过下载中间件下载网络数据。一旦下载器完成页面下载,将下载结果返回给爬虫引擎。引擎将下载器的响应(response对象)经过中间件返回给爬虫。此时request()使用了回调函数parse(),则response做为第一参数传入,进行数据被提取操做。
当你直接使用scrapy.Request()方法时默认是get请求。
而此项目是要爬取一个须要登陆的网站,第一步要作的是模拟登陆,须要post提交form表单。因此就要使用scrapy.FormRequest.from_response()方法:
def start_requests(self): start_url = 'http://jnjybx.jnjy.net.cn/admin/login.aspx?doType=loginout' return [ Request(start_url, callback=self.login) ] # 模拟用户登陆 def login(self, response): return scrapy.FormRequest.from_response( response, formdata={'username': ‘***’, 'password': ‘***’}, meta={'school_id': ***}, callback=self.check_login )
访问网址后咱们直接回调login方法,进行表单提交请求。表单结构需根据不一样网站本身分析,此网站只需提交username和password,若是你须要传递自定义参数,可经过meta属性进行定义传递,回调函数中使用response.meta['school_id']就能够获取传递的参数了。通常网站登陆成功以后会直接返回,登陆后的页面的response,就能够直接回调数据爬取的方法进行爬取了。
但此网站会单独返回一段json来告诉我是否登陆成功,而且并不提供下一步的url,因此我这里多了一步check_login()方法,并在判断登陆成功后的代码段里从新请求了登陆成功后的url。(scrapy是默认保留cookie的!)
模拟登陆完整代码:
# -*- coding: utf-8 -*- import scrapy import json import logging from scrapy.http import Request from scrapy_school_insurance.items import ScrapySchoolInsuranceItem class SchoolInsuranceSpider(scrapy.Spider): name = "school_insurance" allowed_domains = ['jnjy.net.cn'] # 在这里定义了登陆成功后的url,供再次请求使用 target_url = 'http://jnjybx.jnjy.net.cn/admin/index.aspx' def __init__(self, account, **kwargs): super(SchoolInsuranceSpider, self).__init__(**kwargs) self.account = account def start_requests(self): start_url = 'http://jnjybx.jnjy.net.cn/admin/login.aspx?doType=loginout' return [ Request(start_url, callback=self.login) ] # 模拟用户登陆 def login(self, response): return scrapy.FormRequest.from_response( response, formdata={'username': '***', 'password': '***'}, meta={'school_id': ***}, callback=self.check_login ) # 检查登陆是否成功 def check_login(self, response): self.logger.info(response.body) body_json = json.loads(response.body) # 获取参数 school_id = response.meta['school_id'] self.logger.info(school_id) if "ret" in body_json and body_json["ret"] != 0: self.logger.error("Login failed") return else: self.logger.info("Login Success") # 这里从新请求了一次 yield scrapy.Request(self.target_url, meta={'school_id': school_id}, callback=self.find_student_manager)
由于主页并非我须要的页面,因此find_student_manager()方法做用是找到我须要的那个连接,进行请求后再进行分页爬取。
# 检验登陆成功后 跳转 学生管理链接 def find_student_manager(self, response): self.logger.info(response.url) school_id = response.meta['school_id'] self.logger.info(school_id) next_page = response.css('a[href="studentManage.aspx"]::attr(href)').extract_first() logging.info(next_page) if next_page is not None: self.logger.info("next_url:" + next_page) next_page = response.urljoin(next_page) self.logger.info("next_url:" + next_page) yield scrapy.Request(next_page, meta={'school_id': school_id}) else: self.logger.error("not find href you needed")
解析html是须要选择器的,和编写css时给页面加样式的时候操做相似。
scrapy提供两种选择器xpath(),css();
XPath是用来在XML中选择节点的语言,同时能够用在HTML上面。CSS是HTML文档上面的样式语言。
css选择器语法请参考:https://www.cnblogs.com/ruoniao/p/6875227.html
我这里使用css选择器来获取a标签的herf,分析一下这句话:
next_page=response.css('a[href="studentManage.aspx"]::attr(href)').extract_first()
简单来说,.css()返回的是一个SelectorList对象,它是内建List的子类,咱们并不能直接使用它获得数据。.extract()方法就是使SelectorList ——> List,变为单一化的unicode字符串列表,咱们就能够直接使用了。.extract_first()顾名思义,就是取第一个值,也可写为 .extract()[0],须要注意的是list为空的时候会报异常。
再看看这句话:
next_page = response.urljoin(next_page)
经过选择器获取到是一个href的字符串值,是相对url,须要使用.urljoin()方法来构建完整的绝对URL,便于再次请求。
进入须要爬取的页面,开始分页爬取:
# 爬取 须要 的数据 def parse(self, response): school_id = response.meta['school_id'] counter = 0 # id_cards = response.css('tr[target="ID_CARD"]::attr(rel)') # i = 0 view_state = response.css('input#__VIEWSTATE::attr(value)').extract_first() view_state_generator = response.css('input#__VIEWSTATEGENERATOR::attr(value)').extract_first() page_num = response.css('input[name="pageNum"]::attr(value)').extract_first() num_per_page = response.css('input[name="numPerPage"]::attr(value)').extract_first() order_field = response.css('input[name="orderField"]::attr(value)').extract_first() order_direction = response.css('input[name="orderDirection"]::attr(value)').extract_first() self.logger.info(page_num) for sel in response.css('tr[target="ID_CARD"]'): counter = counter + 1 item = ScrapySchoolInsuranceItem() item['classroom'] = sel.css('td::text').extract()[0] item['id_card'] = sel.css('td::text').extract()[1] item['student_name'] = sel.css('td::text').extract()[2] item['parent_name'] = sel.css('td::text').extract()[3] item['phone'] = sel.css('td::text').extract()[4] item['is_pay'] = sel.css('span::text').extract_first() item['school_id'] = school_id yield item self.logger.info(str(counter)+" "+num_per_page) if counter == int(num_per_page): yield scrapy.FormRequest.from_response( response, formdata={'__VIEWSTATE': view_state, '__VIEWSTATEGENERATOR': view_state_generator, 'pageNum': str(int(page_num) + 1), 'numPerPage': num_per_page, 'orderField': order_field, 'orderDirection': order_direction}, meta={'school_id': school_id}, callback=self.parse )
分析页面:
我所须要的数据是的结构,且每页的结构都相同, 其中一小段html:
<tr target="ID_CARD" rel='1301*******0226'> <td >初中二年级7班(南京市***中学)</td> <td >130**********226</td> <td >李**</td> <td >蒋**</td> <td >139******61</td> <td ><span style='color:green'>已支付</span></td> </tr>
因此只须要使用选择器进行循环爬取付值给item,而后回调本身就能够把全部数据爬取下来了。
看Chrome的请求状态,发现是post请求且有两个陌生的参数。分析页面发现有两个隐藏参数__VIEWSTATE,__VIEWSTATEGENERATOR,且每次请求都会改变。
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="***" /> <input type="hidden" name="__VIEWSTATEGENERATOR" id="__VIEWSTATEGENERATOR" value="CCD96271" />
ViewState是ASP.NET中用来保存WEB控件回传时状态值一种机制。__EVENTVALIDATION只是用来验证事件是否从合法的页面发送,只是一个数字签名。
对于咱们爬虫而须要作的就是,获取本页面的这两个值,在请求下一页面的时候做为参数进行请求。分页爬取须要不断回调本身进行递归,此时请求并非get请求,而是url不变的post请求,我这是使用一个计数器counter防止其无限递归下去。
这里你会仔细发现,一开始自定义的school_id也被加入了item中。
前面提到,爬虫引擎会检索返回值,返回items时候会被pipelines调用,pipelines就是处理数据的类。
pipelines.py
# -*- coding: utf-8 -*- import MySQLdb class ScrapySchoolInsurancePipeline(object): def process_item(self, item, spider): db_name = "" if item["school_id"] == '1002': db_name = "scrapy_school_insurance" elif item["school_id"] == '1003': db_name = "db_mcp_1003" if db_name != "": conn = MySQLdb.connect("localhost", "root", "a123", db_name, charset='utf8') cursor = conn.cursor() # 使用cursor()方法获取操做游标 # 使用execute方法执行SQL语句 sql = "insert into `insurance_info` " \ "(classroom,id_card,student,parent,phone,is_pay,school_id) " \ "values (%s, %s, %s, %s, %s, %s, %s)" \ "ON DUPLICATE KEY UPDATE is_pay = %s;" params = (item["classroom"], item["id_card"], item["student_name"], item["parent_name"], item["phone"], item["is_pay"], item["school_id"], item["is_pay"]) cursor.execute(sql, params) 、conn.commit() cursor.close() conn.close()
process_item()方法,是pipeline默认调用的。由于须要根据school_id进行分库插入并无进行setting设置,而是使用MySQLdb库动态连接数据库。执行sql操做和java相似。sql使用“ON DUPLICATE KEY UPDATE”去重更新。
此时咱们已经完成了三步,定义items,编写spider逻辑,pipeline持久化。
下一步就是如何让程序正确的跑起来。
谈运行,首先要说一下本案例setting.py的书写,它设计运行的方方面面。
# -*- coding: utf-8 -*- BOT_NAME = 'scrapy_school_insurance' SPIDER_MODULES = ['scrapy_school_insurance.spiders'] NEWSPIDER_MODULE = 'scrapy_school_insurance.spiders' # 编码格式 FEED_EXPORT_ENCODING = 'utf-8' # Obey robots.txt rules 不遵循网络规范 ROBOTSTXT_OBEY = False EXTENSIONS = { 'scrapy.telnet.TelnetConsole': None } # 设置log级别 # LOG_LEVEL = 'INFO' # 本项目带登录须要开启cookies,通常爬取不须要cookie COOKIES_ENABLED = True 'scrapy_school_insurance.middlewares.ScrapySchoolInsuranceSpiderMiddleware': 543, # 开启 后pipelines才生效 后面的数字表示的是pipeline的执行顺序 ITEM_PIPELINES = { 'scrapy_school_insurance.pipelines.ScrapySchoolInsurancePipeline': 300, }
完成如上操做后,在终端中到达此项目根目录下运行:
scrapy crawl school_insurance
想生成json文件:
scrapy crawl school_insurance -o test.json -t json
如今我有多个帐号存在数据库中,想分别登入读取信息该如何操做?
构思了两个方案:
1.读取全部帐号,存入一个spider中,每次用一个帐号爬取完后退出登陆,清除cookie,再拿第二个帐号登入,进行爬取工做。
2.动态配置spider,每一个帐号对应一个spider,进行顺序执行。
仅从描述上看,第二个方案就比第一个方案靠谱,可行。
采用第二个方案,须要动态配置完以后告诉spider要运行了,也就是使用编程的方式运行spider:Scrapy是构建于Twisted异步网络框架基础之上,所以能够启动Twisted reactor并在reactor中启动spider。CrawlerRunner就会为你启动一个Twisted reactor。 需先新建一个run.py:
#!/bin/env python # -*- coding: utf-8 -*- import logging import MySQLdb.cursors from twisted.internet import reactor from scrapy.utils.project import get_project_settings from scrapy.utils.log import configure_logging from scrapy.crawler import CrawlerRunner import sys sys.path.append("../") from scrapy_school_insurance.spiders.school_insurance_spider import SchoolInsuranceSpider if __name__ == '__main__': settings = get_project_settings() configure_logging(settings) db_names = ['scrapy_school_insurance', 'db_mcp_1003'] results = list() for db_name in db_names: logging.info(db_name) db = MySQLdb.connect("localhost", "root", "a123", db_name, charset='utf8', cursorclass=MySQLdb.cursors.DictCursor) # 使用cursor()方法获取操做游标 cursor = db.cursor() # 使用execute方法执行SQL语句 cursor.execute("select * from school_account") # 使用 fetchone() 方法获取一条数据 result = cursor.fetchone() logging.info(result) results.append(result) db.close() logging.info(results) runner = CrawlerRunner(settings) for result in results: runner.crawl(SchoolInsuranceSpider, account=result) d = runner.join() d.addBoth(lambda _: reactor.stop()) reactor.run() logging.info("all findAll")
须要注意的是数据库
获取帐号信息,启动reactor,启动spider
runner = CrawlerRunner(settings) for result in results: runner.crawl(SchoolInsuranceSpider, account=result)
这句话在spider时,将帐号信息做为参数传递过去了,因此咱们的spider须要修改一下接收参数
加入构造器,供内部调用:
def __init__(self, account, **kwargs): super(SchoolInsuranceSpider, self).__init__(**kwargs) self.account = account
此时login()可修改成:
def login(self, response): return scrapy.FormRequest.from_response( response, formdata={'username': self.account['username'], 'password': self.account['password']}, meta={'school_id': self.account['school_id']}, callback=self.check_login )
完成后在终端运行run.py便可:
@localhost:~/project-workspace/scrapy_school_insurancescrapy_school_insurance/$ python run.py
此时会报 not import SchoolInsuranceSpider,可是已经明明import了,由于路径的问题,run.py启动时并找不到它,需在import前加 sys.path.append("../")python才能够经过路径找到它
最后须要作的就是定时启动python脚本:
使用crontab
详细语法参考:https://blog.csdn.net/netdxy/article/details/50562864
主要是两步
crontab -e
添加定时任务,天天3点执行python脚本,wq,ok
* */3 * * * python ~/project-workspace/scrapy_school_insurance/scrapy_school_insurance/run.py
Scrapy架构组件、运行流程,结合实例理解一下