芝麻HTTP:Python爬蟲實戰之抓取愛問知識人問題並保存至數據庫

本次爲你們帶來的是抓取愛問知識人的問題並將問題和答案保存到數據庫的方法,涉及的內容包括:php

  • Urllib的用法及異常處理
  • Beautiful Soup的簡單應用
  • MySQLdb的基礎用法
  • 正則表達式的簡單應用

環境配置

在這以前,咱們須要先配置一下環境,個人Python的版本爲2.7,須要額外安裝的庫有兩個,一個是Beautiful Soup,一個是MySQLdb,在這裏附上兩個庫的下載地址,html

Beautiful Soup     MySQLdbpython

你們能夠下載以後經過以下命令安裝mysql

python setup.py install

環境配置好以後,咱們即可以開心地擼爬蟲了linux

框架思路

首先咱們隨便找一個分類地址,外語學習 – 愛問知識人,打開以後能夠看到一系列的問題列表。正則表達式

咱們在這個頁面須要獲取的東西有:sql

總的頁碼數,每一頁的全部問題連接。shell

接下來咱們須要遍歷全部的問題,來抓取每個詳情頁面,提取問題,問題內容,回答者,回答時間,回答內容。數據庫

最後,咱們須要把這些內容存儲到數據庫中。編程

要點簡析

其實大部份內容相信你們會了前面的內容,這裏的爬蟲思路已經融匯貫通了,這裏就說一下一些擴展的功能

1.日誌輸出

日誌輸出,咱們要輸出時間和爬取的狀態,好比像下面這樣:

[2015-08-10 03:05:20] 113011 號問題存在其餘答案 我我的認爲應該是櫻桃溝很美的

[2015-08-10 03:05:20] 保存到數據庫,此問題的ID爲 113011

[2015-08-10 03:05:20] 當前爬取第 2 的內容,發現一個問題 百度有一個地方,花兒帶着芳香,水兒流淌奔騰是什麼意思 多多幫忙哦 回答數量 1

[2015-08-10 03:05:19] 保存到數據庫,此問題的ID爲 113010

因此,咱們須要引入時間函數,而後寫一個獲取當前時間的函數

import time
 
#獲取當前時間
def getCurrentTime(self):
    return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))
    
#獲取當前時間
def getCurrentDate(self):
    return time.strftime('%Y-%m-%d',time.localtime(time.time()))

以上分別是獲取帶具體時間和獲取日期的函數,在輸出時,咱們能夠在輸出語句的前面調用這函數便可。

而後咱們須要將緩衝區設置輸出到log中,在程序的最前面加上這兩句便可

f_handler=open('out.log', 'w') 
sys.stdout=f_handler

這樣,全部的print語句輸出的內容就會保存到out.log文件中了。

2.頁碼保存

爬蟲爬取過程當中可能出現各類各樣的錯誤,這樣會致使爬蟲的中斷,若是咱們從新運行爬蟲,那麼就會致使爬蟲從頭開始運行了,這樣顯然是不合理的。因此,咱們須要把當前爬取的頁面保存下來,好比能夠保存到文本中,假如爬蟲中斷了,從新運行爬蟲,讀取文本文件的內容,接着爬取便可。

你們能夠稍微參考一下函數的實現:

#主函數
    def main(self):
        f_handler=open('out.log', 'w') 
        sys.stdout=f_handler
        page = open('page.txt', 'r')
        content = page.readline()
        start_page = int(content.strip()) - 1
        page.close()     
        print self.getCurrentTime(),"開始頁碼",start_page
        print self.getCurrentTime(),"爬蟲正在啓動,開始爬取愛問知識人問題"
        self.total_num = self.getTotalPageNum()
        print self.getCurrentTime(),"獲取到目錄頁面個數",self.total_num,"個"
        if not start_page:
            start_page = self.total_num
        for x in range(1,start_page):
            print self.getCurrentTime(),"正在抓取第",start_page-x+1,"個頁面"
            try:
                self.getQuestions(start_page-x+1)
            except urllib2.URLError, e:
                if hasattr(e, "reason"):
                    print self.getCurrentTime(),"某總頁面內抓取或提取失敗,錯誤緣由", e.reason
            except Exception,e:  
                print self.getCurrentTime(),"某總頁面內抓取或提取失敗,錯誤緣由:",e
            if start_page-x+1 < start_page:
                f=open('page.txt','w')
                f.write(str(start_page-x+1))
                print self.getCurrentTime(),"寫入新頁碼",start_page-x+1
                f.close()

