-
Notifications
You must be signed in to change notification settings - Fork 0
Scraper for basic information #1
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
base: main
Are you sure you want to change the base?
Changes from all commits
9b1bb7b
253dcfb
65b8c7b
3667177
11593dc
3756abe
55dfb7c
8797245
b81e128
642ef0f
50a0de0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,18 @@ | ||
| { | ||
| "actorSpecification": 1, | ||
| "name": "factorio-scraper", | ||
| "title": "Empty TypeScript project", | ||
| "description": "Empty project in TypeScript.", | ||
| "version": "0.0", | ||
| "buildTag": "latest", | ||
| "meta": { | ||
| "templateId": "ts-empty", | ||
| "generatedBy": "<FILL-IN-MODEL>" | ||
| }, | ||
| "dockerfile": "../Dockerfile" | ||
| } | ||
| "actorSpecification": 1, | ||
| "name": "factorio-scraper", | ||
| "title": "Factorio forums scraper", | ||
| "description": "Scraper for factorio forums that extracts topics and their details.", | ||
| "version": "0.1", | ||
| "buildTag": "latest", | ||
| "meta": { | ||
| "templateId": "ts-empty", | ||
| "generatedBy": "GitHub Copilot with Claude-Sonnet-4.5" | ||
| }, | ||
| "dockerfile": "../Dockerfile", | ||
| "inputSchema": "./inputSchema.json", | ||
| "outputSchema": "./outputSchema.json", | ||
| "storages": { | ||
| "dataset": "./datasetSchema.json" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| { | ||
| "actorSpecification": 1, | ||
| "title": "Factorio topics dataset schema", | ||
| "description": "Schema for Factorio forum topics dataset", | ||
| "fields": { | ||
| "type": "object", | ||
| "additionalProperties": false, | ||
| "properties": { | ||
| "publishedAt": { | ||
| "type": "string", | ||
| "format": "date-time" | ||
| }, | ||
| "author": { | ||
| "type": "string" | ||
| }, | ||
| "text": { | ||
| "type": "string" | ||
| }, | ||
| "topic": { | ||
| "type": "object", | ||
| "additionalProperties": false, | ||
| "properties": { | ||
| "category": { | ||
| "type": "string" | ||
| }, | ||
| "title": { | ||
| "type": "string" | ||
| }, | ||
| "url": { | ||
| "type": "string", | ||
| "format": "uri" | ||
| }, | ||
| "author": { | ||
| "type": "string" | ||
| }, | ||
| "createdAt": { | ||
| "type": "string", | ||
| "format": "date-time" | ||
| }, | ||
| "lastPostAt": { | ||
| "type": "string", | ||
| "format": "date-time" | ||
| }, | ||
| "views": { | ||
| "type": "number" | ||
| }, | ||
| "isAnnouncement": { | ||
| "type": "boolean" | ||
| }, | ||
| "totalPosts": { | ||
| "type": "integer" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "views": { | ||
| "overview": { | ||
| "title": "Overview", | ||
| "transformation": { | ||
| "fields": [ | ||
| "topic.category", | ||
| "topic.title", | ||
| "topic.url", | ||
| "topic.author", | ||
| "topic.createdAt", | ||
| "topic.lastPostAt", | ||
| "topic.views", | ||
| "topic.isAnnouncement", | ||
| "topic.totalPosts", | ||
| "author", | ||
| "publishedAt", | ||
| "text" | ||
| ] | ||
| }, | ||
| "display": { | ||
| "component": "table", | ||
| "properties": { | ||
| "topic.category": { | ||
| "label": "Category", | ||
| "format": "text" | ||
| }, | ||
| "topic.title": { | ||
| "label": "Topic Title", | ||
| "format": "text" | ||
| }, | ||
| "topic.url": { | ||
| "label": "Topic URL", | ||
| "format": "link" | ||
| }, | ||
| "topic.author": { | ||
| "label": "Topic Author", | ||
| "format": "text" | ||
| }, | ||
| "topic.createdAt": { | ||
| "label": "Topic Created At", | ||
| "format": "date" | ||
| }, | ||
| "topic.lastPostAt": { | ||
| "label": "Last Post At", | ||
| "format": "date" | ||
| }, | ||
| "topic.views": { | ||
| "label": "Views", | ||
| "format": "number" | ||
| }, | ||
| "topic.isAnnouncement": { | ||
| "label": "Announcement", | ||
| "format": "boolean" | ||
| }, | ||
| "topic.totalPosts": { | ||
| "label": "Total Posts", | ||
| "format": "number" | ||
| }, | ||
| "author": { | ||
| "label": "Post Author", | ||
| "format": "text" | ||
| }, | ||
| "publishedAt": { | ||
| "label": "Post Published At", | ||
| "format": "date" | ||
| }, | ||
| "text": { | ||
| "label": "Post Text", | ||
| "format": "text" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| { | ||
| "title": "Factorio Scraper Input Schema", | ||
| "type": "object", | ||
| "schemaVersion": 1, | ||
| "properties": { | ||
| "category": { | ||
| "title": "Category", | ||
| "type": "string", | ||
| "description": "The category to scrape, case insensitive", | ||
| "editor": "textfield", | ||
| "prefill": "news" | ||
| }, | ||
| "limit": { | ||
| "title": "Limit", | ||
| "type": "number", | ||
| "description": "The maximum number of topics to scrape, there are 25 topics per page", | ||
| "editor": "number", | ||
| "prefill": 100, | ||
| "minimum": 1 | ||
| } | ||
| }, | ||
| "required": ["category"] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "actorOutputSchemaVersion": 1, | ||
| "description": "Output schema for factorio scraper", | ||
| "title": "Output schema for factorio scraper", | ||
| "properties": { | ||
| "dataset": { | ||
| "type": "string", | ||
| "title": "Dataset", | ||
| "template": "{{links.apiDefaultDatasetUrl}}/items" | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,67 +1,40 @@ | ||
| ## Empty TypeScript template | ||
| This scraper collects data from forum of the game [Factorio](https://forums.factorio.com/). | ||
| It does not go around the login, so it does not scrape user detail page. | ||
|
|
||
| <!-- This is an Apify template readme --> | ||
| It scrapes only one category from the forum which is provided as input. | ||
| There is also option to scrape only specific number of topics. | ||
| A topic is a page with posts provide by users. | ||
|
|
||
| Start a new [web scraping](https://apify.com/web-scraping) project quickly and easily in TypeScript (Node.js) with our empty project template. It provides a basic structure for the Actor with [Apify SDK](https://docs.apify.com/sdk/js/) and allows you to easily add your own functionality. | ||
| ## Inputs | ||
|
|
||
| ## Included features | ||
| For up to date information about inputs see the [input page](https://console.apify.com/actors/A448q9dopTHFBMkUf/input) | ||
|
|
||
| - **[Apify SDK](https://docs.apify.com/sdk/js/)** - a toolkit for building [Actors](https://apify.com/actors) | ||
| - **[Crawlee](https://crawlee.dev/)** - web scraping and browser automation library | ||
| | Name | Required | Use | | ||
| | -------- | -------- | ------------------------------------ | | ||
| | Category | yes | Category to scrape from the page | | ||
| | Limit | yes | How many topics to scrape at maximum | | ||
|
|
||
| ## How it works | ||
| ## Output | ||
|
|
||
| Insert your own code between `await Actor.init()` and `await Actor.exit()`. If you would like to use the [Crawlee](https://crawlee.dev/) library simply uncomment its import `import { CheerioCrawler } from '@crawlee/cheerio';`. | ||
| The output is a single comment (post) on the topic with all associated information about the topic this comment belongs to. | ||
|
|
||
| ## Resources | ||
| ### Example output | ||
|
|
||
| - [TypeScript vs. JavaScript: which to use for web scraping?](https://blog.apify.com/typescript-vs-javascript-crawler/) | ||
| - [Node.js tutorials](https://docs.apify.com/academy/node-js) in Academy | ||
| - [Video guide on getting scraped data using Apify API](https://www.youtube.com/watch?v=ViYYDHSBAKM) | ||
| - [Integration with Airbyte](https://apify.com/integrations), Make, Zapier, Google Drive, and other apps | ||
| - A short guide on how to build web scrapers using code templates: | ||
|
|
||
| [web scraper template](https://www.youtube.com/watch?v=u-i-Korzf8w) | ||
|
|
||
|
|
||
| ## Getting started | ||
|
|
||
| For complete information [see this article](https://docs.apify.com/platform/actors/development#build-actor-locally). To run the Actor use the following command: | ||
|
|
||
| ```bash | ||
| apify run | ||
| ``` | ||
|
|
||
| ## Deploy to Apify | ||
|
|
||
| ### Connect Git repository to Apify | ||
|
|
||
| If you've created a Git repository for the project, you can easily connect to Apify: | ||
|
|
||
| 1. Go to [Actor creation page](https://console.apify.com/actors/new) | ||
| 2. Click on **Link Git Repository** button | ||
|
|
||
| ### Push project on your local machine to Apify | ||
|
|
||
| You can also deploy the project on your local machine to Apify without the need for the Git repository. | ||
|
|
||
| 1. Log in to Apify. You will need to provide your [Apify API Token](https://console.apify.com/account/integrations) to complete this action. | ||
|
|
||
| ```bash | ||
| apify login | ||
| ``` | ||
|
|
||
| 2. Deploy your Actor. This command will deploy and build the Actor on the Apify Platform. You can find your newly created Actor under [Actors -> My Actors](https://console.apify.com/actors?tab=my). | ||
|
|
||
| ```bash | ||
| apify push | ||
| ``` | ||
|
|
||
| ## Documentation reference | ||
|
|
||
| To learn more about Apify and Actors, take a look at the following resources: | ||
|
|
||
| - [Apify SDK for JavaScript documentation](https://docs.apify.com/sdk/js) | ||
| - [Apify SDK for Python documentation](https://docs.apify.com/sdk/python) | ||
| - [Apify Platform documentation](https://docs.apify.com/platform) | ||
| - [Join our developer community on Discord](https://discord.com/invite/jyEM2PRvMU) | ||
| { | ||
| "author": "Carl", | ||
| "publishedAt": "2020-09-07T12:30:00+00:00", | ||
| "text": "This game is the best!", | ||
| "topic": { | ||
| "category": "news", | ||
| "title": "Is this the best game?", | ||
| "views": 12345, | ||
| "url": "https://forums.factorio.com/viewtopic.php?t=0000001", | ||
| "author": "FactorioBot", | ||
| "createdAt": "2020-01-01T12:00:01+00:00", | ||
| "lastPostAt": "2020-02-01T23:13:37+00:00", | ||
| "isAnnouncement": true, | ||
| "totalPosts": 123 | ||
| } | ||
| } | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export const BASE_URL = 'https://forums.factorio.com'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,33 @@ | ||
| import { Actor, log } from 'apify'; | ||
| import { Actor } from 'apify'; | ||
| import { CheerioCrawler, Request } from 'crawlee'; | ||
|
|
||
| import { BASE_URL } from './constants.js'; | ||
| import { router } from './routes.js'; | ||
| import { type Input, Label, type UserData } from './types/index.js'; | ||
|
|
||
| await Actor.init(); | ||
|
|
||
| log.info('Hello from the Actor!'); | ||
| const { category, limit } = (await Actor.getInput<Input>())!; | ||
|
|
||
| const proxyConfiguration = await Actor.createProxyConfiguration({ | ||
| useApifyProxy: true, | ||
| }); | ||
|
|
||
| const crawler = new CheerioCrawler({ | ||
| proxyConfiguration, | ||
| requestHandler: router, | ||
| // this is 2 requests per second, this should be fine and not overload the site since it is not so frequently used one | ||
| maxRequestsPerMinute: 120, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit of issue with this option is that (I think) it doesn't distribute the requests in the minute, it will just stop processing once done. So it could still lead to high bursts at the start (not sure how likely it is in practice). You could solve that by adding maxConcurrency limit as well.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes I noticed that it just does huge burst at the beginning and then it slows down. Will try to do this with the |
||
| }); | ||
|
|
||
| const initialRequest = new Request<UserData[typeof Label.FORUM_SEARCH_START]>({ | ||
| url: `${BASE_URL}`, | ||
| label: Label.FORUM_SEARCH_START, | ||
| userData: { | ||
| category, | ||
| limit, | ||
| }, | ||
| }); | ||
| await crawler.run([initialRequest]); | ||
|
|
||
| await Actor.exit(); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import { createCheerioRouter } from 'crawlee'; | ||
|
|
||
| import { forumCategoryHandler } from './routes/forumCategory.js'; | ||
| import { forumSearchStartHandler } from './routes/searchStart.js'; | ||
| import { topicDetailHandler } from './routes/topicDetail.js'; | ||
| import { Label } from './types/label.js'; | ||
|
|
||
| export const router = createCheerioRouter(); | ||
|
|
||
| router.addHandler(Label.FORUM_SEARCH_START, forumSearchStartHandler); | ||
| router.addHandler(Label.FORUM_CATEGORY, forumCategoryHandler); | ||
| router.addHandler(Label.TOPIC_DETAIL, topicDetailHandler); |
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.
We generally prefer to link the Input page to the Actor Store page, which is derived from the inpjt schema. That way there is only single source of truth for the descriptions, you will often forget to change the readme.
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.
I linked the input page statically in the README.