在傳統(tǒng)網(wǎng)絡(luò)爬蟲中簇宽,主要的挑戰(zhàn)一直是手動(dòng)操作的工作量苔巨。使用像 Beautiful Soup(BS4)和 Selenium 這樣的工具時(shí)槐脏,我們需要為每個(gè)新網(wǎng)站編寫解析代碼,需要適配和適應(yīng)不同的 HTML 結(jié)構(gòu)艰躺。這種不斷的修改既耗時(shí)又容易出錯(cuò)呻袭。然而,當(dāng)出現(xiàn)了大模型之后就沒那么復(fù)雜了腺兴。隨著具備視覺功能的大型語言模型(LLM)的出現(xiàn)左电,我們現(xiàn)在可以創(chuàng)建幾乎通用的網(wǎng)絡(luò)爬蟲代理,大大簡化和自動(dòng)化了這一過程页响。
在這篇博客中篓足,我們的主要關(guān)注點(diǎn)是探討三種強(qiáng)大的工具:ScrapeGraph、FireCrawl 和 AgentQL闰蚕。這些創(chuàng)新的庫在革命性地改變網(wǎng)絡(luò)爬蟲領(lǐng)域方面發(fā)揮了關(guān)鍵作用栈拖,提供了先進(jìn)的功能,使我們能夠創(chuàng)建高效且多功能的爬蟲代理没陡。通過深入的討論和實(shí)際示例辱魁,我們將詳細(xì)探討這些工具如何簡化網(wǎng)絡(luò)爬蟲過程烟瞧,并實(shí)現(xiàn)構(gòu)建由 LLM 模型驅(qū)動(dòng)的爬蟲代理的目標(biāo)。
ScrapeGraph
ScrapeGraphAI 是一個(gè)開源框架染簇,它利用大型語言模型(LLM)和直接圖邏輯的力量來簡化網(wǎng)絡(luò)爬蟲過程。使用 ScrapeGraphAI强岸,為網(wǎng)站锻弓、文檔和 XML 文件創(chuàng)建爬蟲管道變得輕而易舉。你只需指定要提取的信息蝌箍,其余的工作由庫來處理青灼。其直觀的界面和先進(jìn)的功能使其成為開發(fā)人員尋求高效、精確的網(wǎng)絡(luò)爬蟲解決方案時(shí)的首選妓盲。
import os
from dotenv import load_dotenv
from scrapegraphai.graphs import SmartScraperGraph
from scrapegraphai.utils import prettify_exec_info
import json
# 從.env加載環(huán)境變量
load_dotenv()
# 從環(huán)境變量加載openai key
openai_key = os.getenv("OPENAI_APIKEY")
# 配置SmartScraperGraph
graph_config = {
"llm": {
"api_key": openai_key,
"model": "gpt-3.5-turbo",
},
}
# 創(chuàng)建SmartScraperGraph并運(yùn)行
smart_scraper_graph = SmartScraperGraph(
prompt="列舉所有的產(chǎn)品和他們的價(jià)格",
# 接收一個(gè)html網(wǎng)頁頁面
source="https://s.taobao.com/search?page=1&q=iphone",
config=graph_config
)
# 執(zhí)行爬蟲并保存結(jié)果
result = smart_scraper_graph.run()
with open("results.json", 'w', encoding='utf-8') as f:
json.dump(result, f, indent=4)
我們導(dǎo)入了必要的模塊和庫杂拨,如 os、dotenv悯衬、SmartScraperGraph 和 json弹沽。首先,我們從 .env 文件中加載環(huán)境變量筋粗,這是安全存儲像 API 密鑰等敏感信息的常見做法策橘。graph_config 字典包含 SmartScraperGraph 所需的配置設(shè)置。在本例中娜亿,它包含 OpenAI API 密鑰并指定使用的 GPT 模型(gpt-3.5-turbo)丽已。接下來,我們創(chuàng)建 SmartScraperGraph 類的實(shí)例买决,向其提供提示(查詢)沛婴、源(要爬取的網(wǎng)頁 URL)和配置設(shè)置。在 smart_scraper_graph 實(shí)例上調(diào)用 run() 方法來執(zhí)行爬蟲并從網(wǎng)頁中提取數(shù)據(jù)督赤。提取的數(shù)據(jù)存儲在 result 變量中嘁灯。最后,使用 json.dump() 方法將提取的數(shù)據(jù)保存到名為 "results.json" 的 JSON 文件中够挂,供進(jìn)一步處理或分析旁仿。
{
"products": [
{
"Name": "Apple/蘋果 iPhone 13 Pro Max蘋果13promax 蘋果13 pro 手機(jī)",
"Price": "¥3238.00"
},
{
"Name": "新款A(yù)pple/蘋果 iPhone 15 Pro Max 蘋果5G手機(jī)15ProMax 國行正品",
"Price": "¥7428.00"
},
{
"Name": "Apple/蘋果 iPhone 15 支持移動(dòng)聯(lián)通電信5G 雙卡雙待手機(jī)",
"Price": "¥6999.00"
},
{
"Name": "Apple/蘋果 iPhone14ProMax雙卡原裝正品蘋果14Promax全網(wǎng)通全新",
"Price": "¥5999.00"
},
{
"Name": "Apple/蘋果 iPhone 15 Pro Max",
"Price": "¥9999.00"
}
]
}
如果你想從不同的來源爬取數(shù)據(jù),只需在代碼中更改 URL孽糖。SmartScraperGraph 的靈活性允許你在不顯著修改代碼的情況下枯冈,針對各種網(wǎng)站或網(wǎng)頁。這意味著你可以根據(jù)具體需求調(diào)整爬蟲過程办悟,輕松從各種來源收集數(shù)據(jù)尘奏。雖然 SmartScraperGraph 在處理某些網(wǎng)站的彈出窗口或攔截器時(shí)可能遇到限制,但需要注意的是病蛉,SmartScraperGraph 是一個(gè)開源庫炫加,這意味著你可以根據(jù)具體要求對其進(jìn)行定制瑰煎。
FireCrawl
Firecrawl 作為一個(gè)強(qiáng)大的解決方案,配備了一系列功能俗孝,旨在克服網(wǎng)絡(luò)爬蟲工作中的固有挑戰(zhàn)酒甸。它高效地管理代理、緩存赋铝、速率限制等復(fù)雜性插勤,確保數(shù)據(jù)檢索過程的順暢。Firecrawl 的爬取能力擴(kuò)展到網(wǎng)站的所有可訪問子頁面革骨,無論是否存在站點(diǎn)地圖农尖,保證全面的數(shù)據(jù)提取。即使面對通過 JavaScript 動(dòng)態(tài)渲染的內(nèi)容良哲,F(xiàn)irecrawl 也能非常高效的地捕獲每一條有價(jià)值的信息盛卡。其輸出經(jīng)過 Markdown 格式化,簡化了與大型語言模型(LLM)和其他應(yīng)用程序的集成筑凫。
你可以注冊 Firecrawl 的免費(fèi)套餐滑沧,獲得基本的爬蟲功能。通過在這注冊漏健,你可以爬取最多 500 個(gè)頁面嚎货,限制為每分鐘 5 次爬取以及 1 個(gè)并發(fā)爬取任務(wù)。
下面是一個(gè)使用FireCrawl爬取
from firecrawl import FirecrawlApp
from openai import OpenAI
from dotenv import load_dotenv
import os
import json
import pandas as pd
from datetime import datetime
def scrape_data(url):
load_dotenv()
# 初始化FirecrawlApp實(shí)例
app = FirecrawlApp(api_key=os.getenv('FIRECRAWL_API_KEY'))
# 爬取單個(gè)URL
scraped_data = app.scrape_url(url, {'pageOptions': {'onlyMainContent': True}})
# 檢查是否爬取到了markdown數(shù)據(jù)
if 'markdown' in scraped_data:
return scraped_data['markdown']
else:
raise KeyError("The key 'markdown' does not exist in the scraped data.")
def save_raw_data(raw_data, timestamp, output_folder='output'):
# 確保輸出文件夾存在
os.makedirs(output_folder, exist_ok=True)
# 保存原始數(shù)據(jù)到Markdown文件
raw_output_path = os.path.join(output_folder, f'rawData_{timestamp}.md')
with open(raw_output_path, 'w', encoding='utf-8') as f:
f.write(raw_data)
print(f"Raw data saved to {raw_output_path}")
def format_data(data, fields=None):
load_dotenv()
# 初始化OpenAI實(shí)例
client = OpenAI(api_key=os.getenv('OPENAI_APIKEY'))
# 如果未提供字段列表蔫浆,則使用默認(rèn)字段
if fields is None:
fields = ["名稱","價(jià)格","地址", "鏈接"]
# 定義系統(tǒng)消息內(nèi)容
system_message = f"""你是一個(gè)智能文本提取和轉(zhuǎn)換助手殖属。你的任務(wù)是從給定的文本中提取結(jié)構(gòu)化信息,并將其轉(zhuǎn)換為純JSON格式瓦盛。JSON
應(yīng)僅包含從文本中提取的結(jié)構(gòu)化數(shù)據(jù)洗显,不包含任何額外的評論、解釋或無關(guān)的信息原环。你可能會(huì)遇到無法找到所需字段數(shù)據(jù)的情況挠唆,或者數(shù)據(jù)會(huì)以外語形式出現(xiàn)。請?zhí)幚硪韵挛谋局雎穑⒁约僇SON格式提供輸出玄组,JSON前后不應(yīng)有任何文字。:"""
# 定義用戶消息內(nèi)容
user_message = f"請?zhí)峁┮幚淼奈谋竞托枰崛〉男畔⒆侄巍?\nPage content:\n\n{data}\n\nInformation to extract: {fields}"
response = client.chat.completions.create(
model="gpt3.5",
response_format={"type": "json_object"},
messages=[
{
"role": "system",
"content": system_message
},
{
"role": "user",
"content": user_message
}
]
)
# 檢查API響應(yīng)是否包含選擇數(shù)據(jù)
if response and response.choices:
formatted_data = response.choices[0].message.content.strip()
print(f"Formatted data received from API: {formatted_data}")
try:
parsed_json = json.loads(formatted_data)
except json.JSONDecodeError as e:
print(f"JSON decoding error: {e}")
print(f"Formatted data that caused the error: {formatted_data}")
raise ValueError("The formatted data could not be decoded into JSON.")
return parsed_json
else:
raise ValueError("The OpenAI API response did not contain the expected choices data.")
def save_formatted_data(formatted_data, timestamp, output_folder='output'):
# Ensure the output folder exists
os.makedirs(output_folder, exist_ok=True)
# 保存格式化數(shù)據(jù)到JSON文件
output_path = os.path.join(output_folder, f'sorted_data_{timestamp}.json')
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(formatted_data, f, indent=4)
print(f"Formatted data saved to {output_path}")
# 檢查格式化數(shù)據(jù)是否為字典且只包含一個(gè)鍵
if isinstance(formatted_data, dict) and len(formatted_data) == 1:
key = next(iter(formatted_data)) # Get the single key
formatted_data = formatted_data[key]
# 轉(zhuǎn)換格式化數(shù)據(jù)為pandas DataFrame
df = pd.DataFrame(formatted_data)
# 準(zhǔn)換格式化數(shù)據(jù)為pandas DataFrame
if isinstance(formatted_data, dict):
formatted_data = [formatted_data]
df = pd.DataFrame(formatted_data)
# 保存格式化數(shù)據(jù)到CSV文件
# excel_output_path = os.path.join(output_folder, f'sorted_data_{timestamp}.xlsx')
df.to_csv(f"{timestamp}.csv", index=False)
if __name__ == "__main__":
# 爬取的URL
url = 'https://bj.ke.com/ershoufang/rs/'
try:
# 生成時(shí)間戳
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# 爬取數(shù)據(jù)
raw_data = scrape_data(url)
# 保存原始數(shù)據(jù)
save_raw_data(raw_data, timestamp)
# 格式化數(shù)據(jù)
formatted_data = format_data(raw_data)
# 保存格式化數(shù)據(jù)
save_formatted_data(formatted_data, timestamp)
except Exception as e:
print(f"An error occurred: {e}")
-
導(dǎo)入庫 導(dǎo)入必要的庫谒麦,例如
firecrawl
俄讹、OpenAI
、dotenv
绕德、os
患膛、json
、pandas
和datetime
耻蛇,以便實(shí)現(xiàn)爬取和數(shù)據(jù)處理所需的各種功能踪蹬。
import firecrawl
import OpenAI
import dotenv
import os
import json
import pandas as pd
from datetime import datetime
-
爬取數(shù)據(jù)
scrape_data()
函數(shù)利用你的 API 密鑰初始化一個(gè) FirecrawlApp胞此,并使用 Firecrawl 爬取一個(gè) URL。它檢索網(wǎng)頁的主要內(nèi)容并以 Markdown 格式返回跃捣。
def scrape_data(api_key, url):
app = firecrawl.FirecrawlApp(api_key)
content = app.scrape(url)
return content
-
保存原始數(shù)據(jù)
save_raw_data()
函數(shù)將原始的 Markdown 數(shù)據(jù)保存到文件中漱牵,為便于識別,文件名附加時(shí)間戳枝缔。
def save_raw_data(data):
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
filename = f"raw_data_{timestamp}.md"
with open(filename, 'w') as file:
file.write(data)
-
格式化數(shù)據(jù)
format_data()
函數(shù)利用 OpenAI 的 GPT 模型從原始數(shù)據(jù)中提取結(jié)構(gòu)化信息布疙。它構(gòu)建系統(tǒng)和用戶消息,提示模型從文本中提取指定字段愿卸。提取的數(shù)據(jù)以 JSON 格式返回。
def format_data(api_key, raw_data):
openai.api_key = api_key
response = openai.Completion.create(
model="text-davinci-003",
prompt=f"從文本中提取結(jié)構(gòu)化數(shù)據(jù):\n\n{raw_data}",
max_tokens=1000
)
return json.loads(response.choices[0].text)
-
保存格式化數(shù)據(jù)
save_formatted_data()
函數(shù)將格式化的數(shù)據(jù)以 JSON 格式保存到文件中截型,文件名附加時(shí)間戳趴荸。此外,它將 JSON 數(shù)據(jù)轉(zhuǎn)換為 pandas DataFrame 并保存為 CSV 文件宦焦,以便進(jìn)一步分析发钝。
def save_formatted_data(data):
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
json_filename = f"formatted_data_{timestamp}.json"
csv_filename = f"formatted_data_{timestamp}.csv"
with open(json_filename, 'w') as json_file:
json.dump(data, json_file)
df = pd.DataFrame(data)
df.to_csv(csv_filename, index=False)
-
主函數(shù) 在主函數(shù)中,指定要爬取的 URL(
url
)波闹。腳本然后嘗試爬取數(shù)據(jù)酝豪、保存原始數(shù)據(jù)、格式化數(shù)據(jù)并保存格式化數(shù)據(jù)精堕。如果在這些步驟中的任何一步發(fā)生錯(cuò)誤孵淘,它將被捕獲并顯示。
if __name__ == "__main__":
api_key = os.getenv("FIRECRAWL_API_KEY")
url = "https://example.com" # 替換為要爬取的實(shí)際 URL
try:
raw_data = scrape_data(api_key, url)
save_raw_data(raw_data)
formatted_data = format_data(api_key, raw_data)
save_formatted_data(formatted_data)
except Exception as e:
print(f"An error occurred: {e}")
JSON 格式的輸出
[
{
"名稱": "南向一居室戶型好歹篓,中間樓層瘫证,老少居住皆宜。",
"地址": "二里莊北里",
"價(jià)格": "345萬",
"鏈接": "https://bj.ke.com/ershoufang/101125066609.html"
},
{
"名稱": "三里河 正規(guī)2 居室 隨時(shí)可看 隨時(shí)簽約",
"地址": "三里河三區(qū)",
"價(jià)格": "798萬",
"鏈接": "https://bj.ke.com/ershoufang/101125059884.html"
},
{
"名稱": "驪龍園 3室2廳 南 北",
"地址": "驪龍園",
"價(jià)格": "559萬",
"鏈接": "https://bj.ke.com/ershoufang/101125057747.html"
},
{
"名稱": "南三環(huán)草橋馬家堡南向一居室商品房社區(qū)",
"地址": "璽萌鵬苑",
"價(jià)格": "270萬",
"鏈接": "https://bj.ke.com/ershoufang/101125055143.html"
},
{
"名稱": "2018年次新小區(qū) 19號線10號線機(jī)場線草橋站",
"地址": "今日草橋",
"價(jià)格": "710萬",
"鏈接": "https://bj.ke.com/ershoufang/101125049365.html"
},
{
"名稱": "裕華園一區(qū) 1室1廳 南 北",
"地址": "裕華園一區(qū)",
"價(jià)格": "71萬",
"鏈接": "https://bj.ke.com/ershoufang/101125059731.html"
},
{
"名稱": "化工大院 南北通透兩居室 不臨街 平改坡 停車方便",
"地址": "化工大院",
"價(jià)格": "620萬",
"鏈接": "https://bj.ke.com/ershoufang/101125053322.html"
},
{
"名稱": "北辰福第二號院 高樓層采光視野好 滿五唯一 無抵押",
"地址": "北辰福第二號院",
"價(jià)格": "409萬",
"鏈接": "https://bj.ke.com/ershoufang/101125048788.html"
},
{
"名稱": "海特花園西區(qū)三居室庄撮,戶型方正背捌,南北通透",
"地址": "海特花園西區(qū)",
"價(jià)格": "445萬",
"鏈接": "https://bj.ke.com/ershoufang/101125045278.html"
},
{
"名稱": "中國鐵建國際城 3室1廳 南 北",
"地址": "中國鐵建國際城",
"價(jià)格": "680萬",
"鏈接": "https://bj.ke.com/ershoufang/101125045266.html"
}
]
使用 Firecrawl,我們可以毫不費(fèi)力地爬取整個(gè)網(wǎng)頁洞斯,而無需擔(dān)心復(fù)雜的細(xì)節(jié)毡庆。只需更改 URL,我們就可以輕松地將爬取工作適應(yīng)不同的來源烙如。盡管 Firecrawl 在處理動(dòng)態(tài)內(nèi)容和速率限制等爬取挑戰(zhàn)方面表現(xiàn)出色么抗,但仍然需要爬取網(wǎng)站上的所有頁面,并導(dǎo)航到后續(xù)頁面直到到達(dá)最后一個(gè)頁面厅翔。這時(shí)乖坠,AgentQL 提供了一個(gè)解決方案。
AgentQL
AgentQL for Web 通過使用自然語言查詢提供了一種革命性的方法來與網(wǎng)頁元素交互刀闷。借助 AgentQL熊泵,用戶可以輕松定位和交互網(wǎng)頁元素仰迁,而無需復(fù)雜的代碼或特定選擇器。這種直觀的界面簡化了網(wǎng)頁自動(dòng)化的過程顽分,使用戶能夠輕松高效地執(zhí)行任務(wù)徐许。無論是點(diǎn)擊按鈕、填寫表單還是瀏覽頁面卒蘸,AgentQL for Web 都簡化了交互過程雌隅,使網(wǎng)頁自動(dòng)化對各類用戶都變得易于訪問。
from dotenv import load_dotenv
import agentql
#from agentql.sync_api import ScrollDirection
import csv
load_dotenv()
PRODUCTS = """
{
results{
products[]{
product_name
product_price
num_reviews
rating
}
}
}
"""
NEXT_PAGE_BTN ="""
{
next_page_button_enabled
next_page_button_disabled
}
"""
session = agentql.start_session("https://s.taobao.com/search?page=1&q=iphone")
session.driver.scroll_to_bottom()
pagination = session.query(NEXT_PAGE_BTN)
print("get pagination")
print(pagination.next_page_button_enabled)
with open("products.csv", "a",newline="") as file:
fieldnames = ["product_name","product_price","num_reviews","rating"]
writer = csv.DictWriter(file,fieldnames=fieldnames)
writer.writeheader()
print(f"enabled button : {pagination.to_data()['next_page_button_enabled']}")
print(f"disabled button : {pagination.to_data()['next_page_button_disabled']}")
while(
pagination.to_data()['next_page_button_enabled'] and
pagination.to_data()['next_page_button_disabled'] is None
):
products = session.query(PRODUCTS)
print("scraped product data")
print(products.to_data())
for product in products.to_data()['results']['products']:
print(f"product: {product}")
writer.writerow(product)
print("data written to csv")
- 查詢定義:定義了兩個(gè) GraphQL 查詢:PRODUCTS 用于檢索產(chǎn)品詳細(xì)信息缸沃,而 NEXT_PAGE_BTN 用于檢查分頁的下一頁按鈕是否可用恰起。
- 會(huì)話初始化:使用 agentql.start_session() 與目標(biāo) URL("https://s.taobao.com/search?page=1&q=iphone")建立會(huì)話。
- 滾動(dòng)到底部:使用 session.driver.scroll_to_bottom() 將瀏覽器窗口滾動(dòng)到頁面底部趾牧。
- 分頁檢查:使用 NEXT_PAGE_BTN 查詢確定下一頁按鈕的狀態(tài)(啟用或禁用)检盼,結(jié)果存儲在 pagination 變量中。
- CSV 文件初始化:創(chuàng)建一個(gè)名為“products.csv”的 CSV 文件翘单,并定義產(chǎn)品信息的字段名稱吨枉。
- 數(shù)據(jù)提取循環(huán):在下一頁按鈕啟用且未找到禁用按鈕的情況下:使用 PRODUCTS 查詢提取產(chǎn)品數(shù)據(jù)。每個(gè)產(chǎn)品的詳細(xì)信息寫入 CSV 文件哄芜。該過程持續(xù)進(jìn)行貌亭,直到爬取完所有頁面。
結(jié)論
ScrapeGraph认臊、Firecrawl 和 AgentQL 代表了最新一代的網(wǎng)絡(luò)爬蟲框架圃庭。每個(gè)框架都有自己的優(yōu)勢,可以滿足不同的爬取需求美尸。 ScrapeGraph 利用 LLM 和直接圖形邏輯實(shí)現(xiàn)多功能爬取管道冤议,F(xiàn)irecrawl 擅長高效處理復(fù)雜的 Web 場景,AgentQL 引入自然語言交互以實(shí)現(xiàn)無縫 Web 元素操作师坎。這些框架共同簡化了從網(wǎng)絡(luò)中提取有價(jià)值數(shù)據(jù)的過程恕酸,為開發(fā)人員提供了強(qiáng)大的工具來輕松處理爬取任務(wù)。