這樣,無論咱們爬蟲中途遇到什麼錯誤,媽媽也不會擔憂了

3.頁面處理

頁面處理過程當中,咱們可能遇到各類各樣奇葩的HTML代碼,和上一節同樣,咱們沿用一個頁面處理類便可。

import re
 
#處理頁面標籤類
class Tool:
    
    #將超連接廣告剔除
    removeADLink = re.compile('<div class="link_layer.*?</div>')
    #去除img標籤,1-7位空格,&nbsp;
    removeImg = re.compile('<img.*?>| {1,7}|&nbsp;')
    #刪除超連接標籤
    removeAddr = re.compile('<a.*?>|</a>')
    #把換行的標籤換爲\n
    replaceLine = re.compile('<tr>|<div>|</div>|</p>')
    #將表格製表<td>替換爲\t
    replaceTD= re.compile('<td>')
    #將換行符或雙換行符替換爲\n
    replaceBR = re.compile('<br><br>|<br>')
    #將其他標籤剔除
    removeExtraTag = re.compile('<.*?>')
    #將多行空行刪除
    removeNoneLine = re.compile('\n+')
    
    def replace(self,x):
        x = re.sub(self.removeADLink,"",x)
        x = re.sub(self.removeImg,"",x)
        x = re.sub(self.removeAddr,"",x)
        x = re.sub(self.replaceLine,"\n",x)
        x = re.sub(self.replaceTD,"\t",x)
        x = re.sub(self.replaceBR,"\n",x)
        x = re.sub(self.removeExtraTag,"",x)
        x = re.sub(self.removeNoneLine,"\n",x)
        #strip()將先後多餘內容刪除
        return x.strip()

咱們能夠用一段含有HTML代碼的文字,通過調用replace方法以後,各類冗餘的HTML代碼就會處理好了。

好比咱們這麼一段代碼:

<article class="article-content">
<h2>前言</h2>
<p>最近發現MySQL服務隔三差五就會掛掉,致使個人網站和爬蟲都沒法正常運做。本身的網站是基於MySQL,在作爬蟲存取一些資料的時候也是基於MySQL,數據量一大了,MySQL它就有點受不了了,時不時會崩掉,雖然我本身有網站監控和郵件通知,可是好多時候仍是須要我來手動鏈接個人服務器從新啓動一下個人MySQL,這樣簡直太不友好了,因此,我就覺定本身寫個腳本,定時監控它,若是發現它掛掉了就重啓它。</p>
<p>好了,閒言碎語很少講,開始咱們的配置之旅。</p>
<p>運行環境:<strong>Ubuntu Linux 14.04</strong></p>
<h2>編寫Shell腳本</h2>
<p>首先,咱們要編寫一個shell腳本,腳本主要執行的邏輯以下:</p>
<p>顯示mysqld進程狀態,若是判斷進程未在運行,那麼輸出日誌到文件,而後啓動mysql服務,若是進程在運行,那麼不執行任何操做,能夠選擇性輸出監測結果。</p>
<p>可能你們對於shell腳本比較陌生,在這裏推薦官方的shell腳本文檔來參考一下</p>
<p><a href="http://wiki.ubuntu.org.cn/Shell%E7%BC%96%E7%A8%8B%E5%9F%BA%E7%A1%80" data-original-title="" title="">Ubuntu Shell 編程基礎</a></p>
<p>shell腳本的後綴爲sh,在任何位置新建一個腳本文件,我選擇在 /etc/mysql 目錄下新建一個 listen.sh 文件。</p>
<p>執行以下命令:</p>

