Python 爬蟲獲取某貼吧所有成員使用者名稱
最近想用Python爬蟲搞搞百度貼吧的操作,所以我得把原來申請的小號找出來用。有一個小號我忘了具體ID,只記得其中幾個字母以及某個加入的貼吧。所以今天就用爬蟲來獲取C語言貼吧的所有成員。
計劃很簡單,爬百度貼吧的會員頁面,把結果存到MySQL資料庫中,等到所有會員都爬完之後。我就可以使用簡單的SQL語句查詢賬號名了。由於C語言貼吧會員有50多萬,所以我還需要在合適的時候(例如插入資料庫失敗)把錯誤資訊列印到日誌檔案中。由於我是Python新手,所以就不弄什麼多執行緒得了,直接一個指令碼用到黑。
看著很簡單,實際也很簡單。寫完了我看了一下,用到的知識只有最基礎的SQL操作、BeautifulSoup解析。
首先第一步就是看一下這個吧的資訊頁有多少頁,關鍵程式碼如下。踩了兩天坑,總算感覺對BeautifulSoup熟悉了一點。程式碼也很簡單,按照class名查詢到總頁數這個標籤,然後用正則表示式匹配到頁數數字。這裡要說一下,正則表示式的分組真好用。以前偷懶只學了一點正則表示式,發現沒啥作用,只有配合分組才能比較精確的查詢字元。
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+)頁')
result = p.match(page_span.string)
global total_pages
total_pages = int(result.group(1))
logger.info(f'會員共{total_pages}頁')
有了總頁數,我們就可以遍歷頁面了,程式碼如下。寫的雖然比較髒,但是能用就行了,大家嫌難看就難看吧。這裡做的事情就很簡單了,從第一頁開始遍歷,一直遍歷到最後一頁。把每一頁的使用者名稱字提取出來,然後用_insert_table(connection, name)
因為我為了省事,直接把百度使用者名稱當做主鍵了。但是保不齊貼吧有什麼bug,導致使用者名稱重複之類的問題,導致插入失敗。所以我用try把儲存這一塊包起來。有異常的話就列印到日誌中,方便排查。日誌分成兩種級別的,INFO級別輸出到控制檯,ERROR級別輸出到檔案。
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}頁')
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}頁{index}第個使用者 {name} 發生異常')
完整的程式碼見下。
"""
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+)頁')
result = p.match(page_span.string)
global total_pages
total_pages = int(result.group(1))
logger.info(f'會員共{total_pages}頁')
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}頁')
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}頁{index}第個使用者 {name} 發生異常')
import datetime
if __name__ == '__main__':
_get_total_pages()
_find_all_users()
還有另一個檔案用來配置日誌的。你也可以把這兩個檔案合在一起,只不過看著可能更亂了。
import logging
# 建立Logger
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
# 建立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)
效能測試
當然由於要爬的資料量比較大,我們還要計算一下可能的執行時間。首先不考慮爬蟲被百度封了的情況。我把程式碼稍作修改,設定只爬前100頁。
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)
結果如下,用時將近兩分鐘。做了簡單計算得出結論,要爬完c語言貼吧的52萬個會員,需要將近7個小時。所以程式還需要改進。
2017-04-04 23:57:59.197993
2017-04-04 23:56:10.064666
0:01:49.133327
首先先從資料庫方面考慮一下。Windows下MySQL預設的資料庫引擎是Innodb,特點是支援事務管理、外來鍵、行級鎖,但是相應的速度比較慢。我把表重新建為MyISAM型別的。然後重新執行一下測試,看看這次速度會不會有變化。
CREATE TABLE tieba_member (
username CHAR(255) PRIMARY KEY
)
ENGINE = MyISAM
這次效能提升的有點快,速度足足提高了76%。可見預設的並不一定是最好的。
2017-04-05 00:15:19.989766
2017-04-05 00:14:53.407476
0:00:26.582290
既然都開始測試了,不妨乾脆點。MySQL還有一種引擎是Memory,直接把資料放到記憶體中。速度肯定會更快!不過測試結果很遺憾,還是26秒。可見資料庫這方面的優化到頭了。
CREATE TABLE tieba_member (
username CHAR(255) PRIMARY KEY
)
ENGINE = MEMORY
不過效能確實提高了很多。經過計算,這次只需要一個半小時即可爬完52萬個使用者。如果在開多個程序,相信速度還會更快。所以這篇文章就差不多完成了。等明天爬完之後,我把結果更新一下,任務就真正完成了!
不過結果很遺憾,爬蟲失敗了。為了速度更快我開了4個程序,分別爬1-5000頁,5001-10000頁,10001-15000頁,以及15000-到最後4部分。但是日誌輸出顯示出現很多重複的使用者名稱,5000頁之後的使用者名稱竟然和第一頁相同。我百思不得其解,在使用瀏覽器測試發現,不知道是百度的防爬蟲機制還是bug之類的,瀏覽器只能顯示到450多頁,在往後就會顯示為空頁面,如果頁數更大,就一直返回第一頁的內容。因此依賴於這個頁面的貼吧爬蟲宣佈失敗。
雖然失敗了,但是還是學習到了不少經驗。我測試了一下爬前450頁,僅用時44秒。說明爬蟲速度倒是還星還行。
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'開始時間{time1}')
print(f'結束時間{time2}')
print(f'用時{time2 - time1}')