使用Python3編寫(xiě)一個(gè)爬蟲(chóng)
需求簡(jiǎn)介
最近廠里有一個(gè)新聞采集類的需求,細(xì)節(jié)大體如下:
- 模擬登錄一個(gè)內(nèi)網(wǎng)網(wǎng)站(SSO)
- 抓取新聞(支持代理服務(wù)器的方式訪問(wèn))
- 加工內(nèi)容樣式,以適配手機(jī)屏幕
- 將正文中的圖片轉(zhuǎn)存到自已的服務(wù)器,并替換img標(biāo)簽中的url
- 圖片存儲(chǔ)服務(wù)器需要復(fù)用已有的FastDFS分布式文件系統(tǒng)
- 采集結(jié)果導(dǎo)入生產(chǎn)庫(kù)
- 支持日志打印
初學(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)了。
-
Chrales
https://www.charlesproxy.com
-
Fiddler
http://www.telerik.com/fiddler
這里推薦盡量使用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ù), request 和 BeautifulSoup
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

