نمایشنامه نویس در ابر: خودکار سازی استخراج داده های بررسی

بیانیه مشکل
الزام این برنامه استخراج داده های مرور از وب سایت های محصول است که در بخش بررسی صفحه بندی دارند و نیاز به پشتیبانی جهانی برای همه نوع صفحه بندی دارند. همچنین برای بازگشت به بررسی استخراج شده ، API GET نیز لازم است.
اجرای
این شامل سه مؤلفه اصلی ، API Gateway ، عملکرد لامبدا و نمونه EC2 است. بیایید به صورت جداگانه به هر مؤلفه شیرجه بزنیم:
دروازه API
همانطور که از نام آن پیداست ، روند اتوماسیون ما را در معرض شبکه قرار می دهد. برای این مورد ، این یک API REST با پاسخ به این صورت است:
{
"reviews_count": 100,
"reviews": [
{
"title": "Review Title",
"body": "Review body text",
"rating": 5,
"reviewer": "Reviewer Name"
},
...
]
}
API که ما به تازگی ایجاد کردیم ، عملکرد Lambda را ایجاد می کند که روند کار را در نمونه EC2 ما با استفاده از SSM مدیریت می کند (بلوک کد برای SSM در بخش بعدی وصل شده است).
نقطه پایانی API باید یک پارامتر جستجوی پرس و جو به نام “صفحه” داشته باشد. نقطه پایانی نهایی باید مانند این باشد: /api/reviews?page={url}
بشر پارامتر جستجوی پرس و جو با استفاده از پارامتر رویداد موجود در عملکرد Lambda به عملکرد Lambda منتقل می شود.
ما باید اطمینان حاصل کنیم که ادغام پروکسی Lambda به درستی تنظیم شده است تا بتوانید عملکرد Lambda را به عنوان پاسخ API ما دریافت کنید.
عملکرد لامبدا
عملکرد لامبدا ما به عنوان واسطه ای کار خواهد کرد که از تماس API شروع می شود و خط لوله اتوماسیون را در EC2 اجرا می کند و تولید خروجی را توسط خط لوله به پاسخ API منتقل می کند.
عملکرد Lambda با استفاده از پارامتر رویداد منتقل شده در عملکرد Lambda مانند این ، پارامتر جستجوی پرس و جو را دریافت می کند:
url = event['queryStringParameters']['page']
همانطور که قبلاً ذکر شد ، از SSM برای مدیریت فرآیند اجرا شده توسط نمونه EC2 استفاده می کند ، در اینجا بلوک کد است که مسئول آن است
ssm = boto3.client('ssm', region_name='ap-south-1')
unique_id = str(uuid.uuid4())
# Send command to EC2 instance
response = ssm.send_command(
InstanceIds=['i-instanceIdOfTheEc2VM'],
DocumentName='AWS-RunPowerShellScript',
Parameters={
'commands': [f'C:\\Users\\Administrator\\AppData\\Local\\Programs\\Python\\Python311\\python.exe C:\\final-automation-w-rating.py "{url}" "{unique_id}"']
}
)
command_id = response['Command']['CommandId']
این بلوک اسکریپت Python موجود در نمونه EC2 را اجرا می کند. ما به عنوان آرگومان خط فرمان برای اسکریپت پایتون در حال عبور از “منحصر به فرد” و “url” هستیم.
سپس یک بلوک همزمان را اجرا می کند که وضعیت این روند را به پایان رساند یا هر 8 ثانیه به پایان برسد.
while True:
try:
invocation_response = ssm.get_command_invocation(
CommandId=command_id,
InstanceId='i-07b0999d978efd1fb'
)
status = invocation_response['Status']
if status in ['Success', 'Failed', 'Cancelled', 'TimedOut']:
print(f"Command finished with status: {status}")
break
print(f"Current status: {status}. Waiting for completion...")
time.sleep(8)
except ssm.exceptions.InvocationDoesNotExist:
print("Invocation does not exist yet. Retrying...")
time.sleep(2)
فرآیند نهایی ، داده های استخراج شده را از نمونه EC2 بدست آورید. برای انجام این کار ، ما از سطل S3 برای انتقال داده های بین عملکرد EC2 و Lambda استفاده خواهیم کرد. پیش از این ما “منحصر به فرد” را به عنوان آرگومان خط فرمان به اسکریپت پایتون منتقل کردیم ، به عنوان نام پرونده برای پرونده JSON که سپس در سطل S3 بارگذاری می شود ، خدمت می کند. از آنجا که ما از عملکرد Lambda منحصر به فرد عبور می کنیم تا پس از اتمام فرآیند EC2 ، بتوانیم داده ها را از سطل S3 بدست آوریم و داده ها را در بیانیه برگشتی که به عنوان پاسخ API عمل می کند ، ریخته کنیم.
s3_client = boto3.client('s3')
bucket_name = 'extracted-reviews'
file_name = f'{unique_id}.json'
try:
s3_response = s3_client.get_object(Bucket=bucket_name, Key=file_name)
file_data = s3_response['Body'].read().decode('utf-8')
json_data = json.loads(file_data)
return {
'statusCode': 200,
"headers": {
'Content-Type': 'application/json',
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
'body': json.dumps(json_data)
}
except Exception as e:
return {
'statusCode': 500,
"headers": {
'Content-Type': 'application/json',
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
'body': json.dumps({'error': str(e)})
}
نمونه EC2
اول ، ما باید تا حد امکان کد را از کد منبع پایین بیاوریم تا اندازه توکن برای LLM کاهش یابد ، که باعث کاهش هزینه های API ، بهبود عملکرد و افزایش دقت می شود. برای این منظور ، از زیبا برای حذف همه چیز در برچسب های زیر استفاده می شود: اسکریپت ، سبک ، IMG ، NAV ، هدر ، پاورقی ، تصویر ، SVG ، مسیر و فرم.
def filter_source(source):
soup = BeautifulSoup(source, 'html.parser')
for script in soup(["script", "style", "img", "nav", "header", "footer", "picture", "svg", "path", "form"]):
script.decompose()
cleaned_body_content = str(soup.body)
return cleaned_body_content
اکنون ، برای رفتن به صفحه بررسی بعدی ، برنامه باید روی دکمه “Next” کلیک کند. برای انجام این کار ، به نام کلاس دکمه نیاز دارد. از آنجا که هر وب سایت دارای یک نام کلاس منحصر به فرد برای دکمه است ، ما نمی توانیم نام کلاس را به سختی کدگذاری کنیم. برای پرداختن به این موضوع ، من از LLM برای تعیین نام کلاس از کد منبع استفاده می کنم. این برنامه همچنین نیاز به بازیابی جزئیات بررسی دارد ، بنابراین من کد را برای شناسایی نام کلاس عناصر بررسی نیز اضافه کرده ام. من از Google AI Studio API (Gemini 1.5 Flash) استفاده می کنم زیرا این برنامه رایگان است و از اندازه ورودی 1 میلیون نشانه پشتیبانی می کند ، که تقریباً تضمین می کند که کد منبع به عنوان ورودی متناسب باشد.
#global variable
review_paginate_next = ""
review_author = ""
review_title = ""
review_text = ""
review_rating = ""
prompt = """extract the following class name for each of the following elements:
- pagination "next page" button of review section
- name of reviewer
- title of review
- text of review
- rating classname
from the provided codebase.
Just return a comma seperated value of classnames, if multiple class name is found for the same section, use the most relevant one which is unique.
Don't trim the values, return the value as it is in source code.
Don't return any other text than mentioned. Here is the code: """
google_api_key = os.getenv('GOOGLE_API_KEY')
def filter_css_selector(source_text, max_retries = 3):
response = requests.post(
url=f"https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={google_api_key}",
headers={
"Content-Type": "application/json"
},
json={
"contents": [
{
"parts": [
{
"text": prompt + source_text
}
]
}
]
}
)
if response.status_code == 200:
data = response.json()
message_content = data['candidates'][0]['content']['parts'][0]['text']
message_content = message_content.strip("\n")
try:
global review_paginate_next, review_author, review_title, review_text, review_rating
review_paginate_next, review_author, review_title, review_text, review_rating = message_content.split(",")
next_buttons.append(f'.{review_paginate_next}')
print(review_paginate_next)
print(review_author)
print(review_title)
print(review_text)
print(review_rating)
except:
# also try with some other model
if (max_retries > 0):
time.sleep(2)
filter_css_selector(source_text, max_retries - 1)
else:
# handles model overload error or any other error encountered by LLM API
print(response.json())
if (max_retries > 0):
time.sleep(2)
filter_css_selector(source_text, max_retries - 1)
هنگامی که ما نام کلاس را می دانیم ، برنامه می تواند صفحه بررسی را با استفاده از BeautifulSoup به صفحه بررسی کند. چرا به جای LLM از گروه زیبا استفاده می کنیم؟ از آنجا که این رعد و برق سریع است ، مقادیر مثبت کاذب مانند LLM را تولید نمی کند (اگرچه اگر نام کلاس نادرست باشد ، می تواند مقادیر را کاملاً از دست بدهد) ، و محدودیت های نرخ آن را ندارد ، بنابراین می توانیم به عنوان بسیاری از صفحات مورد نیاز را ببندیم بشر
def extract_reviews(source):
body_strainer = SoupStrainer('body')
soup = BeautifulSoup(source, 'html.parser', parse_only=body_strainer)
titles = soup.find_all(class_=review_title)
bodies = soup.find_all(class_=review_text)
authors = soup.find_all(class_=review_author)
ratings = soup.find_all(class_=review_rating)
for i in range(max(len(titles), len(bodies), len(authors), len(ratings))):
review = {
"title": titles[i].get_text(strip=True) if i < len(titles) else "",
"body": bodies[i].get_text(strip=True) if i < len(bodies) else "",
"author": authors[i].get_text(strip=True) if i < len(authors) else "",
"rating": ratings[i].get_text(strip=True) if i < len(ratings) else ""
}
reviews.append(review)
اکنون ، داده ها باید از نمونه EC2 به عملکرد Lambda منتقل شوند تا بتوان از طریق دروازه API بازگردید. برای انجام این کار ، من از یک سطل S3 استفاده می کنم. این رویکرد همچنین به آن اجازه می دهد تا به عنوان یک فروشگاه حافظه نهان برای بررسی های قبلاً استخراج شده عمل کند.
def upload_to_s3 (داده ، منحصر به فرد_File_Name):
s3_client = boto3.client ('s3') # ایجاد مشتری S3
bucket_name="extracted-reviews" # Replace with your bucket name
s3_client.put_object(
Bucket=bucket_name,
Key=unique_file_name,
Body=json.dumps(data), # Convert list to JSON string
ContentType="application/json"
)
print(f"Responses uploaded to s3://{bucket_name}/{unique_file_name}")
اکنون ، برای ترکیب همه چیز و خودکار کردن روند ، من از نمایشنامه نویس استفاده می کنم. روی دکمه کلیک می کند ، منبع صفحه را دریافت می کند ، بررسی ها را استخراج می کند و روند کار را تکرار می کند تا تمام بررسی ها استخراج شود (با محدودیت سخت 20 صفحه برای اطمینان از اینکه کاربر مجبور نیست به طور نامحدود منتظر بماند ، زیرا ما از جریان استفاده نمی کنیم برای خروجی داده ها در پرواز). علاوه بر این ، اگر استخراج بررسی به هر دلیلی نتواند ، من یک عملکرد برگشتی را پیاده سازی کرده ام تا اطمینان حاصل شود که حداقل برخی از داده های بررسی را در پاسخ باز می گرداند.
async def scrape(url, file_name):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
await page.goto(url)
await page.wait_for_selector('body')
page_source = await page.content()
cleaned_body_content = filter_source(page_source)
filter_css_selector(cleaned_body_content)
dialog_close_attempt = 1
for elm in next_buttons:
count = 0
while True:
await page.wait_for_selector('body')
page_source = await page.content()
extract_reviews(page_source)
print(count)
count += 1
if (count > 20): break
try:
next_button = page.locator(elm)
await page.mouse.click(x=0, y=page.viewport_size['height'] // 2)
await asyncio.wait_for(next_button.click(), timeout=5)
await page.wait_for_load_state('networkidle')
await page.wait_for_selector('body')
except asyncio.TimeoutError:
break
except Exception as e:
print("Bro, error with pagination? ", e)
break
#Handle infinite scroll
prev_height = -1
max_scrolls = 20 # Set a maximum number of scrolls to prevent infinite loops
scroll_count = 0
while scroll_count < max_scrolls:
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
await page.wait_for_timeout(200)
new_height = await page.evaluate("document.body.scrollHeight")
if new_height == prev_height:
break
prev_height = new_height
scroll_count += 1
page_source = await page.content()
extract_reviews(page_source)
if (len(reviews) == 0):
fallback_review_extraction(cleaned_body_content)
fallback_reviews["reviews_count"] = len(fallback_reviews["reviews"])
upload_to_s3(fallback_reviews, file_name)
# print(fallback_reviews)
else:
reviews_dict = {"reviews_count" : len(reviews), "reviews": reviews}
# print(reviews_dict)
upload_to_s3(reviews_dict, file_name)
چالش های روبرو
- سه چالش من هنگام انتخاب LLM با آن روبرو شدم: اول ، این مقادیر مثبت کاذب را برمی گرداند ، و دوم ، کد منبع در متن ورودی و سوم ، عملکرد قرار نمی گرفت. من چندین مدل رایگان یعنی Llama ، Mistral و Qwen را امتحان کردم ، اما هرکدام کاستی های خود را داشتند. برخی از آنها اندازه توکن ورودی بسیار کمی داشتند ، برخی از آنها تولید تصادفی (مثبت کاذب) تولید می کردند و برخی دیگر هنگام پخت و پز مواد غذایی به اندازه من کند بودند. “Gemini 1.5 Flash” بهترین به نظر می رسد ، با زمان پاسخ از 1.5 ثانیه تا 10 ثانیه (برای بیشتر موارد) ، تولید مقادیر دقیق (نه همیشه بلکه بهتر از سایرین) ، یک اندازه ورودی عظیم تا 1 میلیون (PS: من در آن زمان در هنگام ساخت پروژه هیچ ایده ای در مورد Deepseek نداشتم.)
- یک کادر گفتگو به طور تصادفی ظاهر می شود و دکمه “بعدی” را مسدود می کند. برای جلوگیری از این کار ، قبل از کلیک بر روی هر دکمه ، من اطمینان حاصل می کنم که بر روی مختصات x = 0 ، y = 50 کلیک کنید تا مطمئن شوید که کادر گفتگو قبل از کلیک بر روی دکمه “بعدی” ناپدید می شود.
- از آنجا که نمایشنامه نویس روی کروم کار می کند ، به طور بومی توسط توابع لامبدا پشتیبانی نمی شود. بنابراین ، EC2 به عنوان یک راه حل دستی استفاده شد. خدمات شخص ثالث دیگری برای اجرای مشاغل اتوماسیون در دسترس است ، اما آنها به هزینه های اضافی نیاز دارند.
- انتقال داده ها از EC2 به Lambda به طور مستقیم پشتیبانی نمی شود ، بنابراین مجبور شدم از S3 استفاده کنم (اگرچه به دلیل خواندن و نوشتن عملیات روی سطل S3 به هزینه اضافه می کند).
- AWS API Gateway 29 ثانیه پیش فرض دارد و زمان اجرا خط لوله می تواند از 29 ثانیه فراتر رود. بنابراین ، من باید از طریق “سهمیه خدمات” در AWS ، زمان را به 2 دقیقه افزایش دهم.
نسخه آزمایشی
وب سایت زنده: https://serene-kitten-5a66fb.netlify.app/
demo.mp4
نقاط پایانی API
https://wb6nvu1fl1.execute-api.ap-south-1.amazonaws.com/dev/api/reviews؟page= {product_url}
یادداشت
حتماً URL کامل را در پارامتر پرس و جو مانند این وارد کنید: ?page=https://www.example.com
پاسخ:
{
"statusCode":
"reviews_count": ,
"reviews": [
{
"title": "",
"body": "",
"author": "",
"rating": ""
},
]
}
گردش کار
خط لوله V2 (جریان) |
خط لوله تاخیر |
فن آوری های مورد استفاده:
- Backend: AWS Lambda ، EC2 ، API Gateway ، S3
- فیلمنامه: پایتون (سوپ زیبا ، نمایشنامه نویس)
- LLM: GEMINI-1.5-FLASH
- Frontend: Next.js
مؤلفه ها:
-
فیلتر محتوای HTML: این فرایند با پذیرش URL صفحه وب ، فیلتر کردن کد منبع آن با استفاده از آن آغاز می شود
Beautiful Soup
برای استخراج محتوای معنی دار در حالی که عناصر بی ربط را دور می زنید تا قبل از انتقال آن به LLM ، اندازه نشانه را کاهش دهید. -
استخراج کلاس را استخراج کنید: برای خودکار سازی اقداماتی مانند صفحه بندی ، بررسی های خارج از کشور ، ما به انتخاب کلاس نیاز داریم تا با برنامه های صفحه به صورت برنامه ای در تعامل باشد و از آن استفاده کند ، از آن استفاده می کند
Gemini-1.5-flash
مدل. -
اتوماسیون مرورگر: …
با تشکر از خواندن