引
在簡書中有很多主題頻道,里面有大量優(yōu)秀的文章,我想收集這些文章用于提取對我有用的東西虹蓄;
無疑爬蟲是一個(gè)好的選擇,我選用了python的一個(gè)爬蟲庫scrapy幸撕,它的官方文檔就是很好的教程:http://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/tutorial.html
準(zhǔn)備工作
scrapy安裝
pip install Scrapy
我遇到的問題是薇组,編譯時(shí)候stdarg.h找不到;于是查看報(bào)問題的頭文件目錄坐儿,把stdarg.h拷貝進(jìn)去就OK了律胀,這個(gè)花了我好長時(shí)間。挑童。累铅。
因?yàn)閟crapy依賴于twisted,所以有人安裝scrapy可能會提示缺少twisted站叼,介紹如下:
從https://pypi.python.org/pypi/Twisted/#downloads下載離線文件娃兽,然后執(zhí)行一下安裝。
tar jxvf Twisted-xxx.tar.bz2
cd Twisted-xxx
python setup.py install
mysql
抓取到的數(shù)據(jù)默認(rèn)是用json存儲尽楔,因?yàn)椴煌愋偷臄?shù)據(jù)混合存儲投储,解析和查詢過于繁瑣,所以我選擇數(shù)據(jù)庫阔馋;至于沒有用mongodb玛荞,是因?yàn)槲覚C(jī)子上本來就裝有mysql,而且自己學(xué)習(xí)研究用不到mongodb的一些優(yōu)點(diǎn)呕寝。
mysql數(shù)據(jù)庫是分服務(wù)器(server)勋眯,客戶端(workbench),python的接口鏈接器(mysql-connector);這些都可以從官方找到客蹋,參見 https://dev.mysql.com/downloads/
connector可以直接用pip安裝塞蹭,這里有個(gè)好處就是不用額外操心環(huán)境變量的事兒。
pip install mysql-connector
對于connector的使用讶坯,參見官方說明文檔:
https://dev.mysql.com/doc/connector-python/en/
一個(gè)簡單的框架
創(chuàng)建一個(gè)scrapy工程
scrapy startproject HelloScrapy
啟動一個(gè)工程
scrapy crawl demo
還可以用shell啟動番电,這個(gè)好處是你可以介入每一個(gè)執(zhí)行命令
scrapy shell 'http://www.reibang.com/u/4a4eb4feee62'
需要注意的是,網(wǎng)站一般會有反爬蟲機(jī)制辆琅,抓取會返回403錯(cuò)誤漱办,所以記得把user-agent改了:
settings.py
USER_AGENT = 'HelloWoWo'
開啟爬蟲
你需要在spider目錄下建立一個(gè)scrapy.Spider的子類,定義它的名稱(name, 就是啟動工程時(shí)指定的名稱)婉烟,允許的域名(allowed_domains)娩井,起始的爬取鏈接(start_urls);
然后定義parse函數(shù)似袁,它的參數(shù)response就是響應(yīng)內(nèi)容撞牢,你可以從中解析要獲取的內(nèi)容和新的鏈接;解析的方式可以通過xpath和css叔营,這里用xpath屋彪;然后可以通過yield,把解析到的對象推送到存儲流程中绒尊,如果你想爬蟲系統(tǒng)繼續(xù)爬取新的鏈接畜挥,也可以通過yield來進(jìn)入下一步爬取中。
from HelloScrapy.items import HelloscrapyItem
class DemoScrapy(scrapy.Spider):
? ? name = 'demo'
? ? allowed_domains = ['jianshu.com']
? ? start_urls = [
? ? ? ? 'http://www.reibang.com/u/4a4eb4feee62',
? ? ? ? 'http://www.reibang.com/u/d2a08403ea7f',
? ? ]
? ? def parse(self, response):
? ? ? ? user_sel = response.xpath('//body/div/div/div/div/div/ul/li/div/p/text()')
? ? ? ? item = HelloscrapyItem()
? ? ? ? item['text_num'] = int(user_sel[0].extract())
? ? ? ? item['favor_num'] = int(user_sel[1].extract())
? ? ? ? yield item
Item
上面代碼中的item就是用于描述抓取到的數(shù)據(jù)結(jié)構(gòu)婴谱,它們每個(gè)屬性都用scrapy.Field()表示蟹但。
import scrapy
class HelloscrapyItem(scrapy.Item):
? ? # define the fields for your item here like:
? ? name = scrapy.Field()
? ? text_num = scrapy.Field()
? ? favor_num = scrapy.Field()
Pipeline
它負(fù)責(zé)整個(gè)存儲的過程,可以存儲在json文件中谭羔,也可以通過數(shù)據(jù)庫华糖。
但是首先,你需要在settings.py中聲明你的pipeline(默認(rèn)有的瘟裸,打開注釋然后修改下):
# Configure item pipelines
# See https://doc.scrapy.org/en/latest/topics/item-pipeline.html
ITEM_PIPELINES = {
? 'HelloScrapy.pipelines.MySqlPipeline': 300,
}
如果使用最簡單的json方式客叉,可以定義如下:
其中open_spider,close_spider如名字一樣话告,分別會在啟動spider和結(jié)束spider時(shí)調(diào)用兼搏,所以這里分別定義了文件的打開和關(guān)閉;
而process_item就是對每個(gè)item的存儲處理沙郭,這里將item進(jìn)行json化佛呻,保存在預(yù)定義的文件中。
import json
class HelloscrapyPipeline(object):
? ? def open_spider(self, spider):
? ? ? ? ? self.file = open('./items.txt', 'w')
? ? def close_spider(self, spider):
? ? ? ? self.file.close()
? ? def process_item(self, item, spider):
? ? ? ? line = json.dumps(dict(item))
? ? ? ? self.file.write(line)
? ? ? ? return item
我這邊使用的是mysql病线,介紹如下吓著。
mysql
首先定義一個(gè)mysql的封裝類鲤嫡,支持打開和關(guān)閉一個(gè)數(shù)據(jù)庫,創(chuàng)建一個(gè)表绑莺,插入一條數(shù)據(jù)泛范。
import mysql.connector
from mysql.connector import errorcode
from settings import *
class MySqlDb(object):
? ? def __init__(self, db_name):
? ? ? ? self.db_name = db_name
? ? ? ? self.cnx = None
? ? ? ? self.cursor = None
? ? ? ? pass
? ? def open(self):
? ? ? ? self.cnx = mysql.connector.connect(user=MYSQL_USER_NAME,? ? ? ? password=MYSQL_PASS_WORD)
? ? ? ? self.cursor = self.cnx.cursor()
? ? ? ? self.__ensureDb(self.cnx, self.cursor, self.db_name)
? ? ? ? pass
? ? def close(self):
? ? ? ? if self.cursor:
? ? ? ? ? ? self.cursor.close()
? ? ? ? if self.cnx:
? ? ? ? ? ? self.cnx.close()
? ? ? ? pass
? ? def createTable(self, tbl_ddl):
? ? ? ? if self.cnx and self.cursor:
? ? ? ? ? ? self.__ensureDb(self.cnx, self.cursor, self.db_name)
? ? ? ? ? ? self.__ensureTable(self.cursor, tbl_ddl)
? ? ? ? ? ? pass
? ? def insert(self, sql, values):
? ? ? ? if self.cnx and self.cursor:
? ? ? ? ? ? try:
? ? ? ? ? ? ? ? self.cursor.execute(sql, values)
? ? ? ? ? ? ? ? self.cnx.commit()
? ? ? ? ? ? except:
? ? ? ? ? ? ? ? pass
? ? ? ? pass
? ? def __ensureDb(self, cnx, cursor, db_name):
? ? ? ? try:
? ? ? ? ? ? cnx.database = db_name
? ? ? ? except mysql.connector.Error as err:
? ? ? ? ? ? ? if err.errno == errorcode.ER_BAD_DB_ERROR:
? ? ? ? ? ? ? ? ? try:
? ? ? ? ? ? ? ? ? ? ? cursor.execute("CREATE DATABASE {} DEFAULT CHARACTER SET 'utf8'".format(db_name))
? ? ? ? ? ? ? ? ? ? except mysql.connector.Error as create_err:
? ? ? ? ? ? ? ? ? ? ? ? print("Failed creating database: {}".format(create_err))
? ? ? ? ? ? ? ? ? ? ? ? exit(1)
? ? ? ? ? ? ? ? ? ? cnx.database = db_name
? ? ? ? ? ? ? ? else:
? ? ? ? ? ? ? ? ? ? print err
? ? ? ? ? ? ? ? ? ? exit(1)
? ? def __ensureTable(self, cursor, tbl_ddl):
? ? ? ? try:
? ? ? ? ? ? cursor.execute(tbl_ddl)
? ? ? ? except mysql.connector.Error as err:
? ? ? ? ? ? if err.errno == errorcode.ER_TABLE_EXISTS_ERROR:
? ? ? ? ? ? ? ? pass
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? print err.msg
? ? ? ? else:
? ? ? ? ? ? pass
然后抽象一個(gè)item的基類:
該類的insertToDb定義了插入的過程,由每個(gè)子類提供創(chuàng)建表和插入數(shù)據(jù)的sql語句紊撕。
import scrapy
class BaseItem(scrapy.Item):
? ? def insertToDb(self, mysqldb):
? ? ? ? ? ? table_sql = self.getTableSql()
? ? ? ? ? ? insert_sql = self.getInsertSql()
? ? ? ? ? ? if table_sql and insert_sql:
? ? ? ? ? ? ? ? mysqldb.createTable(table_sql)
? ? ? ? ? ? ? ? mysqldb.insert(insert_sql, dict(self))
? ? ? ? ? ? else:
? ? ? ? ? ? ? ? print 'Empty!!!!!!!!!!!!!!!!!!!!!!!'
? ? ? ? ? ? pass
? ? def getTableSql(self):
? ? ? ? return None
? ? def getInsertSql(self):
? ? ? ? return None
它的一個(gè)子類示意:
import scrapy
from item_base import *
class ArticleItem(BaseItem):
? ? item_type = scrapy.Field()
? ? title = scrapy.Field()
? ? author = scrapy.Field()
? ? author_link = scrapy.Field()
? ? content = scrapy.Field()
? ? def getTableSql(self):
? ? ? ? return "CREATE TABLE `article` (" \
? ? ? ? ? ? "? `title` varchar(256) NOT NULL," \
? ? ? ? ? ? "? `author` varchar(128) NOT NULL," \
? ? ? ? ? ? "? `author_link` varchar(1024) NOT NULL," \
? ? ? ? ? ? "? `content` TEXT(40960) NOT NULL," \
? ? ? ? ? ? "? PRIMARY KEY (`title`)" \
? ? ? ? ? ? ") ENGINE=InnoDB"
? ? def getInsertSql(self):
? ? ? ? return "INSERT INTO article " \
? ? ? ? ? ? ? "(title, author, author_link, content) " \
? ? ? ? ? ? ? "VALUES (%(title)s, %(author)s, %(author_link)s, %(content)s)"
這樣,爬取到的內(nèi)容記錄在不同類型的item中赡突,最后又通過item的insertToDb過程对扶,插入到mysql中。
可以通過workbench直接查看:
爬取技巧
上面的基本元素都有了惭缰,我們繼續(xù)看下爬取過程中的一些小問題浪南。
首先是怎么使用xpath解析網(wǎng)頁元素。
xpath返回的是selector漱受,對應(yīng)網(wǎng)頁中的dom結(jié)構(gòu)络凿,比如我們用chrome調(diào)試器看下網(wǎng)頁的結(jié)構(gòu):
當(dāng)鼠標(biāo)放置一個(gè)地方,真實(shí)網(wǎng)頁中會顯示對應(yīng)的選中區(qū)域的昂羡,所以你可以對照左邊一層層找到它所對應(yīng)的html結(jié)構(gòu)絮记,比如"http://body/div/div/div"。
獲取屬性方法使用@虐先,如@href
xpath('div/div/span/@data-shared-at')
使用@class提取節(jié)點(diǎn)
response.xpath('//body/div[@class="note"]')
抓取html內(nèi)容
content = article_sel.xpath("div[@class='show-content']/div[@class='show-content-free']/*").extract()
content = ''.join(content)
抓取文本
content = article_sel.xpath("div[@class='show-content']/div[@class='show-content-free']//text()").extract()
content = ''.join(content)
其次是怎么讓爬蟲延伸怨愤。
當(dāng)你抓取到一個(gè)感興趣的鏈接后,比如當(dāng)前正在爬取的是某個(gè)人的簡書主頁蛹批,網(wǎng)頁中有很多文章鏈接撰洗,你想繼續(xù)爬取的話,就可以yield出去:
"""
url: 要繼續(xù)爬取的鏈接
callback: 爬取后的響應(yīng)處理
"""
yield scrapy.Request(url=link, callback=self.parse)
但是一般看到的鏈接是相對地址腐芍,所以你要先做一個(gè)處理:
from urlparse import urljoin
link = urljoin('http://www.reibang.com', link)
我們也看到差导,上面的self.parse方法被用在很多網(wǎng)頁請求中,但是這些網(wǎng)頁的格式可能是不一樣的猪勇,那么你需要做一個(gè)分類:
cur_url = response.url
if cur_url.startswith('http://www.reibang.com/u'):
? ? pass
elif cur_url.startswith('http://www.reibang.com/p'):
? ? pass
最后講一下怎么去抓動態(tài)網(wǎng)頁设褐。
你可以分析下簡書某個(gè)專題的網(wǎng)頁格式,它的內(nèi)容列表一般是10條泣刹,但是你往下滑動的時(shí)候它又會增多络断;當(dāng)爬取這個(gè)專題網(wǎng)頁的時(shí)候,你只能解析最開始的10條项玛,怎么辦呢貌笨?
打開調(diào)試器,選擇network/XHR襟沮,當(dāng)你在左邊的網(wǎng)頁中不停往上滑動的時(shí)候锥惋,就會不斷出現(xiàn)右邊新的鏈接昌腰,有沒有發(fā)現(xiàn)什么?
這些網(wǎng)頁都是有規(guī)律的膀跌,xxx?order_by=added_at&page=xx遭商,其中order_by就是這個(gè)專題的Tab目錄,added_at表示最新添加的捅伤,而page就是第幾個(gè)頁劫流。
如果你遍歷所有的page頁,不就把這些動態(tài)網(wǎng)頁抓取到了嗎丛忆?不過有個(gè)壞消息祠汇,就是page頁有上限,目前是200熄诡,不要告訴是我說的可很。。凰浮。
代碼工程
代碼我上傳到了github上我抠,其中HelloScrapy/db/settings.py中的變量是無效的,需要配置為有效的mysql用戶名和密碼袜茧。
嚴(yán)重聲明:
本文涉及的方法和代碼都只用于學(xué)習(xí)和研究菜拓,嚴(yán)禁轉(zhuǎn)載和用于商業(yè)目的,否則后果自負(fù)笛厦!