python使用pywinauto驅(qū)動(dòng)微信客戶端實(shí)現(xiàn)公眾號(hào)爬蟲(chóng)
項(xiàng)目地址
https://github.com/fancyerii/wechat-gongzhonghao-crawler
pywinauto簡(jiǎn)介
pywinauto是一個(gè)python的工具,可以用于控制Windows的GUI程序。詳細(xì)的文檔可以參考這里。
WechatAutomator類(lèi)
自動(dòng)化微信的代碼封裝在了類(lèi)WechatAutomator里,完整的代碼可以參考這里。這里簡(jiǎn)要的介紹一下其中的主要方法:
init_window
這個(gè)方法完成類(lèi)的初始化,它的代碼為:
def init_window(self, exe_path=r"C:\Program Files (x86)\Tencent\WeChat\WeChat.exe", turn_page_interval=3, click_url_interval=1, win_width=1000, win_height=600): app = Application(backend="uia").connect(path=exe_path) self.main_win = app.window(title=u"微信", class_name="WeChatMainWndForPC") self.main_win.set_focus() self.app = app self.visible_top = 70 self.turn_page_interval = turn_page_interval self.click_url_interval = click_url_interval self.browser = None self.win_width = win_width self.win_height = win_height # 為了讓移動(dòng)窗口,同時(shí)使用非uia的backend,這是pywinauto的uia的一個(gè)bug self.app2 = Application().connect(path=exe_path) self.move_window()
我們首先來(lái)看函數(shù)的參數(shù):
- exe_path
- 微信程序的地址
- turn_page_interval
- 抓取翻頁(yè)時(shí)的時(shí)間間隔,默認(rèn)3s
- click_url_interval
- 在抓取一頁(yè)的url時(shí)的間隔,默認(rèn)1s
- win_width
- 設(shè)置窗口的寬度
- win_height
- 設(shè)置窗口的高度,如果顯示器的分辨率較大,可以設(shè)置的更加高一些,從而一頁(yè)包含的文章數(shù)更多一些,從而翻頁(yè)少一點(diǎn)。注意:一定要保證窗口完全可見(jiàn),也就是說(shuō)win_height不能大于實(shí)際分辨率的高度!
- 設(shè)置窗口的高度,如果顯示器的分辨率較大,可以設(shè)置的更加高一些,從而一頁(yè)包含的文章數(shù)更多一些,從而翻頁(yè)少一點(diǎn)。注意:一定要保證窗口完全可見(jiàn),也就是說(shuō)win_height不能大于實(shí)際分辨率的高度!
這個(gè)函數(shù)的主要功能是構(gòu)建Application對(duì)象從而通過(guò)pywinauto實(shí)現(xiàn)控制,這里使用的是uia的backend,然后設(shè)置窗口的大小并且把窗口移到最左上角。因?yàn)楦鶕?jù)so文章,pywinauto 0.6.8存在bug,只能通過(guò)win32的backend來(lái)移到窗口,所以構(gòu)造了self.app2然后調(diào)用move_window()函數(shù)把窗口移到最左上角。
crawl_gongzhonghao
這個(gè)函數(shù)實(shí)現(xiàn)了某個(gè)公眾號(hào)的文章抓取。它的基本控制邏輯如下:
- 首先通過(guò)搜索框根據(jù)名字搜索公眾號(hào)并且點(diǎn)擊它。
- 對(duì)于當(dāng)前頁(yè)點(diǎn)擊所有的鏈接并且下載其內(nèi)容。
- 使用PAGE_DOWN鍵往下翻頁(yè)
- 需要判斷是否繼續(xù)抓取
第一個(gè)是通過(guò)locate_user函數(shù)實(shí)現(xiàn),后面會(huì)介紹。第二個(gè)是通過(guò)process_page函數(shù)實(shí)現(xiàn),后面也會(huì)介紹。判斷是否繼續(xù)抓取的邏輯為:
- 如果翻頁(yè)超過(guò)max_pages,則停止抓取
- 如果碰到某個(gè)url曾經(jīng)抓取過(guò),那說(shuō)明之前的文章都已經(jīng)抓取過(guò)了,則停止抓取
- 如果lastest_date不是None并且一篇文章的發(fā)布日期早于它,則停止抓取
所以我們通常會(huì)在第一次抓取的時(shí)候把max_pages設(shè)置的很大(比如100),然后通過(guò)latest_date來(lái)抓到指定的日期。而之后的抓取則設(shè)置max_pages為較小的值(比如默認(rèn)的6),這樣只要爬蟲(chóng)在兩次抓取之間公眾號(hào)的更新不超過(guò)6頁(yè),那么就不會(huì)漏掉文章。具體的邏輯可以參考main.py,它會(huì)把抓取的文章通過(guò)http請(qǐng)求發(fā)給Server,并且每次抓取的時(shí)候從Server查詢抓取過(guò)的文章存放到states這個(gè)list里states[i][“url”]就存儲(chǔ)了第i篇文章的url。
def crawl_gongzhonghao(self, account_name, articles, states, detail,max_pages=6, latest_date=None, no_item_retry=3): logger.debug(account_name) if not self.locate_user(account_name): return False last_visited_titles = set() visited_urls = set() self.turn_page_up(min(20, max_pages * 2)) pagedown_retry = 0 last_visited_titles = [] for page in range(0, max_pages): items = [] last_visited_titles = self.process_page(account_name, items, last_visited_titles, states, visited_urls, detail) articles.extend(items) if len(items) == 0: pagedown_retry += 1 if pagedown_retry >= no_item_retry: s = "break because of retry {}".format(pagedown_retry) logger.debug(s) WechatAutomator.add_to_detail(s, detail) break else: pagedown_retry = 0 if len(items) > 0 and latest_date is not None: html = items[-1][-1] pub_date = WechatAutomator.get_pubdate(html) if pub_date and pub_date < latest_date: s = "stop because {} < {}".format(pub_date, latest_date) logger.debug(s) WechatAutomator.add_to_detail(s, detail) break url_exist = False for item in items: if WechatAutomator.url_in_states(item[0], states): s = "stop because url exist {}".format(item[0]) logger.debug(s) WechatAutomator.add_to_detail(s, detail) url_exist = True break if url_exist: break self.click_right() self.main_win.type_keys("{PGDN}") time.sleep(self.turn_page_interval) self.turn_page_up(page * 2) return True
locate_user
locate_user函數(shù)的控制流程為:
- 找到左上角的搜索框并且點(diǎn)擊它獲得焦點(diǎn)
- 使用ctrl+a選中可能有的文字(之前的bug?)并且使用后退鍵刪除它們
- 輸入公眾號(hào)名稱(chēng)
- 在彈出的list里點(diǎn)擊這個(gè)公眾號(hào)名稱(chēng)從而進(jìn)入公眾號(hào)
def locate_user(self, user, retry=5): if not self.main_win: raise RuntimeError("you should call init_window first") search_btn = self.main_win.child_window(title="搜索", control_type="Edit") self.click_center(search_btn) self.main_win.type_keys("^a") self.main_win.type_keys("{BACKSPACE}") self.main_win.type_keys(user) for i in range(retry): time.sleep(1) try: search_list = self.main_win.child_window(title="搜索結(jié)果") match_result = search_list.child_window(title=user, control_type="ListItem") self.click_center(match_result) return True except: pass return False
這里主要就是通過(guò)child_window函數(shù)進(jìn)行定位,關(guān)于它的用法這里不介紹。關(guān)于怎么定位元素的方法可以使用Inspect.exe或者print_control_identifiers函數(shù),具體參考這里。
process_page
這個(gè)函數(shù)是最主要的抓取代碼,它處理當(dāng)前一頁(yè)的內(nèi)容,它的控制流程如下:
- 構(gòu)建當(dāng)前頁(yè)的tree
- 使用recursive_get函數(shù)遍歷這顆樹(shù)并且找到每篇文章對(duì)應(yīng)的element
- 遍歷每一篇文章
- 如果文章的名字在上一頁(yè)出現(xiàn)過(guò),則跳過(guò)
- 獲得這篇文章的坐標(biāo)信息
- 如果文章不可見(jiàn)(rect.top >= win_rect.bottom or rect.bottom <= self.visible_top)則跳過(guò)
- 計(jì)算點(diǎn)擊的坐標(biāo)
- 點(diǎn)擊文章打開(kāi)新的窗口
- 在新的窗口中點(diǎn)擊【復(fù)制鏈接】按鈕
- 從剪貼板復(fù)制鏈接url
- 通過(guò)url下載文章內(nèi)容并且parse發(fā)布日期
邏輯比較簡(jiǎn)單,但是有一些很trick的地方:
- 微信翻頁(yè)的實(shí)現(xiàn)
- 微信客戶端的翻頁(yè)和瀏覽器不同,它的內(nèi)容是累加的,比如第一頁(yè)3篇文章,往下翻一頁(yè)可能變成6篇文章,再翻可能變成9篇。這個(gè)時(shí)候這9篇文章都是在tree中的,只不過(guò)最后3篇的坐標(biāo)(top和bottom)是空間的。
- 能否點(diǎn)擊 一篇文章對(duì)應(yīng)的框(圖)可能是部分可見(jiàn)的,甚至它的top非常接近屏幕的最下方,這個(gè)時(shí)候可能點(diǎn)不了。如下圖所示:
與此類(lèi)似的是右上角的黑色頭部(不能滾到并且會(huì)遮擋)也有一定空間,如下圖所示:
- 點(diǎn)擊的位置
因?yàn)檫@個(gè)框可能很窄(bottom-top很小)并且可能在很靠上或者靠下的位置。所以有如下代碼:
# 計(jì)算可見(jiàn)的高度 visible_height = min(rect.bottom, win_rect.bottom) - max(rect.top, win_rect.top+self.visible_top) # 太窄的不點(diǎn)擊,希望下次翻頁(yè)后能顯示更多像素從而可以點(diǎn)擊, # 但是如果微信的某個(gè)文章的框的高度小于10個(gè)像素,那么這篇文章就無(wú)法被點(diǎn)擊 # 不過(guò)作者目前為發(fā)現(xiàn)這么窄的文章 if visible_height < 10: continue # 如果某個(gè)文章的框太大,則拋出異常,目前為止為發(fā)現(xiàn)這樣的問(wèn)題。 if rect.bottom - rect.top >= win_rect.bottom - self.visible_top: raise RuntimeError("{}-{}>={}-{}".format(rect.bottom, rect.top, win_rect.bottom, self.visible_top)) # 如果下部部分可見(jiàn),那么點(diǎn)擊上方是比較”安全“的 if rect.bottom >= win_rect.bottom: click_up = True # 如果下部完全可見(jiàn),則點(diǎn)擊下方是”安全“的 else: click_up = False
完整代碼如下:
def process_page(self, account_name, items, lastpage_clicked_titles, states, visited_urls, detail): clicked_titles = set() text = self.main_win.child_window(title=account_name, control_type="Text", found_index=0) parent = text while parent: parent = parent.parent() if '會(huì)話列表' == parent.element_info.name: break paths = [0, 2, 0, 0, 0, 1, 0] for idx in paths: parent = parent.children()[idx] elems = [] self.recursive_get(parent, elems) win_rect = self.main_win.rectangle() for elem in elems: rect = elem.rectangle() if elem.element_info.name in lastpage_clicked_titles: continue if rect.top >= win_rect.bottom or rect.bottom <= self.visible_top: continue visible_height = min(rect.bottom, win_rect.bottom) - max(rect.top, win_rect.top+self.visible_top) if visible_height < 10: continue if rect.bottom - rect.top >= win_rect.bottom - self.visible_top: raise RuntimeError("{}-{}>={}-{}".format(rect.bottom, rect.top, win_rect.bottom, self.visible_top)) if rect.bottom >= win_rect.bottom: click_up = True else: click_up = False if self.is_bad_elem(elem): s = "not good elem {}".format(elem.element_info.name[0:10]) logger.debug(s) WechatAutomator.add_to_detail(s, detail) continue try: self.click_url(rect, win_rect, click_up) copy_btn = self.browser.child_window(title="復(fù)制鏈接地址") self.click_center(copy_btn, click_main=False) url = clipboard.GetData() if elem.element_info.name != '圖片': clicked_titles.add(elem.element_info.name) if url and not url in visited_urls: visited_urls.add(url) html = None try:html = requests.get(url).text except:s = "fail get {}".format(url)logger.debug(s)WechatAutomator.add_to_detail(s, detail) items.append((url, rect, elem.element_info.name, html)) except: traceback.print_exc() pass finally: if self.browser: try:self.browser.close() except:pass self.browser = None time.sleep(self.click_url_interval) return clicked_titles
以上就是python使用pywinauto驅(qū)動(dòng)微信客戶端實(shí)現(xiàn)公眾號(hào)爬蟲(chóng)的詳細(xì)內(nèi)容,更多關(guān)于python 公眾號(hào)爬蟲(chóng)的資料請(qǐng)關(guān)注本站其它相關(guān)文章!
版權(quán)聲明:本站文章來(lái)源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請(qǐng)保持原文完整并注明來(lái)源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來(lái)源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來(lái),僅供學(xué)習(xí)參考,不代表本站立場(chǎng),如有內(nèi)容涉嫌侵權(quán),請(qǐng)聯(lián)系alex-e#qq.com處理。