專欄作者:小小明
非常擅長解決各類複雜數據處理的邏輯,各類結構化與非結構化數據互轉,字符串解析匹配等等。
至今已經幫助很多數據從業者解決工作中的實際問題,如果你在數據處理上遇到什麼困難,歡迎評論區與我交流。
大家好,我是小小明。
今天我要給大家分享的是如何爬取豆瓣上深圳近期即將上映的電影影訊,並分別用普通的單線程、多線程和協程來爬取,從而對比單線程、多線程和協程在網絡爬蟲中的性能。
具體要爬的網址是:https://movie.douban.com/cinema/later/shenzhen/
除了要爬入口頁以外還需爬取每個電影的詳情頁,具體要爬取的結構信息如下:
爬取測試下面我演示使用xpath解析數據。
入口頁數據讀取:
import requests
from lxml import etree
import pandas as pd
import re
main_url = "https://movie.douban.com/cinema/later/shenzhen/"
headers = {
"Accept-Encoding": "Gzip",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
r = requests.get(main_url, headers=headers)
r結果:
<Response [200]>檢查一下所需數據的xpath:
可以看到每個電影信息都位於id為showing-soon下面的div裡面,再分別分析內部的電影名稱、url和想看人數所處的位置,於是可以寫出如下代碼:
html = etree.HTML(r.text)
all_movies = html.xpath("//div[@id='showing-soon']/div")
result = []
for e in all_movies:
# imgurl, = e.xpath(".//img/@src")
name, = e.xpath(".//div[@class='intro']/h3/a/text()")
url, = e.xpath(".//div[@class='intro']/h3/a/@href")
# date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
like_num, = e.xpath(
".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
result.append((name, int(like_num[:like_num.find("人")]), url))
main_df = pd.DataFrame(result, columns=["影名", "想看人數", "url"])
main_df結果:
然後再選擇一個詳情頁的url進行測試,我選擇了熊出沒·狂野大陸這部電影,因為文本數據相對最複雜,也最具備代表性:
url = main_df.at[17, "url"]
url結果:
'https://movie.douban.com/subject/34825886/'分析詳情頁結構:
文本信息都在這個位置中,下面我們直接提取這個div下面的所有文本節點:
r = requests.get(url, headers=headers)
html = etree.HTML(r.text)
movie_infos = html.xpath("//div[@id='info']//text()")
print(movie_infos)結果:
['\n ', '導演', ': ', '丁亮', '\n ', '編劇', ': ', '徐芸', ' / ', '崔鐵志', ' / ', '張宇', '\n ', '主演', ': ', '張偉', ' / ', '張秉君', ' / ', '譚笑', '\n ', '類型:', ' ', '喜劇', ' / ', '科幻', ' / ', '動畫', '\n \n ', '製片國家/地區:', ' 中國大陸', '\n ', '語言:', ' 漢語普通話', '\n ', '上映日期:', ' ', '2021-02-12(中國大陸)', ' / ', '2020-08-01(上海電影節)', '\n ', '片長:', ' ', '100分鐘', '\n ', '又名:', ' 熊出沒大電影7 / 熊出沒科幻大電影 / Boonie Bears: The Wild Life', '\n ', 'IMDb連結:', ' ', 'tt11654032', '\n\n']為了閱讀方便,拼接一下:
movie_info_txt = "".join(movie_infos)
print(movie_info_txt)結果:
導演: 丁亮
編劇: 徐芸 / 崔鐵志 / 張宇
主演: 張偉 / 張秉君 / 譚笑
類型: 喜劇 / 科幻 / 動畫
製片國家/地區: 中國大陸
語言: 漢語普通話
上映日期: 2021-02-12(中國大陸) / 2020-08-01(上海電影節)
片長: 100分鐘
又名: 熊出沒大電影7 / 熊出沒科幻大電影 / Boonie Bears: The Wild Life
IMDb連結: tt11654032接下來就簡單了:
row = {}
for line in re.split("[\n ]*\n[\n ]*", movie_info_txt):
line = line.strip()
arr = line.split(": ", maxsplit=1)
if len(arr) != 2:
continue
k, v = arr
row[k] = v
row結果:
{'導演': '丁亮',
'編劇': '徐芸 / 崔鐵志 / 張宇',
'主演': '張偉 / 張秉君 / 譚笑',
'類型': '喜劇 / 科幻 / 動畫',
'製片國家/地區': '中國大陸',
'語言': '漢語普通話',
'上映日期': '2021-02-12(中國大陸) / 2020-08-01(上海電影節)',
'片長': '100分鐘',
'又名': '熊出沒大電影7 / 熊出沒科幻大電影 / Boonie Bears: The Wild Life',
'IMDb連結': 'tt11654032'}可以看到成功的切割出了每一項。
下面根據上面的測試基礎,我們完善整體的爬蟲代碼:
單線程爬蟲import requests
from lxml import etree
import pandas as pd
import re
main_url = "https://movie.douban.com/cinema/later/shenzhen/"
headers = {
"Accept-Encoding": "Gzip",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
r = requests.get(main_url, headers=headers)
html = etree.HTML(r.text)
all_movies = html.xpath("//div[@id='showing-soon']/div")
result = []
for e in all_movies:
imgurl, = e.xpath(".//img/@src")
name, = e.xpath(".//div[@class='intro']/h3/a/text()")
url, = e.xpath(".//div[@class='intro']/h3/a/@href")
print(url)
# date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
like_num, = e.xpath(
".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
r = requests.get(url, headers=headers)
html = etree.HTML(r.text)
row = {}
row["電影名稱"] = name
for line in re.split("[\n ]*\n[\n ]*", "".join(html.xpath("//div[@id='info']//text()")).strip()):
line = line.strip()
arr = line.split(": ", maxsplit=1)
if len(arr) != 2:
continue
k, v = arr
row[k] = v
row["想看人數"] = int(like_num[:like_num.find("人")])
# row["url"] = url
# row["圖片地址"] = imgurl
# print(row)
result.append(row)
df = pd.DataFrame(result)
df.sort_values("想看人數", ascending=False, inplace=True)
df.to_csv("shenzhen_movie.csv", index=False)結果:
https://movie.douban.com/subject/26752564/
https://movie.douban.com/subject/35172699/
https://movie.douban.com/subject/34992142/
https://movie.douban.com/subject/30349667/
https://movie.douban.com/subject/30283209/
https://movie.douban.com/subject/33457717/
https://movie.douban.com/subject/30487738/
https://movie.douban.com/subject/35068230/
https://movie.douban.com/subject/27039358/
https://movie.douban.com/subject/30205667/
https://movie.douban.com/subject/30476403/
https://movie.douban.com/subject/30154423/
https://movie.douban.com/subject/27619748/
https://movie.douban.com/subject/26826330/
https://movie.douban.com/subject/26935283/
https://movie.douban.com/subject/34841067/
https://movie.douban.com/subject/34880302/
https://movie.douban.com/subject/34825886/
https://movie.douban.com/subject/34779692/
https://movie.douban.com/subject/35154209/爬到的文件:
整體耗時:
42.5秒。
多線程爬蟲單線程的爬取耗時還是挺長的,下面看看使用多線程的爬取效率:
import requests
from lxml import etree
import pandas as pd
import re
from concurrent.futures import ThreadPoolExecutor, wait, ALL_COMPLETED
def fetch_content(url):
print(url)
headers = {
"Accept-Encoding": "Gzip", # 使用gzip壓縮傳輸數據讓訪問更快
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
r = requests.get(url, headers=headers)
return r.text
url = "https://movie.douban.com/cinema/later/shenzhen/"
init_page = fetch_content(url)
html = etree.HTML(init_page)
all_movies = html.xpath("//div[@id='showing-soon']/div")
result = []
for e in all_movies:
# imgurl, = e.xpath(".//img/@src")
name, = e.xpath(".//div[@class='intro']/h3/a/text()")
url, = e.xpath(".//div[@class='intro']/h3/a/@href")
# date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
like_num, = e.xpath(
".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
result.append((name, int(like_num[:like_num.find("人")]), url))
main_df = pd.DataFrame(result, columns=["影名", "想看人數", "url"])
max_workers = main_df.shape[0]
with ThreadPoolExecutor(max_workers=max_workers) as executor:
future_tasks = [executor.submit(fetch_content, url) for url in main_df.url]
wait(future_tasks, return_when=ALL_COMPLETED)
pages = [future.result() for future in future_tasks]
result = []
for url, html_text in zip(main_df.url, pages):
html = etree.HTML(html_text)
row = {}
for line in re.split("[\n ]*\n[\n ]*", "".join(html.xpath("//div[@id='info']//text()")).strip()):
line = line.strip()
arr = line.split(": ", maxsplit=1)
if len(arr) != 2:
continue
k, v = arr
row[k] = v
row["url"] = url
result.append(row)
detail_df = pd.DataFrame(result)
df = main_df.merge(detail_df, on="url")
df.drop(columns=["url"], inplace=True)
df.sort_values("想看人數", ascending=False, inplace=True)
df.to_csv("shenzhen_movie2.csv", index=False)
df結果:
耗時8秒。
由於每個子頁面都是單獨的線程爬取,每個線程幾乎都是同時在工作,所以最終耗時僅取決於爬取最慢的子頁面。
協程異步爬蟲由於我在jupyter中運行,為了使協程能夠直接在jupyter中直接運行,所以我在代碼中增加了下面兩行代碼,在普通編輯器裡面可以去掉:
import nest_asyncio
nest_asyncio.apply()這個問題是因為jupyter所依賴的高版本Tornado存在bug,將Tornado退回到低版本也可以解決這個問題。
下面我使用協程來完成這個需求的爬取:
import aiohttp
from lxml import etree
import pandas as pd
import re
import asyncio
import nest_asyncio
nest_asyncio.apply()
async def fetch_content(url):
print(url)
header = {
"Accept-Encoding": "Gzip", # 使用gzip壓縮傳輸數據讓訪問更快
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
async with aiohttp.ClientSession(
headers=header, connector=aiohttp.TCPConnector(ssl=False)
) as session:
async with session.get(url) as response:
return await response.text()
async def main():
url = "https://movie.douban.com/cinema/later/shenzhen/"
init_page = await fetch_content(url)
html = etree.HTML(init_page)
all_movies = html.xpath("//div[@id='showing-soon']/div")
result = []
for e in all_movies:
# imgurl, = e.xpath(".//img/@src")
name, = e.xpath(".//div[@class='intro']/h3/a/text()")
url, = e.xpath(".//div[@class='intro']/h3/a/@href")
# date, movie_type, pos = e.xpath(".//div[@class='intro']/ul/li[@class='dt']/text()")
like_num, = e.xpath(
".//div[@class='intro']/ul/li[@class='dt last']/span/text()")
result.append((name, int(like_num[:like_num.find("人")]), url))
main_df = pd.DataFrame(result, columns=["影名", "想看人數", "url"])
tasks = [fetch_content(url) for url in main_df.url]
pages = await asyncio.gather(*tasks)
result = []
for url, html_text in zip(main_df.url, pages):
html = etree.HTML(html_text)
row = {}
for line in re.split("[\n ]*\n[\n ]*", "".join(html.xpath("//div[@id='info']//text()")).strip()):
line = line.strip()
arr = line.split(": ", maxsplit=1)
if len(arr) != 2:
continue
k, v = arr
row[k] = v
row["url"] = url
result.append(row)
detail_df = pd.DataFrame(result)
df = main_df.merge(detail_df, on="url")
df.drop(columns=["url"], inplace=True)
df.sort_values("想看人數", ascending=False, inplace=True)
return df
df = asyncio.run(main())
df.to_csv("shenzhen_movie3.csv", index=False)
df結果:
耗時僅7秒,相對比多線程更快一點。
由於request庫不支持協程,所以我使用了支持協程的aiohttp進行頁面抓取。當然實際爬取的耗時還取絕於當時的網絡,但整體來說,協程爬取會比多線程爬蟲稍微快一些。
回顧今天我向你演示了,單線程爬蟲、多線程爬蟲和協程爬蟲。可以看到一般情況下協程爬蟲速度最快,多線程爬蟲略慢一點,單線程爬蟲則必須上一個頁面爬取完成才能繼續爬取。
但協程爬蟲相對來說並不是那麼好編寫,數據抓取無法使用request庫,只能使用aiohttp。所以在實際編寫爬蟲時,我們一般都會使用多線程爬蟲來提速,但必須注意的是網站都有ip訪問頻率限制,爬的過快可能會被封ip,所以一般我們在多線程提速的同時使用代理ip來並發的爬取數據。
彩蛋:xpath+pandas解析表格並提取url我們在深圳影訊的底部能夠看到一個[查看全部即將上映的影片] (https://movie.douban.com/coming)的按鈕,點進去能夠看到一張完整近期上映電影的列表,發現這個列表是個table標籤的數據:
那就簡單了,解析table我們可能壓根就不需要用xpath,直接用pandas即可,但片名中包含的url地址還需解析,所以我採用xpath+pandas來解析這個網頁,看看我的代碼吧:
import pandas as pd
import requests
from lxml import etree
headers = {
"Accept-Encoding": "Gzip",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36"
}
r = requests.get("https://movie.douban.com/coming", headers=headers)
html = etree.HTML(r.text)
table_tag = html.xpath("//table")[0]
df, = pd.read_html(etree.tostring(table_tag))
urls = table_tag.xpath(".//td[2]/a/@href")
df["url"] = urls
df結果
這樣就能到了主頁面的完整數據,再簡單的處理一下即可。
結語感謝各位讀者,有什麼想法和收穫歡迎留言評論噢!