使用Python3編寫(xiě)一個(gè)爬蟲(chóng)

使用Python3編寫(xiě)一個(gè)爬蟲(chóng)

需求簡(jiǎn)介

最近廠里有一個(gè)新聞采集類的需求,細(xì)節(jié)大體如下:

  1. 模擬登錄一個(gè)內(nèi)網(wǎng)網(wǎng)站(SSO)
  2. 抓取新聞(支持代理服務(wù)器的方式訪問(wèn))
  3. 加工內(nèi)容樣式,以適配手機(jī)屏幕
  4. 將正文中的圖片轉(zhuǎn)存到自已的服務(wù)器,并替換img標(biāo)簽中的url
  5. 圖片存儲(chǔ)服務(wù)器需要復(fù)用已有的FastDFS分布式文件系統(tǒng)
  6. 采集結(jié)果導(dǎo)入生產(chǎn)庫(kù)
  7. 支持日志打印

初學(xué)Python3,正好用這個(gè)需求練練手,最后很驚訝的是只用200多行代碼就實(shí)現(xiàn)了,如果換成Java的話大概需要1200行吧。果然應(yīng)了那句老話:人生苦短,我用Python

登錄頁(yè)面抓包

第一步當(dāng)然是抓包,然后再根據(jù)抓到的內(nèi)容,模擬進(jìn)行HTTP請(qǐng)求。

常用的抓包工具,有Mac下的Charles和Windows下的Fiddler。
它們的原理都是在本機(jī)開(kāi)一個(gè)HTTP或SOCKS代理服務(wù)器端口,然后將瀏覽器的代理服務(wù)器設(shè)置成這個(gè)端口,這樣瀏覽器中所有的HTTP請(qǐng)求都會(huì)先經(jīng)過(guò)抓包工具記錄下來(lái)了。

這里推薦盡量使用Fiddler,原因是Charles對(duì)于cookie的展示是有bug的,舉個(gè)例子,真實(shí)情況:請(qǐng)求A返回了LtpaToken這個(gè)cookie,請(qǐng)求B中返回了sid這個(gè)cookie。但在Charles中的展示是:請(qǐng)求A中已經(jīng)同時(shí)返回了LtpaToken和sid兩個(gè)cookie,這就很容易誤導(dǎo)人了。
另外Fiddler現(xiàn)在已經(jīng)有了Linux的Beta版本,貌似是用類似wine的方式實(shí)現(xiàn)的。

如果網(wǎng)站使用了單點(diǎn)登錄,可能會(huì)涉及到手工生成cookie。所以不僅需要分析每一條HTTP請(qǐng)求的request和response,以及帶回來(lái)的cookie,還要對(duì)頁(yè)面中的javascript進(jìn)行分析,看一下是如何生成cookie的。

模擬登錄

將頁(yè)面分析完畢之后,就可以進(jìn)行模擬HTTP請(qǐng)求了。
這里有兩個(gè)非常好用的第三方庫(kù), requestBeautifulSoup

requests 庫(kù)是用來(lái)代替urllib的,可以非常人性化的的生成HTTP請(qǐng)求,模擬session以及偽造cookie更是方便。
BeautifulSoup 用來(lái)代替re模塊,進(jìn)行HTML內(nèi)容解析,可以用tag, class, id來(lái)定位想要提取的內(nèi)容,也支持正則表達(dá)式等。

具體的使用方式直接看官方文檔就可以了,寫(xiě)的非常詳細(xì),這里直接給出地址:
requests官方文檔
BeautifulSoup官方文檔

通過(guò)pip3來(lái)安裝這兩個(gè)模塊:

sudo apt-get install python3-pip
sudo pip3 install requests
sudo pip3 install beautifulsoup4

導(dǎo)入模塊:

import requests
from bs4 import BeautifulSoup

模擬登錄:

def sso_login():
    # 調(diào)用單點(diǎn)登錄工號(hào)認(rèn)證頁(yè)面
    response = session.post(const.SSO_URL,
                            data={'login': const.LOGIN_USERNAME, 'password': const.LOGIN_PASSWORD, 'appid': 'np000'})

    # 分析頁(yè)面,取token及l(fā)tpa
    soup = BeautifulSoup(response.text, 'html.parser')
    token = soup.form.input.get('value')
    ltpa = soup.form.input.input.input.get('value')
    ltpa_value = ltpa.split(';')[0].split('=', 1)[1]

    # 手工設(shè)置Cookie
    session.cookies.set('LtpaToken', ltpa_value, domain='unicom.local', path='/')

    # 調(diào)用云門戶登錄頁(yè)面(2次)
    payload = {'token': token}
    session.post(const.LOGIN_URL, data=payload, proxies=const.PROXIES)
    response = session.post(const.LOGIN_URL, data=payload, proxies=const.PROXIES)
    if response.text == "success":
        logging.info("登錄成功")
        return True
    else:
        logging.info("登錄失敗")
        return False

