引
在簡書中有很多主題頻道,里面有大量優(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用戶名和密碼。
嚴重聲明:
本文涉及的方法和代碼都只用于學習和研究,嚴禁轉載和用于商業(yè)目的,否則后果自負!