利用Scrapy實現公司內部門戶消息郵件通知

1、項目背景

  我本人所在公司是一個國有企業,自建有較大的內部網絡門戶羣,幾乎全部部門發佈各種通知、工做要求等消息都在門戶網站進行。因爲對應的上級部門比較多,各種通知通告、領導講話等內容類目繁多,要看一遍真須要花費點時間。更重要的是有些會議通知等時效性比較強的消息一旦遺漏錯太重要會議就比較麻煩。爲了改變這種情況,就想創建一個內部網絡消息跟蹤、通知系統。html

2、基本功能

  主要功能:系統功能比較簡單,主要就是爬取內部網絡固定的一些通知頁面,發現新的通知就向指定的人發送通知郵件。
  涉及到的功能點:
  1.常規頁面請求
  2.post請求
  3.數據存儲
  4.識別新消息
  5.郵件通知
  6.定時啓動,循環運行python

3、詳細說明

(一)文件結構

clipboard.png

  上圖顯示了完成狀態的文件結構,與新建的scrapy項目相比增長的文件有兩部分:
  一是spiders目錄下的6個爬蟲文件,對應了6個欄目,之後根據須要還會再增長;
  二是涉及定時啓動、循環運行功能的幾個文件,分別是commands文件夾、noticeStart.py、setup.py、autorun.batmysql

(二)各部分代碼

1. items.py

import scrapy

class JlshNoticeItem(scrapy.Item):
    # define the fields for your item here like:
    # name = scrapy.Field()
    noticeType = scrapy.Field()     #通知類別
    noticeTitle = scrapy.Field()    #通知標題
    noticeDate = scrapy.Field()     #通知日期
    noticeUrl = scrapy.Field()      #通知URL
    noticeContent = scrapy.Field()  #通知內容

2. spider

  篇幅關係,這裏只拿一個爬蟲作例子,其它的爬蟲只是名稱和start_url不一樣,下面代碼儘可能作到逐句註釋。web

代碼

from scrapy import Request
from scrapy import FormRequest
from scrapy.spiders import Spider
from jlsh_notice.items import JlshNoticeItem
from jlsh_notice.settings import DOWNLOAD_DELAY
from scrapy.crawler import CrawlerProcess
from datetime import date
import requests
import lxml
import random
import re
#=======================================================
class jlsh_notice_spider_gongsitongzhi(Spider):
    #爬蟲名稱
    name = 'jlsh_gongsitongzhi'
    
    start_urls = [
        'http://www.jlsh.petrochina/sites/jlsh/news/inform/Pages/default.aspx', #公司通知
    ]
#=======================================================
    #處理函數
    def parse(self, response):
        noticeList = response.xpath('//ul[@class="w_newslistpage_list"]//li')
#=======================================================
        #建立item實例
        item = JlshNoticeItem()
        for i, notice in enumerate(noticeList):
            item['noticeType'] = '公司通知'

            item['noticeTitle'] = notice.xpath('.//a/@title').extract()[0]

            item['noticeUrl'] = notice.xpath('.//a/@href').extract()[0]
#=======================================================
            dateItem = notice.xpath('.//span[2]/text()').extract()[0]
            pattern = re.compile(r'\d+')
            datetime = pattern.findall(dateItem)
            yy = int(datetime[0])+2000
            mm = int(datetime[1])
            dd = int(datetime[2])
            item['noticeDate'] = date(yy,mm,dd)
#=======================================================
            content_html = requests.get(item['noticeUrl']).text
            content_lxml = lxml.etree.HTML(content_html)
            content_table = content_lxml.xpath( \
                '//div[@id="contentText"]/div[2]/div | \
                //div[@id="contentText"]/div[2]/p')
            
            
            item['noticeContent'] = []
            for j, p in enumerate(content_table):
                p = p.xpath('string(.)')
                #print('p:::::',p)
                p = p.replace('\xa0',' ')
                p = p.replace('\u3000', ' ')
                item['noticeContent'].append(p)

            yield item
#=======================================================
        pages = response.xpath('//div[@class="w_newslistpage_pager"]//span')
        nextPage = 0
        for i, page_tag in enumerate(pages):
            page = page_tag.xpath('./a/text()').extract()[0]
            page_url = page_tag.xpath('./a/@href').extract()
            if page == '下一頁>>':
                pattern = re.compile(r'\d+')
                page_url = page_url[0]
                nextPage = int(pattern.search(page_url).group(0))
                break