這里用到了BeautifulSoup進(jìn)行HTML解析,取出頁(yè)面中的token、ltpa等字段。
然后使用session.cookies.set偽造了一個(gè)cookie,注意其中的domain參數(shù),設(shè)置成1級(jí)域名。
然后用這個(gè)session,去調(diào)用網(wǎng)站頁(yè)面,換回sid這個(gè)token。并可以根據(jù)頁(yè)面的返回信息,來(lái)簡(jiǎn)單判斷一下成功還是失敗。

列表頁(yè)面抓取

登錄成功之后,接下來(lái)的列表頁(yè)面抓取就要簡(jiǎn)單的多了,不考慮分頁(yè)的話,直接取一個(gè)list出來(lái)遍歷即可。

def capture_list(list_url):
    response = session.get(list_url, proxies=const.PROXIES)
    response.encoding = "UTF-8"
    soup = BeautifulSoup(response.text, 'html.parser')
    news_list = soup.find('div', 'xinwen_list').find_all('a')
    news_list.reverse()
    logging.info("開(kāi)始采集")
    for news_archor in news_list:
        news_cid = news_archor.attrs['href'].split('=')[1]
        capture_content(news_cid)
    logging.info("結(jié)束采集")

這里使用了response.encoding = "UTF-8"來(lái)手工解決亂碼問(wèn)題。

新聞頁(yè)面抓取

新聞頁(yè)面抓取,涉及到插臨時(shí)表,這里沒(méi)有使用每三方庫(kù),直接用SQL方式插入。
其中涉及到樣式處理與圖片轉(zhuǎn)存,另寫(xiě)一個(gè)模塊pconvert來(lái)實(shí)現(xiàn)。

def capture_content(news_cid):
    # 建立DB連接
    conn = mysql.connector.connect(user=const.DB_USERNAME, password=const.DB_PASSWORD, host=const.DB_HOST,
                                   port=const.DB_PORT, database=const.DB_DATABASE)
    cursor = conn.cursor()

    # 判斷是否已存在
    cursor.execute('select count(*) from material_prepare where news_cid = %s', (news_cid,))
    news_count = cursor.fetchone()[0]
    if news_count > 0:
        logging.info("采集" + news_cid + ':已存在')
    else:
        logging.info("采集" + news_cid + ':新增')
        news_url = const.NEWS_BASE_URL + news_cid
        response = session.post(news_url, proxies=const.PROXIES)
        response.encoding = "UTF-8"
        soup = BeautifulSoup(response.text, 'html.parser')
        # logging.info(soup)
        news_title = soup.h3.text.strip()[:64]
        news_brief = soup.find('div', 'brief').p.text.strip()[:100]
        news_author = soup.h5.span.a.text.strip()[:100]
        news_content = soup.find('table', 'unis_detail_content').tr.td.prettify()[66:-7].strip()
        # 樣式處理
        news_content = pconvert.convert_style(news_content)
        # 將圖片轉(zhuǎn)存至DFS并替換URL
        news_content = pconvert.convert_img(news_content)
        # 入表
        cursor.execute(
            'INSERT INTO material_prepare (news_cid, title, author, summary, content, add_time, status)  VALUES  (%s, %s, %s, %s, %s, now(), "0")'
            , [news_cid, news_title, news_author, news_brief, news_content])
    # 提交
    conn.commit()
    cursor.close()

樣式處理

文本樣式處理,還是要用到BeautifulSoup,因?yàn)樵颊军c(diǎn)上的新聞內(nèi)容樣式是五花八門的,根據(jù)實(shí)際情況,一邊寫(xiě)一個(gè)test函數(shù)來(lái)生成文本,一邊在瀏覽器上慢慢調(diào)試。

def convert_style(rawtext):
    newtext = '<div style="margin-left: 0px; margin-right:0px; letter-spacing: 1px; word-spacing:2px;line-height: 1.7em; font-size:18px;text-align:justify; text-justify:inter-ideograph">' \
              + rawtext + '</div>'
    newtext = newtext.replace(' align="center"', '')
    soup = BeautifulSoup(newtext, 'html.parser')
    img_tags = soup.find_all("img")
    for img_tag in img_tags:
        del img_tag.parent['style']
    return soup.prettify()

圖片轉(zhuǎn)存至DFS

