爬蟲小記(一)--- 爬取簡書專題


在簡書中有很多主題頻道,里面有大量優(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用戶名和密碼袜茧。

https://github.com/callmejacob/spider


嚴(yán)重聲明:

本文涉及的方法和代碼都只用于學(xué)習(xí)和研究菜拓,嚴(yán)禁轉(zhuǎn)載和用于商業(yè)目的,否則后果自負(fù)笛厦!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末尘惧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子递递,更是在濱河造成了極大的恐慌喷橙,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件登舞,死亡現(xiàn)場離奇詭異贰逾,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)菠秒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門疙剑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人践叠,你說我怎么就攤上這事言缤。” “怎么了禁灼?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵管挟,是天一觀的道長。 經(jīng)常有香客問我弄捕,道長僻孝,這世上最難降的妖魔是什么导帝? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮穿铆,結(jié)果婚禮上您单,老公的妹妹穿的比我還像新娘。我一直安慰自己荞雏,他們只是感情好虐秦,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著凤优,像睡著了一般悦陋。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上别洪,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天,我揣著相機(jī)與錄音柳刮,去河邊找鬼挖垛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛秉颗,可吹牛的內(nèi)容都是我干的痢毒。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼蚕甥,長吁一口氣:“原來是場噩夢啊……” “哼哪替!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起菇怀,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤凭舶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后爱沟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體帅霜,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年呼伸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了身冀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡括享,死狀恐怖搂根,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情铃辖,我是刑警寧澤剩愧,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站娇斩,受9級特大地震影響隙咸,放射性物質(zhì)發(fā)生泄漏沐悦。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一五督、第九天 我趴在偏房一處隱蔽的房頂上張望藏否。 院中可真熱鬧,春花似錦充包、人聲如沸副签。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽淆储。三九已至,卻和暖如春家浇,著一層夾襖步出監(jiān)牢的瞬間本砰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工钢悲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留点额,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓莺琳,卻偏偏與公主長得像还棱,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子惭等,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容