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


在簡書中有很多主題頻道,里面有大量優(yōu)秀的文章,我想收集這些文章用于提取對我有用的東西;

無疑爬蟲是一個好的選擇,我選用了python的一個爬蟲庫scrapy,它的官方文檔就是很好的教程:http://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/tutorial.html


準備工作


scrapy安裝

pip install Scrapy

我遇到的問題是,編譯時候stdarg.h找不到;于是查看報問題的頭文件目錄,把stdarg.h拷貝進去就OK了,這個花了我好長時間。。。

因為scrapy依賴于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ù)默認是用json存儲,因為不同類型的數(shù)據(jù)混合存儲,解析和查詢過于繁瑣,所以我選擇數(shù)據(jù)庫;至于沒有用mongodb,是因為我機子上本來就裝有mysql,而且自己學習研究用不到mongodb的一些優(yōu)點。

mysql數(shù)據(jù)庫是分服務器(server),客戶端(workbench),python的接口鏈接器(mysql-connector);這些都可以從官方找到,參見 https://dev.mysql.com/downloads/

connector可以直接用pip安裝,這里有個好處就是不用額外操心環(huán)境變量的事兒。

pip install mysql-connector

對于connector的使用,參見官方說明文檔:

https://dev.mysql.com/doc/connector-python/en/


一個簡單的框架


創(chuàng)建一個scrapy工程

scrapy startproject HelloScrapy

啟動一個工程

scrapy crawl demo

還可以用shell啟動,這個好處是你可以介入每一個執(zhí)行命令

scrapy shell 'http://www.itdecent.cn/u/4a4eb4feee62'

需要注意的是,網(wǎng)站一般會有反爬蟲機制,抓取會返回403錯誤,所以記得把user-agent改了:

settings.py

USER_AGENT = 'HelloWoWo'

開啟爬蟲

你需要在spider目錄下建立一個scrapy.Spider的子類,定義它的名稱(name, 就是啟動工程時指定的名稱),允許的域名(allowed_domains),起始的爬取鏈接(start_urls);

然后定義parse函數(shù),它的參數(shù)response就是響應內容,你可以從中解析要獲取的內容和新的鏈接;解析的方式可以通過xpath和css,這里用xpath;然后可以通過yield,把解析到的對象推送到存儲流程中,如果你想爬蟲系統(tǒng)繼續(xù)爬取新的鏈接,也可以通過yield來進入下一步爬取中。

from HelloScrapy.items import HelloscrapyItem

class DemoScrapy(scrapy.Spider):

? ? name = 'demo'

? ? allowed_domains = ['jianshu.com']

? ? start_urls = [

? ? ? ? 'http://www.itdecent.cn/u/4a4eb4feee62',

? ? ? ? 'http://www.itdecent.cn/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ù)結構,它們每個屬性都用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

它負責整個存儲的過程,可以存儲在json文件中,也可以通過數(shù)據(jù)庫。

但是首先,你需要在settings.py中聲明你的pipeline(默認有的,打開注釋然后修改下):

# 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和結束spider時調用,所以這里分別定義了文件的打開和關閉;

而process_item就是對每個item的存儲處理,這里將item進行json化,保存在預定義的文件中。

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

首先定義一個mysql的封裝類,支持打開和關閉一個數(shù)據(jù)庫,創(chuàng)建一個表,插入一條數(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

然后抽象一個item的基類:

該類的insertToDb定義了插入的過程,由每個子類提供創(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

它的一個子類示意:

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)"

這樣,爬取到的內容記錄在不同類型的item中,最后又通過item的insertToDb過程,插入到mysql中。

可以通過workbench直接查看:


爬取技巧

上面的基本元素都有了,我們繼續(xù)看下爬取過程中的一些小問題。

首先是怎么使用xpath解析網(wǎng)頁元素。

xpath返回的是selector,對應網(wǎng)頁中的dom結構,比如我們用chrome調試器看下網(wǎng)頁的結構:

當鼠標放置一個地方,真實網(wǎng)頁中會顯示對應的選中區(qū)域的,所以你可以對照左邊一層層找到它所對應的html結構,比如"http://body/div/div/div"。

獲取屬性方法使用@,如@href

xpath('div/div/span/@data-shared-at')

使用@class提取節(jié)點

response.xpath('//body/div[@class="note"]')

抓取html內容

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)

其次是怎么讓爬蟲延伸。

當你抓取到一個感興趣的鏈接后,比如當前正在爬取的是某個人的簡書主頁,網(wǎng)頁中有很多文章鏈接,你想繼續(xù)爬取的話,就可以yield出去:

"""

url: 要繼續(xù)爬取的鏈接

callback: 爬取后的響應處理

"""

yield scrapy.Request(url=link, callback=self.parse)

但是一般看到的鏈接是相對地址,所以你要先做一個處理:

from urlparse import urljoin

link = urljoin('http://www.itdecent.cn', link)

我們也看到,上面的self.parse方法被用在很多網(wǎng)頁請求中,但是這些網(wǎng)頁的格式可能是不一樣的,那么你需要做一個分類:

cur_url = response.url

if cur_url.startswith('http://www.itdecent.cn/u'):

? ? pass

elif cur_url.startswith('http://www.itdecent.cn/p'):

? ? pass

最后講一下怎么去抓動態(tài)網(wǎng)頁。

你可以分析下簡書某個專題的網(wǎng)頁格式,它的內容列表一般是10條,但是你往下滑動的時候它又會增多;當爬取這個專題網(wǎng)頁的時候,你只能解析最開始的10條,怎么辦呢?

打開調試器,選擇network/XHR,當你在左邊的網(wǎng)頁中不停往上滑動的時候,就會不斷出現(xiàn)右邊新的鏈接,有沒有發(fā)現(xiàn)什么?

這些網(wǎng)頁都是有規(guī)律的,xxx?order_by=added_at&page=xx,其中order_by就是這個專題的Tab目錄,added_at表示最新添加的,而page就是第幾個頁。

如果你遍歷所有的page頁,不就把這些動態(tài)網(wǎng)頁抓取到了嗎?不過有個壞消息,就是page頁有上限,目前是200,不要告訴是我說的。。。


代碼工程


代碼我上傳到了github上,其中HelloScrapy/db/settings.py中的變量是無效的,需要配置為有效的mysql用戶名和密碼。

https://github.com/callmejacob/spider


嚴重聲明:

本文涉及的方法和代碼都只用于學習和研究,嚴禁轉載和用于商業(yè)目的,否則后果自負!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容