mirror of
https://github.com/wassname/detect_bs_text.git
synced 2026-07-16 11:18:59 +08:00
42 KiB
42 KiB
In [5]:
import json
from pathlib import Path
last_date = '2024-01-01'In [6]:
import requests
from loguru import logger
import time
from dataclasses import dataclass
from markdownify import markdownify
@dataclass
class GreaterWrong:
"""
This class allows you to scrape posts and comments from GreaterWrong.
GreaterWrong contains all the posts from LessWrong (which contains the Alignment Forum) and the EA Forum.
from https://github.com/StampyAI/alignment-research-dataset/blob/main/align_data/sources/greaterwrong/greaterwrong.py#L156
"""
base_url: str = 'https://www.lesswrong.com'
start_year: int = 2000
min_karma: int = -10000
"""Posts must have at least this much karma to be returned."""
af: bool = False
"""Whether alignment forum posts should be returned"""
limit = 50
COOLDOWN = 0.5
done_key = "url"
lazy_eval = True
source_type = 'GreaterWrong'
_outputted_items = (set(), set())
def make_query(self, after: str):
return f'''
{{
posts(input: {{
terms: {{
excludeEvents: true
view: "old"
af: {self.af}
limit: {self.limit}
karmaThreshold: {self.min_karma}
after: "{after}"
filter: "tagged"
}}
}}) {{
totalCount
results {{
_id
title
slug
pageUrl
postedAt
modifiedAt
emojiReactors
score
extendedScore
baseScore
voteCount
commentCount
wordCount
tags {{
name
}}
user {{
displayName
}}
coauthors {{
displayName
}}
af
htmlBody
allVotes {{
authorId
_id
power
afPower
isUnvote
votedAt
}}
}}
}}
}}
'''
def fetch_posts(self, query: str):
res = requests.post(
f"{self.base_url}/graphql",
# The GraphQL endpoint returns a 403 if the user agent isn't set... Makes sense, but is annoying
headers={
"User-Agent": "Mozilla /5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/113.0"
},
json={"query": query},
)
try:
res.raise_for_status()
except requests.exceptions.HTTPError:
logger.error(f"Failed to fetch posts: {res.text}")
raise
try:
return res.json()["data"]["posts"]
except KeyError:
raise ValueError(f"Could not parse response: {res.text}")
@property
def items_list(self):
next_date = self.last_date_published
logger.info("Starting from {next_date}")
last_item = None
while next_date:
logger.info(f"Fetching posts after {next_date}")
posts = self.fetch_posts(self.make_query(next_date))
if not posts["results"]:
return
# If the only item we find was the one we advanced our iterator to, we're done
if len(posts["results"]) == 1 and last_item and posts["results"][0]["pageUrl"] == last_item["pageUrl"]:
return
for post in posts["results"]:
if post["htmlBody"]:
yield post
last_item = posts["results"][-1]
new_next_date = posts["results"][-1]["postedAt"]
if next_date == new_next_date:
raise ValueError(f'could not advance through dataset, next date did not advance after {next_date}')
next_date = new_next_date
time.sleep(self.COOLDOWN)
def process_entry(self, item):
return self.make_data_entry(
{
"title": item["title"],
"text": markdownify(item["htmlBody"]).strip(),
"url": item["pageUrl"],
"date_published": self._get_published_date(item),
"modified_at": item["modifiedAt"],
"source": self.name,
"source_type": self.source_type,
"votes": item["voteCount"],
"karma": item["baseScore"],
"tags": [t["name"] for t in item["tags"]],
"words": item["wordCount"],
"comment_count": item["commentCount"],
"authors": self.extract_authors(item),
}
)In [7]:
gw = GreaterWrong()
gw.last_date_published = '2023-01-01'
import pandas as pd
from tqdm.auto import tqdm
cache_file = Path('output/01greaterwrong.json')
cache_file.parent.mkdir(parents=True, exist_ok=True)In [8]:
if cache_file.exists():
with cache_file.open() as f:
posts = json.load(f)
print(f'Loaded {len(posts)} posts from cache')
else:
posts = []
for post in tqdm(gw.items_list):
posts.append(post)
cache_file.write_text(json.dumps(posts, indent=2))
len(posts)0it [00:00, ?it/s]
[32m2025-07-26 11:17:04.797[0m | [1mINFO [0m | [36m__main__[0m:[36mitems_list[0m:[36m110[0m - [1mStarting from {next_date}[0m [32m2025-07-26 11:17:04.798[0m | [1mINFO [0m | [36m__main__[0m:[36mitems_list[0m:[36m113[0m - [1mFetching posts after 2023-01-01[0m [32m2025-07-26 11:17:05.927[0m | [31m[1mERROR [0m | [36m__main__[0m:[36mfetch_posts[0m:[36m98[0m - [31m[1mFailed to fetch posts: {"errors":[{"message":"Expected value of type \"JSON\", found {excludeEvents: true, view: \"old\", af: False, limit: 50, karmaThreshold: -10000, after: \"2023-01-01\", filter: \"tagged\"}; JSON cannot represent value: False","locations":[{"line":4,"column":24}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"Cannot query field \"allVotes\" on type \"Post\".","locations":[{"line":40,"column":21}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]} [0m
[31m---------------------------------------------------------------------------[39m [31mHTTPError[39m Traceback (most recent call last) [36mCell[39m[36m [39m[32mIn[8][39m[32m, line 8[39m [32m 5[39m [38;5;28;01melse[39;00m: [32m 7[39m posts = [] [32m----> [39m[32m8[39m [43m [49m[38;5;28;43;01mfor[39;49;00m[43m [49m[43mpost[49m[43m [49m[38;5;129;43;01min[39;49;00m[43m [49m[43mtqdm[49m[43m([49m[43mgw[49m[43m.[49m[43mitems_list[49m[43m)[49m[43m:[49m [32m 9[39m [43m [49m[43mposts[49m[43m.[49m[43mappend[49m[43m([49m[43mpost[49m[43m)[49m [32m 11[39m cache_file.write_text(json.dumps(posts, indent=[32m2[39m)) [36mFile [39m[32m/media/wassname/SGIronWolf/projects5/bs_writing_detector/.venv/lib/python3.11/site-packages/tqdm/notebook.py:250[39m, in [36mtqdm_notebook.__iter__[39m[34m(self)[39m [32m 248[39m [38;5;28;01mtry[39;00m: [32m 249[39m it = [38;5;28msuper[39m().[34m__iter__[39m() [32m--> [39m[32m250[39m [43m [49m[38;5;28;43;01mfor[39;49;00m[43m [49m[43mobj[49m[43m [49m[38;5;129;43;01min[39;49;00m[43m [49m[43mit[49m[43m:[49m [32m 251[39m [43m [49m[38;5;66;43;03m# return super(tqdm...) will not catch exception[39;49;00m [32m 252[39m [43m [49m[38;5;28;43;01myield[39;49;00m[43m [49m[43mobj[49m [32m 253[39m [38;5;66;03m# NB: except ... [ as ...] breaks IPython async KeyboardInterrupt[39;00m [36mFile [39m[32m/media/wassname/SGIronWolf/projects5/bs_writing_detector/.venv/lib/python3.11/site-packages/tqdm/std.py:1181[39m, in [36mtqdm.__iter__[39m[34m(self)[39m [32m 1178[39m time = [38;5;28mself[39m._time [32m 1180[39m [38;5;28;01mtry[39;00m: [32m-> [39m[32m1181[39m [43m [49m[38;5;28;43;01mfor[39;49;00m[43m [49m[43mobj[49m[43m [49m[38;5;129;43;01min[39;49;00m[43m [49m[43miterable[49m[43m:[49m [32m 1182[39m [43m [49m[38;5;28;43;01myield[39;49;00m[43m [49m[43mobj[49m [32m 1183[39m [43m [49m[38;5;66;43;03m# Update and possibly print the progressbar.[39;49;00m [32m 1184[39m [43m [49m[38;5;66;43;03m# Note: does not call self.update(1) for speed optimisation.[39;49;00m [36mCell[39m[36m [39m[32mIn[6][39m[32m, line 114[39m, in [36mGreaterWrong.items_list[39m[34m(self)[39m [32m 112[39m [38;5;28;01mwhile[39;00m next_date: [32m 113[39m logger.info([33mf[39m[33m"[39m[33mFetching posts after [39m[38;5;132;01m{[39;00mnext_date[38;5;132;01m}[39;00m[33m"[39m) [32m--> [39m[32m114[39m posts = [38;5;28;43mself[39;49m[43m.[49m[43mfetch_posts[49m[43m([49m[38;5;28;43mself[39;49m[43m.[49m[43mmake_query[49m[43m([49m[43mnext_date[49m[43m)[49m[43m)[49m [32m 115[39m [38;5;28;01mif[39;00m [38;5;129;01mnot[39;00m posts[[33m"[39m[33mresults[39m[33m"[39m]: [32m 116[39m [38;5;28;01mreturn[39;00m [36mCell[39m[36m [39m[32mIn[6][39m[32m, line 96[39m, in [36mGreaterWrong.fetch_posts[39m[34m(self, query)[39m [32m 87[39m res = requests.post( [32m 88[39m [33mf[39m[33m"[39m[38;5;132;01m{[39;00m[38;5;28mself[39m.base_url[38;5;132;01m}[39;00m[33m/graphql[39m[33m"[39m, [32m 89[39m [38;5;66;03m# The GraphQL endpoint returns a 403 if the user agent isn't set... Makes sense, but is annoying[39;00m [32m (...)[39m[32m 93[39m json={[33m"[39m[33mquery[39m[33m"[39m: query}, [32m 94[39m ) [32m 95[39m [38;5;28;01mtry[39;00m: [32m---> [39m[32m96[39m [43mres[49m[43m.[49m[43mraise_for_status[49m[43m([49m[43m)[49m [32m 97[39m [38;5;28;01mexcept[39;00m requests.exceptions.HTTPError: [32m 98[39m logger.error([33mf[39m[33m"[39m[33mFailed to fetch posts: [39m[38;5;132;01m{[39;00mres.text[38;5;132;01m}[39;00m[33m"[39m) [36mFile [39m[32m/media/wassname/SGIronWolf/projects5/bs_writing_detector/.venv/lib/python3.11/site-packages/requests/models.py:1026[39m, in [36mResponse.raise_for_status[39m[34m(self)[39m [32m 1021[39m http_error_msg = ( [32m 1022[39m [33mf[39m[33m"[39m[38;5;132;01m{[39;00m[38;5;28mself[39m.status_code[38;5;132;01m}[39;00m[33m Server Error: [39m[38;5;132;01m{[39;00mreason[38;5;132;01m}[39;00m[33m for url: [39m[38;5;132;01m{[39;00m[38;5;28mself[39m.url[38;5;132;01m}[39;00m[33m"[39m [32m 1023[39m ) [32m 1025[39m [38;5;28;01mif[39;00m http_error_msg: [32m-> [39m[32m1026[39m [38;5;28;01mraise[39;00m HTTPError(http_error_msg, response=[38;5;28mself[39m) [31mHTTPError[39m: 400 Client Error: Bad Request for url: https://www.lesswrong.com/graphql
In [ ]:
df = pd.DataFrame(posts)
df.drop(columns=['emojiReactors'], inplace=True)
for col in ['postedAt', 'modifiedAt']:
df[col] = pd.to_datetime(df[col])
p_file = Path('output/01greaterwrong.parquet')
df.to_parquet(p_file)
df.info()<class 'pandas.core.frame.DataFrame'> RangeIndex: 9346 entries, 0 to 9345 Data columns (total 18 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 _id 9346 non-null object 1 title 9346 non-null object 2 slug 9346 non-null object 3 pageUrl 9346 non-null object 4 postedAt 9346 non-null datetime64[ns, UTC] 5 modifiedAt 9346 non-null datetime64[ns, UTC] 6 score 9346 non-null float64 7 extendedScore 7034 non-null object 8 baseScore 9346 non-null int64 9 voteCount 9346 non-null int64 10 commentCount 9346 non-null int64 11 wordCount 9346 non-null int64 12 tags 9346 non-null object 13 user 9270 non-null object 14 coauthors 9346 non-null object 15 af 9346 non-null bool 16 htmlBody 9346 non-null object 17 allVotes 9346 non-null object dtypes: bool(1), datetime64[ns, UTC](2), float64(1), int64(4), object(10) memory usage: 1.2+ MB
In [ ]:
df = df[['title', 'pageUrl', 'modifiedAt', 'htmlBody', 'score', 'baseScore', 'voteCount', 'wordCount', 'slug']]
df = df[
(df['modifiedAt'] > last_date)
& (df['voteCount'] > 10)
].sort_values('score', ascending=False)In [ ]:
df.describe()| score | baseScore | voteCount | wordCount | |
|---|---|---|---|---|
| count | 2385.000000 | 2385.000000 | 2385.000000 | 2385.000000 |
| mean | 0.018153 | 71.339203 | 36.330398 | 2963.753040 |
| std | 0.104511 | 68.800261 | 38.117311 | 3937.558236 |
| min | -0.017480 | -50.000000 | 11.000000 | 0.000000 |
| 25% | 0.001787 | 32.000000 | 16.000000 | 730.000000 |
| 50% | 0.003472 | 50.000000 | 24.000000 | 1660.000000 |
| 75% | 0.007957 | 86.000000 | 40.000000 | 3445.000000 |
| max | 3.236718 | 677.000000 | 499.000000 | 57468.000000 |
In [ ]:
import numpy as np
v = np.log(df['baseScore']+0.001)
v = (v - v.min())/v.max() - 1
v = np.clip(v, 0, 1)
df['novelty'] = v
df['novelty'].hist(bins=26)/media/wassname/SGIronWolf/projects5/bs_writing_detector/.venv/lib/python3.11/site-packages/pandas/core/arraylike.py:396: RuntimeWarning: invalid value encountered in log result = getattr(ufunc, method)(*inputs, **kwargs)
<Axes: >
In [ ]:
def to_markdown(row: dict) -> str:
md = markdownify(row["htmlBody"]).strip()
return f"""---
title: "{row['title'].replace('"', "'")}"
date: {row['modifiedAt']}
url: {row['pageUrl']}
novelty: {row['novelty']}
score: {row['score']}
baseScore: {row['baseScore']}
voteCount: {row['voteCount']}
---
{md}
"""
for i in range(15):
for ii in [i, -i-1]:
row = df.iloc[i]
s = to_markdown(row)
f = Path(f'../samples/{row["modifiedAt"].year}_lw_{row["slug"]}.md')
f.write_text(s)
print(f"{f} {row['score']:>4}")../samples/2025_lw_parkinson-s-law-and-the-ideology-of-statistics-1.md 3.236717700958252 ../samples/2025_lw_parkinson-s-law-and-the-ideology-of-statistics-1.md 3.236717700958252 ../samples/2025_lw_what-s-the-short-timeline-plan.md 2.114389657974243 ../samples/2025_lw_what-s-the-short-timeline-plan.md 2.114389657974243 ../samples/2025_lw_the-laws-of-large-numbers.md 1.2245203256607056 ../samples/2025_lw_the-laws-of-large-numbers.md 1.2245203256607056 ../samples/2025_lw_the-intelligence-curse.md 1.2061121463775635 ../samples/2025_lw_the-intelligence-curse.md 1.2061121463775635 ../samples/2025_lw_human-study-on-ai-spear-phishing-campaigns.md 0.9995136260986328 ../samples/2025_lw_human-study-on-ai-spear-phishing-campaigns.md 0.9995136260986328 ../samples/2025_lw_the-subset-parity-learning-problem-much-more-than-you-wanted.md 0.9548193216323853 ../samples/2025_lw_the-subset-parity-learning-problem-much-more-than-you-wanted.md 0.9548193216323853 ../samples/2025_lw_2024-in-ai-predictions.md 0.8065339922904968 ../samples/2025_lw_2024-in-ai-predictions.md 0.8065339922904968 ../samples/2025_lw_debating-buying-nvda-in-2019.md 0.7926478385925293 ../samples/2025_lw_debating-buying-nvda-in-2019.md 0.7926478385925293 ../samples/2025_lw_review-planecrash.md 0.689734160900116 ../samples/2025_lw_review-planecrash.md 0.689734160900116 ../samples/2024_lw_by-default-capital-will-matter-more-than-ever-after-agi.md 0.6629015207290649 ../samples/2024_lw_by-default-capital-will-matter-more-than-ever-after-agi.md 0.6629015207290649 ../samples/2025_lw_the-field-of-ai-alignment-a-postmortem-and-what-to-do-about.md 0.5714353919029236 ../samples/2025_lw_the-field-of-ai-alignment-a-postmortem-and-what-to-do-about.md 0.5714353919029236 ../samples/2024_lw_the-plan-2024-update.md 0.542655885219574 ../samples/2024_lw_the-plan-2024-update.md 0.542655885219574 ../samples/2025_lw_comment-on-death-and-the-gorgon.md 0.5308915376663208 ../samples/2025_lw_comment-on-death-and-the-gorgon.md 0.5308915376663208 ../samples/2025_lw_my-agi-safety-research-2024-review-25-plans.md 0.49594494700431824 ../samples/2025_lw_my-agi-safety-research-2024-review-25-plans.md 0.49594494700431824 ../samples/2025_lw_preference-inversion.md 0.48199906945228577 ../samples/2025_lw_preference-inversion.md 0.48199906945228577
In [ ]:
In [ ]: