Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions .actor/actor.json
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"
}
}
131 changes: 131 additions & 0 deletions .actor/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"
}
}
}
}
}
}
23 changes: 23 additions & 0 deletions .actor/inputSchema.json
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"]
}
12 changes: 12 additions & 0 deletions .actor/outputSchema.json
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"
}
}
}
89 changes: 31 additions & 58 deletions README.md
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

Copy link
Copy Markdown
Collaborator

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.

Copy link
Copy Markdown
Owner Author

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.


## 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
}
}
```
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const BASE_URL = 'https://forums.factorio.com';
30 changes: 28 additions & 2 deletions src/main.ts
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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The 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 maxConcurrency setting.

});

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();
12 changes: 12 additions & 0 deletions src/routes.ts
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);
Loading