通過處理後便會變成以下的樣子:

前言
最近發現MySQL服務隔三差五就會掛掉,致使個人網站和爬蟲都沒法正常運做。本身的網站是基於MySQL,在作爬蟲存取一些資料的時候也是基於MySQL,數據量一大了,MySQL它就有點受不了了,時不時會崩掉,雖然我本身有網站監控和郵件通知,可是好多時候仍是須要我來手動鏈接個人服務器從新啓動一下個人MySQL,這樣簡直太不友好了,因此,我就覺定本身寫個腳本,定時監控它,若是發現它掛掉了就重啓它。
好了,閒言碎語很少講,開始咱們的配置之旅。
運行環境:UbuntuLinux14.04
編寫Shell腳本
首先,咱們要編寫一個shell腳本,腳本主要執行的邏輯以下:
顯示mysqld進程狀態,若是判斷進程未在運行,那麼輸出日誌到文件,而後啓動mysql服務,若是進程在運行,那麼不執行任何操做,能夠選擇性輸出監測結果。
可能你們對於shell腳本比較陌生,在這裏推薦官方的shell腳本文檔來參考一下
UbuntuShell編程基礎
shell腳本的後綴爲sh,在任何位置新建一個腳本文件,我選擇在/etc/mysql目錄下新建一個listen.sh文件。
執行以下命令:

通過上面的處理,全部亂亂的代碼都會被處理好了。

4.保存到數據庫

在這裏,咱們想實現一個通用的方法,就是把存儲的一個個內容變成字典的形式,而後執行插入語句的時候,自動構建對應的sql語句,插入數據。

好比咱們構造以下的字典:

#構造最佳答案的字典
good_ans_dict = {
        "text": good_ans[0],
        "answerer": good_ans[1],
        "date": good_ans[2],
        "is_good": str(good_ans[3]),
        "question_id": str(insert_id)
        }

構造sql語句並插入到數據庫的方法以下:

#插入數據
    def insertData(self, table, my_dict):
         try:
             self.db.set_character_set('utf8')
             cols = ', '.join(my_dict.keys())
             values = '"," '.join(my_dict.values())
             sql = "INSERT INTO %s (%s) VALUES (%s)" % (table, cols, '"'+values+'"')
             try:
                 result = self.cur.execute(sql)
                 insert_id = self.db.insert_id()
                 self.db.commit()
                 #判斷是否執行成功
                 if result:
                     return insert_id
                 else:
                     return 0
             except MySQLdb.Error,e:
                 #發生錯誤時回滾
                 self.db.rollback()
                 #主鍵惟一,沒法插入
                 if "key 'PRIMARY'" in e.args[1]:
                     print self.getCurrentTime(),"數據已存在,未插入數據"
                 else:
                     print self.getCurrentTime(),"插入數據失敗,緣由 %d: %s" % (e.args[0], e.args[1])
         except MySQLdb.Error,e:
             print self.getCurrentTime(),"數據庫錯誤,緣由%d: %s" % (e.args[0], e.args[1])

這裏咱們只須要傳入那個字典,便會構建出對應字典鍵值和鍵名的sql語句,完成插入。

5.PHP讀取日誌

咱們將運行結果輸出到了日誌裏,那麼怎麼查看日誌呢?很簡單,在這裏提供兩種方法

方法一:

PHP倒序輸出全部日誌內容

<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="refresh" content = "5"> 
    </head>
    <body>
        <?php
            $fp = file("out.log");
            if ($fp) {
                for($i = count($fp) - 1;$i >= 0; $i --) 
                echo $fp[$i]."<br>";
            }
        ?>
    </body>
</html>

此方法能夠看到全部的輸入日誌,可是若是日誌太大了,那麼就會報耗費內存太大,沒法輸出。爲此咱們就有了第二種方法,利用linux命令,輸出後十行內容。

方法二:

<html>
    <head>
        <meta charset="utf-8">
        <meta http-equiv="refresh" content = "5"> 
    </head>
    <body>
        <?php 
            $ph = popen('tail -n 100 out.log','r');
            while($r = fgets($ph)){
                echo $r."<br>";
            }
            pclose($ph);
        ?>
    </body>
</html>

上面兩種方法都是5秒刷新一次網頁來查看最新的日誌。

源代碼放送

好了,閒言碎語很少講,直接上源碼了

spider.py
spider.py


# -*- coding:utf-8 -*-

import urllib
import urllib2
import re
import time
import types
import page
import mysql
import sys
from bs4 import BeautifulSoup

class Spider:
    
    #初始化
    def __init__(self):
        self.page_num = 1
        self.total_num = None
        self.page_spider = page.Page()
        self.mysql = mysql.Mysql()
        
    #獲取當前時間
    def getCurrentTime(self):
        return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))
    
    #獲取當前時間
    def getCurrentDate(self):
        return time.strftime('%Y-%m-%d',time.localtime(time.time()))
    
    #經過網頁的頁碼數來構建網頁的URL
    def getPageURLByNum(self, page_num):
        page_url = "http://iask.sina.com.cn/c/978-all-" + str(page_num) + ".html"
        return page_url
    
    
    #經過傳入網頁頁碼來獲取網頁的HTML
    def getPageByNum(self, page_num):
        request = urllib2.Request(self.getPageURLByNum(page_num))
        try:
            response = urllib2.urlopen(request)
        except urllib2.URLError, e:
            if hasattr(e, "code"):
                print self.getCurrentTime(),"獲取頁面失敗,錯誤代號", e.code
                return None
            if hasattr(e, "reason"):
                print self.getCurrentTime(),"獲取頁面失敗,緣由", e.reason
                return None
        else:
            page =  response.read().decode("utf-8")
            return page
    
    #獲取全部的頁碼數
    def getTotalPageNum(self):
        print self.getCurrentTime(),"正在獲取目錄頁面個數,請稍候"
        page = self.getPageByNum(1)
        #匹配全部的頁碼數,\u4e0b\u4e00\u9875是下一頁的UTF8編碼
        pattern = re.compile(u'<span class="more".*?>.*?<span.*?<a href.*?class="">(.*?)</a>\s*<a.*?\u4e0b\u4e00\u9875</a>', re.S)
        match = re.search(pattern, page)
        if match:
            return match.group(1)
        else:
            print self.getCurrentTime(),"獲取總頁碼失敗"
    
    #分析問題的代碼,獲得問題的提問者,問題內容,回答個數,提問時間
    def getQuestionInfo(self, question):
        if not type(question) is types.StringType:
            question = str(question)
        #print question
        pattern = re.compile(u'<span.*?question-face.*?>.*?<img.*?alt="(.*?)".*?</span>.*?<a href="(.*?)".*?>(.*?)</a>.*?answer_num.*?>(\d*).*?</span>.*?answer_time.*?>(.*?)</span>', re.S)
        match = re.search(pattern, question)
        if match:
            #得到提問者
            author = match.group(1)
            #問題連接
            href = match.group(2)
            #問題詳情
            text = match.group(3)
            #回答個數
            ans_num = match.group(4)
            #回答時間
            time = match.group(5)
            time_pattern = re.compile('\d{4}\-\d{2}\-\d{2}', re.S)
            time_match = re.search(time_pattern, time)
            if not time_match:
                time = self.getCurrentDate()
            return [author, href, text, ans_num, time]
        else:
            return None
        
    #獲取所有問題
    def getQuestions(self, page_num):
        #得到目錄頁面的HTML
        page = self.getPageByNum(page_num)
        soup = BeautifulSoup(page)
        #分析得到全部問題
        questions = soup.select("div.question_list ul li")
        #遍歷每個問題
        for question in questions:
            #得到問題的詳情
            info = self.getQuestionInfo(question)
            if info:
                #獲得問題的URL
                url = "http://iask.sina.com.cn/" + info[1]
                #經過URL來獲取問題的最佳答案和其餘答案
                ans = self.page_spider.getAnswer(url)
                print self.getCurrentTime(),"當前爬取第",page_num,"的內容,發現一個問題",info[2],"回答數量",info[3]
                #構造問題的字典,插入問題
                ques_dict = {
                            "text": info[2],
                            "questioner": info[0],
                            "date": info[4],
                            "ans_num": info[3],
                            "url": url
                            }
                #得到插入的問題的自增ID 
                insert_id = self.mysql.insertData("iask_questions",ques_dict)
                #獲得最佳答案
                good_ans = ans[0]
                print self.getCurrentTime(),"保存到數據庫,此問題的ID爲",insert_id
                #若是存在最佳答案,那麼就插入
                if good_ans:
                    print self.getCurrentTime(),insert_id,"號問題存在最佳答案",good_ans[0]
                    #構造最佳答案的字典
                    good_ans_dict = {
                            "text": good_ans[0],
                            "answerer": good_ans[1],
                            "date": good_ans[2],
                            "is_good": str(good_ans[3]),
                            "question_id": str(insert_id)
                            }
                    #插入最佳答案
                    if self.mysql.insertData("iask_answers",good_ans_dict):
                        print self.getCurrentTime(),"保存最佳答案成功"
                    else:
                        print self.getCurrentTime(),"保存最佳答案失敗"
                #得到其餘答案
                other_anses = ans[1]
                #遍歷每個其餘答案
                for other_ans in other_anses:
                    #若是答案存在
                    if other_ans:
                        print self.getCurrentTime(),insert_id,"號問題存在其餘答案",other_ans[0]
                        #構造其餘答案的字典
                        other_ans_dict = {
                                "text": other_ans[0],
                                "answerer": other_ans[1],
                                "date": other_ans[2],
                                "is_good": str(other_ans[3]),
                                "question_id": str(insert_id)
                                }
                        #插入這個答案
                        if self.mysql.insertData("iask_answers",other_ans_dict):
                            print self.getCurrentTime(),"保存其餘答案成功"
                        else:
                            print self.getCurrentTime(),"保存其餘答案失敗"
        
    #主函數
    def main(self):
        f_handler=open('out.log', 'w') 
        sys.stdout=f_handler
        page = open('page.txt', 'r')
        content = page.readline()
        start_page = int(content.strip()) - 1
        page.close()     
        print self.getCurrentTime(),"開始頁碼",start_page
        print self.getCurrentTime(),"爬蟲正在啓動,開始爬取愛問知識人問題"
        self.total_num = self.getTotalPageNum()
        print self.getCurrentTime(),"獲取到目錄頁面個數",self.total_num,"個"
        if not start_page:
            start_page = self.total_num
        for x in range(1,start_page):
            print self.getCurrentTime(),"正在抓取第",start_page-x+1,"個頁面"
            try:
                self.getQuestions(start_page-x+1)
            except urllib2.URLError, e:
                if hasattr(e, "reason"):
                    print self.getCurrentTime(),"某總頁面內抓取或提取失敗,錯誤緣由", e.reason
            except Exception,e:  
                print self.getCurrentTime(),"某總頁面內抓取或提取失敗,錯誤緣由:",e
            if start_page-x+1 < start_page:
                f=open('page.txt','w')
                f.write(str(start_page-x+1))
                print self.getCurrentTime(),"寫入新頁碼",start_page-x+1
                f.close()