#=======================================================
        if nextPage > 0 :
            postUrl = self.start_urls[0]
            formdata = {
                'MSOWebPartPage_PostbackSource':'',
                'MSOTlPn_SelectedWpId':'',
                'MSOTlPn_View':'0',
                'MSOTlPn_ShowSettings':'False',
                'MSOGallery_SelectedLibrary':'',
                'MSOGallery_FilterString':'',
                'MSOTlPn_Button':'none',
                '__EVENTTARGET':'',
                '__EVENTARGUMENT':'',
                '__REQUESTDIGEST':'',
                'MSOSPWebPartManager_DisplayModeName':'Browse',
                'MSOSPWebPartManager_ExitingDesignMode':'false',
                'MSOWebPartPage_Shared':'',
                'MSOLayout_LayoutChanges':'',
                'MSOLayout_InDesignMode':'',
                '_wpSelected':'',
                '_wzSelected':'',
                'MSOSPWebPartManager_OldDisplayModeName':'Browse',
                'MSOSPWebPartManager_StartWebPartEditingName':'false',
                'MSOSPWebPartManager_EndWebPartEditing':'false',
                '_maintainWorkspaceScrollPosition':'0',
                '__LASTFOCUS':'',
                '__VIEWSTATE':'',
                '__VIEWSTATEGENERATOR':'15781244',
                'query':'',
                'database':'GFHGXS-GFJLSH',
                'sUsername':'',
                'sAdmin':'',
                'sRoles':'',
                'activepage':str(nextPage),
                '__spDummyText1':'',
                '__spDummyText2':'',
                '_wpcmWpid':'',
                'wpcmVal':'',
            }

            yield FormRequest(postUrl,formdata=formdata, callback=self.parse)

說明,如下說明要配合上面源碼來看,不單獨標註了

start_urls #要爬取的頁面地址,因爲各個爬蟲要爬取的頁面規則略有差別,因此作了6個爬蟲,而不是在一個爬蟲中寫入6個URL。經過查看scrapy源碼,咱們可以看到,start_urls中的地址會傳給一個內件函數start_request(這個函數能夠根據本身須要進行重寫),start_request向這個start_urls發送請求之後,所獲得的response會直接轉到下面parse函數處理。
xpath ,下圖是頁面源碼:
clipboard.png

經過xpath獲取到response中class類是'w_newslistpage_list'的ul標籤下的全部li標籤,這裏所獲得的就是通知的列表,接下來咱們在這個列表中作循環。正則表達式

先看下li標籤內的結構:
clipboard.png

notice.xpath('.//a/@title').extract()[0] #獲取li標籤內a標籤中的title屬性內容,這裏就是通知標題
notice.xpath('.//a/@href').extract()[0] #獲取li標籤內a標籤中的href屬性內容,這裏就是通知連接
notice.xpath('.//span[2]/text()').extract()[0] #獲取li標籤內第二個span標籤中的內容,這裏是通知發佈的日期
接下來幾行就是利用正則表達式講日期中的年、月、日三組數字提取出來,在轉換爲日期類型存入item中。sql

再下一段,是得到通知內容,這裏其實有兩種方案,一個是用scrapy的request發送給內部爬蟲引擎,獲得另一個response後再進行處理,另外一種就是我如今這樣直接去請求頁面。因爲內容頁面比較簡單,只要得到html代碼便可,因此就不麻煩scrapy處理了。
request.get獲得請求頁面的html代碼
利用lxml庫的etree方法格式化html爲xml結構
利用xpath獲取到div[@id="contentText"]內全部p標籤、div標籤節點。(能夠獲得99%以上的頁面內容)
所獲得的全部節點將是一個list類型數據,全部咱們作一個for in循環
p.xpath('string(.)') 是獲取到p標籤或div標籤中的全部文本,而無視其餘html標籤。
用replace替換到頁面中的半角、全角空格(xa0、u3000)
每獲得一行清洗過的數據,就將其存入item['noticeContent']中
最後將item輸出
在scrapy中,yield item後,item會提交給scrapy引擎,再又引擎發送給pipeline處理。pipeline一會再說。
接下來的代碼就是處理翻頁。這裏的頁面翻頁是利用js提交請求完成的,提交請求後,會response一個新的頁面列表
clipboard.png

首先利用xpath找到頁面導航欄的節點,在獲取到的全部節點中作for in循環,直到找到帶有「下一頁」的節點,這裏有下一頁的頁碼,仍是利用正則表達式來獲得它,並將其轉爲int類型。數據庫

yield FormRequest(postUrl,formdata=formdata, callback=self.parse)
利用scrpay自帶的FormRequest發送post請求,這裏的formdata是跟蹤post請求時獲得的,要根據本身的網站調整,callback指示講獲得的response反饋給parse函數處理(也就是新的一頁列表)
到此爲止,就是spider文件的全部,這個文件惟一對外的輸出就是item,它會有scrapy引擎轉給pipeline處理

