最近想用Python爬蟲搞搞百度貼吧的操作破讨,所以我得把原來(lái)申請(qǐng)的小號(hào)找出來(lái)用慨绳。有一個(gè)小號(hào)我忘了具體ID褒纲,只記得其中幾個(gè)字母以及某個(gè)加入的貼吧汤锨。所以今天就用爬蟲來(lái)獲取C語(yǔ)言貼吧的所有成員办铡。
計(jì)劃很簡(jiǎn)單辞做,爬百度貼吧的會(huì)員頁(yè)面琳要,把結(jié)果存到MySQL數(shù)據(jù)庫(kù)中,等到所有會(huì)員都爬完之后秤茅。我就可以使用簡(jiǎn)單的SQL語(yǔ)句查詢賬號(hào)名了稚补。由于C語(yǔ)言貼吧會(huì)員有50多萬(wàn),所以我還需要在合適的時(shí)候(例如插入數(shù)據(jù)庫(kù)失斏┥ )把錯(cuò)誤信息打印到日志文件中孔厉。由于我是Python新手,所以就不弄什么多線程得了帖努,直接一個(gè)腳本用到黑撰豺。
看著很簡(jiǎn)單,實(shí)際也很簡(jiǎn)單拼余。寫完了我看了一下污桦,用到的知識(shí)只有最基礎(chǔ)的SQL操作、BeautifulSoup解析匙监。
首先第一步就是看一下這個(gè)吧的信息頁(yè)有多少頁(yè)凡橱,關(guān)鍵代碼如下。踩了兩天坑亭姥,總算感覺(jué)對(duì)BeautifulSoup熟悉了一點(diǎn)稼钩。代碼也很簡(jiǎn)單,按照class名查找到總頁(yè)數(shù)這個(gè)標(biāo)簽达罗,然后用正則表達(dá)式匹配到頁(yè)數(shù)數(shù)字坝撑。這里要說(shuō)一下,正則表達(dá)式的分組真好用粮揉。以前偷懶只學(xué)了一點(diǎn)正則表達(dá)式巡李,發(fā)現(xiàn)沒(méi)啥作用,只有配合分組才能比較精確的查找字符扶认。
html = request.urlopen(base_url).read().decode(encoding)
soup = BeautifulSoup(html, 'lxml')
page_span = soup.find('span', class_='tbui_total_page')
p = re.compile(r'共(\d+)頁(yè)')
result = p.match(page_span.string)
global total_pages
total_pages = int(result.group(1))
logger.info(f'會(huì)員共{total_pages}頁(yè)')
有了總頁(yè)數(shù)侨拦,我們就可以遍歷頁(yè)面了,代碼如下辐宾。寫的雖然比較臟狱从,但是能用就行了,大家嫌難看就難看吧叠纹。這里做的事情就很簡(jiǎn)單了矫夯,從第一頁(yè)開(kāi)始遍歷,一直遍歷到最后一頁(yè)吊洼。把每一頁(yè)的用戶名字提取出來(lái),然后用_insert_table(connection, name)
函數(shù)存到MySQL中制肮。
因?yàn)槲覟榱耸∈旅扒希苯影寻俣扔脩裘?dāng)做主鍵了递沪。但是保不齊貼吧有什么bug,導(dǎo)致用戶名重復(fù)之類的問(wèn)題综液,導(dǎo)致插入失敗款慨。所以我用try把保存這一塊包起來(lái)。有異常的話就打印到日志中谬莹,方便排查檩奠。日志分成兩種級(jí)別的,INFO級(jí)別輸出到控制臺(tái)附帽,ERROR級(jí)別輸出到文件埠戳。
def _find_all_users():
global connection
for i in range(start_page, total_pages + 1):
target_url = f'{base_url}&pn={i}'
logger.info(f'正在分析第{i}頁(yè)')
html = request.urlopen(target_url).read().decode(encoding)
soup = BeautifulSoup(html, 'lxml')
outer_div = soup.find('div', class_='forum_info_section member_wrap clearfix bawu-info')
inner_spans = outer_div.find_all('span', class_='member')
for index, span in enumerate(inner_spans):
name_link = span.find('a', class_='user_name')
name = name_link.string
logger.info(f'已找到 {name}')
try:
_insert_table(connection, name)
except:
logger.error(f'第{i}頁(yè){index}第個(gè)用戶 {name} 發(fā)生異常')
完整的代碼見(jiàn)下。
"""
Python寫的百度貼吧工具
"""
import pymysql
host = 'localhost'
db_name = 'tieba'
username = 'root'
password = '12345678'
def _get_connection(host, username, password, db_name):
return pymysql.connect(host=host,
user=username,
password=password,
charset='utf8mb4',
db=db_name)
def _create_table(connection):
create_table_sql = """
CREATE TABLE tieba_member(
username CHAR(255) PRIMARY KEY
)
"""
with connection.cursor() as cursor:
cursor.execute(create_table_sql)
connection.commit()
def _insert_table(connection, username):
insert_table_sql = """
INSERT INTO tieba_member
VALUES(%s)"""
with connection.cursor() as cursor:
cursor.execute(insert_table_sql, (username,))
connection.commit()
import urllib.request as request
from bs4 import BeautifulSoup
import re
import tieba.log_config
import logging
logger = logging.getLogger()
encoding = 'GBK'
base_url = 'http://tieba.baidu.com/bawu2/platform/listMemberInfo?word=c%D3%EF%D1%D4'
# base_url = 'http://tieba.baidu.com/bawu2/platform/listMemberInfo?word=%B9%FD%C1%CB%BC%B4%CA%C7%BF%CD'
start_page = 1
total_pages = None
connection = _get_connection(host, username, password, db_name)
def _get_total_pages():
html = request.urlopen(base_url).read().decode(encoding)
soup = BeautifulSoup(html, 'lxml')
page_span = soup.find('span', class_='tbui_total_page')
p = re.compile(r'共(\d+)頁(yè)')
result = p.match(page_span.string)
global total_pages
total_pages = int(result.group(1))
logger.info(f'會(huì)員共{total_pages}頁(yè)')
def _find_all_users():
global connection
for i in range(start_page, total_pages + 1):
target_url = f'{base_url}&pn={i}'
logger.info(f'正在分析第{i}頁(yè)')
html = request.urlopen(target_url).read().decode(encoding)
soup = BeautifulSoup(html, 'lxml')
outer_div = soup.find('div', class_='forum_info_section member_wrap clearfix bawu-info')
inner_spans = outer_div.find_all('span', class_='member')
for index, span in enumerate(inner_spans):
name_link = span.find('a', class_='user_name')
name = name_link.string
logger.info(f'已找到 {name}')
try:
_insert_table(connection, name)
except:
logger.error(f'第{i}頁(yè){index}第個(gè)用戶 {name} 發(fā)生異常')
import datetime
if __name__ == '__main__':
_get_total_pages()
_find_all_users()
還有另一個(gè)文件用來(lái)配置日志的蕉扮。你也可以把這兩個(gè)文件合在一起整胃,只不過(guò)看著可能更亂了。
import logging
# 創(chuàng)建Logger
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# 創(chuàng)建Handler
# 終端Handler
consoleHandler = logging.StreamHandler()
consoleHandler.setLevel(logging.DEBUG)
# 文件Handler
fileHandler = logging.FileHandler('log.log', mode='a', encoding='UTF-8')
fileHandler.setLevel(logging.ERROR)
# Formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
consoleHandler.setFormatter(formatter)
fileHandler.setFormatter(formatter)
# 添加到Logger中
logger.addHandler(consoleHandler)
logger.addHandler(fileHandler)
性能測(cè)試
當(dāng)然由于要爬的數(shù)據(jù)量比較大喳钟,我們還要計(jì)算一下可能的運(yùn)行時(shí)間屁使。首先不考慮爬蟲被百度封了的情況。我把代碼稍作修改奔则,設(shè)定只爬前100頁(yè)蛮寂。
import datetime
if __name__ == '__main__':
# _get_total_pages()
total_pages = 100
time1 = datetime.datetime.today()
_find_all_users()
time2 = datetime.datetime.today()
print(time2)
print(time1)
print(time2 - time1)
結(jié)果如下,用時(shí)將近兩分鐘易茬。做了簡(jiǎn)單計(jì)算得出結(jié)論酬蹋,要爬完c語(yǔ)言貼吧的52萬(wàn)個(gè)會(huì)員,需要將近7個(gè)小時(shí)疾呻。所以程序還需要改進(jìn)除嘹。
2017-04-04 23:57:59.197993
2017-04-04 23:56:10.064666
0:01:49.133327
首先先從數(shù)據(jù)庫(kù)方面考慮一下。Windows下MySQL默認(rèn)的數(shù)據(jù)庫(kù)引擎是Innodb岸蜗,特點(diǎn)是支持事務(wù)管理尉咕、外鍵、行級(jí)鎖璃岳,但是相應(yīng)的速度比較慢年缎。我把表重新建為MyISAM類型的。然后重新運(yùn)行一下測(cè)試铃慷,看看這次速度會(huì)不會(huì)有變化单芜。
CREATE TABLE tieba_member (
username CHAR(255) PRIMARY KEY
)
ENGINE = MyISAM
這次性能提升的有點(diǎn)快,速度足足提高了76%犁柜≈摒可見(jiàn)默認(rèn)的并不一定是最好的。
2017-04-05 00:15:19.989766
2017-04-05 00:14:53.407476
0:00:26.582290
既然都開(kāi)始測(cè)試了,不妨干脆點(diǎn)扒腕。MySQL還有一種引擎是Memory绢淀,直接把數(shù)據(jù)放到內(nèi)存中。速度肯定會(huì)更快瘾腰!不過(guò)測(cè)試結(jié)果很遺憾皆的,還是26秒√E瑁可見(jiàn)數(shù)據(jù)庫(kù)這方面的優(yōu)化到頭了费薄。
CREATE TABLE tieba_member (
username CHAR(255) PRIMARY KEY
)
ENGINE = MEMORY
不過(guò)性能確實(shí)提高了很多。經(jīng)過(guò)計(jì)算栖雾,這次只需要一個(gè)半小時(shí)即可爬完52萬(wàn)個(gè)用戶楞抡。如果在開(kāi)多個(gè)進(jìn)程,相信速度還會(huì)更快岩灭。所以這篇文章就差不多完成了拌倍。等明天爬完之后,我把結(jié)果更新一下噪径,任務(wù)就真正完成了柱恤!
不過(guò)結(jié)果很遺憾,爬蟲失敗了找爱。為了速度更快我開(kāi)了4個(gè)進(jìn)程梗顺,分別爬1-5000頁(yè),5001-10000頁(yè)车摄,10001-15000頁(yè)寺谤,以及15000-到最后4部分。
但是日志輸出顯示出現(xiàn)很多重復(fù)的用戶名吮播,5000頁(yè)之后的用戶名竟然和第一頁(yè)相同变屁。我百思不得其解,在使用瀏覽器測(cè)試發(fā)現(xiàn)意狠,不知道是百度的防爬蟲機(jī)制還是bug之類的粟关,瀏覽器只能顯示到450多頁(yè),在往后就會(huì)顯示為空頁(yè)面环戈,如果頁(yè)數(shù)更大闷板,就一直返回第一頁(yè)的內(nèi)容。因此依賴于這個(gè)頁(yè)面的貼吧爬蟲宣布失敗院塞。
雖然失敗了遮晚,但是還是學(xué)習(xí)到了不少經(jīng)驗(yàn)。我測(cè)試了一下爬前450頁(yè)拦止,僅用時(shí)44秒县遣。說(shuō)明爬蟲速度倒是還星還行。
import datetime
from multiprocessing import Process
if __name__ == '__main__':
total_pages = _get_total_pages()
processes = []
processes.append(Process(target=_find_all_users, args=(1, 150)))
processes.append(Process(target=_find_all_users, args=(151, 300)))
processes.append(Process(target=_find_all_users, args=(301, 450)))
time1 = datetime.datetime.today()
for process in processes:
process.start()
for process in processes:
process.join()
time2 = datetime.datetime.today()
print(f'開(kāi)始時(shí)間{time1}')
print(f'結(jié)束時(shí)間{time2}')
print(f'用時(shí){time2 - time1}')