spider = Spider()
spider.main()       

page = Page()
page.getAnswer(None)

# -*- coding:utf-8 -*-
import urllib
import urllib2
import re
import time
import types 
import tool
from bs4 import BeautifulSoup
 
#抓取分析某一問題和答案
class Page:
    
    def __init__(self):
        self.tool = tool.Tool()
    
    #獲取當前時間
    def getCurrentDate(self):
        return time.strftime('%Y-%m-%d',time.localtime(time.time()))
    
    #獲取當前時間
    def getCurrentTime(self):
        return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))
 
    #經過頁面的URL來獲取頁面的代碼
    def getPageByURL(self, url):
        try:
            request = urllib2.Request(url)
            response = urllib2.urlopen(request)
            return response.read().decode("utf-8") 
        except urllib2.URLError, e:

tool.py



#-*- coding:utf-8 -*-
import re
 
#處理頁面標籤類
class Tool:
    
    #將超連接廣告剔除
    removeADLink = re.compile('<div class="link_layer.*?</div>')
    #去除img標籤,1-7位空格,&nbsp;
    removeImg = re.compile('<img.*?>| {1,7}|&nbsp;')
    #刪除超連接標籤
    removeAddr = re.compile('<a.*?>|</a>')
    #把換行的標籤換爲\n
    replaceLine = re.compile('<tr>|<div>|</div>|</p>')
    #將表格製表<td>替換爲\t
    replaceTD= re.compile('<td>')
    #將換行符或雙換行符替換爲\n
    replaceBR = re.compile('<br><br>|<br>')
    #將其他標籤剔除
    removeExtraTag = re.compile('<.*?>')