3. pipeline

代碼

from scrapy import signals
from scrapy.contrib.exporter import CsvItemExporter
from jlsh_notice import settings
import pymysql
import time

import smtplib
from email.mime.text import MIMEText
from email.utils import formataddr


class JlshNoticePipeline(object):
    def process_item(self, item, spider):
        return item


# 用於數據庫存儲
class MySQLPipeline(object):
    def process_item(self, item, spider):
#=======================================================
        self.connect = pymysql.connect(
            host=settings.MYSQL_HOST,
            port=3306,
            db=settings.MYSQL_DBNAME,
            user=settings.MYSQL_USER,
            passwd=settings.MYSQL_PASSWD,
            charset='utf8',
            use_unicode=True)

        # 經過cursor執行增刪查改
        self.cursor = self.connect.cursor()
#=======================================================
        # 查重處理
        self.cursor.execute(
            """select * from jlsh_weblist where 
                noticeType = %s and 
                noticeTitle = %s and
                noticeDate = %s """,
            (item['noticeType'], item['noticeTitle'], item['noticeDate']))
        # 是否有重複數據
        repetition = self.cursor.fetchone()
#=======================================================
        # 重複
        if repetition:
            print('===== Pipelines.MySQLPipeline ===== 數據重複,跳過,繼續執行..... =====')
        else:
            # 插入數據
            content_html = ''
            for p in item['noticeContent']:
                content_html = content_html + '<p>' + p + '</p>'

            self.cursor.execute(
                """insert into jlsh_weblist(noticeType, noticeTitle, noticeDate, noticeUrl, noticeContent, record_time)
                value (%s, %s, %s, %s, %s, %s)""",
                (item['noticeType'], item['noticeTitle'], item['noticeDate'], item['noticeUrl'], content_html, time.localtime(time.time())))

            try:
                # 提交sql語句
                self.connect.commit()
                print('===== Insert Success ! =====', \
                    item['noticeType'], item['noticeTitle'], item['noticeDate'], item['noticeUrl'])
            except Exception as error:
                # 出現錯誤時打印錯誤日誌
                print('===== Insert error: %s ====='%error)
#=======================================================
            #定向發送郵件
            if settings.SEND_MAIL:
                sender='***@***.com'    # 發件人郵箱帳號
                password = '********'              # 發件人郵箱密碼
                receiver='*****@*****.com'      # 收件人郵箱帳號,我這邊發送給本身
                title = item['noticeTitle']
                content = """
                    <p>%s</p>
                    <p><a href="%s">%s</a></p>
                    <p>%s</p>
                    %s
                    """ % (item['noticeType'], item['noticeUrl'], item['noticeTitle'], item['noticeDate'], content_html)

                ret=self.sendMail(sender, password, receiver, title, content)
                if ret:
                    print("郵件發送成功")
                else:
                    print("郵件發送失敗")
                pass
            
        self.connect.close()
        return item
#=======================================================
    def sendMail(self, sender, password, receiver, title, content):
        ret=True
        try:
            msg=MIMEText(content,'html','utf-8')
            msg['From']=formataddr(['', sender])            # 括號裏的對應發件人郵箱暱稱、發件人郵箱帳號
            msg['To']=formataddr(["",receiver])             # 括號裏的對應收件人郵箱暱稱、收件人郵箱帳號
            msg['Subject']="郵件的主題 " + title    # 郵件的主題,也能夠說是標題
    
            server=smtplib.SMTP("smtp.*****.***", 25)  # 發件人郵箱中的SMTP服務器,端口是25
            server.login(sender, password)  # 括號中對應的是發件人郵箱帳號、郵箱密碼
            server.sendmail(sender,[receiver,],msg.as_string())  # 括號中對應的是發件人郵箱帳號、收件人郵箱帳號、發送郵件
            server.quit()  # 關閉鏈接
        except Exception:  # 若是 try 中的語句沒有執行,則會執行下面的 ret=False
            ret=False
        return ret
#=======================================================

說明

