iT邦幫忙

3

python 自動化第430頁習題,能寫得更好嗎?

  • 分享至 

  • xImage

書本第430頁的練習:
登入IMAP server。
下載所有mail、
bs4找出連結的HTML標籤,存成URL串列,
以webbrowser.open()在瀏覽器中開啟。

雖然此題寫出來了,但自覺寫法笨拙。畢竟是照課本的教法一個個疊出來。
沒有正規學習python基本語法,就學這個各種應用,
可能寫得很醜……

3個問題
一、此篇程式碼是否有更有效率的寫法?感覺bs4的部份可以直接抓到,不需再用到regex,可惜我對html陌生。
二、gmail的「促銷內容」之標籤為何?'INBOX'似乎含全部,想單一抓出促銷內容類別的。
三、regex有沒有辦法直接取得第一個連結?像下面這樣要寫2層篩選,萬一有5個不就得「寫5次」?

import os, shutil, imapclient, pprint

a = 'c:\\temp'
b = 'p430cuse'
os.chdir(a)
os.makedirs(b, exist_ok=True)
os.chdir(os.path.join(a, b))
#print(os.getcwd())  

for n in os.listdir():
    os.unlink(n)    # 清掉前次的記錄

i = imapclient.IMAPClient('imap.gmail.com', ssl=True)   
# 以ssl加密方式,連進gmail的IMAP server

i.login('xxx@gmail.com', 'xxx')        # 登入信箱

i.select_folder('INBOX', readonly=True) # 選擇收件匣目錄
# Promotions Tab 促銷內容

a = i.list_folders()
#pprint.pprint(a) 
# ((b'\\All', b'\\HasNoChildren'), b'/', '[Gmail]/全部郵件'),

a = i.search(['ALL']) # 選擇收件匣所有信件
del a[11:] # 保留前11封
print(a) 
# 郵件編號[86199, 86256, 86314, 86316, 86325, 86348, 86420, 86435, 86592, 86844, 87170]

import pyzmail, bs4, re, webbrowser
# 解析郵件、解析html碼、regex解析文字、開啟網頁

def s(k):
    j = re.compile(r'<a href="(.*).html(.*).html(.*)') 
    g = j.sub(r"\1.html", k)
    if g[-5:] == '.html':   # 如果符合條件j,就能解析出網址g,網址g以html結尾
        return g            # 有解析出g就返回結束此自定函數,不然繼續往下篩選
    j = re.compile(r'<a href="(.*).tw(.*).tw(.*)')
    g = j.sub(r"\1.tw", k)  # 網址以tw結尾
    if g[-3:] == '.tw':
        return g
    j = re.compile(r'<a href="(.*).com(.*).com(.*)')
    g = j.sub(r"\1.com", k) # 網址以com結尾,這個放在「條件tw」之後篩選。
    if g[-4:] == '.com':
        return g
    j = re.compile(r'<a href="(.*).com(.*)')
    g = j.sub(r"\1.com", k)
    if g[-4:] == '.com':
        return g

"""原始網址範例,這很多層,所以上面regex條件會重複多遍
[<a href="https://www.momoshop.com.tw/edm/cmmedm.jsp?lpn=Nh9W&amp;n=1&amp
;cid=&amp;oid=&amp;mdiv=1000-bt_P1_1_e1&amp;ctype=B">
<img border="0" src="https://img2.momoshop.com.tw/ecm/img/online/100/b
t__01_P1_1_e2.png?t=15815" style=" width:134px; height:
120px; "/></a>]"""    

p = []

for x in range(len(a)):                     # 11 mail
    y = i.fetch(a[x], ['BODY[]', 'FLAGS'])  # 取得單一信文
    z = pyzmail.PyzMessage.factory(y[a[x]][b'BODY[]'])  
    # 將信文製成pyzmail物件
    if z.html_part != None: # 若信文不為空
        b = z.html_part.get_payload().decode(z.html_part.charset) 
        # 將信文的html部份,以html語法來解析
        d = bs4.BeautifulSoup(b, "html5lib") # 讀取html部份,製成bs4物件
        e = d.select('a')   # a href="link URL" 從bs4物件中,挑選出連結
        #print(e)
        e = e[:1] # 每封信只留一個連結

        if len(e) != 0:         # 也有整封信純文字無連結的
            f = s(str(e[0]))    # 使用regex,將連結去掉"a hrf="及後面的多餘文字
            p.append(f)         # 將取得的乾淨連結放進串列

p = list(dict.fromkeys(p))      
# 不同的信件有些第一個連結可能是相同的,串列去重複

import pprint
# pprint.pprint(p) 好看列印法

for n in p:
    webbrowser.open(n) # 以瀏覽器開啟所有網址

"""範例網址列表
['https://www.momoshop.com.tw',
 'https://buy.itunes.apple.com',
 'http://www.economist.com',
 'mailto:chiawei.hung@fubon.com',
 'https://www.fubon.com']
"""