因?yàn)樵颊军c(diǎn)是在內(nèi)網(wǎng)中的,采集下來(lái)的HTML中,<img>標(biāo)簽的地址是內(nèi)網(wǎng)地址,所以在公網(wǎng)中是展現(xiàn)不出來(lái)的,需要將圖片轉(zhuǎn)存,并用新的URL替換原有的URL。

def convert_img(rawtext):
    soup = BeautifulSoup(rawtext, 'html.parser')
    img_tags = soup.find_all("img")
    for img_tag in img_tags:
        raw_img_url = img_tag['src']
        dfs_img_url = convert_url(raw_img_url)
        img_tag['src'] = dfs_img_url
        del img_tag['style']
    return soup.prettify()

圖片轉(zhuǎn)存最簡(jiǎn)單的方式是保存成本地的文件,然后再通過(guò)nginx或httpd服務(wù)將圖片開(kāi)放出去:

pic_name = raw_img_url.split('/')[-1]
pic_path = TMP_PATH + '/' + pic_name
with open(pic_path, 'wb') as pic_file:
   pic_file.write(pic_content)

但這里我們需要復(fù)用已有的FastDFS分布式文件系統(tǒng),要用到它的一個(gè)客戶端的庫(kù)fdfs_client-py
fdfs_client-py不能直接使用pip3安裝,需要直接使用一個(gè)python3版的源碼,并手工修改其中代碼。操作過(guò)程如下:

git clone https://github.com/jefforeilly/fdfs_client-py.git
cd dfs_client-py
vi ./fdfs_client/storage_client.py
將第12行 from fdfs_client.sendfile import * 注釋掉
python3 setup.py install

sudo pip3 install mutagen

客戶端的使用上沒(méi)有什么特別的,直接調(diào)用upload_by_buffer,傳一個(gè)圖片的buffer進(jìn)去就可以了,成功后會(huì)返回自動(dòng)生成的文件名。

from fdfs_client.client import *
dfs_client = Fdfs_client('conf/dfs.conf')
def convert_url(raw_img_url):
    response = requests.get(raw_img_url, proxies=const.PROXIES)
    pic_buffer = response.content
    pic_ext = raw_img_url.split('.')[-1]
    response = dfs_client.upload_by_buffer(pic_buffer, pic_ext)
    dfs_img_url = const.DFS_BASE_URL + '/' + response['Remote file_id']
    return dfs_img_url

其中dfs.conf文件中,主要就是配置一下 tracker_server

日志處理

這里使用配置文件的方式處理日志,類似JAVA中的log4j吧,首先新建一個(gè)log.conf

[loggers]
keys=root

[handlers]
keys=stream_handler,file_handler

[formatters]
keys=formatter

[logger_root]
level=DEBUG
handlers=stream_handler,file_handler

[handler_stream_handler]
class=StreamHandler
level=DEBUG
formatter=formatter
args=(sys.stderr,)

[handler_file_handler]
class=FileHandler
level=DEBUG
formatter=formatter
args=('logs/pspider.log','a','utf8')

[formatter_formatter]
format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s

這里通過(guò)配置handlers,可以同時(shí)將日志打印到stderr和文件。
注意args=('logs/pspider.log','a','utf8') 這一行,用來(lái)解決文本文件中的中文亂碼問(wèn)題。

日志初始化:

import logging
from logging.config import fileConfig

fileConfig('conf/log.conf')

日志打?。?/p>

logging.info("test")

完整源碼

到此為止,就是如何用Python3寫(xiě)一個(gè)爬蟲(chóng)的全部過(guò)程了。
采集不同的站點(diǎn),肯定是要有不同的處理,但方法都是大同小異。
最后,將源碼做了部分裁剪,分享在了GitHub上。
https://github.com/xiiiblue/pspider

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

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,979評(píng)論 25 709
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,545評(píng)論 19 139
  • HTTP cookie(也稱為web cookie,網(wǎng)絡(luò)cookie,瀏覽器cookie或者簡(jiǎn)稱cookie)是網(wǎng)...
    留七七閱讀 18,378評(píng)論 2 71
  • 遇見(jiàn) 夏至已至 流火七月 任暴風(fēng)驟雨猝然來(lái)襲 任驕陽(yáng)烈日烘烤大地 初心不忘 激情持續(xù) 讓燦爛笑容從心底洋溢 世間事...
    梁子仗劍走天下閱讀 624評(píng)論 3 11
  • 今天午睡,從13點(diǎn)一直睡到18點(diǎn),睡得天昏地暗、迷迷糊糊、不分白晝,醒了之后狀態(tài)也不是很好,不太精神,腦子像漿糊,...
    老鹿在跑步閱讀 367評(píng)論 0 1

友情鏈接更多精彩內(nèi)容