-
-
Notifications
You must be signed in to change notification settings - Fork 24.8k
Add ScrapeUnblocker document loader #6641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Kontuzijus
wants to merge
3
commits into
FlowiseAI:main
Choose a base branch
from
ScrapeUnblocker:feature/scrapeunblocker-document-loader
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+349
−0
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
32 changes: 32 additions & 0 deletions
32
packages/components/credentials/ScrapeUnblockerApi.credential.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import { INodeParams, INodeCredential } from '../src/Interface' | ||
|
|
||
| class ScrapeUnblockerApiCredential implements INodeCredential { | ||
| label: string | ||
| name: string | ||
| version: number | ||
| description: string | ||
| inputs: INodeParams[] | ||
|
|
||
| constructor() { | ||
| this.label = 'ScrapeUnblocker API' | ||
| this.name = 'scrapeUnblockerApi' | ||
| this.version = 1.0 | ||
| this.description = | ||
| 'Get your API key from the <a target="_blank" href="https://www.scrapeunblocker.com?utm_source=flowise&utm_medium=integration&utm_campaign=flowise-loader">ScrapeUnblocker</a> dashboard.' | ||
| this.inputs = [ | ||
| { | ||
| label: 'ScrapeUnblocker API Key', | ||
| name: 'scrapeUnblockerApiKey', | ||
| type: 'password' | ||
| }, | ||
| { | ||
| label: 'ScrapeUnblocker API URL', | ||
| name: 'scrapeUnblockerApiUrl', | ||
| type: 'string', | ||
| default: 'https://api.scrapeunblocker.com' | ||
| } | ||
| ] | ||
| } | ||
| } | ||
|
|
||
| module.exports = { credClass: ScrapeUnblockerApiCredential } |
317 changes: 317 additions & 0 deletions
317
packages/components/nodes/documentloaders/ScrapeUnblocker/ScrapeUnblocker.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,317 @@ | ||
| import { TextSplitter } from '@langchain/classic/text_splitter' | ||
| import { Document } from '@langchain/core/documents' | ||
| import { BaseDocumentLoader } from '@langchain/classic/document_loaders/base' | ||
| import { INode, INodeData, INodeParams, ICommonObject, INodeOutputsValue } from '../../../src/Interface' | ||
| import { | ||
| getCredentialData, | ||
| getCredentialParam, | ||
| handleDocumentLoaderDocuments, | ||
| handleDocumentLoaderMetadata, | ||
| handleDocumentLoaderOutput | ||
| } from '../../../src/utils' | ||
| import { AxiosRequestConfig } from 'axios' | ||
| import { secureAxiosRequest } from '../../../src/httpSecurity' | ||
|
|
||
| interface ScrapeUnblockerLoaderParameters { | ||
| apiKey?: string | ||
| apiUrl: string | ||
| mode: 'scrape' | 'search' | ||
| url?: string | ||
| keyword?: string | ||
| parsedData?: boolean | ||
| proxyCountry?: string | ||
| pagesToCheck?: number | ||
| } | ||
|
|
||
| class ScrapeUnblockerLoader extends BaseDocumentLoader { | ||
| private apiKey: string | ||
| private apiUrl: string | ||
| private mode: 'scrape' | 'search' | ||
| private url?: string | ||
| private keyword?: string | ||
| private parsedData: boolean | ||
| private proxyCountry?: string | ||
| private pagesToCheck?: number | ||
|
|
||
| constructor(loaderParams: ScrapeUnblockerLoaderParameters) { | ||
| super() | ||
| const { apiKey, apiUrl, mode, url, keyword, parsedData, proxyCountry, pagesToCheck } = loaderParams | ||
| if (!apiKey) { | ||
| throw new Error('ScrapeUnblocker API key not set. Please set it in the credential.') | ||
| } | ||
|
|
||
| this.apiKey = apiKey | ||
| this.apiUrl = apiUrl.replace(/\/$/, '') | ||
| this.mode = mode | ||
| this.url = url | ||
| this.keyword = keyword | ||
| this.parsedData = parsedData ?? false | ||
| this.proxyCountry = proxyCountry | ||
| this.pagesToCheck = pagesToCheck | ||
| } | ||
|
|
||
| private async request(path: string, params: Record<string, unknown>): Promise<any> { | ||
| // Empty values are dropped so the API applies its own defaults. | ||
| const query: Record<string, unknown> = {} | ||
| for (const [key, value] of Object.entries(params)) { | ||
| if (value !== undefined && value !== null && value !== '' && value !== false) { | ||
| query[key] = value | ||
| } | ||
| } | ||
|
|
||
| const config: AxiosRequestConfig = { | ||
| method: 'POST', | ||
| url: `${this.apiUrl}${path}`, | ||
| params: query, | ||
| headers: { 'X-ScrapeUnblocker-Key': this.apiKey } | ||
| } | ||
|
|
||
| const response = await secureAxiosRequest(config) | ||
| return response.data | ||
| } | ||
|
|
||
| private async loadPage(): Promise<Document[]> { | ||
| if (!this.url) { | ||
| throw new Error('ScrapeUnblocker: URL is required in Scrape mode.') | ||
| } | ||
|
|
||
| const data = await this.request('/getPageSource', { | ||
| url: this.url, | ||
| parsed_data: this.parsedData, | ||
| proxy_country: this.proxyCountry | ||
| }) | ||
|
|
||
| const pageContent = typeof data === 'string' ? data : JSON.stringify(data) | ||
|
|
||
| return [ | ||
| new Document({ | ||
| pageContent, | ||
| metadata: { | ||
| source: this.url, | ||
| type: this.parsedData ? 'parsed_data' : 'html' | ||
| } | ||
| }) | ||
| ] | ||
| } | ||
|
|
||
| private async loadSearch(): Promise<Document[]> { | ||
| if (!this.keyword) { | ||
| throw new Error('ScrapeUnblocker: Keyword is required in Search mode.') | ||
| } | ||
|
|
||
| const data = await this.request('/serpApi', { | ||
| keyword: this.keyword, | ||
| proxy_country: this.proxyCountry, | ||
| pages_to_check: this.pagesToCheck | ||
| }) | ||
|
|
||
| const organic = Array.isArray(data?.organic) ? data.organic.filter((result: any) => result) : [] | ||
| if (!organic.length) { | ||
| return [ | ||
| new Document({ | ||
| pageContent: JSON.stringify(data ?? {}), | ||
| metadata: { source: this.keyword, type: 'serp' } | ||
| }) | ||
| ] | ||
| } | ||
|
|
||
| return organic.map( | ||
| (result: any) => | ||
| new Document({ | ||
| pageContent: result.description || result.title || '', | ||
| metadata: { | ||
| title: result.title, | ||
| source: result.url, | ||
| type: 'serp' | ||
| } | ||
| }) | ||
| ) | ||
| } | ||
|
|
||
| public async load(): Promise<Document[]> { | ||
| if (this.mode === 'scrape') { | ||
| return this.loadPage() | ||
| } else if (this.mode === 'search') { | ||
| return this.loadSearch() | ||
| } | ||
| throw new Error(`Unrecognized mode '${this.mode}'. Expected one of 'scrape', 'search'.`) | ||
| } | ||
| } | ||
|
|
||
| class ScrapeUnblocker_DocumentLoaders implements INode { | ||
| label: string | ||
| name: string | ||
| description: string | ||
| type: string | ||
| icon: string | ||
| version: number | ||
| category: string | ||
| baseClasses: string[] | ||
| inputs: INodeParams[] | ||
| credential: INodeParams | ||
| outputs: INodeOutputsValue[] | ||
|
|
||
| constructor() { | ||
| this.label = 'ScrapeUnblocker' | ||
| this.name = 'scrapeUnblocker' | ||
| this.version = 1.0 | ||
| this.type = 'Document' | ||
| this.icon = 'scrapeunblocker.png' | ||
| this.category = 'Document Loaders' | ||
| this.description = 'Load data from pages protected by anti-bot systems using ScrapeUnblocker' | ||
| this.baseClasses = [this.type] | ||
| this.inputs = [ | ||
| { | ||
| label: 'Text Splitter', | ||
| name: 'textSplitter', | ||
| type: 'TextSplitter', | ||
| optional: true | ||
| }, | ||
| { | ||
| label: 'Mode', | ||
| name: 'mode', | ||
| type: 'options', | ||
| options: [ | ||
| { | ||
| label: 'Scrape', | ||
| name: 'scrape', | ||
| description: 'Fetch a single page, bypassing anti-bot protection' | ||
| }, | ||
| { | ||
| label: 'Search', | ||
| name: 'search', | ||
| description: 'Search Google and load the organic results' | ||
| } | ||
| ], | ||
| default: 'scrape' | ||
| }, | ||
| { | ||
| label: 'Web Page URL', | ||
| name: 'url', | ||
| type: 'string', | ||
| placeholder: 'https://www.scrapeunblocker.com', | ||
| show: { | ||
| mode: ['scrape'] | ||
| } | ||
| }, | ||
| { | ||
| label: 'Keyword', | ||
| name: 'keyword', | ||
| type: 'string', | ||
| placeholder: 'best web scraping api', | ||
| show: { | ||
| mode: ['search'] | ||
| } | ||
| }, | ||
| { | ||
| label: 'Parsed Data', | ||
| name: 'parsedData', | ||
| type: 'boolean', | ||
| description: 'Return AI-parsed structured JSON instead of raw HTML', | ||
| default: false, | ||
| optional: true, | ||
| additionalParams: true, | ||
| show: { | ||
| mode: ['scrape'] | ||
| } | ||
| }, | ||
| { | ||
| label: 'Pages To Check', | ||
| name: 'pagesToCheck', | ||
| type: 'number', | ||
| description: 'How many search result pages to load', | ||
| default: 1, | ||
| optional: true, | ||
| additionalParams: true, | ||
| show: { | ||
| mode: ['search'] | ||
| } | ||
| }, | ||
| { | ||
| label: 'Proxy Country', | ||
| name: 'proxyCountry', | ||
| type: 'string', | ||
| description: 'Two-letter country code for the exit IP, for geo-restricted content', | ||
| placeholder: 'us', | ||
| optional: true, | ||
| additionalParams: true | ||
| }, | ||
| { | ||
| label: 'Additional Metadata', | ||
| name: 'additional_metadata', | ||
| type: 'json', | ||
| description: 'Additional metadata to be added to the extracted documents', | ||
| optional: true, | ||
| additionalParams: true | ||
| }, | ||
| { | ||
| label: 'Omit Metadata Keys', | ||
| name: 'omitMetadataKeys', | ||
| type: 'string', | ||
| rows: 4, | ||
| description: | ||
| 'Each document loader comes with a default set of metadata keys that are extracted from the document. You can use this field to omit some of the default metadata keys. The value should be a list of keys, seperated by comma. Use * to omit all metadata keys execept the ones you specify in the Additional Metadata field', | ||
| placeholder: 'key1, key2, key3.nestedKey1', | ||
| optional: true, | ||
| additionalParams: true | ||
| } | ||
| ] | ||
| this.credential = { | ||
| label: 'Credential', | ||
| name: 'credential', | ||
| type: 'credential', | ||
| credentialNames: ['scrapeUnblockerApi'] | ||
| } | ||
| this.outputs = [ | ||
| { | ||
| label: 'Document', | ||
| name: 'document', | ||
| description: 'Array of document objects containing metadata and pageContent', | ||
| baseClasses: [...this.baseClasses, 'json'] | ||
| }, | ||
| { | ||
| label: 'Text', | ||
| name: 'text', | ||
| description: 'Concatenated string from pageContent of documents', | ||
| baseClasses: ['string', 'json'] | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> { | ||
| const textSplitter = nodeData.inputs?.textSplitter as TextSplitter | ||
| const mode = nodeData.inputs?.mode as 'scrape' | 'search' | ||
| const url = nodeData.inputs?.url as string | ||
| const keyword = nodeData.inputs?.keyword as string | ||
| const parsedData = nodeData.inputs?.parsedData as boolean | ||
| const proxyCountry = nodeData.inputs?.proxyCountry as string | ||
| const pagesToCheck = nodeData.inputs?.pagesToCheck as number | ||
| const additionalMetadata = nodeData.inputs?.additional_metadata | ||
| const _omitMetadataKeys = nodeData.inputs?.omitMetadataKeys as string | ||
| const output = nodeData.outputs?.output as string | ||
|
|
||
| const credentialData = await getCredentialData(nodeData.credential ?? '', options) | ||
| const apiKey = getCredentialParam('scrapeUnblockerApiKey', credentialData, nodeData) | ||
| const apiUrl = getCredentialParam('scrapeUnblockerApiUrl', credentialData, nodeData, 'https://api.scrapeunblocker.com') | ||
|
|
||
| const input: ScrapeUnblockerLoaderParameters = { | ||
| apiKey, | ||
| apiUrl, | ||
| mode, | ||
| url, | ||
| keyword, | ||
| parsedData, | ||
| proxyCountry, | ||
| pagesToCheck | ||
| } | ||
|
|
||
| const loader = new ScrapeUnblockerLoader(input) | ||
|
|
||
| let docs = await handleDocumentLoaderDocuments(loader, textSplitter) | ||
| docs = handleDocumentLoaderMetadata(docs, _omitMetadataKeys, additionalMetadata) | ||
|
|
||
| return handleDocumentLoaderOutput(docs, output) | ||
| } | ||
| } | ||
|
|
||
| module.exports = { nodeClass: ScrapeUnblocker_DocumentLoaders } | ||
Binary file added
BIN
+24 KB
packages/components/nodes/documentloaders/ScrapeUnblocker/scrapeunblocker.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apply defensive programming by filtering out any null or undefined items in the
organicarray before mapping them toDocumentinstances. This prevents potential runtimeTypeErrorcrashes if the API returns unexpected nullish elements.References
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, with one adjustment. I applied the filter at the point where
organicis built rather than at the.map():Filtering only at the
.map()would leave the!organic.lengthguard above it working on the unfiltered array, so a response like{organic: [null, null]}would skip the fallback document and return zero documents instead. Filtering first means such a response correctly falls through to the raw-JSON fallback document.