看更多先前的討論...收起先前的討論...
froce iT邦大師 1 級 ‧ 2023-02-07 16:15:57 檢舉
1. 當然有,像是bs4取出a後直接對a的href去做regex search,類似的code整合之類的,但這我覺得不該給你,這是你自己該去想的

2. 有list_folders和list_sub_folders,根據GMAIL的網頁版,我猜促銷內容是在category/promotions下。
https://imapclient.readthedocs.io/en/master/api.html

3. 看不懂你的意思

另外,我個人會建議看這類直接教你實戰的書本身沒問題,但一定要先看過基本語法的書,然後搭配實戰的題目,和他用的lib,自己做。
這類實戰書的題目基本上太雜,初學很難掌握,照著範例抄沒意義,除非你懂得自己去閱讀大量的外部資料。像是他引用的lib的官網。

舉例來說:我手上沒你那本書,但我看到 imapclient ,我自動會去看 imapclient 的官網,看他是拿來做什麼,有什麼功能,甚至是有哪些更先進的同類型lib。那自然能解決你的第1和2的問題。
bs4應該就能直接取出a href的連結,用regex比較不行,因為課本也說regex在html方面不好用。我本題用regx是不想花太多時間去找出這個寫法,所以寫一個regex的自定義函數來抓,自覺bs4應該有「一句語法」便可以取代這個囉嗦繁雜的函數。
我也覺得書本身沒問題,是我拿到書就學起來了。
一般書本前面不太會提及「在學此本書前,您應具備或熟練哪些基本知識」。
很多坊間的、網上的課程也是,「入學」之前不會告訴你,你最好有哪些基礎。
促銷內容是否將此句的INBOX換成promotions呢?我先前自猜但覺得不太可能對,因為list_folders就沒有這個tag。i.select_folder('INBOX', readonly=True)
以上只是心得與討論,不是抱怨喔。
froce iT邦大師 1 級 ‧ 2023-02-07 17:31:33 檢舉
你用list_folder看有什麼資料夾不就清楚了?何必猜呢?
我是因為懶得建環境才用猜的
ccutmis iT邦高手 2 級 ‧ 2023-02-07 23:07:11 檢舉
三、regex有沒有辦法直接取得第一個連結?像下面這樣要寫2層篩選,萬一有5個不就得「寫5次」?

link_ls=re.findall('<a href="([^"]*)"', html_source,re.M)
for lnk in link_ls:
...
link_ls[0] 就是第一個連結,不過能用bs4解析的好像就不必用re硬拆才對
froce iT邦大師 1 級 ‧ 2023-02-08 14:10:34 檢舉
bs4應該用get就能取href屬性了。
但要拆到網域就得用re或其他lib了。
謝謝ccutmis:
一句解決掉我的長篇大論regex條件m(__)m
而且效果和froce大大的get效果一致。超厲害。
謝謝froce大大:
我有get到您的get了!
您和ccutmis大大一樣,都立刻抓到課本要的效果。
非常感謝m(__)m
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 個回答

0
re.Zero
iT邦研究生 5 級 ‧ 2023-02-08 21:45:05
最佳解答

Update: 修改 regex-Pattern、程式碼註解。


一、此篇程式碼是否有更有效率的寫法?感覺bs4的部份可以直接抓到,不需再用到regex,可惜我對html陌生。

如果沒有解析 HTML 的必要,我是偏好只用 regex~
(只是找 <a> 的 URL 就用 bs4,這對我來說算是小題大作~)
另,regex 是很好用也很重要的技術,推薦學習!
(只是要注意,不同環境下的 regex 實作會有差異~)

二、gmail的「促銷內容」之標籤為何?'INBOX'似乎含全部,想單一抓出促銷內容類別的。

這裡所言,Google 在 Gmail API 有提供自動類別標籤功能,但在 Gmail IMAP 擴充功能 沒有提供。
(至少我是試不出來~)

上本週作業(?)給你參考~
沒用 bs4 的版本:

## 
import os, datetime, re, webbrowser
## https://pypi.org/project/imap-tools/
from imap_tools import MailBox, AND
## 丟組態用的類別物件;
class myCfg:
	path = r'c:\temp\p430cuse'
	host = r'imap.gmail.com'
	luser = r'username@gmail.com'
	lpass = r'password'
## 我看到你建立並切換工作目錄,但沒檔案操作存取?只是練習?
if not os.path.exists(myCfg.path):
	os.makedirs(myCfg.path, exist_ok=True)
os.chdir(myCfg.path)
print(os.getcwd())
## 
for n in os.listdir():
	os.unlink(n)
