背景介绍
上个月刚到公司实习,就被分配到了数据部门做数据标注。
接到的任务需求是根据一个Youtube包含了目标用户主页url的表格,从Youtube上爬取这些用户的相关信息。
那么首先根据领导提出的要求做需求分析。
1 需求分析
需要根据已知Youtube用户主页url,获取到用户的账号名称、账号ID、认证标志、用户所在国家、用户所使用的语种、用户最新发文日期。
我们以 www.youtube.com/@DanAllenGa… 这个用户主页为例进行需求分析。
1.1我们需要先找到每个元素所在的位置
我们现在可以直接看到账号名称"Dan Allen Gaming",账号id"@DanAllenGaming",账号描述"Daily videos on the best...",账号认证标志
。
在我们点开视频栏目之后会发现用户发布的视频中包含了 视频标题和发布时间,而且刚好视频是按发布时间从最新排序的。那么我们只需要提取发布的第一个视频的时间,就可以得到用户最新发文日期,提取前n个视频的标题,然后再识别这些标题的语言,使用最多的那个语言就可以当作用户所使用的语种。
再点开用户简介,发现可以得到用户所在国家。
到目前为止,我们已经确定了查找账号名称、账号ID、认证标志、用户所在国家、用户所使用的语种、用户最新发文日期的基本思路。下面开始写代码实战。
2 从渲染后的网页中查找账号基本信息
2.1 获取渲染后的网页内容
如今,许多网页采用了动态内容加载技术,其中一部分信息只有在页面完全加载后才会显示。这种技术通常依赖于JavaScript和AJAX请求,通过与服务器进行异步通信来获取额外的数据并更新页面内容。由于这些信息并不会在初始HTML中直接呈现,因此传统的静态网页抓取工具如request库可能无法获取到这些动态加载的内容。为了获取渲染后的网页内容,通常需要使用能够执行JavaScript的工具或框架,例如Puppeteer、Selenium等。这些工具可以模拟用户的浏览器行为,等待页面加载完成,然后提取所需的信息。这种方法不仅能够获取完整的网页内容,还可以处理复杂的交互和动态更新,确保数据的准确性和完整性。
由于我之前用过Selenium,所以在这里直接选取了Selenium。Selenium的安装和配置可以参考网上教程,这里我先不做过多赘述。
1) 从selenium库导入相关模块和初始配置
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
# 浏览器初始化
option = Options()
# option.add_argument('--headless')
# 设置不加载图片,加快页面加载速度,但可能会加大被检测到的几率
# options.add_experimental_option("prefs", {"profile.managed_default_content_settings.images": 2})
# 关闭浏览器上部提示语:Chrome正在受到自动软件的控制(修改js特征)
option.add_experimental_option("excludeSwitches", ["enable-automation"])
# 禁用了Chrome的自动化扩展,进一步减少被检测到的风险。
option.add_experimental_option("useAutomationExtension", False)
# 禁用浏览器的某些特性,使得浏览器看起来不像是被自动化工具控制的
option.add_argument("disable-blink-features=AutomationControlled")
# 创建Chrome WebDriver实例
driver = webdriver.Chrome(options=option)
# 修改了navigator对象的webdriver属性,使其返回undefined,从而隐藏Selenium的自动化控制痕迹
driver.execute_cdp_cmd(
"Page.addScriptToEvaluateOnNewDocument",
{
"source": """
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined
})
"""
},
)
2) 获取完整的网页源码
这一步的目的是通过selenium模拟点击,直至得到完整网页源码。首先从用户主页跳转到用户视频专栏,再把用户个人简介点击出来。最后我们就能够通过driver.page_source获取到完整的网页源码,此时的网页源码汇总已经包含了我们能获取到的所有网页元素。
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup
import time
## 控制浏览器跳转到用户首页
driver.get("https://www.youtube.com/@DanAllenGaming")
wait = WebDriverWait(driver, 10)
## 跳转到视频板块页面
try:
## 定位视频按钮
video_tab = wait.until(
EC.element_to_be_clickable(
(By.XPATH, "//yt-tab-shape[@tab-title='视频']")
)
)
print("找到了视频板块")
# 点击视频按钮
video_tab.click()
time.sleep(2)
wait.until(
EC.presence_of_all_elements_located((By.CSS_SELECTOR, "div.style-scope.ytd-rich-grid-media#details"))
)
time.sleep(3)
except:
print("无视频板块")
Video_html = str(driver.page_source)
# 获取完整的网页源码
soup_video = BeautifulSoup(Video_html, "html5lib")
# 定位个人主页按钮并且点击
more_button = wait.until(
EC.element_to_be_clickable(
(
By.CSS_SELECTOR,
"truncated-text.truncated-text-wiz button.truncated-text-wiz__absolute-button",
)
)
)
# 点击按钮
more_button.click()
print("已经点击个人主页")
time.sleep(2)
Video_html = str(driver.page_source)
# 获取完整的网页源码
soup_video = BeautifulSoup(Video_html, "html5lib")
print("已经获取到了完整的网络源码")
2.2 从上一步中解析好的网页源码中,并查找我们想要的元素
在2.1中,我们已经获取到了完整的网页源码并使用BeautifulSoup把网页源码解析成了xml格式。接下来我们可以使用BeautifulSoup的方法来定位并提取出我们需要的元素。
关于selenium和BeautifulSoup的元素定位方法,可以去看别的笔记或者直接问大模型,时间原因这里先不展开。
import requests
from urllib.parse import quote
from collections import Counter
## 谷歌翻译api
def detect_language(text):
"""
function: 使用google爬虫api检测文本语言(也可以进行翻译)。
inputs: 要检测语言的文本
outputs: 一个字符编码
"""
# 定义接口 URL
## sl=原语言&tl=目标语言&q={quote(要检测或翻译的内容)}
url = f"https://translate.googleapis.com/translate_a/single?client=gtx&dt=t&sl=auto&tl=en&q={quote(text)}"
## 增加容错,减少访问api失败导致程序中断的情况
for _ in range(5):
try:
res = requests.get(url)
if res.status_code == 200:
try:
return res.json()[2].lower()
except (IndexError, ValueError, KeyError):
return "NULL"
else:
# print(f"请求失败,状态码: {res.status_code}")
assert False
except requests.RequestException as e:
print(f"访问谷歌翻译api出错,正在重试: {e}")
time.sleep(1)
return "NULL"
## 查找列表中使用最多的元素
def most_common_element(arr):
count = Counter(arr)
most_common = count.most_common(1)
print(most_common)
return (
most_common[0][0]
if most_common and most_common[0][1] > int(len(arr) / 2)
else ""
)
## 语言编码转中文语言名称
def lan_en2lan_cn(text=""):
text = text.lower()
lan_cn = "中文,英文,日文,韩文,俄文,阿拉伯文,藏文,维吾尔文,法文,德文,马来文,泰米尔文,菲律宾文,泰文,老挝文,高棉文,缅甸文,印尼文,越南文,阿布哈兹文,阿尔巴尼亚文,阿法尔文,阿坎文,阿拉贡文,阿姆哈拉文,阿萨姆文,阿塞拜疆文,阿瓦尔文,阿维斯陀文,埃维文,艾马拉文,爱尔兰文,爱沙尼亚文,奥吉布瓦文,奥克文,奥利亚文,奥洛莫文,奥塞梯文,巴利文,巴什基尔文,巴斯克文,白俄罗斯文,班巴拉文,保加利亚文,北恩德贝勒文,北萨米文,比哈尔文,比斯拉马文,冰岛文,波兰文,波斯尼亚文,波斯文,不丹文,布列塔尼文,查莫罗文,朝鲜文,车臣文,楚瓦什文,丹麦文,迪维希文,恩敦加文,法罗文,梵文,斐济文,芬兰文,弗里西亚文,富拉文,刚果文,格陵兰文,格鲁吉亚文,古吉拉特文,古教会斯拉夫文,瓜拉尼文,哈萨克文,海地克里奥尔文,豪萨文,荷兰文,赫雷罗文,基库尤文,基隆迪文,吉尔吉斯文,加利西亚文,加泰隆文,柬埔寨文,捷克文,卡纳达文,卡努里文,凯楚亚文,康沃尔文,科米文,科萨文,科西嘉文,克里文,克罗地亚文,克什米尔文,库尔德文,宽亚玛文,拉丁文,拉脱维亚文,立陶宛文,林堡文,林加拉文,卢巴文,卢干达文,卢森堡文,卢旺达文,罗马尼亚文,罗曼什文,马达加斯加文,马恩岛文,马耳他文,马拉提文,马拉亚拉姆文,马其顿文,马绍尔文,毛利文,蒙古文,孟加拉文,摩尔达维亚文,纳瓦霍文,南恩德贝勒文,南非文,南索托文,瑙鲁文,尼泊尔文,尼扬贾文,挪威文,旁遮普文,葡萄牙文,普什图文,瑞典文,萨丁尼亚文,萨摩亚文,塞茨瓦纳文,塞尔维亚文,塞文,桑戈文,僧伽罗文,僧加罗文,绍纳文,世界文,书面挪威文,斯洛伐克文,斯洛文尼亚文,斯瓦特文,斯瓦希里文,四川彝文,苏格兰盖尔文,索马里文,他加禄文,塔吉克文,塔塔尔文,塔希提文,泰卢固文,汤加文,特威文,提格里尼亚文,土耳其文,土库曼文,威尔士文,文达文,沃拉普克文,沃伦文,沃洛夫文,乌尔都文,乌克兰文,乌兹别克文,西班牙文,希伯来文,希伯莱文,希腊文,希里莫图文,新挪威文,信德文,匈牙利文,巽他文,亚美尼亚文,伊博文,伊多文,依地文,依努庇克文,意大利文,因纽特文,印地文,约鲁巴文,爪哇文,壮文,宗加文,祖鲁文,粤语,希伯来文".split(
","
)
lan_en = "zh_cn,en,ja,ko,ru,ar,bo,ug,fr,de,ms,ta,fil,th,lo,km,my,id,vi,ab,sq,aa,ak,an,am,as,az,av,ae,ee,ay,ga,et,oj,oc,or,om,os,pi,ba,eu,be,bm,bg,nd,se,bh,bi,is,pl,bs,fa,dz,br,ch,ko,ce,cv,da,dv,ng,fo,sa,fj,fi,fy,ff,kg,kl,ka,gu,cu,gn,kk,ht,ha,nl,hz,ki,rn,ky,gl,ca,kh,cs,kn,kr,qu,kw,kv,xh,co,cr,hr,ks,ku,kj,la,lv,lt,li,ln,lu,lg,lb,rw,ro,rm,mg,gv,mt,mr,ml,mk,mh,mi,mn,bn,mo,nv,nr,af,st,na,ne,ny,no,pa,pt,ps,sv,sc,sm,tn,sr,sww,sg,si,si,sn,eo,nb,sk,sl,ss,sw,ii,gd,so,tl,tg,tt,ty,te,to,tw,ti,tr,tk,cy,ve,vo,wa,wo,ur,uk,uz,es,he,xb,el,ho,nn,sd,hu,su,hy,ig,io,yi,ik,it,iu,hi,yo,jv,za,ts,zu,yue,iw".split(
","
)
dic = {}
for i in range(len(lan_en)):
dic[lan_en[i]] = lan_cn[i]
# print(dic)
return dic[text] if text in dic else text
## 查找账户名
soup_name = soup_video.find("meta", {"itemprop": "name"})
# print("soup_name:", soup_name)
account_name = soup_name["content"]
## 查找账号id
# 账号id 已实现
soup_id = soup_video.find(
"div",
{
"class": "yt-content-metadata-view-model-wiz__metadata-row yt-content-metadata-view-model-wiz__metadata-row--metadata-row-inline"
},
)
## 如果匹配到订阅信息了,说明没有id
tmp = soup_id.text
if tmp[0] != "@":
id = ""
else:
id = tmp[1:]
print(f"账号id:{id}")
# 查找账户所在国家或地区
soup_country_icon = soup_video.find(
"yt-icon", {"icon": "privacy_public"}
)
# 这个标志一定存在
country = soup_country_icon.parent.parent.find_all(
"td", {"class": "style-scope ytd-about-channel-renderer"}
)[-1].text
print(f"账户所在国家或地区:{country}")
## 查找认证标志
# 认证标志 已实现
## 认证标志不一定存在,需要特判
soup_verified = soup_video.find(
"yt-icon",
{
"class": "yt-core-attributed-string__image-element yt-core-attributed-string__image-element--image-alignment-vertical-center"
},
)
verified_flag = 5 if soup_verified else 0
print(f"认证标志:{verified_flag}")
# 查找账户描述
## 账户描述一定存在
soup_description = soup_video.find("meta", {"itemprop": "description"})
description = soup_description["content"]
print(f"账号描述:{description}")
# 查找发布视频信息(视频标题和发布时间) 已实现
## 不一定发布视频,需要特判
soup_videos = soup_video.find_all(
"div", {"class": "style-scope ytd-rich-grid-media", "id": "details"}
)
if not soup_videos:
print("未发布视频")
tmp = []
for soup_item in soup_videos:
try:
video_title = soup_item.find(
"yt-formatted-string", {"id": "video-title"}
).text
for item in soup_item.find_all("span"):
tmp_text = item.text
if "前" in tmp_text:
video_time = tmp_text
break
# print(video_title)
# print(video_time)
tmp.append({"video_title": video_title, "video_time": video_time})
except:
print("解析错误")
break
videos_infos = tmp
# print(f"num(video):{len(self.videos_infos)}")
# 获取备注
## 如果self.videos_infos为空,备注则为未发文,若不为空且包含"年"字样,则备注为x年未发布
memo=""
if len(videos_infos)<1:
memo = "未发文"
elif "年" in videos_infos[0]["video_time"]:
memo = videos_infos[0]["video_time"].replace("前", "") + "未发布"
print(f"备注:{memo}")
# 获取账户使用的主语种
# self.videos_infos可能为空,需要特判
lan_most=""
if videos_infos:
titles = [
item["video_title"]
for index, item in enumerate(videos_infos)
if index < 8
]
print(f"titles:{titles}")
titles = [detect_language(title) for title in titles]
lan_most = most_common_element(titles)
print(f"账号使用的主语言:{lan_most}{lan_en2lan_cn(lan_most)}")
通过以上代码我们已经实现了从youtube用户的主页中提取出 账号名称、账号ID、认证标志、用户所在国家、用户所使用的语种、用户最新发文日期
3 根据账号基本信息得出更高级的信息
有些信息我们无法直接从用户主页得到,我们是否可以根据从用户主页提取出的已有的一些信息,来推断出这些信息无法直接得到的信息?答案是可以的。我们可以借助大模型强大的背景知识和分类归纳能力来完成这件事。
3.1 模型根据账户已知信息推断出用户类型
当账户简介中没有显示账号所在国家时,可以使用该账号中的账号名称、账号描述、账号所使用的语言进行判断国家。
def query_country(text, model=query_gpt4o_mini) -> str:
#query_gpt4o_mini
"""
根据给出的账号简介等信息,得出此单位所属的国家\n\n
output: "国家" | ""
"""
try:
prompt1 = f"""
# 指令
你是一个全球通。我将会提供一个社交账号的相关信息,请你根据这些信息判断该账号所属单位**所在的国家**。如果你不确定,请输出<不确定>。如果该单位在多个国家设有分支机构,请只输出其总部所在的国家。
**输出的国家名称需包含在<>内,且必须是中文**。输出的必须是一个**国家名**。
## 账号信息
{text}
## 参考信息
1. 如果该单位是某个大使馆,请输出该大使馆所在的国家。例如:
- 如果该单位是俄罗斯驻美国大使馆,则输出<美国>。
- 如果该单位是中国驻首尔大使馆,则输出<韩国>。
"""
res_lan = model(prompt1)
print(res_lan)
matched = re.findall(r"<(.*?)>", res_lan)
res = matched[-1]
# print(f"模型推断的国家:{res}")
return res if res != "不确定" else ""
except:
print("查找国家出错")
return "查找国家出错"
4 读取修改并保存表格信息
由于这批要处理的数据是处在excel表格中,所以要用到能够处理表格的库。最常用的有两个库:pandas和openpyxl.
pandas 是一个用于数据操作和分析的开源 Python 库。它提供了高效的数据结构和数据分析工具,特别适用于处理结构化数据。但不支持表格样式的设置。
openpyxl 是一个用于读写 Excel 2010 xlsx/xlsm/xltx/xltm 文件的 Python 库。它允许用户创建、修改和解析 Excel 文件。支持 Excel 中的各种格式和样式(如单元格格式、字体、颜色、边框等),支持在 Excel 文件中创建和修改图表等。
因为我要保持原单元格的样式,所以在这里主要使用了openpyxl库。
import openpyxl
def find_colum_number(wb=None, column_title=""):
"""
根据列名找所在行号,方便之后得到每一行指定列的内容
"""
# wb = copy.deepcopy(wa)
# 选择工作表
sheet = wb.active # 或者使用 sheet = workbook['SheetName']
# sheet = copy.deepcopy(wb).active
# 查找某一标题所在的列号
column_index = None
# 遍历第一行的所有单元格
for cell in sheet[1]: # 假设标题在第一行
if column_title == cell.value:
column_index = cell.column
break
if column_index:
return column_index
else:
print(f"未找到列名 '{column_title}'")
return False
input_file = "example.xlsx"
# 打开一个本地工作簿
wb = openpyxl.load_workbook(input_file)
# 获取活动工作表
ws = wb.active
demo_index = find_colum_number(wb,"demo")
# 遍历行
for index,row in enumerate(ws.iter_rows(min_row=1, max_col=2, max_row=2), start=2):
for cell in row:
# 读取内容
text = str(row[demo_index]) # 读取每一行的demo列的内容
text += "_已修改"
# 把修改后的内容写回单元格
ws.cell(row=index, column=demo_index + 1).value = text
# 保存原表格的内容到一个新的文件
wb.save("example_modified.xlsx")
总结
在这篇文章中,我们详细介绍了如何通过爬取和解析YouTube用户主页信息来完成数据标注任务。具体步骤包括以下几个方面:
-
需求分析:明确了需要从YouTube用户主页中获取的具体信息,包括账号名称、账号ID、认证标志、用户所在国家、用户所使用的语种、用户最新发文日期等。
-
使用Selenium获取渲染后的网页内容:由于许多网页采用动态内容加载技术,传统的静态网页抓取工具无法获取到完整的信息,因此我们选择了Selenium来模拟浏览器行为,获取完整的网页内容。
-
从渲染后的网页中提取信息:通过BeautifulSoup解析获取到的网页源码,并使用相应的定位方法提取出所需的元素信息。
-
根据已有信息推断更高级的信息:利用大模型的强大背景知识和分类归纳能力,根据提取出的已有信息推断出用户类型等更高级的信息。
-
读取、修改并保存表格信息:使用openpyxl库读取Excel表格中的数据,进行必要的修改后保存到新的文件中,保持了原单元格的样式。
通过以上步骤,我们实现了从YouTube用户主页中提取并处理所需信息的完整流程,为数据标注任务提供了有效的解决方案。希望这篇文章能为有类似需求的读者提供有价值的参考和帮助。
此外,这是我第一次撰写文章,应该存在很多不足,还恳请各位佬给出宝贵建议。