這裏的pipeline是我本身創建的,寫好後在setting中改一下設置就能夠了。由於scrapy的去重機制只針對爬蟲一次運行過程有效,屢次循環爬取就不行了,因此爲了實現不爬取重複數據,使用mysql就是比較靠譜的選擇了。
pymysql是python連接mysql的包,沒有的話pip安裝便可。
首先創建一個pymysql.connect實例,將鏈接mysql的幾個參數寫進去,我這裏是先寫到setting裏面再導入,也能夠直接寫,值得注意的是port參數(默認是3306)不要加引號,由於它必須是int類型的。
接下來創建一個cursor實例,用於對數據表進行增刪查改。
cursor.execute() 方法是定義要執行的sql命令,這裏注意就是execute只是定義,不是執行。
cursor.fetchone() 方法是執行sql並返回成功與否的結果。這裏進行了數據查詢,若是可以查到,說明這條記錄已經創建,若是沒有,就能夠新增數據了。
由mysql數據庫不接受list類型的數據,因此接下來要對item['noticeContent']作一下處理(他是list類型的,還記得麼^_^)。在item['noticeContent']中作for in循環,把他的每一項內容用<p>標籤包起來,組成一個長字符串。
接下來仍是寫sql命令:insert into .....
寫完之後用connect.commit()提交執行
最後就是發送郵件了,自建一個sendMail函數,發送郵件用到了兩個python包:smtplib 和 email,具體沒啥說的,照着寫就好了,我是一次成功。。
到此爲止,就能夠運行爬蟲了,能夠看到數據庫中已經有了爬取的內容。。。

clipboard.png

4. settings.py

註冊pipeline
ITEM_PIPELINES = {
    'jlsh_notice.pipelines.MySQLPipeline': 300,
}
log輸出的定義,四個任選其一
LOG_LEVEL = 'INFO'
LOG_LEVEL = 'DEBUG'
LOG_LEVEL = 'WARNING'
LOG_LEVEL = 'CRITICAL'
關於爬蟲終止條件的定義,默認不設置
#在指定時間事後,就終止爬蟲程序.
CLOSESPIDER_TIMEOUT = 60

#抓取了指定數目的Item以後,就終止爬蟲程序.
CLOSESPIDER_ITEMCOUNT = 10

#在收到了指定數目的響應以後,就終止爬蟲程序.
CLOSESPIDER_PAGECOUNT = 100

#在發生了指定數目的錯誤以後,就終止爬蟲程序.
CLOSESPIDER_ERRORCOUNT = 100

5. 實現自動執行

(1) 同時執行多個爬蟲。

首先,在項目目錄下創建新的目錄(與spider目錄同級),名爲「commands」,內建兩個文件:服務器

__init__.py (空文件,可是要有)
crawlall.py
from scrapy.commands import ScrapyCommand
from scrapy.utils.project import get_project_settings
 
 
class Command(ScrapyCommand):
 
    requires_project = True
 
    def syntax(self):
        return '[options]'
 
    def short_desc(self):
        return 'Runs all of the spiders'
 
    def run(self, args, opts):
        spider_list = self.crawler_process.spiders.list()
        for name in spider_list:
            self.crawler_process.crawl(name, **opts.__dict__)
        self.crawler_process.start()
而後在項目目錄下創建一個setup.py文件
from setuptools import setup, find_packages

setup(name='scrapy-mymodule',
    entry_points={
        'scrapy.commands': [
            'crawlall=jlsh_notice.commands:crawlall',
            ],
        },
    )
這個時候,在scrapy項目目錄下執行scrapy crawlall便可運行全部的爬蟲

(2) 每隔一段時間運行爬蟲。

在項目目錄下新建一個noticeStart.py文件(名稱任意),利用python中的os和time包實現每隔一段時間運行一個命令。網絡

import time
import os

while True:
    os.system("scrapy crawlall")
    remindTime = 5
    remindCount = 0
    sleepTime = 60
    while remindCount * remindTime < sleepTime:
        time.sleep(remindTime*60)
        remindCount = remindCount + 1
        print('已等待%s分鐘,距離下一次蒐集數據還有%s分鐘......'%(remindCount*remindTime,(sleepTime/remindTime-(remindCount))*remindTime))

(3) 實現開機運行。

首先:因爲cmd命令打開目錄在c盤,個人scrapy項目在e盤,因此要作一個bat文件跳轉目錄並運行py文件app

autorun.bat
e:
cd e:\PythonProjects\ScrapyProjects\jlsh_notice\jlsh_notice\
python noticeStart.py

其次:打開計劃任務程序,建立基本任務,運行程序選擇剛剛的bat文件,值得說明的是,計劃任務觸發器不要設置啓動後當即執行,否則可能會失敗,要延遲1分鐘運行。

clipboard.png

到此爲止,全部的代碼完成,之後還要根據實際狀況增長更多的通知類別,也能夠根據不一樣領導的關注點不一樣,分別發送郵件提醒。歡迎有興趣的朋友留言交流。。。

相關文章
相關標籤/搜索