## 
## regex-Pattern: 我是直接抓 HTTP|mailto 協定的連結; 
## (因為你的範例內有放 mailto 的,所以我也放進去~)
## 這不是完全符合 URL 的式子,只是我臨時隨便寫的(懶得再翻 RFC 之類的文件~);
## 要更詳細可以去 Google: 'Python regex URL',畢竟這算是老問題了~
myURLPattern = r'(?i)\b(?:https?|mailto):\S+'
## 這是抓 HTML-A-Tag 用的; match-group#1 可取得 URL;
myTagPattern = r'(?i)<a [^>]*href="((?:https?|mailto):[^"]*)"[^>]*>'
## https://docs.python.org/zh-tw/3/tutorial/datastructures.html#sets
myResult = set() ## 一個 set 是一組無序且沒有重複的元素;
## 
with MailBox(myCfg.host).login(myCfg.luser, myCfg.lpass) as mailbox:
	for msg in mailbox.fetch(
		AND(date_gte=datetime.date(2023, 1, 1)), ## 篩出 2023-01-01 之後的信件;
		mark_seen=False ## 不觸動信件的已讀狀態;
	):
		## 列印信件資訊;
		print('■', msg.date, msg.subject, len(msg.html or msg.text))
		if msg.html:
			## 取得 Match 物件之生成疊代器(generator iterator);
			myMatch = re.compile(myTagPattern).finditer(msg.html)
			## 這裡用 Match.group() 取符合的子群組之內容;
			## https://docs.python.org/3/library/re.html#re.Match.group
			try: myResult.add(next(myMatch).group(1))
			except StopIteration: pass
			## 利用 set 沒有重複的特性而沒寫 set.add() 的檢查;
			## 使用 next() 是因為 re.finditer() 回傳生成疊代器;
			## https://docs.python.org/3.10/library/functions.html#next
		elif msg.text:
			myMatch = re.compile(myURLPattern).finditer(msg.text)
			## 另一種處理 next() 的 StopIteration 之方式;
			if (r := next(myMatch, None)):
				myResult.add(r.group(0))
## 我習慣先斷線(by out-of-with)再處理結果;
mySorted = sorted(myResult)
for i in mySorted: print('■',i)
for i in mySorted: webbrowser.open(i)
## 

用上 bs4 的版本:
(if msg.html: 這區段的內容才跟上例有差異,其他沒差~)

## 
import os, datetime, re, webbrowser
from imap_tools import MailBox, AND
from bs4 import BeautifulSoup as bSoup
## 
class myCfg:
	path = r'c:\temp\p430cuse'
	host = r'imap.gmail.com'
	luser = r'username@gmail.com'
	lpass = r'password'
## 
if not os.path.exists(myCfg.path):
	os.makedirs(myCfg.path, exist_ok=True)
os.chdir(myCfg.path)
print(os.getcwd())
## 
for n in os.listdir():
	os.unlink(n)
## 
myURLPattern = r'(?i)\b(?:https?|mailto):\S+'
myTagPattern = r'(?i)<a [^>]*href="((?:https?|mailto):[^"]*)"[^>]*>'
myResult = set()
## 
with MailBox(myCfg.host).login(myCfg.luser, myCfg.lpass) as mailbox:
	for msg in mailbox.fetch(
		AND(date_gte=datetime.date(2023, 1, 1)),
		mark_seen=False
	):
		print('■', msg.date, msg.subject, len(msg.html or msg.text))
		if msg.html:
			mySoup = bSoup(msg.html, 'html.parser')
			## https://www.crummy.com/software/BeautifulSoup/bs4/doc/#a-function
			myA = mySoup.find('a', href = re.compile('^(?:https?|mailto):').search)
			if myA: myResult.add(myA['href'])
		elif msg.text:
			myMatch = re.compile(myURLPattern).finditer(msg.text)
			if (r := next(myMatch, None)):
				myResult.add(r.group(0))
mySorted = sorted(myResult)
for i in mySorted: print('■',i)
for i in mySorted: webbrowser.open(i)
## 

謝謝re.Zero大大:D

2
Ray
iT邦大神 1 級 ‧ 2023-02-07 16:24:15

偷偷請 ChatGPT 寫寫看:
(請自行判斷有沒有 Bug)

import imaplib
import re
import webbrowser
from bs4 import BeautifulSoup

# Connect to IMAP server
imap = imaplib.IMAP4_SSL("imap.gmail.com")
imap.login("your_email_address", "your_email_password")

# Select inbox and download all mails
imap.select("inbox")
status, messages = imap.search(None, "ALL")
messages = messages[0].split()

# Find all links in HTML tags
links = []
for message in messages:
    status, data = imap.fetch(message, "(RFC822)")
    soup = BeautifulSoup(data[0][1].decode("utf-8"), "html.parser")
    for link in soup.find_all("a"):
        if "href" in link.attrs:
            url = link.attrs["href"]
            if re.match("^http", url):
                links.append(url)

# Open all links in a web browser
for url in links:
    webbrowser.open(url)

# Close the IMAP connection
imap.close()
imap.logout()

謝謝樓上。

這個我有測試過,第一次有過,第二次跑不出來,會卡在RFC822的地方。稍後研究。

我要發表回答

立即登入回答