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架構組件、運行流程,結合實例理解一下