1. 程式人生 > >Python 爬蟲獲取某貼吧所有成員使用者名稱

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)

函式存到MySQL中。

因為我為了省事,直接把百度使用者名稱當做主鍵了。但是保不齊貼吧有什麼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}')