I'm a 16-year-old so I never studied programming, although I do try to adopt the best practices I can when given the chance.
I would like to clean up the code posted below by making it comply with SOLID principle, and any other general improvements that are offered.
The class in question is the core entry point of my whole application. Each instance of this application acts as a "worker" for a scraper network.
public class ScraperWorker
{
private readonly ScraperQueue _scraperQueue;
private readonly Dictionary<string, ISocialEventHandler> _eventHandlers;
private readonly ScraperWorkerDao _scraperWorkerDao;
private readonly ScraperQueueDao _scraperQueueDao;
private readonly Client _bugSnagClient;
private readonly ILogger _logger;
private bool _isProcessing;
public ScraperWorker(
ScraperQueue scraperQueue,
Dictionary<string, ISocialEventHandler> eventHandlers,
ScraperWorkerDao scraperWorkerDao,
ScraperQueueDao scraperQueueDao,
Client bugSnagClient,
ILogger logger)
{
_scraperQueue = scraperQueue;
_eventHandlers = eventHandlers;
_scraperWorkerDao = scraperWorkerDao;
_scraperQueueDao = scraperQueueDao;
_bugSnagClient = bugSnagClient;
_logger = logger;
Process();
}
public void Start()
{
_isProcessing = true;
}
public void Stop()
{
_isProcessing = false;
}
private void Process()
{
var ticksSinceStatusUpdate = 0;
new Thread(() =>
{
while (true)
{
ticksSinceStatusUpdate++;
if (ticksSinceStatusUpdate >= 10)
{
RenewWorkerStatus(); // Checks if we have paused the worker via an external service.
ticksSinceStatusUpdate = 0;
}
if (!_isProcessing)
{
_logger.Warning($"The worker is currently paused, sleeping for 30 seconds.");
Thread.Sleep(TimeSpan.FromSeconds(30));
continue;
}
if (!_scraperQueue.TryGetItem(out var item))
{
_logger.Warning($"The queue is currently empty, sleeping for 30 seconds.");
Thread.Sleep(TimeSpan.FromSeconds(30));
continue;
}
_scraperWorkerDao.UpdateWorkerLastSeen(StaticState.WorkerId);
var eventHandler = ResolveEventHandlerFromItem(item.Item);
eventHandler.SetCurrentItem(item.Item);
if (eventHandler.IsLoginNeeded())
{
eventHandler.Login();
}
var result = ProcessQueueItem(item, eventHandler);
if (result.IsSuccess)
{
_logger.Success($"Finished processing {item.Item}");
}
else
{
_logger.Trace($"Finished processing {item.Item}");
}
Console.WriteLine();
_scraperQueueDao.MarkItemAsComplete(item.Id);
_scraperQueueDao.StoreItemResultInDatabase(result);
}
}).Start();
}
private void RenewWorkerStatus()
{
_isProcessing = _scraperWorkerDao.IsWorkerRunning(StaticState.WorkerId);
}
private ScraperQueueItemResult ProcessQueueItem(ScraperQueueItem item, ISocialEventHandler eventHandler)
{
_logger.Trace($"Processing {item.Item}");
try
{
eventHandler.NavigateToProfile();
if (eventHandler.IsProfileVisitsThrottled())
{
eventHandler.SwitchAccount(true);
return ProcessQueueItem(item, eventHandler);
}
if (!eventHandler.TryWaitForProfileToLoad())
{
return new ScraperQueueItemResult(
item.Id, "Method 'TryWaitForProfileToLoad' returned false.", eventHandler.GetPageSource(), false
);
}
var profile = eventHandler.CreateProfile();
if (!profile.ShouldScrape(out var validationResult))
{
return new ScraperQueueItemResult(
item.Id, validationResult.ToString(), eventHandler.GetPageSource(), false
);
}
var connectionsToStore = new List<ProfileConnection>();
if (profile.ShouldCollectConnections())
{
profile.Connections = eventHandler.GetConnections();
connectionsToStore = eventHandler.GetFilteredConnections(profile.Connections);
if (connectionsToStore.Any())
{
_logger.Trace(
$"Collected {profile.Connections.Count} / {profile.FollowerCount}, storing {connectionsToStore.Count} of them in the database.");
}
}
if (profile.ShouldSave(out validationResult))
{
if (!profile.IsPrivate)
{
profile.Posts = eventHandler.GetPosts(profile.Username);
}
profile.Save();
if (connectionsToStore.Any())
{
_scraperQueue.AddItems(eventHandler.ConvertConnectionsToQueueItems(connectionsToStore), profile.Id);
}
return new ScraperQueueItemResult(
item.Id, "success", "", true
);
}
if (connectionsToStore.Any())
{
_scraperQueue.AddItems(eventHandler.ConvertConnectionsToQueueItems(connectionsToStore), profile.Id);
}
return new ScraperQueueItemResult(
item.Id, validationResult.ToString(), eventHandler.GetPageSource(), false
);
}
catch (Exception e)
{
_bugSnagClient.Notify(e, report =>
{
report.Event.Metadata.Add("queue_item", item.Item);
report.Event.Metadata.Add("current_url", eventHandler.GetUrl());
report.Event.Metadata.Add("page_source", eventHandler.GetPageSource());
report.Event.Metadata.Add("created_at", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
});
// TODO: Try and requeue the item?
_logger.Error(e.Message);
return new ScraperQueueItemResult(
item.Id, e.Message, eventHandler.GetPageSource(), false
);
}
}
private ISocialEventHandler ResolveEventHandlerFromItem(string item)
{
var host = new Uri(item).Host.Replace("www.", "");
if (_eventHandlers.ContainsKey(host))
{
return _eventHandlers[host];
}
throw new Exception($"Failed to resolve event handler for host '{host}'");
}
}
ISocialEventHandler (each site inherits this and has its own implementation on how a site should be scraped)
public interface ISocialEventHandler
{
string GetLoginPageUrl();
string GetLogoutPageUrl();
bool IsLoginNeeded();
void Login();
void Logout();
ISocialProfile CreateProfile();
List<ProfileConnection> GetConnections();
void NavigateToProfile();
bool TryWaitForProfileToLoad();
string GetUrl();
string GetUsername();
string GetName();
string GetProfilePicture();
bool GetIsPrivate();
int GetFollowerCount();
int GetFollowingCount();
List<ProfilePost> GetPosts(string owner);
List<ProfilePostMedia> GetPostMedia(string owner);
void SetCurrentItem(string currentItem);
List<ProfileConnection> GetFilteredConnections(List<ProfileConnection> connections);
string GetPageSource();
List<ScraperQueueItem> ConvertConnectionsToQueueItems(List<ProfileConnection> connections);
bool IsProfileVisitsThrottled();
void SwitchAccount(bool markCurrentAsThrottled);
}
ScraperQueueItem (just an entity)
public long Id { get; }
public string Item { get; }
public int TypeId { get; } // determines provider
public bool IsPrivate { get; }
public bool Confirmed { get; }
public ScraperQueueItem(long id, string item, int typeId, bool isPrivate, bool confirmed)
{
Id = id;
Item = item;
TypeId = typeId;
IsPrivate = isPrivate;
Confirmed = confirmed;
}
ScraperQueueItemResult (used to map from and to the database, another entity)
public long ItemId { get; set; }
public string Content { get; set; }
public string PageSource { get; set; }
public bool IsSuccess { get; set; }
public ScraperQueueItemResult(long itemId, string content, string pageSource, bool isSuccess)
{
ItemId = itemId;
Content = content;
PageSource = pageSource.Truncate(1000);
IsSuccess = isSuccess;
}
ProfileConnection (used to map, another entity)
public class ProfileConnection
{
public string Item { get; set; }
public bool IsPrivate { get; set; }
}
ProfilePost (another DB mapper to and from, entity):
public class ProfilePost
{
public List<ProfilePostMedia> Media { get; }
public ProfilePost(List<ProfilePostMedia> media)
{
Media = media;
}
}
ProfileMostMedia (another DB mapper to and from, entity):
public class ProfilePostMedia
{
public string CdnUrl { get; set; }
public string MetaData { get; set; }
public ProfilePostMedia(string cdnUrl, string metaData)
{
CdnUrl = cdnUrl;
MetaData = metaData;
}
}
Task
andasync/await
instead of manualThread
management. Asynchronous Programming. \$\endgroup\$