mysql.py


# -*- coding:utf-8 -*-
 
 
import MySQLdb
import time
 
class Mysql:
    
    #獲取當前時間
    def getCurrentTime(self):
        return time.strftime('[%Y-%m-%d %H:%M:%S]',time.localtime(time.time()))
    
    #數據庫初始化
    def __init__(self):
        try:
            self.db = MySQLdb.connect('ip','username','password','db_name')
            self.cur = self.db.cursor()
        except MySQLdb.Error,e:
             print self.getCurrentTime(),"鏈接數據庫錯誤,緣由%d: %s" % (e.args[0], e.args[1])
 
    #插入數據
    def insertData(self, table, my_dict):
         try:
             self.db.set_character_set('utf8')
             cols = ', '.join(my_dict.keys())
             values = '"," '.join(my_dict.values())
             sql = "INSERT INTO %s (%s) VALUES (%s)" % (table, cols, '"'+values+'"')
             try:
                 result = self.cur.execute(sql)
                 insert_id = self.db.insert_id()
                 self.db.commit()
                 #判斷是否執行成功
                 if result:
                     return insert_id
                 else:
                     return 0
             except MySQLdb.Error,e:
                 #發生錯誤時回滾
                 self.db.rollback()
                 #主鍵惟一,沒法插入
                 if "key 'PRIMARY'" in e.args[1]:
                     print self.getCurrentTime(),"數據已存在,未插入數據"
                 else:
                     print self.getCurrentTime(),"插入數據失敗,緣由 %d: %s" % (e.args[0], e.args[1])
         except MySQLdb.Error,e:
             print self.getCurrentTime(),"數據庫錯誤,緣由%d: %s" % (e.args[0], e.args[1])
數據庫建表SQL以下:

PgSQL

CREATE TABLE IF NOT EXISTS `iask_answers` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `text` text NOT NULL COMMENT '回答內容',
  `question_id` int(18) NOT NULL COMMENT '問題ID',
  `answerer` varchar(255) NOT NULL COMMENT '回答者',
  `date` varchar(255) NOT NULL COMMENT '回答時間',
  `is_good` int(11) NOT NULL COMMENT '是不是最佳答案',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
 
CREATE TABLE IF NOT EXISTS `iask_questions` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '問題ID',
  `text` text NOT NULL COMMENT '問題內容',
  `questioner` varchar(255) NOT NULL COMMENT '提問者',
  `date` date NOT NULL COMMENT '提問時間',
  `ans_num` int(11) NOT NULL COMMENT '回答數量',
  `url` varchar(255) NOT NULL COMMENT '問題連接',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
運行的時候執行以下命令便可

nohup python spider.py &

運行結果查看

咱們把PHP文件和log文件放在同一目錄下,運行PHP文件,即可以看到以下的內容:

相關文章
相關標籤/搜索